From 16c6758eb4128d199797ad417dd6a6580e53a61a Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 10 Jul 2026 15:38:22 +0100 Subject: [PATCH 1/7] journal-remote: remove disabled compression entry before freeing compression_config_put() frees the COMPRESSION_NONE value when a later algorithm is selected, but lefts the map entry pointing at it. Remove the entry first so subsequent parsing, mangling, and teardown cannot access or free stale memory. Follow-up for c259c9e25329c93cb1b7363f89d917ffa5ce57c1 --- src/journal-remote/journal-compression-util.c | 2 +- src/journal-remote/meson.build | 2 +- src/journal-remote/test-journal-header-util.c | 27 +++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/journal-remote/journal-compression-util.c b/src/journal-remote/journal-compression-util.c index 8e415878b9..7247258191 100644 --- a/src/journal-remote/journal-compression-util.c +++ b/src/journal-remote/journal-compression-util.c @@ -41,7 +41,7 @@ static int compression_config_put(OrderedHashmap **configs, Compression c, int l free(ordered_hashmap_remove(*configs, INT_TO_PTR(cc->algorithm))); } else /* otherwise, drop 'none' if stored. */ - free(ordered_hashmap_get(*configs, INT_TO_PTR(COMPRESSION_NONE))); + free(ordered_hashmap_remove(*configs, INT_TO_PTR(COMPRESSION_NONE))); return 1; } diff --git a/src/journal-remote/meson.build b/src/journal-remote/meson.build index 441a173eab..b20ba1ec59 100644 --- a/src/journal-remote/meson.build +++ b/src/journal-remote/meson.build @@ -67,7 +67,7 @@ executables += [ test_template + { 'sources' : files('test-journal-header-util.c'), 'conditions' : ['ENABLE_REMOTE', 'HAVE_LIBCURL'], - 'import' : ['systemd-journal-upload'], + 'import' : ['systemd-journal-remote', 'systemd-journal-upload'], }, fuzz_template + { 'sources' : files('fuzz-journal-remote.c'), diff --git a/src/journal-remote/test-journal-header-util.c b/src/journal-remote/test-journal-header-util.c index e88bb164ef..3dae9e8c9c 100644 --- a/src/journal-remote/test-journal-header-util.c +++ b/src/journal-remote/test-journal-header-util.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include "hashmap.h" +#include "journal-compression-util.h" #include "journal-header-util.h" #include "tests.h" @@ -18,4 +19,30 @@ TEST(header_put) { ASSERT_ERROR(header_put(&headers, "", "Value"), EINVAL); } +TEST(compression_none_then_algorithm) { + _cleanup_ordered_hashmap_free_ OrderedHashmap *configs = NULL; + Compression compression = COMPRESSION_NONE; + + for (Compression c = 1; c < _COMPRESSION_MAX; c++) + if (compression_supported(c)) { + compression = c; + break; + } + + if (compression == COMPRESSION_NONE) + return (void) log_tests_skipped("No compression algorithm supported"); + + ASSERT_OK_POSITIVE(config_parse_compression( + "test", "test.conf", 1, "Upload", 1, "Compression", false, "no", &configs, NULL)); + ASSERT_TRUE(ordered_hashmap_contains(configs, INT_TO_PTR(COMPRESSION_NONE))); + + ASSERT_OK_POSITIVE(config_parse_compression( + "test", "test.conf", 2, "Upload", 1, "Compression", false, + compression_to_string(compression), &configs, NULL)); + ASSERT_FALSE(ordered_hashmap_contains(configs, INT_TO_PTR(COMPRESSION_NONE))); + ASSERT_NOT_NULL(ordered_hashmap_get(configs, INT_TO_PTR(compression))); + + ASSERT_OK(compression_configs_mangle(&configs)); +} + DEFINE_TEST_MAIN(LOG_DEBUG); From 14830891f33d0c947c5b8f5ebc405fc45a6caaa4 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 10 Jul 2026 16:09:49 +0100 Subject: [PATCH 2/7] journal-remote: zero-initialize MHD daemon wrapper MHDDaemonWrapper_free() unconditionally unrefs both event sources, but setup_microhttpd_server() could return before initializing either pointer. Zero-initialize the wrapper so cleanup after MHD startup failures safely unrefs NULL. Follow-up for 3c67c8bd4c5784bbcc644e489b9a39c0d2bc0e42 --- src/journal-remote/journal-remote-main.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/journal-remote/journal-remote-main.c b/src/journal-remote/journal-remote-main.c index 504471fc8a..afa4e55edf 100644 --- a/src/journal-remote/journal-remote-main.c +++ b/src/journal-remote/journal-remote-main.c @@ -532,8 +532,9 @@ static int setup_microhttpd_server(RemoteServer *s, d = new(MHDDaemonWrapper, 1); if (!d) return log_oom(); - - d->fd = (uint64_t) fd; + *d = (MHDDaemonWrapper) { + .fd = (uint64_t) fd, + }; d->daemon = sym_MHD_start_daemon( flags, From fea9a644323b70ac697a8b277520c5ad99403c01 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 10 Jul 2026 17:51:53 +0100 Subject: [PATCH 3/7] resolved: preserve unchanged question on asymmetric redirect dns_question_cname_redirect() returns no replacement when a question already matches a CNAME target or a DNAME does not apply. If only one of the UTF-8 and IDNA questions redirects, dns_query_cname_redirect() then installs NULL for the unchanged side. Keep a reference to the original question on whichever side does not redirect, and update the existing asymmetric DNAME test to verify it. Follow-up for 23b298bce75a0d1f4f15f34458af9678b4a30c3a --- src/resolve/resolved-dns-query.c | 5 +++++ src/resolve/test-dns-query.c | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/resolve/resolved-dns-query.c b/src/resolve/resolved-dns-query.c index 6ec6569ae7..1498b0e3b7 100644 --- a/src/resolve/resolved-dns-query.c +++ b/src/resolve/resolved-dns-query.c @@ -1339,6 +1339,11 @@ static int dns_query_cname_redirect(DnsQuery *q, const DnsResourceRecord *cname) if (r == 0 && k == 0) /* No actual cname happened? */ return -ELOOP; + if (r == 0) + nq_idna = dns_question_ref(q->question_idna); + if (k == 0) + nq_utf8 = dns_question_ref(q->question_utf8); + if (q->answer_protocol == DNS_PROTOCOL_DNS) /* Don't permit CNAME redirects from unicast DNS to LLMNR or MulticastDNS, so that global resources * cannot invade the local namespace. The opposite way we permit: local names may redirect to global diff --git a/src/resolve/test-dns-query.c b/src/resolve/test-dns-query.c index 9c1e2a73a5..e53d38c94a 100644 --- a/src/resolve/test-dns-query.c +++ b/src/resolve/test-dns-query.c @@ -522,7 +522,7 @@ TEST(dns_query_process_cname_one_success_match_dname_utf8_different) { ASSERT_EQ(query->n_cname_redirects, 1u); ASSERT_EQ(dns_question_size(query->collected_questions), 2u); - ASSERT_NULL(query->question_utf8); + ASSERT_EQ(dns_question_size(query->question_utf8), 1u); ASSERT_EQ(dns_question_size(query->question_idna), 1u); key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_A, "www.\xF0\x9F\x98\xB1.com"); @@ -535,6 +535,11 @@ TEST(dns_query_process_cname_one_success_match_dname_utf8_different) { ASSERT_TRUE(dns_question_contains_key(query->collected_questions, key)); dns_resource_key_unref(key); + key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_A, "www.\xF0\x9F\x98\xB1.com"); + ASSERT_NOT_NULL(key); + ASSERT_TRUE(dns_question_contains_key(query->question_utf8, key)); + dns_resource_key_unref(key); + key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_A, "www.v2.xn--tl8h.com"); ASSERT_NOT_NULL(key); ASSERT_TRUE(dns_question_contains_key(query->question_idna, key)); From 57db794b9dc99e611ef1be21d60011c13fec5ed0 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 10 Jul 2026 17:59:24 +0100 Subject: [PATCH 4/7] vconsole: reject empty layout during keymap conversion with error A leading-dash console keymap or leading-comma X11 layout can produce an empty layout while converting between the two formats. find_converted_keymap() then asserts on it, allowing malformed input to abort callers such as localed. Return EINVAL for an empty derived layout instead. Follow-up for 6d0f5027364518ef17ef91b61dad945e1c4fd0f5 --- src/locale/test-localed-util.c | 10 ++++++++++ src/shared/vconsole-util.c | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/locale/test-localed-util.c b/src/locale/test-localed-util.c index 8e41406298..9bcc802009 100644 --- a/src/locale/test-localed-util.c +++ b/src/locale/test-localed-util.c @@ -79,6 +79,11 @@ TEST(vconsole_convert_to_x11) { ASSERT_OK(vconsole_convert_to_x11(&vc, x11_context_verify, &xc)); ASSERT_TRUE(x11_context_isempty(&xc)); + log_info("/* test leading-dash keymap */"); + ASSERT_OK(free_and_strdup(&vc.keymap, "-foo")); + ASSERT_ERROR(vconsole_convert_to_x11(&vc, x11_context_verify, &xc), EINVAL); + ASSERT_TRUE(x11_context_isempty(&xc)); + log_info("/* test without variant, new mapping (es:) */"); ASSERT_OK(free_and_strdup(&vc.keymap, "es")); ASSERT_OK(vconsole_convert_to_x11(&vc, x11_context_verify, &xc)); @@ -148,6 +153,11 @@ TEST(x11_convert_to_vconsole) { ASSERT_OK(x11_convert_to_vconsole(&xc, &vc)); ASSERT_TRUE(vc_context_isempty(&vc)); + log_info("/* test leading-comma layout */"); + ASSERT_OK(free_and_strdup(&xc.layout, ",us")); + ASSERT_ERROR(x11_convert_to_vconsole(&xc, &vc), EINVAL); + ASSERT_TRUE(vc_context_isempty(&vc)); + log_info("/* test without variant, new mapping (es:) */"); ASSERT_OK(free_and_strdup(&xc.layout, "es")); ASSERT_OK(x11_convert_to_vconsole(&xc, &vc)); diff --git a/src/shared/vconsole-util.c b/src/shared/vconsole-util.c index 9e56ce823d..a780b273aa 100644 --- a/src/shared/vconsole-util.c +++ b/src/shared/vconsole-util.c @@ -352,9 +352,11 @@ int find_converted_keymap(const X11Context *xc, char **ret) { int r; assert(xc); - assert(!isempty(xc->layout)); assert(ret); + if (isempty(xc->layout)) + return -EINVAL; + if (xc->variant) n = strjoin(xc->layout, "-", xc->variant); else From 9dd2dab37e91ab4b000802d98aa7c4713836d53a Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 10 Jul 2026 18:22:34 +0100 Subject: [PATCH 5/7] rm-rf: fail closed when the root check fails path_is_root_at() returns a negative errno when it cannot determine whether a target is the root file system. rm_rf_at() treats those errors as a negative answer and continued with destructive operations. Propagate root-check errors before modifying the target, while preserving REMOVE_MISSING_OK for an absent path. Follow-up for c0228b4fa3e4e633afa5eb8417e6cd3e311cd250 --- src/shared/rm-rf.c | 15 ++++++++++++++- src/test/test-rm-rf.c | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/shared/rm-rf.c b/src/shared/rm-rf.c index 573d6aeb04..56a56ed9ab 100644 --- a/src/shared/rm-rf.c +++ b/src/shared/rm-rf.c @@ -443,7 +443,20 @@ int rm_rf_at(int dir_fd, const char *path, RemoveFlags flags) { /* We refuse to clean the root file system with this call. This is extra paranoia to never cause a * really seriously broken system. */ - if (path_is_root_at(dir_fd, path) > 0) + r = path_is_root_at(dir_fd, path); + if (r == -ENOENT) { + struct stat st; + + if (fstatat(dir_fd, path, &st, AT_SYMLINK_NOFOLLOW) < 0) + r = -errno; + else + r = 0; /* The entry exists, but is most likely a dangling symlink. */ + } + if (FLAGS_SET(flags, REMOVE_MISSING_OK) && r == -ENOENT) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to determine whether '%s' is the root file system: %m", path); + if (r > 0) return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Attempted to remove entire root file system, and we can't allow that."); diff --git a/src/test/test-rm-rf.c b/src/test/test-rm-rf.c index d029f608e2..3a59be7d97 100644 --- a/src/test/test-rm-rf.c +++ b/src/test/test-rm-rf.c @@ -119,4 +119,20 @@ TEST(rm_rf_chmod) { test_rm_rf_chmod_inner(); } +TEST(rm_rf_dangling_symlink) { + _cleanup_(rm_rf_physical_and_freep) char *d = NULL; + const char *link; + + ASSERT_OK(mkdtemp_malloc("/tmp/test-rm-rf-dangling.XXXXXXX", &d)); + link = strjoina(d, "/link"); + + ASSERT_OK_ERRNO(symlink("missing", link)); + ASSERT_OK(rm_rf(link, REMOVE_ROOT|REMOVE_PHYSICAL)); + ASSERT_ERROR_ERRNO(lstat(link, &(struct stat) {}), ENOENT); + + ASSERT_OK_ERRNO(symlink("missing", link)); + ASSERT_OK(rm_rf(link, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_MISSING_OK)); + ASSERT_ERROR_ERRNO(lstat(link, &(struct stat) {}), ENOENT); +} + DEFINE_TEST_MAIN(LOG_DEBUG); From c3eafb10a0b886876316bca6f9af06db67308ad0 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 10 Jul 2026 18:48:35 +0100 Subject: [PATCH 6/7] shift-uid: close consumed directory fds on early return recurse_fd() consumes each directory fd passed to it, but it has no cleanup set up until after take_fdopendir() succeeds. Errors and skipped procfs, sysfs, or read-only subtrees therefore leak the incoming fd. Follow-up for b1fb2d971c810e0bdf9ff0ae567a1c6c230e4e5d --- src/shared/shift-uid.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shared/shift-uid.c b/src/shared/shift-uid.c index e455306be9..e1c53dcb88 100644 --- a/src/shared/shift-uid.c +++ b/src/shared/shift-uid.c @@ -295,8 +295,9 @@ static int is_fs_fully_userns_compatible(const struct statfs *sfs) { F_TYPE_EQUAL(sfs->f_type, SYSFS_MAGIC); } -static int recurse_fd(int fd, const struct stat *st, uid_t shift, bool is_toplevel) { +static int recurse_fd(int input_fd, const struct stat *st, uid_t shift, bool is_toplevel) { _cleanup_closedir_ DIR *d = NULL; + _cleanup_close_ int fd = input_fd; bool changed = false; struct statfs sfs; int r; From 9d4cb85cf5fb71242d705363812adebf92720c79 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 10 Jul 2026 19:08:57 +0100 Subject: [PATCH 7/7] sysext: validate work directory metadata before removal unmerge_hierarchy() joined the persisted work_dir value directly with --root=. An empty value therefore resolved to the root itself and was passed to rm_rf(). Require the decoded metadata to name a non-empty, safe, normalized relative path before constructing the removal target. Add coverage using a disposable root with deliberately emptied metadata. Follow-up for 9cfad502f4aa103ef0d2191cbb6b82fecfbc5044 --- src/sysext/sysext.c | 4 +++ test/units/TEST-50-DISSECT.sysext.sh | 43 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c index 0f93322af0..0cb55a5361 100644 --- a/src/sysext/sysext.c +++ b/src/sysext/sysext.c @@ -1808,6 +1808,10 @@ static int unmerge_hierarchy(const Context *c, const char *p, const char *submou l = cunescape_length(escaped_work_dir_in_root, r, 0, &work_dir_in_root); if (l < 0) return log_error_errno(l, "Failed to unescape work directory path: %m"); + if (path_is_absolute(work_dir_in_root) || !path_is_normalized(work_dir_in_root)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid work directory path '%s'.", work_dir_in_root); + work_dir = path_join(c->root, work_dir_in_root); if (!work_dir) return log_oom(); diff --git a/test/units/TEST-50-DISSECT.sysext.sh b/test/units/TEST-50-DISSECT.sysext.sh index fd7a164478..142c7649bb 100755 --- a/test/units/TEST-50-DISSECT.sysext.sh +++ b/test/units/TEST-50-DISSECT.sysext.sh @@ -798,6 +798,49 @@ test ! -f "$fake_root$hierarchy/now-is-mutable" ) +( init_trap +: "Malformed work_dir metadata is rejected without removing unrelated paths" +[[ -z "$roots_dir" ]] && exit 0 + +fake_root="$roots_dir/empty-work-dir" +hierarchy=/opt +extension_data_dir="$fake_root/var/lib/extensions.mutable$hierarchy" +work_dir_file="$fake_root$hierarchy/.systemd-sysext/work_dir" +traversal_target="$roots_dir/work-dir-traversal-target" +absolute_target="$fake_root/work-dir-absolute-target" + +[[ "$FSTYPE" == "fuseblk" ]] && exit 0 + +prepare_root "$fake_root" "$hierarchy" +prepare_extension_image "$fake_root" "$hierarchy" +prepare_read_only_hierarchy "$fake_root" "$hierarchy" +touch "$fake_root/root-sentinel" +mkdir "$traversal_target" "$absolute_target" +touch "$traversal_target/sentinel" "$absolute_target/sentinel" +prepend_trap "rm -rf ${traversal_target@Q} ${absolute_target@Q}" + +run_systemd_sysext "$fake_root" --mutable=yes merge +prepend_trap "rm -rf ${extension_data_dir@Q}" + +# Mutable overlays bind-mount the metadata directory read-only. Unmount that bind first to simulate +# corrupted on-disk metadata, then try malformed work_dir values. +umount "$fake_root$hierarchy/.systemd-sysext" +work_dir=$(<"$work_dir_file") + +for invalid_work_dir in "" ../work-dir-traversal-target /work-dir-absolute-target; do + printf '%s\n' "$invalid_work_dir" >"$work_dir_file" + (! run_systemd_sysext "$fake_root" unmerge) + test -f "$fake_root/root-sentinel" + test -f "$traversal_target/sentinel" + test -f "$absolute_target/sentinel" +done + +# Restore valid metadata and unmerge normally, so the test case leaves no mounted hierarchy behind. +printf '%s\n' "$work_dir" >"$work_dir_file" +run_systemd_sysext "$fake_root" unmerge +) + + ( init_trap : "/var/lib/extensions.mutable/… does not exist, auto-mutability, read-only merged" fake_root=${roots_dir:+"$roots_dir/simple-read-only-explicit"}