diff --git a/src/basic/build-path.c b/src/basic/build-path.c new file mode 100644 index 00000000000..d81ed561dc9 --- /dev/null +++ b/src/basic/build-path.c @@ -0,0 +1,207 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include + +#include "build-path.h" +#include "errno-list.h" +#include "macro.h" +#include "path-util.h" +#include "process-util.h" +#include "unistd.h" + +static int get_runpath_from_dynamic(const ElfW(Dyn) *d, const char **ret) { + size_t runpath_index = SIZE_MAX, rpath_index = SIZE_MAX; + const char *strtab = NULL; + + assert(d); + + /* Iterates through the PT_DYNAMIC section to find the DT_RUNPATH/DT_RPATH entries */ + + for (; d->d_tag != DT_NULL; d++) { + + switch (d->d_tag) { + + case DT_RUNPATH: + runpath_index = (size_t) d->d_un.d_val; + break; + + case DT_RPATH: + rpath_index = (size_t) d->d_un.d_val; + break; + + case DT_STRTAB: + strtab = (const char *) d->d_un.d_val; + break; + } + + /* runpath wins, hence if we have the table and runpath we can exit the loop early */ + if (strtab && runpath_index != SIZE_MAX) + break; + } + + if (!strtab) + return -ENOTRECOVERABLE; + + /* According to dl.so runpath wins of both runpath and rpath are defined. */ + if (runpath_index != SIZE_MAX) { + if (ret) + *ret = strtab + runpath_index; + return 1; + } + + if (rpath_index != SIZE_MAX) { + if (ret) + *ret = strtab + rpath_index; + return 1; + } + + if (ret) + *ret = NULL; + + return 0; +} + +static int get_runpath(const char **ret) { + unsigned long phdr, phent, phnum; + + /* Finds the rpath/runpath in the program headers of the main executable we are running in */ + + phdr = getauxval(AT_PHDR); /* Start offset of phdr */ + if (phdr == 0) + return -ENOTRECOVERABLE; + + phnum = getauxval(AT_PHNUM); /* Number of entries in phdr */ + if (phnum == 0) + return -ENOTRECOVERABLE; + + phent = getauxval(AT_PHENT); /* Size of entries in phdr */ + if (phent < sizeof(ElfW(Phdr))) /* Safety check, that our idea of the structure matches the file */ + return -ENOTRECOVERABLE; + + ElfW(Addr) bias = 0, dyn = 0; + bool found_bias = false, found_dyn = false; + + /* Iterate through the Phdr structures to find the PT_PHDR and PT_DYNAMIC sections */ + for (unsigned long i = 0; i < phnum; i++) { + const ElfW(Phdr) *p = (const ElfW(Phdr)*) (phdr + (i * phent)); + + switch (p->p_type) { + + case PT_PHDR: + if (p->p_vaddr > phdr) /* safety overflow check */ + return -ENOTRECOVERABLE; + + bias = (ElfW(Addr)) phdr - p->p_vaddr; + found_bias = true; + break; + + case PT_DYNAMIC: + dyn = p->p_vaddr; + found_dyn = true; + break; + } + + if (found_bias && found_dyn) + break; + } + + if (!found_dyn) + return -ENOTRECOVERABLE; + + return get_runpath_from_dynamic((const ElfW(Dyn)*) (bias + dyn), ret); +} + +int get_build_exec_dir(char **ret) { + int r; + + /* Returns the build execution directory if we are invoked in a build environment. Specifically, this + * checks if the main program binary has an rpath/runpath set (i.e. an explicit directory where to + * look for shared libraries) to $ORIGIN. If so we know that this is not a regular installed binary, + * but one which shall acquire its libraries from below a directory it is located in, i.e. a build + * directory or similar. In that case it typically makes sense to also search for our auxiliary + * executables we fork() off in a directory close to our main program binary, rather than in the + * system. + * + * This function is supposed to be used when looking for "callout" binaries that are closely related + * to the main program (i.e. speak a specific protocol between each other). And where it's generally + * a good idea to use the binary from the build tree (if there is one) instead of the system. + * + * Note that this does *not* actually return the rpath/runpath but the instead the directory the main + * executable was found in. This follows the logic that the result is supposed to be used for + * executable binaries (i.e. stuff in bindir), not for shared libraries (i.e. stuff in libdir), and + * hence the literal shared library path would just be wrong. + * + * TLDR: if we look for callouts in this dir first, running binaries from the meson build tree + * automatically uses the right callout. + * + * Returns: + * -ENOEXEC → We are not running in an rpath/runpath $ORIGIN environment + * -ENOENT → We don't know our own binary path + * -NOTRECOVERABLE → Dynamic binary information missing? + */ + + static int runpath_cached = -ERRNO_MAX-1; + if (runpath_cached == -ERRNO_MAX-1) { + const char *runpath = NULL; + + runpath_cached = get_runpath(&runpath); + + /* We only care if the runpath starts with $ORIGIN/ */ + if (runpath_cached > 0 && !startswith(runpath, "$ORIGIN/")) + runpath_cached = 0; + } + if (runpath_cached < 0) + return runpath_cached; + if (runpath_cached == 0) + return -ENOEXEC; + + _cleanup_free_ char *exe = NULL; + r = get_process_exe(0, &exe); + if (r < 0) + return runpath_cached = r; + + return path_extract_directory(exe, ret); +} + +static int find_build_dir_binary(const char *fn, char **ret) { + int r; + + assert(fn); + assert(ret); + + _cleanup_free_ char *build_dir = NULL; + r = get_build_exec_dir(&build_dir); + if (r < 0) + return r; + + _cleanup_free_ char *np = path_join(build_dir, fn); + if (!np) + return -ENOMEM; + + *ret = TAKE_PTR(np); + return 0; +} + +int invoke_callout_binary(const char *path, char *const argv[]) { + int r; + + assert(path); + + /* Just like execv(), but tries to execute the specified binary in the build dir instead, if known */ + + _cleanup_free_ char *fn = NULL; + r = path_extract_filename(path, &fn); + if (r < 0) + return r; + if (r == O_DIRECTORY) /* Uh? */ + return -EISDIR; + + _cleanup_free_ char *np = NULL; + if (find_build_dir_binary(fn, &np) >= 0) + execv(np, argv); + + execv(path, argv); + return -errno; +} diff --git a/src/basic/build-path.h b/src/basic/build-path.h new file mode 100644 index 00000000000..bf42783f1b0 --- /dev/null +++ b/src/basic/build-path.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int get_build_exec_dir(char **ret); + +int invoke_callout_binary(const char *path, char *const argv[]); diff --git a/src/basic/meson.build b/src/basic/meson.build index 6b30908ce18..a9722d2121c 100644 --- a/src/basic/meson.build +++ b/src/basic/meson.build @@ -10,6 +10,7 @@ basic_sources = files( 'audit-util.c', 'btrfs.c', 'build.c', + 'build-path.c', 'bus-label.c', 'cap-list.c', 'capability-util.c', diff --git a/src/test/meson.build b/src/test/meson.build index ef741388d55..88e46cc03ed 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -50,6 +50,7 @@ simple_tests += files( 'test-bitmap.c', 'test-blockdev-util.c', 'test-bootspec.c', + 'test-build-path.c', 'test-bus-util.c', 'test-calendarspec.c', 'test-cgroup-setup.c', diff --git a/src/test/test-build-path.c b/src/test/test-build-path.c new file mode 100644 index 00000000000..661b5fc9e6a --- /dev/null +++ b/src/test/test-build-path.c @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "build-path.h" +#include "log.h" +#include "string-util.h" + +int main(int argc, char* argv[]) { + _cleanup_free_ char *p = NULL; + int r; + + r = get_build_exec_dir(&p); + if (r == -ENOEXEC) + log_info("Not run from build dir."); + else if (r < 0) + log_error_errno(r, "Failed to find build dir: %m"); + else + log_info("%s", strna(p)); + + return 0; +}