]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
dlfcn-util: let's make our dlopen() code fail if we enter a container namespace
authorLennart Poettering <lennart@poettering.net>
Wed, 19 Nov 2025 16:13:50 +0000 (17:13 +0100)
committerLennart Poettering <lennart@poettering.net>
Mon, 24 Nov 2025 08:19:33 +0000 (09:19 +0100)
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.

src/basic/dlfcn-util.c
src/basic/dlfcn-util.h
src/core/exec-invoke.c
src/nspawn/nspawn.c
src/shared/bpf-dlopen.c
src/shared/idn-util.c
src/shared/tpm2-util.c
src/shared/userdb.c
src/test/test-dlopen.c

index 6a7fe15a851c35aa87d11e8d62db0a21fa1032f8..26c5a44ffe0bfb96236e5119d5df46b0783ade74 100644 (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;
+}
index e9ecd8d1e5c4b19bcd9a14c4c7617892186eb151..367c47ddb8610e48cc77fbd9869792d8eef61bc0 100644 (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);
index 1d4fa3756dcce0acec22751d95a18f4437ae82d2..b8c4a92402f383b2490065c19fcfa71904dcb070 100644 (file)
@@ -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
index c3c9d75a50d3b40faa5462ab46c5c2f0de794d39..e4bf8159358be174cf5b44e6b2d6bccf005ff5f9 100644 (file)
@@ -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. */
 
index 1c64498044ac5bc1fa70dc747f5848e6bad2e1c4..0e7632eb343e88a2553c2e9c5f01ec221831053c 100644 (file)
@@ -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()");
 
index 47f0bd52886e0ce562236ebcc13c889259051dd8..0f39b55942862371284b4bb6d4e1dcc096aa7214 100644 (file)
@@ -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()");
index e51c789a3e840f81269cc27a802a2139563561cb..cdf986f4b04e667421867882d41dec42de8f837c 100644 (file)
@@ -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);
 
index a6b4a5dae96fb9919becb49720835f3719a6ff79..18a13a1e7aac6f553baabbee6f445a256e5c16a0 100644 (file)
@@ -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),
index b44c55fbeba7ef8a41efccfed19ff13415ee9a42..aefd2164734c51400488789b32de7c8ba7bdb205 100644 (file)
@@ -3,6 +3,7 @@
 #include <dlfcn.h>
 #include <stdlib.h>
 
+#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);