From fe72fe36f3bdd59505cbe661e4a9b1ffe8114871 Mon Sep 17 00:00:00 2001 From: Andria Brown Date: Sun, 21 Jul 2024 03:54:31 -0600 Subject: [PATCH 1/4] New test for --bundle-dir and --prog-name. --- staticx/__main__.py | 4 ++++ test/bundle-dir-prog-name.sh | 34 ++++++++++++++++++++++++++++++++++ test/run_all.sh | 3 +++ 3 files changed, 41 insertions(+) create mode 100755 test/bundle-dir-prog-name.sh diff --git a/staticx/__main__.py b/staticx/__main__.py index 3b94901..23da659 100644 --- a/staticx/__main__.py +++ b/staticx/__main__.py @@ -24,6 +24,10 @@ def parse_args(): help = 'Strip binaries before adding to archive (reduces size)') ap.add_argument('--no-compress', action='store_true', help = "Don't compress the archive (increases size)") + ap.add_argument('--bundle-dir', + help = 'The directory that the program extracts to at runtime. When not specified, staticx extracts to /tmp/staticx-XXXXXX.') + ap.add_argument('--prog-name', + help = 'The name that the program extracts to at runtime. When not specified, staticx uses the name of the input program.') # Special / output-related options ap.add_argument('-V', '--version', action='version', diff --git a/test/bundle-dir-prog-name.sh b/test/bundle-dir-prog-name.sh new file mode 100755 index 0000000..6a390e3 --- /dev/null +++ b/test/bundle-dir-prog-name.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +echo -e "\n\n--------------------------------------------------------------------------------" +echo -e "Test StaticX --bundle-dir and --prog-name options." + +cd "$(dirname "${BASH_SOURCE[0]}")" + +app="$(which sh)" +outfile="./sh.staticx" + +# The values we're going to be testing for. +bundle_dir=$(mktemp --dry-run --tmpdir -d systemd-private-60ce0209a865480b817f3623184c43e2-dmesg.service-XXXXXX) +prog_name="tmp" + +# Make a staticx executable from bash so we can check the STATICX_ env vars to know the bundle directory and program path. +echo -e "\nMaking staticx executable (\$STATICX_FLAGS=$STATICX_FLAGS):" +staticx $STATICX_FLAGS --bundle-dir $bundle_dir --prog-name $prog_name $app $outfile + +# Verify STATICX_BUNDLE_DIR matches the `--bundle-dir` option. +test_bundle_dir=$($outfile -c 'echo $STATICX_BUNDLE_DIR') +echo "STATICX_BUNDLE_DIR: $test_bundle_dir" +if [[ "$test_bundle_dir" != "$bundle_dir" ]]; then + echo "STATICX_BUNDLE_DIR does not match --bundle-dir: \"$test_bundle_dir\" != \"$bundle_dir\"" + exit 1 +fi + +# Verify that the program is renamed to `--prog-name` when specified. +test_prog_name=$($outfile -c 'echo $0') +echo "The program name is: $test_prog_name" +if [[ "$test_prog_name" != "$prog_name" ]]; then + echo "The program name does not match --prog-name: \"$test_prog_name\" != \"$prog_name\"" + exit 1 +fi diff --git a/test/run_all.sh b/test/run_all.sh index 9a52328..631026b 100755 --- a/test/run_all.sh +++ b/test/run_all.sh @@ -9,6 +9,9 @@ cd "$(dirname "${BASH_SOURCE[0]}")" # Test environment variables ./staticx-env-vars.sh +# Test --bundle-dir and --prog-name options +./bundle-dir-prog-name.sh + # Run test an executable linked against musl-libc musl/run_test.sh From 6f4159db861b4335cb630a28254288bee5983192 Mon Sep 17 00:00:00 2001 From: Andria Brown Date: Sun, 21 Jul 2024 11:47:44 -0600 Subject: [PATCH 2/4] Documented --bundle-dir and --prog-name (touched up env vars to avoid confusion) --- docs/usage.rst | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 52b9381..a3701fb 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -3,11 +3,12 @@ Usage Synopsis -------- -.. code-block:: +.. code-block:: shell staticx [-h] - [-l LIB] [--strip] [--no-compress] [-V] - [--loglevel LEVEL] + [-l LIB] [--strip] [--no-compress] + [--bundle-dir BUNDLE_DIR] [--prog-name PROG_NAME] + [-V] [--loglevel LEVEL] PROG OUTPUT Positional Arguments: @@ -19,12 +20,17 @@ Positional Arguments: Options: -h, --help Show help message and exit + -l LIB Add additional library (absolute path) This option can be given multiple times. --strip Strip binaries before adding to archive (reduces size) --no-compress Don't compress the archive (increases size) + --bundle-dir BUNDLE_DIR + The directory that the program extracts to at runtime. When not specified, staticx extracts to ``/tmp/staticx-XXXXXX``. + --prog-name PROG_NAME + The name that the program extracts to at runtime. When not specified, staticx uses the name of the input program. --loglevel LEVEL Set the logging level (default: WARNING) Options: DEBUG,INFO,WARNING,ERROR,CRITICAL @@ -70,6 +76,5 @@ Run-time Information -------------------- StaticX sets the following environment variables for the wrapped user program: -- ``STATICX_BUNDLE_DIR``: The absolute path of the "bundle" directory, the - temporary dir where the archive has been extracted. -- ``STATICX_PROG_PATH``: The absolute path of the program being executed. +- ``STATICX_BUNDLE_DIR``: The absolute path of the "bundle" directory, a temporary directory, where the archive has been extracted. +- ``STATICX_PROG_PATH``: The absolute path of the program being executed (not the extracted program in the bundle directory). From 4ec618dbc43791612322c575ac558687c597963e Mon Sep 17 00:00:00 2001 From: Andria Brown Date: Sun, 21 Jul 2024 13:57:53 -0600 Subject: [PATCH 3/4] Using the --prog-name option when adding the program to the archive; fixed prog name test --- .vscode/settings.json | 3 +++ staticx/__main__.py | 5 +++-- staticx/api.py | 13 ++++++++----- test/bundle-dir-prog-name.sh | 2 +- 4 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9792498 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": false +} \ No newline at end of file diff --git a/staticx/__main__.py b/staticx/__main__.py index 23da659..ad99e3c 100644 --- a/staticx/__main__.py +++ b/staticx/__main__.py @@ -54,8 +54,9 @@ def main(): libs = args.libs, strip = args.strip, compress = not args.no_compress, - debug = args.debug, - ) + bundle_dir = args.bundle_dir, + prog_name = args.prog_name, + debug = args.debug) except Error as e: if args.debug: raise diff --git a/staticx/api.py b/staticx/api.py index 7a435fe..1a5ebc7 100755 --- a/staticx/api.py +++ b/staticx/api.py @@ -23,7 +23,7 @@ class StaticxGenerator: """StaticxGenerator is responsible for producing a staticx-ified executable. """ - def __init__(self, prog, strip=False, compress=True, debug=False, cleanup=True): + def __init__(self, prog, strip=False, compress=True, bundle_dir=None, prog_name=None, debug=False, cleanup=True): """ Parameters: prog: Dynamic executable to staticx @@ -32,6 +32,8 @@ def __init__(self, prog, strip=False, compress=True, debug=False, cleanup=True): self.orig_prog = prog self.strip = strip self.compress = compress + self.bundle_dir = bundle_dir + self.prog_name = prog_name self.debug = debug self.cleanup = cleanup @@ -133,7 +135,7 @@ def generate(self, output): with self.sxar as ar: run_hooks(self) - ar.add_program(self.tmpprog, basename(self.orig_prog)) + ar.add_program(self.tmpprog, self.prog_name or basename(self.orig_prog)) ar.add_interp_symlink(orig_interp) # Add all of the libraries @@ -294,7 +296,7 @@ def _fixup_prog(self): force_rpath=True, no_default_lib=True) -def generate(prog, output, libs=None, strip=False, compress=True, debug=False): +def generate(prog, output, libs=None, strip=False, compress=True, bundle_dir=None, prog_name=None, debug=False): """Main API: Generate a staticx executable Parameters: @@ -319,8 +321,9 @@ def generate(prog, output, libs=None, strip=False, compress=True, debug=False): prog=prog, strip=strip, compress=compress, - debug=debug, - ) + bundle_dir=bundle_dir, + prog_name=prog_name, + debug=debug) with gen: for lib in (libs or []): gen.add_library(lib) diff --git a/test/bundle-dir-prog-name.sh b/test/bundle-dir-prog-name.sh index 6a390e3..565e4e0 100755 --- a/test/bundle-dir-prog-name.sh +++ b/test/bundle-dir-prog-name.sh @@ -26,7 +26,7 @@ if [[ "$test_bundle_dir" != "$bundle_dir" ]]; then fi # Verify that the program is renamed to `--prog-name` when specified. -test_prog_name=$($outfile -c 'echo $0') +test_prog_name=$($outfile -c 'basename $0') echo "The program name is: $test_prog_name" if [[ "$test_prog_name" != "$prog_name" ]]; then echo "The program name does not match --prog-name: \"$test_prog_name\" != \"$prog_name\"" From edf7d472396cfe2fa2770087482cc5a460616cce Mon Sep 17 00:00:00 2001 From: Andria Brown Date: Thu, 25 Jul 2024 03:54:58 -0600 Subject: [PATCH 4/4] implemented --bundle-dir for bootloader; documentation --- bootloader/common.h | 1 + bootloader/main.c | 74 +++++++++++++++++++++++++++++++++----------- staticx/api.py | 25 ++++++++++----- staticx/archive.py | 4 +++ staticx/constants.py | 7 +++-- 5 files changed, 82 insertions(+), 29 deletions(-) diff --git a/bootloader/common.h b/bootloader/common.h index 2875b51..60bab56 100644 --- a/bootloader/common.h +++ b/bootloader/common.h @@ -5,6 +5,7 @@ #define ARCHIVE_SECTION ".staticx.archive" #define INTERP_FILENAME ".staticx.interp" #define PROG_FILENAME ".staticx.prog" +#define BUNDLE_DIR_FILENAME ".staticx.bundle-dir" static inline void * ptr_add(void *p, size_t off) diff --git a/bootloader/main.c b/bootloader/main.c index 10d5fc9..7c68b4b 100644 --- a/bootloader/main.c +++ b/bootloader/main.c @@ -11,6 +11,7 @@ #include #include #include +#include #include "xz.h" #include "error.h" #include "mmap.h" @@ -31,7 +32,7 @@ /* The "bundle" directory, where the archive is extracted */ -static const char *m_bundle_dir; +static char *m_bundle_dir; static char * path_join(const char *p1, const char *p2) @@ -382,30 +383,58 @@ cleanup_bundle_dir(void) fprintf(stderr, "staticx: Failed to cleanup %s: %m\n", m_bundle_dir); } - free((void*)m_bundle_dir); + free(m_bundle_dir); m_bundle_dir = NULL; } /** - * Returns the path to the actual user program to execute. - * We resolve this manually rather than executing the symlink - * to ensure the program sees the original argv[0]. See #134. - * - * Note however that we do not currently pass-through argv[0]. - * See #133. + * Returns the path stored in the specified symlink file. + * + * We resolve this manually rather than executing the symlink for the program to ensure that the program sees the original argv[0] (see #134). Note however that we do not currently pass-through argv[0] (see #133). */ -static char * -get_real_prog_path(void) +static char *get_symlink(char *file_path) { - // PROG_FILENAME is a symlink to the user's program - char *linkpath = path_join(m_bundle_dir, PROG_FILENAME); + char *absolute_path = path_join(m_bundle_dir, file_path); - char *result = realpath(linkpath, NULL); - if (!result) - error(2, errno, "Failed to get realpath for %s", linkpath); + char *linked_path = malloc(MAXPATHLEN + 1); + ssize_t path_length = readlink(absolute_path, linked_path, MAXPATHLEN); - free(linkpath); - return result; + if (path_length == -1) + error(2, errno, "Failed to read the symlink for %s", absolute_path); + if (path_length > MAXPATHLEN) + error(2, errno, "Length of symlink value (%zu) for %s is too long", path_length, absolute_path); + + free(absolute_path); + linked_path[path_length] = '\0'; + return linked_path; +} + +/** + * Moves and renames the bundle from `m_bundle_dir` to the `destination` and updates `m_bundle_dir` (do not free `destination`). Fails when a file or folder already exists at the `destination`. + */ +inline static void move_bundle(char *destination) +{ + char *origin = m_bundle_dir; + if (access(destination, F_OK) == 0) + error(2, 0, "Failed to move bundle from %s to %s because the destination already exists", origin, destination); + + int result = rename(origin, destination); + if (result == -1) + error(2, errno, "Failed to move the bundle from %s to %s", origin, destination); + + free(origin); + m_bundle_dir = destination; +} + +/** + * Checks whether or not a file exists within the bundle. Does not follow symlinks. + */ +static bool file_exists(char *file_path) +{ + char *absolute_path = path_join(m_bundle_dir, file_path); + bool exists = faccessat(0, absolute_path, F_OK, AT_SYMLINK_NOFOLLOW) == 0; + free(absolute_path); + return exists; } static void identify(void) @@ -432,8 +461,17 @@ main(int argc, char **argv) /* Extract the archive embedded in this program */ extract_archive(m_bundle_dir); + /* Check for custom bundle directory and move the bundle */ + if (file_exists(BUNDLE_DIR_FILENAME)) { + move_bundle(get_symlink(BUNDLE_DIR_FILENAME)); + debug_printf("Moved the bundle dir to: %s\n", m_bundle_dir); + } + /* Get path to user application inside temp dir */ - char *prog_path = get_real_prog_path(); + char *prog_name = get_symlink(PROG_FILENAME); + char *prog_path = path_join(m_bundle_dir, prog_name); + debug_printf("Program located at: %s\n", prog_path); + free(prog_name); /* Patch the user application ELF to run in the temp dir */ patch_app(prog_path); diff --git a/staticx/api.py b/staticx/api.py index 1a5ebc7..2d1c52a 100755 --- a/staticx/api.py +++ b/staticx/api.py @@ -25,9 +25,13 @@ class StaticxGenerator: def __init__(self, prog, strip=False, compress=True, bundle_dir=None, prog_name=None, debug=False, cleanup=True): """ - Parameters: - prog: Dynamic executable to staticx - debug: Run in debug mode (use debug bootloader) + Parameters: + prog: Dynamic executable to staticx + strip: Strip binaries to reduce size + compress: Whether or not to compress the archive + bundle_dir: The directory that the program extracts to at runtime + prog_name: The name that the program extracts to at runtime + debug: Run in debug mode (use debug bootloader) """ self.orig_prog = prog self.strip = strip @@ -137,6 +141,8 @@ def generate(self, output): ar.add_program(self.tmpprog, self.prog_name or basename(self.orig_prog)) ar.add_interp_symlink(orig_interp) + if self.bundle_dir is not None: + ar.add_bundle_dir_symlink(self.bundle_dir) # Add all of the libraries for libpath in get_shobj_deps(self.orig_prog): @@ -300,11 +306,14 @@ def generate(prog, output, libs=None, strip=False, compress=True, bundle_dir=Non """Main API: Generate a staticx executable Parameters: - prog: Dynamic executable to staticx - output: Path to result - libs: Extra libraries to include - strip: Strip binaries to reduce size - debug: Run in debug mode (use debug bootloader) + prog: Dynamic executable to staticx + output: Path to result + libs: Extra libraries to include + strip: Strip binaries to reduce size + compress: Whether or not to compress the archive + bundle_dir: The directory that the program extracts to at runtime + prog_name: The name that the program extracts to at runtime + debug: Run in debug mode (use debug bootloader) """ logging.info(f"Running StaticX version {__version__}") diff --git a/staticx/archive.py b/staticx/archive.py index e06ece4..2fdcc6e 100644 --- a/staticx/archive.py +++ b/staticx/archive.py @@ -129,3 +129,7 @@ def add_file(self, path, arcname=None): def add_interp_symlink(self, interp): """Add symlink for ld.so interpreter""" self.add_symlink(INTERP_FILENAME, basename(interp)) + + def add_bundle_dir_symlink(self, bundle_dir): + """Adds a symlink that holds the path that the bundle directory will be moved to.""" + self.add_symlink(BUNDLE_DIR_FILENAME, bundle_dir) diff --git a/staticx/constants.py b/staticx/constants.py index 0cda9d0..565e6c6 100644 --- a/staticx/constants.py +++ b/staticx/constants.py @@ -1,6 +1,7 @@ -ARCHIVE_SECTION = ".staticx.archive" -INTERP_FILENAME = ".staticx.interp" -PROG_FILENAME = ".staticx.prog" +ARCHIVE_SECTION = ".staticx.archive" +INTERP_FILENAME = ".staticx.interp" +PROG_FILENAME = ".staticx.prog" +BUNDLE_DIR_FILENAME = ".staticx.bundle-dir" MAX_INTERP_LEN = 256 MAX_RPATH_LEN = 256