From f134bca68b5b2283483b95567313736359e4612b Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Fri, 22 May 2026 09:15:04 -0700 Subject: [PATCH 1/7] dissect-image: fix swallowed open() error in mountfsd_make_directory mountfsd_make_directory() opens the parent directory after a successful path_extract_filename() call, but reports failure using the stale path_extract_filename() return value: r = path_extract_filename(path, &dirname); if (r < 0) return log_debug_errno(r, ...); ... _cleanup_close_ int fd = open(parent, O_DIRECTORY|O_CLOEXEC); if (fd < 0) return log_debug_errno(r, "Failed to open '%s': %m", parent); At the open() check site r holds the non-negative return of path_extract_filename(). log_debug_errno() with a non-negative first argument returns 0, so the function reports success even though open() failed. Callers that pass a non-NULL ret_directory_fd then consume an uninitialized fd value, and the diagnostic message is suppressed because log_debug_errno(0, ...) emits nothing. Fix by passing errno instead of r, matching the convention used in mountfsd_mount_image() and mountfsd_mount_directory() in the same file. The real open() failure (ENOENT, EACCES, ENOTDIR, EMFILE, ...) now propagates to the caller and the log message is preserved. Fixes: 1be8caa6be6f ("importd: support unpacking tarballs to foreign UID range") Assisted-by: kres (claude-opus-4-7) Signed-off-by: Chris Mason --- src/shared/dissect-image.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 17c14db3ff..f1149cdaa6 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -5509,7 +5509,7 @@ int mountfsd_make_directory( _cleanup_close_ int fd = open(parent, O_DIRECTORY|O_CLOEXEC); if (fd < 0) - return log_debug_errno(r, "Failed to open '%s': %m", parent); + return log_debug_errno(errno, "Failed to open '%s': %m", parent); return mountfsd_make_directory_fd(vl, fd, dirname, mode, flags, ret_directory_fd); } From fa0cd9b79f85dc6d24cd7d3f7c554b35bd5191fd Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Fri, 22 May 2026 09:23:03 -0700 Subject: [PATCH 2/7] dissect-image: fix wrong errno on pread failure In acquire_sig_for_roothash() the pread() failure branch returns -ENOMEM, which is copy-pasted from the malloc() check immediately above it: _cleanup_free_ char *buf = new(char, partition_size+1); if (!buf) return -ENOMEM; ssize_t n = pread(fd, buf, partition_size, partition_offset); if (n < 0) return -ENOMEM; pread() sets errno to the actual I/O failure (EIO, EINTR, EBADF, ...). Aliasing those to -ENOMEM misleads dissect_log_error() and any caller that branches on the returned code; verity signature lookup failures get reported as out-of-memory. Fix by returning -errno from the pread() failure branch. The malloc() branch above is correct and unchanged. Fixes: 98ca65c36aa9 ("dissect: check that roothash in signature matches before selecting partition") Assisted-by: kres (claude-opus-4-7) Signed-off-by: Chris Mason --- src/shared/dissect-image.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index f1149cdaa6..db813a7eb4 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -717,7 +717,7 @@ static int acquire_sig_for_roothash( ssize_t n = pread(fd, buf, partition_size, partition_offset); if (n < 0) - return -ENOMEM; + return -errno; if ((uint64_t) n != partition_size) return -EIO; From 2eff83757409db0a3eedfcdff1f2445a291c8ab7 Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Fri, 22 May 2026 09:13:50 -0700 Subject: [PATCH 3/7] dissect-image: cast to uint64_t before *512 for sig lookup In the PARTITION_ROOT_VERITY_SIG / PARTITION_USR_VERITY_SIG branch of dissect_image(), the call to acquire_sig_for_roothash() passes the partition offset and size as acquire_sig_for_roothash(fd, start * 512, size * 512, &root_hash, NULL); where start and size are blkid_loff_t (int64_t). The literal 512 is int, so the multiplication is performed in int64_t and overflows for LBA values in (INT64_MAX/512, INT64_MAX]. The overflowed signed result is then converted to the uint64_t parameters of acquire_sig_for_roothash(), yielding a near-UINT64_MAX partition_offset that slips past the sole offset guard (an exact == UINT64_MAX sentinel) and reaches pread() with a corrupt disk offset. An overflowed partition_size would be rejected by the 4 MiB EFBIG check at src/shared/dissect-image.c:710-712 before pread(); the dangerous scenario is a corrupt offset combined with a legitimate small size. The three sibling call sites at src/shared/dissect-image.c:1284, 1589-1590 and 1671-1672 all cast to uint64_t before multiplying by 512; only this call site was missed. Fix by casting start and size to uint64_t before the multiplication, matching the sibling sites and making the arithmetic well-defined. Fixes: 98ca65c36aa9 ("dissect: check that roothash in signature matches before selecting partition") Assisted-by: kres (claude-opus-4-7) Signed-off-by: Chris Mason --- src/shared/dissect-image.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index db813a7eb4..e00dce3fbd 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -1417,8 +1417,8 @@ static int dissect_image( r = acquire_sig_for_roothash( fd, - start * 512, - size * 512, + (uint64_t) start * 512, + (uint64_t) size * 512, &root_hash, /* ret_root_hash_sig= */ NULL); if (r < 0) From 700d8e73d4c203b34d6f82c893fbb6c9b5d956d8 Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Fri, 22 May 2026 09:14:51 -0700 Subject: [PATCH 4/7] sd-dhcp6-client: fix ref leak on duplicate option add sd_dhcp6_client_add_option() and sd_dhcp6_client_add_vendor_option() store the caller's sd_dhcp6_option in an ordered container whose value destructor is sd_dhcp6_option_unref, registered via DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR in dhcp6_option_hash_ops. The container therefore owns exactly one ref per stored slot. Both helpers guarded the ref bump only on r < 0: r = ordered_{hashmap,set}_ensure_put(..., v); if (r < 0) return r; sd_dhcp6_option_ref(v); ordered_hashmap_ensure_put() (and the ordered_set wrapper) forward the underlying hashmap_put_boldly tri-state verbatim: 1 on fresh insert, 0 when the identical key+value pair is already stored (no new slot allocated), <0 on error. The r == 0 path fell through and bumped the refcount without a corresponding slot. The extra ref is permanently stranded once the container's destructor runs. In a long-running networkd that reapplies .network files on reload this leaks one sd_dhcp6_option allocation per duplicate-add. Fix by tightening the guard to r <= 0 in both helpers, so sd_dhcp6_option_ref(v) is reached only on a genuine new slot (r == 1). Propagate the tri-valued result by returning r from both helpers instead of a hard-coded constant. sd_dhcp6_client_add_vendor_option() is a public libsystemd sd_ API; its success return changes from a constant 1 to {0, 1}. sd_dhcp6_client_add_option() now also propagates the tri-valued result (previously a hard-coded 0 on success), giving the two parallel helpers identical return contracts. The sole non-fuzz in-tree callers are both in dhcp6_configure() (src/network/networkd-dhcp6.c), which treat any r >= 0 as success and do not distinguish 0 from 1, so the contract widening is caller-safe. Fixes: e7d5fe17db9d ("DHCP client: make SendOption work for DHCPv6 too.") Assisted-by: kres (claude-opus-4-7) Signed-off-by: Chris Mason --- src/libsystemd-network/sd-dhcp6-client.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libsystemd-network/sd-dhcp6-client.c b/src/libsystemd-network/sd-dhcp6-client.c index 5e71d63de5..e2415a5b47 100644 --- a/src/libsystemd-network/sd-dhcp6-client.c +++ b/src/libsystemd-network/sd-dhcp6-client.c @@ -182,12 +182,12 @@ int sd_dhcp6_client_add_vendor_option(sd_dhcp6_client *client, sd_dhcp6_option * } r = ordered_set_ensure_put(&client->vendor_options, &dhcp6_option_hash_ops, v); - if (r < 0) + if (r <= 0) return r; sd_dhcp6_option_ref(v); - return 1; + return r; } static int client_ensure_duid(sd_dhcp6_client *client) { @@ -541,11 +541,11 @@ int sd_dhcp6_client_add_option(sd_dhcp6_client *client, sd_dhcp6_option *v) { assert_return(v, -EINVAL); r = ordered_hashmap_ensure_put(&client->extra_options, &dhcp6_option_hash_ops, UINT_TO_PTR(v->option), v); - if (r < 0) + if (r <= 0) return r; sd_dhcp6_option_ref(v); - return 0; + return r; } static void client_set_state(sd_dhcp6_client *client, DHCP6State state) { From 4e02162a6e88a38e509e0646d6d39f14a8e14490 Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Fri, 22 May 2026 08:49:27 -0700 Subject: [PATCH 5/7] core/exec-invoke: chdir("/") after chroot in apply_root_directory apply_root_directory() calls chroot() but never pairs it with a chdir("/"). Moving cwd into the new root is left to the subsequent apply_working_directory() call. That delegation breaks when the unit sets WorkingDirectory=-/path (working_directory_missing_ok): apply_root_directory() apply_working_directory() chroot(new_root) --> chdir(wd) fails (ENOENT) return 0 return 0 /* missing_ok */ chroot(2) changes only the process root, not cwd. With cwd still resolving to the pre-chroot host dentry, the process runs with the kernel root pointing at the new root while relative-path accesses and /proc/self/cwd reach paths outside RootDirectory=. Only the chroot-only branch (exec_needs_mount_namespace() == false) is affected. The mount-namespace branch uses pivot_root(), which restructures the mount tree and makes the pre-chroot dentry unreachable. Fix by issuing chdir("/") immediately after the successful chroot() in apply_root_directory() and propagating any failure with EXIT_CHROOT, matching the standard chroot+chdir idiom and making the chroot self-contained regardless of how apply_working_directory() later handles a missing directory. Assisted-by: kres (claude-opus-4-7) Signed-off-by: Chris Mason --- src/core/exec-invoke.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 4f935c1439..183442aef3 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -4133,12 +4133,22 @@ static int apply_root_directory( assert(exit_status); if (params->flags & EXEC_APPLY_CHROOT) - if (!needs_mount_ns && context->root_directory) + if (!needs_mount_ns && context->root_directory) { if (chroot(runtime->ephemeral_copy ?: context->root_directory) < 0) { *exit_status = EXIT_CHROOT; return -errno; } + /* chroot(2) changes only the process root, not cwd. Move cwd into the new root + * immediately so that a subsequent apply_working_directory() failure that is + * silently ignored via working_directory_missing_ok cannot leave cwd pointing + * at a pre-chroot dentry outside the new root. */ + if (chdir("/") < 0) { + *exit_status = EXIT_CHROOT; + return -errno; + } + } + return 0; } From 99aff237831f318a817ab2dd786ab3d80979c7bb Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Fri, 22 May 2026 09:15:50 -0700 Subject: [PATCH 6/7] core/exec-invoke: log write() allow-list widening apply_syscall_filter() unconditionally inserts the write() syscall into c->syscall_filter when exec_fd or handoff_timestamp_fd is in use, so the parent can receive the exec status / handoff timestamp from the child. When the unit configured a positive SystemCallFilter= allow-list that deliberately omits write(), the resulting widening of the operator's policy happens silently with no trace in the journal. Emit a log_debug() before the seccomp_filter_set_add_by_name() call when syscall_allow_list is true, so the widening is at least observable to operators inspecting the unit's debug log. While here, document that mutating c->syscall_filter through a 'const ExecContext *c' is intentional: apply_syscall_filter() runs only in the post-fork child, which owns a private copy of the address space, so the hashmap change is never observed by the manager. No functional change for the allow-list itself; write() is still added exactly as before. Fixes: 84b79215ccc5 ("core: do not filter out write() if required in the very late stage") Assisted-by: kres (claude-opus-4-7) Signed-off-by: Chris Mason --- src/core/exec-invoke.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 183442aef3..7c16ffc7f0 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -1638,8 +1638,16 @@ static int apply_syscall_filter(const ExecContext *c, const ExecParameters *p) { action = negative_action; } - /* Sending over exec_fd or handoff_timestamp_fd requires write() syscall. */ + /* Sending over exec_fd or handoff_timestamp_fd requires write() syscall. + * + * Note: this mutates c->syscall_filter despite the 'const ExecContext *c' qualifier. + * That is intentional and safe here because apply_syscall_filter() runs only in the + * post-fork child, which holds a private copy of the address space; the hashmap + * change is never visible to the manager process. */ if (p->exec_fd >= 0 || p->handoff_timestamp_fd >= 0) { + if (c->syscall_allow_list) + log_debug("SystemCallFilter= allow-list in effect; adding 'write' syscall required for exec handoff."); + r = seccomp_filter_set_add_by_name(c->syscall_filter, c->syscall_allow_list, "write"); if (r < 0) return r; From 3f0aa5852ec4f53f7412cd8b86fd9d064bebd108 Mon Sep 17 00:00:00 2001 From: Chris Mason Date: Fri, 22 May 2026 09:13:55 -0700 Subject: [PATCH 7/7] libsystemd-network: propagate sd_event_default() failure Four *_attach_event helpers swallow sd_event_default() failures and return success while leaving obj->event NULL: sd_dhcp_client_attach_event (sd-dhcp-client.c) sd_dhcp6_client_attach_event (sd-dhcp6-client.c) sd_ndisc_attach_event (sd-ndisc.c) sd_radv_attach_event (sd-radv.c) Each contains the same copy-pasted typo in the NULL-event branch: r = sd_event_default(&obj->event); if (r < 0) return 0; /* swallows -ENOMEM / -ECHILD */ The caller is told the attach succeeded, but obj->event is still NULL. A subsequent *_start() then trips assert_return(obj->event, -EINVAL) and returns a spurious -EINVAL (or aborts on assert-abort builds), and any consumer of *_get_event() dereferences NULL. The sibling helper sd_dhcp_server_attach_event already uses the correct pattern: r = sd_event_default(&server->event); if (r < 0) return r; Fix by returning r instead of 0 at each of the four sites so the sd_event_default() errno is propagated to the caller and ownership stays with the caller. Assisted-by: kres (claude-opus-4-7) Signed-off-by: Chris Mason --- src/libsystemd-network/sd-dhcp-client.c | 2 +- src/libsystemd-network/sd-dhcp6-client.c | 2 +- src/libsystemd-network/sd-ndisc.c | 2 +- src/libsystemd-network/sd-radv.c | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index 5675c7d787..c6e73dbab0 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -1350,7 +1350,7 @@ int sd_dhcp_client_attach_event(sd_dhcp_client *client, sd_event *event, int64_t else { r = sd_event_default(&client->event); if (r < 0) - return 0; + return r; } client->event_priority = priority; diff --git a/src/libsystemd-network/sd-dhcp6-client.c b/src/libsystemd-network/sd-dhcp6-client.c index e2415a5b47..b78d1c29ec 100644 --- a/src/libsystemd-network/sd-dhcp6-client.c +++ b/src/libsystemd-network/sd-dhcp6-client.c @@ -1507,7 +1507,7 @@ int sd_dhcp6_client_attach_event(sd_dhcp6_client *client, sd_event *event, int64 else { r = sd_event_default(&client->event); if (r < 0) - return 0; + return r; } client->event_priority = priority; diff --git a/src/libsystemd-network/sd-ndisc.c b/src/libsystemd-network/sd-ndisc.c index b6a55a5095..51e12d4528 100644 --- a/src/libsystemd-network/sd-ndisc.c +++ b/src/libsystemd-network/sd-ndisc.c @@ -137,7 +137,7 @@ int sd_ndisc_attach_event(sd_ndisc *nd, sd_event *event, int64_t priority) { else { r = sd_event_default(&nd->event); if (r < 0) - return 0; + return r; } nd->event_priority = priority; diff --git a/src/libsystemd-network/sd-radv.c b/src/libsystemd-network/sd-radv.c index 948f7bc382..df5fcc6c02 100644 --- a/src/libsystemd-network/sd-radv.c +++ b/src/libsystemd-network/sd-radv.c @@ -58,7 +58,7 @@ int sd_radv_attach_event(sd_radv *ra, sd_event *event, int64_t priority) { else { r = sd_event_default(&ra->event); if (r < 0) - return 0; + return r; } ra->event_priority = priority;