mirror of
https://github.com/systemd/systemd.git
synced 2025-01-05 13:18:06 +03:00
basic: add "build path" logic
We have a number of components these days that are split into multiple binaries, i.e. a primary one, and a worker callout usually. These binaries are closely related, they typically speak a protocol that is internal, and not safe to mix and match. Examples for this: - homed and its worker binary homework - userdbd and its worker binary userwork - import and the various pull/import/export handlers - sysupdate the same - the service manager and the executor binary Running any of these daemons directly from the meson build tree is messy, since the implementations will typically invoke the installed callout binaries, not the ones from the build tree. This is very annoying, and not obvious at first. Now, we could always invoke relevant binaries from $(dirname /proc/self/exe) first, before using the OS installed ones. But that's typically not what is desired, because this means in the installed case (i.e. the usual one) we'll look for these callout binaries at a place they typically will not be found (because these callouts generally are located in libexecdir, not bindir when installed). Hence, let's try to do things a bit smarter, and follow what build systems such as meson have already been doing to make sure dynamic library discovery works correctly when binaries are run from a build directory: let's start looking at rpath/runpath in the main binary that is executed: if there's an rpath/runpath set, then we'll look for the callout binaries next to the main binary, otherwise we won't. This should generally be the right thing to do as meson strips the rpath during installation, and thus we'll look for the callouts in the build dir if in build dir mode, and in the OS otherwise.
This commit is contained in:
parent
3c6d9d1c39
commit
91d149cfb4
207
src/basic/build-path.c
Normal file
207
src/basic/build-path.c
Normal file
@ -0,0 +1,207 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <elf.h>
|
||||
#include <link.h>
|
||||
#include <sys/auxv.h>
|
||||
|
||||
#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;
|
||||
}
|
6
src/basic/build-path.h
Normal file
6
src/basic/build-path.h
Normal file
@ -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[]);
|
@ -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',
|
||||
|
@ -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',
|
||||
|
20
src/test/test-build-path.c
Normal file
20
src/test/test-build-path.c
Normal file
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user