Skip to content
Snippets Groups Projects
Commit a50fb11d authored by Ludovico de Nittis's avatar Ludovico de Nittis
Browse files

Merge branch 'wip/t29490-adverb' into 'master'

pv-adverb: Improve LD_AUDIT, LD_PRELOAD handling

See merge request !350
parents 9ae1cd33 bfbe9c46
No related branches found
No related tags found
1 merge request!350pv-adverb: Improve LD_AUDIT, LD_PRELOAD handling
......@@ -18,8 +18,8 @@ pressure-vessel-adverb - wrap processes in various ways
[**--[no-]exit-with-parent**]
[**--fd** *FD*...]
[**--[no-]generate-locales**]
[**--ld-audit** *MODULE*...]
[**--ld-preload** *MODULE*...]
[**--ld-audit** *MODULE*[**:arch=***TUPLE*]...]
[**--ld-preload** *MODULE*[**:**...]...]
[**--pass-fd** *FD*...]
[**--shell** **none**|**after**|**fail**|**instead**]
[**--subreaper**]
......@@ -70,15 +70,27 @@ exit status.
**LOCPATH** environment variable.
**--no-generate-locales** disables this behaviour, and is the default.
**--ld-audit** *MODULE*
**--ld-audit** *MODULE*[**:arch=***TUPLE*]
: Add *MODULE* to **LD_AUDIT** before executing *COMMAND*.
The optional *TUPLE* is the same as for **--ld-preload**, below.
**--ld-preload** *MODULE*
**--ld-preload** *MODULE*[**:arch=***TUPLE*]
: Add *MODULE* to **LD_PRELOAD** before executing *COMMAND*.
Some adjustments may be performed to the provided *MODULE*, e.g.
multiple preloads of gameoverlayrenderer.so for different ABIs may be
joined together into a single path by leveraging the dynamic linker
token expansion feature.
If the optional **:arch=***TUPLE* is given, the *MODULE* is only used for
the given architecture, and is paired with other modules (if any) that
share its basename; for example,
`/home/me/.steam/root/ubuntu12_32/gameoverlayrenderer.so:arch=i386-linux-gnu`
and
`/home/me/.steam/root/ubuntu12_64/gameoverlayrenderer.so:arch=x86_64-linux-gnu`
will be combined into a single **LD_PRELOAD** entry of the form
`/tmp/pressure-vessel-libs-123456/${PLATFORM}/gameoverlayrenderer.so`.
For a **LD_PRELOAD** module named `gameoverlayrenderer.so` in a directory
named `ubuntu12_32` or `ubuntu12_64`, the architecture is automatically
set to `i386-linux-gnu` or `x86_64-linux-gnu` respectively, if not
otherwise given. Other special-case behaviour might be added in future
if required.
**--lock-file** *FILENAME*
: Lock the file *FILENAME* according to the most recently seen
......
......@@ -76,24 +76,70 @@ typedef struct
{
gchar *root_path;
gchar *platform_token_path;
GHashTable *abi_paths;
/* Same order as pv_multiarch_details */
gchar *abi_paths[PV_N_SUPPORTED_ARCHITECTURES];
} LibTempDirs;
static PreloadModule opt_preload_modules[] =
typedef enum
{
{ "LD_AUDIT", NULL },
{ "LD_PRELOAD", NULL },
PRELOAD_VARIABLE_INDEX_LD_AUDIT,
PRELOAD_VARIABLE_INDEX_LD_PRELOAD,
} PreloadVariableIndex;
/* Indexed by PreloadVariableIndex */
static const char *preload_variables[] =
{
"LD_AUDIT",
"LD_PRELOAD",
};
typedef struct
{
char *name;
gsize index_in_preload_variables;
/* An index in pv_multiarch_details, or G_MAXSIZE if unspecified */
gsize abi_index;
} AdverbPreloadModule;
static void
adverb_preload_module_clear (gpointer p)
{
AdverbPreloadModule *self = p;
g_free (self->name);
}
static GArray *opt_preload_modules = NULL;
static gpointer
generic_strdup (gpointer p)
{
return g_strdup (p);
}
static void
ptr_array_add_unique (GPtrArray *arr,
const void *item,
GEqualFunc equal_func,
GBoxedCopyFunc copy_func)
{
if (!g_ptr_array_find_with_equal_func (arr, item, equal_func, NULL))
g_ptr_array_add (arr, copy_func ((gpointer) item));
}
static void
lib_temp_dirs_free (LibTempDirs *lib_temp_dirs)
{
gsize abi;
if (lib_temp_dirs->root_path != NULL)
_srt_rm_rf (lib_temp_dirs->root_path);
g_clear_pointer (&lib_temp_dirs->root_path, g_free);
g_clear_pointer (&lib_temp_dirs->platform_token_path, g_free);
g_clear_pointer (&lib_temp_dirs->abi_paths, g_hash_table_unref);
for (abi = 0; abi < G_N_ELEMENTS (lib_temp_dirs->abi_paths); abi++)
g_clear_pointer (&lib_temp_dirs->abi_paths[abi], g_free);
g_free (lib_temp_dirs);
}
......@@ -205,15 +251,80 @@ opt_fd_cb (const char *name,
return TRUE;
}
static gboolean
opt_ld_something (const char *option,
gsize index_in_preload_variables,
const char *value,
gpointer data,
GError **error)
{
AdverbPreloadModule module = { NULL, 0, G_MAXSIZE };
g_auto(GStrv) parts = NULL;
const char *architecture = NULL;
parts = g_strsplit (value, ":", 0);
if (parts[0] != NULL)
{
gsize i;
for (i = 1; parts[i] != NULL; i++)
{
if (g_str_has_prefix (parts[i], "abi="))
{
gsize abi;
architecture = parts[i] + strlen ("abi=");
for (abi = 0; abi < PV_N_SUPPORTED_ARCHITECTURES; abi++)
{
if (strcmp (architecture, pv_multiarch_details[abi].tuple) == 0)
{
module.abi_index = abi;
break;
}
}
if (module.abi_index == G_MAXSIZE)
{
g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE,
"Unsupported ABI %s",
architecture);
return FALSE;
}
}
else
{
g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE,
"Unexpected option in %s=\"%s\": %s",
option, value, parts[i]);
return FALSE;
}
}
value = parts[0];
}
if (opt_preload_modules == NULL)
{
opt_preload_modules = g_array_new (FALSE, FALSE, sizeof (AdverbPreloadModule));
g_array_set_clear_func (opt_preload_modules, adverb_preload_module_clear);
}
module.index_in_preload_variables = index_in_preload_variables;
module.name = g_strdup (value);
g_array_append_val (opt_preload_modules, module);
return TRUE;
}
static gboolean
opt_ld_audit_cb (const gchar *option_name,
const gchar *value,
gpointer data,
GError **error)
{
pv_append_preload_module (opt_preload_modules, G_N_ELEMENTS (opt_preload_modules),
"LD_AUDIT", value);
return TRUE;
return opt_ld_something (option_name, PRELOAD_VARIABLE_INDEX_LD_AUDIT,
value, data, error);
}
static gboolean
......@@ -222,9 +333,8 @@ opt_ld_preload_cb (const gchar *option_name,
gpointer data,
GError **error)
{
pv_append_preload_module (opt_preload_modules, G_N_ELEMENTS (opt_preload_modules),
"LD_PRELOAD", value);
return TRUE;
return opt_ld_something (option_name, PRELOAD_VARIABLE_INDEX_LD_PRELOAD,
value, data, error);
}
static gboolean
......@@ -462,7 +572,6 @@ generate_lib_temp_dirs (LibTempDirs *lib_temp_dirs,
gsize abi;
g_return_val_if_fail (lib_temp_dirs != NULL, FALSE);
g_return_val_if_fail (lib_temp_dirs->abi_paths == NULL, FALSE);
info = srt_system_info_new (NULL);
......@@ -474,27 +583,34 @@ generate_lib_temp_dirs (LibTempDirs *lib_temp_dirs,
lib_temp_dirs->platform_token_path = g_build_filename (lib_temp_dirs->root_path,
"${PLATFORM}", NULL);
lib_temp_dirs->abi_paths = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
for (abi = 0; abi < PV_N_SUPPORTED_ARCHITECTURES; abi++)
{
g_autofree gchar *libdl_platform = NULL;
g_autofree gchar *abi_path = NULL;
libdl_platform = srt_system_info_dup_libdl_platform (info,
pv_multiarch_details[abi].tuple,
error);
if (!libdl_platform)
return glnx_prefix_error (error,
"Unknown expansion of the dl string token $PLATFORM");
if (g_getenv ("PRESSURE_VESSEL_TEST_STANDARDIZE_PLATFORM") != NULL)
{
/* In unit tests it isn't straightforward to find the real
* ${PLATFORM}, so we use a predictable mock implementation:
* whichever platform happens to be listed first. */
libdl_platform = g_strdup (pv_multiarch_details[abi].platforms[0]);
}
else
{
libdl_platform = srt_system_info_dup_libdl_platform (info,
pv_multiarch_details[abi].tuple,
error);
if (!libdl_platform)
return glnx_prefix_error (error,
"Unknown expansion of the dl string token $PLATFORM");
}
abi_path = g_build_filename (lib_temp_dirs->root_path, libdl_platform, NULL);
if (g_mkdir (abi_path, 0700) != 0)
return glnx_throw_errno_prefix (error, "Unable to create \"%s\"", abi_path);
g_hash_table_insert (lib_temp_dirs->abi_paths,
g_strdup (pv_multiarch_details[abi].tuple),
g_steal_pointer (&abi_path));
lib_temp_dirs->abi_paths[abi] = g_steal_pointer (&abi_path);
}
return TRUE;
......@@ -932,57 +1048,46 @@ main (int argc,
g_clear_error (error);
}
for (i = 0; i < G_N_ELEMENTS (opt_preload_modules); i++)
if (opt_preload_modules != NULL)
{
const char *variable = opt_preload_modules[i].variable;
const GPtrArray *values = opt_preload_modules[i].values;
g_autofree gchar *platform_overlay_path = NULL;
g_autoptr(GPtrArray) search_path = NULL;
g_autoptr(GString) buffer = NULL;
gsize j;
if (values == NULL)
continue;
GPtrArray *preload_search_paths[G_N_ELEMENTS (preload_variables)] = { NULL };
search_path = g_ptr_array_new_with_free_func (g_free);
buffer = g_string_new ("");
if (g_strcmp0 (variable, "LD_PRELOAD") != 0 || !all_abi_paths_created)
/* Iterate through all modules, populating preload_search_paths */
for (i = 0; i < opt_preload_modules->len; i++)
{
/* Currently we perform adjustments only for LD_PRELOAD.
* Also if we were not able to create the temporary libraries
* directories, we simply avoid any adjustment and try to continue */
for (j = 0; j < values->len; j++)
{
const char *preload = g_ptr_array_index (values, j);
g_assert (preload != NULL);
if (*preload == '\0')
continue;
const AdverbPreloadModule *module = &g_array_index (opt_preload_modules,
AdverbPreloadModule, i);
GPtrArray *search_path = preload_search_paths[module->index_in_preload_variables];
const char *preload = module->name;
const char *base;
gsize abi_index = module->abi_index;
pv_search_path_append (buffer, preload);
}
if (buffer->len != 0)
flatpak_bwrap_set_env (wrapped_command, variable, buffer->str, TRUE);
/* No adjustment needed, continue to the next preload module */
continue;
}
g_debug ("Adjusting %s...", variable);
platform_overlay_path = g_build_filename (lib_temp_dirs->platform_token_path,
"gameoverlayrenderer.so", NULL);
for (j = 0; j < values->len; j++)
{
const char *preload = g_ptr_array_index (values, j);
g_assert (preload != NULL);
if (*preload == '\0')
continue;
if (g_str_has_suffix (preload, "/gameoverlayrenderer.so"))
base = glnx_basename (preload);
if (search_path == NULL)
{
g_autofree gchar *link = NULL;
preload_search_paths[module->index_in_preload_variables]
= search_path
= g_ptr_array_new_full (opt_preload_modules->len, g_free);
}
/* If we were not able to create the temporary library
* directories, we simply avoid any adjustment and try to continue */
if (!all_abi_paths_created)
{
g_ptr_array_add (search_path, g_strdup (preload));
continue;
}
if (abi_index == G_MAXSIZE
&& module->index_in_preload_variables == PRELOAD_VARIABLE_INDEX_LD_PRELOAD
&& strcmp (base, "gameoverlayrenderer.so") == 0)
{
for (gsize abi = 0; abi < PV_N_SUPPORTED_ARCHITECTURES; abi++)
{
g_autofree gchar *expected_suffix = g_strdup_printf ("/%s/gameoverlayrenderer.so",
......@@ -990,20 +1095,30 @@ main (int argc,
if (g_str_has_suffix (preload, expected_suffix))
{
link = g_build_filename (g_hash_table_lookup (lib_temp_dirs->abi_paths,
pv_multiarch_details[abi].tuple),
"gameoverlayrenderer.so", NULL);
abi_index = abi;
break;
}
}
if (link == NULL)
if (abi_index == G_MAXSIZE)
{
g_ptr_array_add (search_path, g_strdup (preload));
g_debug ("Preloading gameoverlayrenderer.so from an unexpected path \"%s\", "
"just leave it as is without adjusting", preload);
continue;
g_debug ("Preloading %s from an unexpected path \"%s\", "
"just leave it as is without adjusting",
base, preload);
}
}
if (abi_index != G_MAXSIZE)
{
g_autofree gchar *link = NULL;
g_autofree gchar *platform_path = NULL;
g_debug ("Module %s is for %s",
preload, pv_multiarch_details[abi_index].tuple);
platform_path = g_build_filename (lib_temp_dirs->platform_token_path,
base, NULL);
link = g_build_filename (lib_temp_dirs->abi_paths[abi_index],
base, NULL);
if (symlink (preload, link) != 0)
{
......@@ -1015,22 +1130,32 @@ main (int argc,
link, preload);
goto out;
}
g_debug ("created symlink %s -> %s", link, preload);
if (!g_ptr_array_find_with_equal_func (search_path, platform_overlay_path,
g_str_equal, NULL))
g_ptr_array_add (search_path, g_strdup (platform_overlay_path));
g_debug ("created symlink %s -> %s", link, preload);
ptr_array_add_unique (search_path, platform_path,
g_str_equal, generic_strdup);
}
else
{
g_debug ("Module %s is for all architectures", preload);
g_ptr_array_add (search_path, g_strdup (preload));
}
}
g_ptr_array_foreach (search_path, (GFunc) append_to_search_path, buffer);
/* Serialize search_paths[PRELOAD_VARIABLE_INDEX_LD_AUDIT] into
* LD_AUDIT, etc. */
for (i = 0; i < G_N_ELEMENTS (preload_variables); i++)
{
GPtrArray *search_path = preload_search_paths[i];
g_autoptr(GString) buffer = g_string_new ("");
const char *variable = preload_variables[i];
if (search_path != NULL)
g_ptr_array_foreach (search_path, (GFunc) append_to_search_path, buffer);
if (buffer->len != 0)
flatpak_bwrap_set_env (wrapped_command, variable, buffer->str, TRUE);
if (buffer->len != 0)
flatpak_bwrap_set_env (wrapped_command, variable, buffer->str, TRUE);
}
}
if (opt_generate_locales)
......@@ -1190,7 +1315,7 @@ out:
if (locales_temp_dir != NULL)
_srt_rm_rf (locales_temp_dir);
pv_preload_modules_free (opt_preload_modules, G_N_ELEMENTS (opt_preload_modules));
g_clear_pointer (&opt_preload_modules, g_array_unref);
if (local_error != NULL)
pv_log_failure ("%s", local_error->message);
......
......@@ -5,6 +5,7 @@
import logging
import os
import re
import signal
import subprocess
import sys
......@@ -18,6 +19,7 @@ except ImportError:
from testutils import (
BaseTest,
run_subprocess,
test_main,
)
......@@ -25,21 +27,156 @@ from testutils import (
logger = logging.getLogger('test-adverb')
EX_USAGE = 64
class TestAdverb(BaseTest):
def run_subprocess(
self,
args, # type: typing.Union[typing.List[str], str]
check=False,
input=None, # type: typing.Optional[bytes]
timeout=None, # type: typing.Optional[int]
**kwargs # type: typing.Any
):
logger.info('Running: %r', args)
return run_subprocess(
args, check=check, input=input, timeout=timeout, **kwargs
)
def setUp(self) -> None:
super().setUp()
if 'PRESSURE_VESSEL_UNINSTALLED' in os.environ:
self.adverb = self.command_prefix + [
'env',
'-u', 'LD_AUDIT',
'-u', 'LD_PRELOAD',
# In unit tests it isn't straightforward to find the real
# ${PLATFORM}, so we use a predictable mock implementation
# that always uses PvMultiarchDetails.platforms[0].
'PRESSURE_VESSEL_TEST_STANDARDIZE_PLATFORM=1',
os.path.join(
self.top_builddir,
'pressure-vessel',
'pressure-vessel-adverb'
),
]
self.helper = self.command_prefix + [
os.path.join(
self.top_builddir,
'tests',
'pressure-vessel',
'test-helper'
),
]
else:
self.skipTest('Not available as an installed-test')
def test_ld_preload(self) -> None:
completed = run_subprocess(
self.adverb + [
'--ld-audit=/nonexistent/libaudit.so',
'--ld-preload=/nonexistent/libpreload.so',
'--ld-preload=/nonexistent/ubuntu12_32/gameoverlayrenderer.so',
'--ld-preload=/nonexistent/ubuntu12_64/gameoverlayrenderer.so',
('--ld-preload'
'=/nonexistent/lib32/libMangoHud.so'
':abi=i386-linux-gnu'),
('--ld-preload'
'=/nonexistent/lib64/libMangoHud.so'
':abi=x86_64-linux-gnu'),
('--ld-preload'
'=/nonexistent/lib64/64-bit-only.so'
':abi=x86_64-linux-gnu'),
'--',
'sh', '-euc',
# The hard-coded i686 and xeon_phi here must match up with
# pv_multiarch_details[i].platforms[0], which is what is used
# as a mock implementation under
# PRESSURE_VESSEL_TEST_STANDARDIZE_PLATFORM=1.
r'''
ld_audit="$LD_AUDIT"
ld_preload="$LD_PRELOAD"
unset LD_AUDIT
unset LD_PRELOAD
echo "LD_AUDIT=$ld_audit"
echo "LD_PRELOAD=$ld_preload"
IFS=:
for item in $ld_preload; do
case "$item" in
(*\$\{PLATFORM\}*)
i686="$(echo "$item" |
sed -e 's/[$]{PLATFORM}/i686/g')"
xeon_phi="$(echo "$item" |
sed -e 's/[$]{PLATFORM}/xeon_phi/g')"
printf "i686: symlink to "
readlink "$i686" || echo "(nothing)"
printf "xeon_phi: symlink to "
readlink "$xeon_phi" || echo "(nothing)"
;;
(*)
echo "literal $item"
;;
esac
done
''',
],
check=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=2,
universal_newlines=True,
)
stdout = completed.stdout
assert stdout is not None
lines = stdout.splitlines()
self.assertEqual(
lines[0],
'LD_AUDIT=/nonexistent/libaudit.so',
)
self.assertEqual(
re.sub(r':/[^:]*?/pressure-vessel-libs-....../',
r':/tmp/pressure-vessel-libs-XXXXXX/',
lines[1]),
('LD_PRELOAD=/nonexistent/libpreload.so'
':/tmp/pressure-vessel-libs-XXXXXX/'
'${PLATFORM}/gameoverlayrenderer.so'
':/tmp/pressure-vessel-libs-XXXXXX/'
'${PLATFORM}/libMangoHud.so'
':/tmp/pressure-vessel-libs-XXXXXX/'
'${PLATFORM}/64-bit-only.so')
)
self.assertEqual(lines[2], 'literal /nonexistent/libpreload.so')
self.assertEqual(
lines[3],
'i686: symlink to /nonexistent/ubuntu12_32/gameoverlayrenderer.so',
)
self.assertEqual(
lines[4],
('xeon_phi: symlink to '
'/nonexistent/ubuntu12_64/gameoverlayrenderer.so'),
)
self.assertEqual(
lines[5],
'i686: symlink to /nonexistent/lib32/libMangoHud.so',
)
self.assertEqual(
lines[6],
'xeon_phi: symlink to /nonexistent/lib64/libMangoHud.so',
)
self.assertEqual(
lines[7],
'i686: symlink to (nothing)',
)
self.assertEqual(
lines[8],
'xeon_phi: symlink to /nonexistent/lib64/64-bit-only.so',
)
def test_stdio_passthrough(self) -> None:
proc = subprocess.Popen(
self.adverb + [
......@@ -106,6 +243,29 @@ class TestAdverb(BaseTest):
proc.wait()
self.assertEqual(proc.returncode, 0)
def test_wrong_options(self) -> None:
for option in (
'--an-unknown-option',
'--ld-preload=/nonexistent/libfoo.so:abi=hal9000-movieos',
'--ld-preload=/nonexistent/libfoo.so:abi=i386-linux-gnu:foo',
'--ld-preload=/nonexistent/libfoo.so:foo=bar',
'--pass-fd=-1',
'--shell=wrong',
'--terminal=wrong',
):
proc = subprocess.Popen(
self.adverb + [
option,
'--',
'sh', '-euc', 'exit 42',
],
stdout=2,
stderr=2,
universal_newlines=True,
)
proc.wait()
self.assertEqual(proc.returncode, EX_USAGE)
def tearDown(self) -> None:
super().tearDown()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment