From: Lennart Poettering Date: Wed, 19 Nov 2025 16:13:50 +0000 (+0100) Subject: dlfcn-util: let's make our dlopen() code fail if we enter a container namespace X-Git-Tag: v259-rc2~34^2~11 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=2c7bdaf9f144ad339c72628579183fc849f2b794;p=thirdparty%2Fsystemd.git dlfcn-util: let's make our dlopen() code fail if we enter a container namespace Now that we dlopen() so many deps, it might happen by accident that we end up dlopen()ening stuff when we entered a container, which we should really avoid, to not mix host and container libraries. Let's add a global variable we can set when we want to block dlopen() to ever succeed. This is then checked primarily in dlopen_many_sym_or_warn(), where we'll generate EPERM plus a log message. There are a couple of other places we invoke dlopen(), without going through dlopen_many_sym_or_warn(). This adds the same check there. --- diff --git a/src/basic/dlfcn-util.c b/src/basic/dlfcn-util.c index 6a7fe15a851..26c5a44ffe0 100644 --- a/src/basic/dlfcn-util.c +++ b/src/basic/dlfcn-util.c @@ -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; +} diff --git a/src/basic/dlfcn-util.h b/src/basic/dlfcn-util.h index e9ecd8d1e5c..367c47ddb86 100644 --- a/src/basic/dlfcn-util.h +++ b/src/basic/dlfcn-util.h @@ -72,3 +72,11 @@ int dlopen_many_sym_or_warn_sentinel(void **dlp, const char *filename, int log_l * _SONAME_ARRAY 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); diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 1d4fa3756dc..b8c4a92402f 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -5818,6 +5818,10 @@ int exec_invoke( } } + /* Let's now disable further dlopen()ing of libraries, since we are about to do namespace + * shenanigans, and do not want to mix resources from host and namespace */ + block_dlopen(); + if (needs_sandboxing && !have_cap_sys_admin && exec_needs_cap_sys_admin(context, params)) { /* If we're unprivileged, set up the user namespace first to enable use of the other namespaces. * Users with CAP_SYS_ADMIN can set up user namespaces last because they will be able to diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index c3c9d75a50d..e4bf8159358 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -4372,6 +4372,9 @@ static int outer_child( if (pid == 0) { fd_outer_socket = safe_close(fd_outer_socket); + /* In the child refuse dlopen(), so that we never mix shared libraries from payload and parent */ + block_dlopen(); + /* The inner child has all namespaces that are requested, so that we all are owned by the * user if user namespaces are turned on. */ diff --git a/src/shared/bpf-dlopen.c b/src/shared/bpf-dlopen.c index 1c64498044a..0e7632eb343 100644 --- a/src/shared/bpf-dlopen.c +++ b/src/shared/bpf-dlopen.c @@ -2,6 +2,7 @@ #include "bpf-dlopen.h" #include "dlfcn-util.h" +#include "errno-util.h" #include "initrd-util.h" #include "log.h" @@ -73,7 +74,6 @@ static int bpf_print_func(enum libbpf_print_level level, const char *fmt, va_lis } int dlopen_bpf_full(int log_level) { - _cleanup_(dlclosep) void *dl = NULL; static int cached = 0; int r; @@ -87,17 +87,20 @@ int dlopen_bpf_full(int log_level) { DISABLE_WARNING_DEPRECATED_DECLARATIONS; - dl = dlopen("libbpf.so.1", RTLD_NOW|RTLD_NODELETE); - if (!dl) { + _cleanup_(dlclosep) void *dl = NULL; + r = dlopen_safe("libbpf.so.1", &dl, /* reterr_dlerror= */ NULL); + if (r < 0) { /* libbpf < 1.0.0 (we rely on 0.1.0+) provide most symbols we care about, but * unfortunately not all until 0.7.0. See bpf-compat.h for more details. * Once we consider we can assume 0.7+ is present we can just use the same symbol * list for both files, and when we assume 1.0+ is present we can remove this dlopen */ - dl = dlopen("libbpf.so.0", RTLD_NOW|RTLD_NODELETE); - if (!dl) - return cached = log_full_errno(in_initrd() ? LOG_DEBUG : log_level, SYNTHETIC_ERRNO(EOPNOTSUPP), - "Neither libbpf.so.1 nor libbpf.so.0 are installed, cgroup BPF features disabled: %s", - dlerror()); + const char *dle = NULL; + r = dlopen_safe("libbpf.so.0", &dl, &dle); + if (r < 0) { + log_full_errno(in_initrd() ? LOG_DEBUG : log_level, r, + "Neither libbpf.so.1 nor libbpf.so.0 are installed, cgroup BPF features disabled: %s", dle ?: STRERROR(r)); + return (cached = -EOPNOTSUPP); /* turn into recognizable error */ + } log_debug("Loaded 'libbpf.so.0' via dlopen()"); diff --git a/src/shared/idn-util.c b/src/shared/idn-util.c index 47f0bd52886..0f39b559428 100644 --- a/src/shared/idn-util.c +++ b/src/shared/idn-util.c @@ -41,7 +41,6 @@ DLSYM_PROTOTYPE(stringprep_ucs4_to_utf8) = NULL; DLSYM_PROTOTYPE(stringprep_utf8_to_ucs4) = NULL; int dlopen_idn(void) { - _cleanup_(dlclosep) void *dl = NULL; int r; ELF_NOTE_DLOPEN("idn", @@ -52,14 +51,21 @@ int dlopen_idn(void) { if (idn_dl) return 0; /* Already loaded */ - dl = dlopen("libidn.so.12", RTLD_NOW|RTLD_NODELETE); - if (!dl) { + r = check_dlopen_blocked("libidn.so.12"); + if (r < 0) + return r; + + _cleanup_(dlclosep) void *dl = NULL; + r = dlopen_safe("libidn.so.12", &dl, /* reterr_dlerror= */ NULL); + if (r < 0) { /* libidn broke ABI in 1.34, but not in a way we care about (a new field got added to an * open-coded struct we do not use), hence support both versions. */ - dl = dlopen("libidn.so.11", RTLD_NOW|RTLD_NODELETE); - if (!dl) - return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), - "libidn support is not installed: %s", dlerror()); + const char *dle = NULL; + r = dlopen_safe("libidn.so.11", &dl, &dle); + if (r < 0) { + log_debug_errno(r, "libidn support is not available: %s", dle ?: STRERROR(r)); + return -EOPNOTSUPP; /* turn into recognizable error */ + } log_debug("Loaded 'libidn.so.11' via dlopen()"); } else log_debug("Loaded 'libidn.so.12' via dlopen()"); diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index e51c789a3e8..cdf986f4b04 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -13,6 +13,7 @@ #include "dirent-util.h" #include "dlfcn-util.h" #include "efi-api.h" +#include "errno-util.h" #include "extract-word.h" #include "fd-util.h" #include "fileio.h" @@ -742,9 +743,12 @@ int tpm2_context_new(const char *device, Tpm2Context **ret_context) { if (!filename_is_valid(fn)) return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "TPM2 driver name '%s' not valid, refusing.", driver); - context->tcti_dl = dlopen(fn, RTLD_NOW|RTLD_NODELETE); - if (!context->tcti_dl) - return log_debug_errno(SYNTHETIC_ERRNO(ENOPKG), "Failed to load %s: %s", fn, dlerror()); + const char *dle = NULL; + r = dlopen_safe(fn, &context->tcti_dl, &dle); + if (r < 0) { + log_debug_errno(r, "Failed to load %s: %s", fn, dle ?: STRERROR(r)); + return -ENOPKG; /* Turn into recognizable error */ + } log_debug("Loaded '%s' via dlopen()", fn); diff --git a/src/shared/userdb.c b/src/shared/userdb.c index a6b4a5dae96..18a13a1e7aa 100644 --- a/src/shared/userdb.c +++ b/src/shared/userdb.c @@ -1977,21 +1977,22 @@ int membershipdb_by_group_strv(const char *name, UserDBFlags flags, char ***ret) } int userdb_block_nss_systemd(int b) { - _cleanup_(dlclosep) void *dl = NULL; - int (*call)(bool b); + int r; /* Note that we might be called from libnss_systemd.so.2 itself, but that should be fine, really. */ - dl = dlopen(LIBDIR "/libnss_systemd.so.2", RTLD_NOW|RTLD_NODELETE); - if (!dl) { + _cleanup_(dlclosep) void *dl = NULL; + const char *dle; + r = dlopen_safe(LIBDIR "/libnss_systemd.so.2", &dl, &dle); + if (r < 0) { /* If the file isn't installed, don't complain loudly */ - log_debug("Failed to dlopen(libnss_systemd.so.2), ignoring: %s", dlerror()); + log_debug_errno(r, "Failed to dlopen(libnss_systemd.so.2), ignoring: %s", dle ?: STRERROR(r)); return 0; } log_debug("Loaded '%s' via dlopen()", LIBDIR "/libnss_systemd.so.2"); - call = dlsym(dl, "_nss_systemd_block"); + int (*call)(bool b) = dlsym(dl, "_nss_systemd_block"); if (!call) /* If the file is installed but lacks the symbol we expect, things are weird, let's complain */ return log_debug_errno(SYNTHETIC_ERRNO(ELIBBAD), diff --git a/src/test/test-dlopen.c b/src/test/test-dlopen.c index b44c55fbeba..aefd2164734 100644 --- a/src/test/test-dlopen.c +++ b/src/test/test-dlopen.c @@ -3,6 +3,7 @@ #include #include +#include "dlfcn-util.h" #include "shared-forward.h" int main(int argc, char **argv) { @@ -10,7 +11,7 @@ int main(int argc, char **argv) { int i; for (i = 0; i < argc - 1; i++) - assert_se(handles[i] = dlopen(argv[i + 1], RTLD_NOW|RTLD_NODELETE)); + assert_se(dlopen_safe(argv[i + 1], handles + i, /* reterr_dlerror= */ NULL) >= 0); for (i--; i >= 0; i--) assert_se(dlclose(handles[i]) == 0);