From 2c7bdaf9f144ad339c72628579183fc849f2b794 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Nov 2025 17:13:50 +0100 Subject: [PATCH 01/12] 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. --- src/basic/dlfcn-util.c | 64 +++++++++++++++++++++++++++++++++++++---- src/basic/dlfcn-util.h | 8 ++++++ src/core/exec-invoke.c | 4 +++ src/nspawn/nspawn.c | 3 ++ src/shared/bpf-dlopen.c | 19 ++++++------ src/shared/idn-util.c | 20 ++++++++----- src/shared/tpm2-util.c | 10 +++++-- src/shared/userdb.c | 13 +++++---- src/test/test-dlopen.c | 3 +- 9 files changed, 113 insertions(+), 31 deletions(-) diff --git a/src/basic/dlfcn-util.c b/src/basic/dlfcn-util.c index 6a7fe15a85..26c5a44ffe 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 e9ecd8d1e5..367c47ddb8 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 1d4fa3756d..b8c4a92402 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 c3c9d75a50..e4bf815935 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 1c64498044..0e7632eb34 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 47f0bd5288..0f39b55942 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 e51c789a3e..cdf986f4b0 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 a6b4a5dae9..18a13a1e7a 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 b44c55fbeb..aefd216473 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); From 800d11c36cd97da2122eae5369939818d50f491d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Nov 2025 17:23:58 +0100 Subject: [PATCH 02/12] nspawn: load three libraries we'll need later before we fork() a child --- src/nspawn/meson.build | 1 + src/nspawn/nspawn.c | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/nspawn/meson.build b/src/nspawn/meson.build index 98bbb48b09..815b74cb15 100644 --- a/src/nspawn/meson.build +++ b/src/nspawn/meson.build @@ -47,6 +47,7 @@ executables += [ include_directories('.') ], 'dependencies' : [ + libmount_cflags, libseccomp_cflags, libselinux_cflags, ], diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index e4bf815935..4f9cd4dbdc 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -58,6 +58,7 @@ #include "image-policy.h" #include "in-addr-util.h" #include "io-util.h" +#include "libmount-util.h" #include "log.h" #include "loop-util.h" #include "loopback-setup.h" @@ -5965,6 +5966,10 @@ static int run(int argc, char *argv[]) { if (arg_cleanup) return do_cleanup(); + (void) dlopen_libmount(); + (void) dlopen_libseccomp(); + (void) dlopen_libselinux(); + r = cg_has_legacy(); if (r < 0) goto finish; From efaf5a763d6a06645dba8e88ebc15e887d59cbef Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 24 Nov 2025 09:55:03 +0100 Subject: [PATCH 03/12] execute: load a bunch of libs before we disable dlopen() --- src/core/exec-invoke.c | 8 ++++++++ src/core/meson.build | 3 +++ 2 files changed, 11 insertions(+) diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index b8c4a92402..6aa01dceaf 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -32,6 +32,7 @@ #include "constants.h" #include "copy.h" #include "coredump-util.h" +#include "cryptsetup-util.h" #include "dissect-image.h" #include "dynamic-user.h" #include "env-util.h" @@ -48,6 +49,7 @@ #include "io-util.h" #include "iovec-util.h" #include "journal-send.h" +#include "libmount-util.h" #include "manager.h" #include "memfd-util.h" #include "mkdir-label.h" @@ -5818,6 +5820,12 @@ int exec_invoke( } } + /* Load a bunch of libraries we'll possibly need later, before we turn off dlopen() */ + (void) dlopen_bpf(); + (void) dlopen_cryptsetup(); + (void) dlopen_libmount(); + (void) dlopen_libseccomp(); + /* 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(); diff --git a/src/core/meson.build b/src/core/meson.build index fbadd0f6a6..10a7ae2f8c 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -212,6 +212,9 @@ executables += [ 'link_with' : executor_libs, 'dependencies' : [ libapparmor_cflags, + libbpf_cflags, + libcryptsetup_cflags, + libmount_cflags, libpam_cflags, libseccomp_cflags, libselinux_cflags, From 6069de1ed34735482dc63ba7431f605b858ea719 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 24 Nov 2025 09:55:46 +0100 Subject: [PATCH 04/12] pid1: pull in libmount unconditionally --- src/core/main.c | 7 +++++++ src/core/meson.build | 2 ++ 2 files changed, 9 insertions(+) diff --git a/src/core/main.c b/src/core/main.c index d7651d0df9..f4acdb02c6 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -59,6 +59,7 @@ #include "killall.h" #include "kmod-setup.h" #include "label-util.h" +#include "libmount-util.h" #include "limits-util.h" #include "load-fragment.h" #include "log.h" @@ -3310,6 +3311,12 @@ int main(int argc, char *argv[]) { goto finish; } + r = dlopen_libmount(); + if (r < 0) { + error_message = "Failed to load libmount.so"; + goto finish; + } + r = initialize_runtime(skip_setup, first_boot, &saved_rlimit_nofile, diff --git a/src/core/meson.build b/src/core/meson.build index 10a7ae2f8c..2aa47f548b 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -133,6 +133,7 @@ libcore_static = static_library( implicit_include_directories : false, c_args : ['-fvisibility=default'], dependencies : [libaudit_cflags, + libbpf_cflags, libdl, libm, libmount_cflags, @@ -200,6 +201,7 @@ executables += [ 'dependencies' : [ libapparmor_cflags, libkmod_cflags, + libmount_cflags, libseccomp_cflags, libselinux_cflags, ], From e683dce1f1747de2fd48bfd67849cbb70a13c47c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 21 Nov 2025 08:53:14 +0100 Subject: [PATCH 05/12] portabled: load a bunch of libs before we fork off a dissector child processes --- src/portable/meson.build | 2 ++ src/portable/portable.c | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/portable/meson.build b/src/portable/meson.build index 9c9d04c461..d9c266ff9e 100644 --- a/src/portable/meson.build +++ b/src/portable/meson.build @@ -29,6 +29,8 @@ executables += [ 'sources' : systemd_portabled_sources, 'link_with' : portabled_link_with, 'dependencies' : [ + libcryptsetup_cflags, + libmount_cflags, libselinux_cflags, threads, ], diff --git a/src/portable/portable.c b/src/portable/portable.c index 503b5b08a2..7dfe1aa99c 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -12,6 +12,7 @@ #include "chase.h" #include "conf-files.h" #include "copy.h" +#include "cryptsetup-util.h" #include "data-fd-util.h" #include "dirent-util.h" #include "discover-image.h" @@ -28,6 +29,7 @@ #include "glyph-util.h" #include "install.h" #include "iovec-util.h" +#include "libmount-util.h" #include "log-context.h" #include "log.h" #include "loop-util.h" @@ -420,6 +422,10 @@ static int portable_extract_by_path( BLOCK_SIGNALS(SIGCHLD); + /* Load some libraries before we fork workers off that want to use them */ + (void) dlopen_cryptsetup(); + (void) dlopen_libmount(); + r = mkdtemp_malloc("/tmp/inspect-XXXXXX", &tmpdir); if (r < 0) return log_debug_errno(r, "Failed to create temporary directory: %m"); From 5b2f52814b1d106ce1061cd742529860bd37f8ad Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 21 Nov 2025 08:53:28 +0100 Subject: [PATCH 06/12] dissect-image: load library before we fork off metadata extractor child process --- src/shared/dissect-image.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 081fec2877..6a427012a8 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -49,6 +49,7 @@ #include "io-util.h" #include "iovec-util.h" #include "json-util.h" +#include "libmount-util.h" #include "loop-util.h" #include "mkdir-label.h" #include "mount-util.h" @@ -3907,6 +3908,10 @@ int dissected_image_acquire_metadata( assert(m); + r = dlopen_libmount(); + if (r < 0) + return r; + for (; n_meta_initialized < _META_MAX; n_meta_initialized++) { assert(paths[n_meta_initialized]); From 52594c31848048575e95ae0af3f97d18397c70f8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 21 Nov 2025 08:53:44 +0100 Subject: [PATCH 07/12] udev: load a bunch of libs before we fork off worker processes --- src/udev/meson.build | 3 ++- src/udev/udevd.c | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/udev/meson.build b/src/udev/meson.build index 555e83fa7f..7df5b3de85 100644 --- a/src/udev/meson.build +++ b/src/udev/meson.build @@ -116,7 +116,8 @@ endif udev_dependencies = [ libacl_cflags, libblkid_cflags, - libkmod, + libkmod_cflags, + libmount_cflags, threads, ] diff --git a/src/udev/udevd.c b/src/udev/udevd.c index fd16654127..ab3106334b 100644 --- a/src/udev/udevd.c +++ b/src/udev/udevd.c @@ -8,10 +8,14 @@ #include #include +#include "acl-util.h" +#include "blkid-util.h" #include "errno-util.h" #include "fd-util.h" #include "label-util.h" +#include "libmount-util.h" #include "log.h" +#include "module-util.h" #include "process-util.h" #include "rlimit-util.h" #include "terminal-util.h" @@ -52,6 +56,12 @@ int run_udevd(int argc, char *argv[]) { if (r < 0 && r != -EEXIST) return log_error_errno(r, "Failed to create /run/udev: %m"); + /* Load some shared libraries before we fork any workers */ + (void) dlopen_libacl(); + (void) dlopen_libblkid(); + (void) dlopen_libkmod(); + (void) dlopen_libmount(); + if (arg_daemonize) { pid_t pid; From 09596e7d1a540a78e984c196d776102914b4239f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 21 Nov 2025 17:09:21 +0100 Subject: [PATCH 08/12] repart: load libraries before forking off child --- src/repart/meson.build | 2 ++ src/repart/repart.c | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/repart/meson.build b/src/repart/meson.build index 1478ae278a..b922ecb713 100644 --- a/src/repart/meson.build +++ b/src/repart/meson.build @@ -16,6 +16,7 @@ executables += [ 'dependencies' : [ libblkid_cflags, libfdisk, + libmount_cflags, libopenssl, threads, ], @@ -35,6 +36,7 @@ executables += [ 'dependencies' : [ libblkid_cflags, libfdisk, + libmount_cflags, libopenssl, threads, ], diff --git a/src/repart/repart.c b/src/repart/repart.c index ea44775eec..497654d012 100644 --- a/src/repart/repart.c +++ b/src/repart/repart.c @@ -50,6 +50,7 @@ #include "initrd-util.h" #include "io-util.h" #include "json-util.h" +#include "libmount-util.h" #include "list.h" #include "loop-util.h" #include "main-func.h" @@ -6616,6 +6617,8 @@ static int partition_populate_filesystem(Context *context, Partition *p, const c * appear in the host namespace. Hence we fork a child that has its own file system namespace and * detached mount propagation. */ + (void) dlopen_libmount(); + r = safe_fork("(sd-copy)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE, NULL); if (r < 0) return r; From c64a48605803319510154ae06dbe5a94c5cfe64a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 21 Nov 2025 17:09:54 +0100 Subject: [PATCH 09/12] sysext: load libraries before forking off worker child --- src/sysext/meson.build | 5 +++++ src/sysext/sysext.c | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/src/sysext/meson.build b/src/sysext/meson.build index 25f6b91335..daaabec777 100644 --- a/src/sysext/meson.build +++ b/src/sysext/meson.build @@ -6,6 +6,11 @@ executables += [ 'public' : true, 'conditions' : ['ENABLE_SYSEXT'], 'sources' : files('sysext.c'), + 'dependencies' : [ + libblkid_cflags, + libcryptsetup_cflags, + libmount_cflags, + ], }, ] diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c index d9c89eef74..ab1f69f8c9 100644 --- a/src/sysext/sysext.c +++ b/src/sysext/sysext.c @@ -11,6 +11,7 @@ #include "sd-varlink.h" #include "argv-util.h" +#include "blkid-util.h" #include "blockdev-util.h" #include "build.h" #include "bus-unit-util.h" @@ -18,6 +19,7 @@ #include "capability-util.h" #include "chase.h" #include "conf-parser.h" +#include "cryptsetup-util.h" #include "devnum-util.h" #include "discover-image.h" #include "dissect-image.h" @@ -33,6 +35,7 @@ #include "image-policy.h" #include "initrd-util.h" #include "label-util.h" /* IWYU pragma: keep */ +#include "libmount-util.h" #include "log.h" #include "loop-util.h" #include "main-func.h" @@ -523,6 +526,8 @@ static int unmerge( bool need_to_reload; int r; + (void) dlopen_libmount(); + r = need_reload(image_class, hierarchies, no_reload); if (r < 0) return r; @@ -2100,6 +2105,10 @@ static int merge(ImageClass image_class, pid_t pid; int r; + (void) dlopen_cryptsetup(); + (void) dlopen_libblkid(); + (void) dlopen_libmount(); + r = safe_fork("(sd-merge)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_NEW_MOUNTNS, &pid); if (r < 0) return log_error_errno(r, "Failed to fork off child: %m"); From 9252e26f4cbf92428cab595045716fee783f375e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 24 Nov 2025 09:41:36 +0100 Subject: [PATCH 10/12] core: load libcryptsetup before forking off child that might need it --- src/core/meson.build | 1 + src/core/service.c | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/core/meson.build b/src/core/meson.build index 2aa47f548b..c7beb8742a 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -134,6 +134,7 @@ libcore_static = static_library( c_args : ['-fvisibility=default'], dependencies : [libaudit_cflags, libbpf_cflags, + libcryptsetup_cflags, libdl, libm, libmount_cflags, diff --git a/src/core/service.c b/src/core/service.c index a9d90354b6..12f4733274 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -14,6 +14,7 @@ #include "bus-error.h" #include "bus-util.h" #include "chase.h" +#include "cryptsetup-util.h" #include "dbus-service.h" #include "dbus-unit.h" #include "devnum-util.h" @@ -5552,6 +5553,8 @@ static int service_live_mount( u->id); } + (void) dlopen_cryptsetup(); + service_unwatch_control_pid(s); s->live_mount_result = SERVICE_SUCCESS; s->control_command = NULL; From a08f2ea02dc4fcbd7f2f73a5a24e0af32b34fe55 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Nov 2025 14:43:22 +0100 Subject: [PATCH 11/12] process-util: also disable dlopen() in safe_fork() --- src/basic/process-util.c | 10 ++++++++++ src/libsystemd/meson.build | 5 ++++- src/libsystemd/sd-device/test-sd-device.c | 9 ++++++++- src/test/test-execute.c | 7 +++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/basic/process-util.c b/src/basic/process-util.c index 5759de38f4..592e4c329c 100644 --- a/src/basic/process-util.c +++ b/src/basic/process-util.c @@ -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"); diff --git a/src/libsystemd/meson.build b/src/libsystemd/meson.build index ec1ac471d9..976f0e9987 100644 --- a/src/libsystemd/meson.build +++ b/src/libsystemd/meson.build @@ -203,7 +203,6 @@ simple_tests += files( 'sd-bus/test-bus-vtable.c', 'sd-device/test-device-util.c', 'sd-device/test-sd-device-monitor.c', - 'sd-device/test-sd-device.c', 'sd-journal/test-journal-flush.c', 'sd-journal/test-journal-interleaving.c', 'sd-journal/test-journal-stream.c', @@ -214,6 +213,10 @@ simple_tests += files( ) libsystemd_tests += [ + { + 'sources' : files('sd-device/test-sd-device.c'), + 'dependencies' : [ threads, libmount_cflags ], + }, { 'sources' : files('sd-bus/test-bus-address.c'), 'dependencies' : threads diff --git a/src/libsystemd/sd-device/test-sd-device.c b/src/libsystemd/sd-device/test-sd-device.c index dc4a32e8a1..02525c83ec 100644 --- a/src/libsystemd/sd-device/test-sd-device.c +++ b/src/libsystemd/sd-device/test-sd-device.c @@ -13,6 +13,7 @@ #include "fd-util.h" #include "fs-util.h" #include "hashmap.h" +#include "libmount-util.h" #include "mkdir.h" #include "mount-util.h" #include "mountpoint-util.h" @@ -833,8 +834,14 @@ TEST(devname_from_devnum) { } static int intro(void) { + int r; + if (path_is_mount_point("/sys") <= 0) - return log_tests_skipped("/sys is not mounted"); + return log_tests_skipped("/sys/ is not mounted"); + + r = dlopen_libmount(); + if (r < 0) + return log_tests_skipped("libmount not available."); return EXIT_SUCCESS; } diff --git a/src/test/test-execute.c b/src/test/test-execute.c index d043521034..bccc6dc7fb 100644 --- a/src/test/test-execute.c +++ b/src/test/test-execute.c @@ -20,6 +20,7 @@ #include "fd-util.h" #include "fileio.h" #include "fs-util.h" +#include "libmount-util.h" #include "manager.h" #include "mkdir.h" #include "mount-util.h" @@ -1604,6 +1605,8 @@ TEST(run_tests_unprivileged) { } static int intro(void) { + int r; + #if HAS_FEATURE_ADDRESS_SANITIZER if (strstr_ptr(ci_environment(), "travis") || strstr_ptr(ci_environment(), "github-actions")) return log_tests_skipped("Running on Travis CI/GH Actions under ASan, see https://github.com/systemd/systemd/issues/10696"); @@ -1621,6 +1624,10 @@ static int intro(void) { if (path_is_read_only_fs("/sys") > 0) return log_tests_skipped("/sys is mounted read-only"); + r = dlopen_libmount(); + if (r < 0) + return log_tests_skipped("libmount not available."); + /* Create dummy network interface for testing PrivateNetwork=yes */ have_net_dummy = system("ip link add dummy-test-exec type dummy") == 0; From ed3fcaae11da24270b7b4a7c73be288ec86e4e0a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Nov 2025 14:43:41 +0100 Subject: [PATCH 12/12] crash-handler: also disable dlopen(), just in case --- src/core/crash-handler.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/crash-handler.c b/src/core/crash-handler.c index 124d4b886b..495e9efc9b 100644 --- a/src/core/crash-handler.c +++ b/src/core/crash-handler.c @@ -7,6 +7,7 @@ #include "constants.h" #include "crash-handler.h" +#include "dlfcn-util.h" #include "exit-status.h" #include "format-util.h" #include "log.h" @@ -70,6 +71,9 @@ _noreturn_ static void crash(int sig, siginfo_t *siginfo, void *context) { * memory allocation is not async-signal-safe anyway — see signal-safety(7) for details —, and thus * is not permissible in signal handlers.) */ + block_dlopen(); /* paranoia: never end up doing dlopen() as side-effect from some call anymore from + * here */ + if (getpid_cached() != 1) /* Pass this on immediately, if this is not PID 1 */ propagate_signal(sig, siginfo);