1
0
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:
Lennart Poettering 2024-02-20 11:30:27 +01:00
parent 3c6d9d1c39
commit 91d149cfb4
5 changed files with 235 additions and 0 deletions

207
src/basic/build-path.c Normal file
View 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
View 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[]);

View File

@ -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',

View File

@ -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',

View 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;
}