block dlopen() once we transition into some foreign namespace (#39824)

Let's make sure we never accidentally end up loading code from a foreign
namespace.
This commit is contained in:
Lennart Poettering
2025-11-24 14:14:16 +01:00
committed by GitHub
27 changed files with 220 additions and 34 deletions
+58 -6
View File
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "dlfcn-util.h"
#include "errno-util.h"
#include "log.h"
void* safe_dlclose(void *dl) {
@@ -47,18 +48,20 @@ int dlsym_many_or_warn_sentinel(void *dl, int log_level, ...) {
}
int dlopen_many_sym_or_warn_sentinel(void **dlp, const char *filename, int log_level, ...) {
_cleanup_(dlclosep) void *dl = NULL;
int r;
if (*dlp)
return 0; /* Already loaded */
dl = dlopen(filename, RTLD_NOW|RTLD_NODELETE);
if (!dl)
return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"%s is not installed: %s", filename, dlerror());
_cleanup_(dlclosep) void *dl = NULL;
const char *dle = NULL;
r = dlopen_safe(filename, &dl, &dle);
if (r < 0) {
log_debug_errno(r, "Shared library '%s' is not available: %s", filename, dle ?: STRERROR(r));
return -EOPNOTSUPP; /* Turn into recognizable error */
}
log_debug("Loaded '%s' via dlopen()", filename);
log_debug("Loaded shared library '%s' via dlopen().", filename);
va_list ap;
va_start(ap, log_level);
@@ -73,3 +76,52 @@ int dlopen_many_sym_or_warn_sentinel(void **dlp, const char *filename, int log_l
*dlp = TAKE_PTR(dl);
return 1;
}
static bool dlopen_blocked = false;
void block_dlopen(void) {
dlopen_blocked = true;
}
int dlopen_safe(const char *filename, void **ret, const char **reterr_dlerror) {
int r;
assert(filename);
/* A wrapper around dlopen(), that takes dlopen_blocked into account, and tries to normalize the
* error reporting a bit. */
int flags = RTLD_NOW|RTLD_NODELETE; /* Always set RTLD_NOW + RTLD_NODELETE, for security reasons */
/* If dlopen() is blocked we'll still try it, but set RTLD_NOLOAD, so that it will still work if
* already loaded (for example because the binary linked to things regularly), but fail if not. */
if (dlopen_blocked)
flags |= RTLD_NOLOAD;
errno = 0;
void *p = dlopen(filename, flags);
if (!p) {
if (dlopen_blocked) {
(void) dlerror(); /* consume error, so that no later call will return it */
if (reterr_dlerror)
*reterr_dlerror = NULL;
return log_debug_errno(SYNTHETIC_ERRNO(EPERM), "Refusing loading of '%s', as loading further dlopen() modules has been blocked.", filename);
}
r = errno_or_else(ENOPKG);
if (reterr_dlerror)
*reterr_dlerror = dlerror();
else
(void) dlerror(); /* consume error, so that no later call will return it */
return r;
}
if (ret)
*ret = TAKE_PTR(p);
return 0;
}
+8
View File
@@ -72,3 +72,11 @@ int dlopen_many_sym_or_warn_sentinel(void **dlp, const char *filename, int log_l
* _SONAME_ARRAY<X+1> will need to be added). */
#define ELF_NOTE_DLOPEN(feature, description, priority, ...) \
_ELF_NOTE_DLOPEN("[{\"feature\":\"" feature "\",\"description\":\"" description "\",\"priority\":\"" priority "\",\"soname\":" _SONAME_ARRAY(__VA_ARGS__) "}]", UNIQ_T(s, UNIQ))
/* If called dlopen_many_sym_or_warn() will fail with EPERM. This can be used to block lazy loading of shared
* libs, if we transfer a process into a different namespace. Note that this does not work for all calls of
* dlopen(), just those through our dlopen_safe() wrapper (which we use comprehensively in our
* codebase). This hence has *no* effect on NSS. (Would be great if we could change that...) */
void block_dlopen(void);
int dlopen_safe(const char *filename, void **ret, const char **reterr_dlerror);
+10
View File
@@ -22,6 +22,7 @@
#include "argv-util.h"
#include "cgroup-util.h"
#include "dirent-util.h"
#include "dlfcn-util.h"
#include "env-file.h"
#include "errno-util.h"
#include "escape.h"
@@ -1695,6 +1696,15 @@ int pidref_safe_fork_full(
r, "Failed to rename process, ignoring: %m");
}
/* let's disable dlopen() in the child, as a paranoia safety precaution: children should not live for
* long and only do minimal work before exiting or exec()ing. Doing dlopen() is not either. If people
* want dlopen() they should do it before forking. This is a safety precuation in particular for
* cases where the child does namespace shenanigans: we should never end up loading a module from a
* foreign environment. Note that this has no effect on NSS! (i.e. it only has effect on uses of our
* dlopen_safe(), which we use comprehensively in our codebase, but glibc NSS doesn't bother, of
* course.) */
block_dlopen();
if (flags & (FORK_DEATHSIG_SIGTERM|FORK_DEATHSIG_SIGINT|FORK_DEATHSIG_SIGKILL))
if (prctl(PR_SET_PDEATHSIG, fork_flags_to_signal(flags)) < 0) {
log_full_errno(prio, errno, "Failed to set death signal: %m");