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:
+58
-6
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user