diff --git a/pressure-vessel/adverb.1.md b/pressure-vessel/adverb.1.md index 85fe81fb806b471217c349dcff3aae509106d8f3..8ebdbbdb9833d60b739a5d0bf259d0559bfe0b72 100644 --- a/pressure-vessel/adverb.1.md +++ b/pressure-vessel/adverb.1.md @@ -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 diff --git a/pressure-vessel/adverb.c b/pressure-vessel/adverb.c index b5ef2b9a099924a157542b51b60b7709ecf7f604..12056a0a669c86e26a467c39d965bd0582da4782 100644 --- a/pressure-vessel/adverb.c +++ b/pressure-vessel/adverb.c @@ -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); diff --git a/tests/pressure-vessel/adverb.py b/tests/pressure-vessel/adverb.py index 9088e43e7d7b9b76e7cb2ba3eb9ab94810e7ed44..0c176d88d880ee69028037d96794853323ab3792 100755 --- a/tests/pressure-vessel/adverb.py +++ b/tests/pressure-vessel/adverb.py @@ -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()