From 8729fa0e4b1ba89e4c18c7dfe367e744ae892a81 Mon Sep 17 00:00:00 2001 From: dongshengyuan <545258830@qq.com> Date: Thu, 16 Jul 2026 14:18:01 +0800 Subject: [PATCH 1/7] id128: reject app-specific IDs for new Reject --app-specific= for the new verb. The option only makes sense for verbs that derive IDs from an existing base ID. Reproducer: systemd-id128 new --app-specific=4f68bce3e8cd4db196e7fbcaf984b709 Before: the command printed a random ID and exited successfully. Follow-up: 0d1d512f7f42071595f0c950f911f3557fda09ea --- src/id128/id128.c | 4 ++++ test/units/TEST-74-AUX-UTILS.id128.sh | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/id128/id128.c b/src/id128/id128.c index a4452f1cc5..5326ef1f51 100644 --- a/src/id128/id128.c +++ b/src/id128/id128.c @@ -52,6 +52,10 @@ static int verb_new(int argc, char *argv[], uintptr_t _data, void *userdata) { sd_id128_t id; int r; + if (!sd_id128_is_null(arg_app)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Verb \"new\" cannot be combined with --app-specific=."); + r = sd_id128_randomize(&id); if (r < 0) return log_error_errno(r, "Failed to generate ID: %m"); diff --git a/test/units/TEST-74-AUX-UTILS.id128.sh b/test/units/TEST-74-AUX-UTILS.id128.sh index 5da2323461..518642442d 100755 --- a/test/units/TEST-74-AUX-UTILS.id128.sh +++ b/test/units/TEST-74-AUX-UTILS.id128.sh @@ -36,7 +36,6 @@ systemd-id128 show root --value --json=short | jq -e '.id | type == "string"' systemd-id128 --json=short new | jq -e '.id | type == "string"' systemd-id128 new -p systemd-id128 new -u -systemd-id128 new -a 4f68bce3e8cd4db196e7fbcaf984b709 systemd-id128 machine-id assert_eq "$(systemd-id128 --json=short machine-id | jq --raw-output .id)" "$( Date: Thu, 16 Jul 2026 14:21:02 +0800 Subject: [PATCH 2/7] sysusers: validate shell credentials Validate passwd.shell. credentials with the same login-shell rules used for sysusers.d shell fields before writing passwd entries. Reproducer: CREDENTIALS_DIRECTORY=... systemd-sysusers --root=... with --inline 'u creduser 999 "Cred User" / -' Before: relative-shell was written as the login shell and the command exited successfully. Follow-up: 99e9f896fb491d13c6d02f6f5bbfceabae833f05 --- src/sysusers/sysusers.c | 48 +++++++++++++++++++----- test/units/TEST-74-AUX-UTILS.sysusers.sh | 9 +++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/sysusers/sysusers.c b/src/sysusers/sysusers.c index 0970b76de2..8eb1ae13c5 100644 --- a/src/sysusers/sysusers.c +++ b/src/sysusers/sysusers.c @@ -468,6 +468,35 @@ static const char* pick_shell(const Item *i) { return NOLOGIN; } +static int read_shell_credential(const Item *i, char **ret) { + _cleanup_free_ char *cn = NULL, *shell = NULL; + int r; + + assert(i); + + cn = strjoin("passwd.shell.", i->name); + if (!cn) + return -ENOMEM; + + r = read_credential(cn, (void**) &shell, /* ret_size= */ NULL); + if (r < 0) { + log_debug_errno(r, "Couldn't read credential '%s', ignoring: %m", cn); + if (ret) + *ret = NULL; + return 0; + } + + path_simplify(shell); + if (!valid_shell(shell)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Shell credential '%s' specifies invalid login shell '%s'.", + cn, shell); + + if (ret) + *ret = TAKE_PTR(shell); + return 0; +} + static int write_temporary_passwd( Context *c, const char *passwd_path, @@ -488,6 +517,12 @@ static int write_temporary_passwd( goto done; if (arg_dry_run) { + ORDERED_HASHMAP_FOREACH(i, c->todo_uids) { + r = read_shell_credential(i, /* ret= */ NULL); + if (r < 0) + return r; + } + log_info("Would write /etc/passwd%s", glyph(GLYPH_ELLIPSIS)); goto done; } @@ -540,7 +575,7 @@ static int write_temporary_passwd( } ORDERED_HASHMAP_FOREACH(i, c->todo_uids) { - _cleanup_free_ char *creds_shell = NULL, *cn = NULL; + _cleanup_free_ char *creds_shell = NULL; struct passwd n = { .pw_name = i->name, @@ -559,15 +594,10 @@ static int write_temporary_passwd( .pw_shell = (char*) pick_shell(i), }; - /* Try to pick up the shell for this account via the credentials logic */ - cn = strjoin("passwd.shell.", i->name); - if (!cn) - return -ENOMEM; - - r = read_credential(cn, (void**) &creds_shell, NULL); + r = read_shell_credential(i, &creds_shell); if (r < 0) - log_debug_errno(r, "Couldn't read credential '%s', ignoring: %m", cn); - else + return r; + if (creds_shell) n.pw_shell = creds_shell; r = putpwent_sane(&n, passwd); diff --git a/test/units/TEST-74-AUX-UTILS.sysusers.sh b/test/units/TEST-74-AUX-UTILS.sysusers.sh index 6e3fcac8c5..3322d337d8 100755 --- a/test/units/TEST-74-AUX-UTILS.sysusers.sh +++ b/test/units/TEST-74-AUX-UTILS.sysusers.sh @@ -30,6 +30,15 @@ at_exit() { trap at_exit EXIT +root="$(mktemp -d)" +cred="$(mktemp -d)" +mkdir -p "$root/etc" +printf 'relative-shell' >"$cred/passwd.shell.creduser" +(! env CREDENTIALS_DIRECTORY="$cred" systemd-sysusers --dry-run --root="$root" --inline 'u creduser 999 "Cred User" / -') +(! env CREDENTIALS_DIRECTORY="$cred" systemd-sysusers --root="$root" --inline 'u creduser 999 "Cred User" / -') +(! grep -F creduser "$root/etc/passwd" >/dev/null 2>&1) +rm -rf "$root" "$cred" + # Ensure that a non-responsive NSS socket doesn't make sysusers fail mount -t tmpfs tmpfs /run/systemd/userdb/ touch /run/systemd/userdb/io.systemd.DynamicUser From 50eaa51ce126ac618b096bb324f431d8b0be7286 Mon Sep 17 00:00:00 2001 From: dongshengyuan <545258830@qq.com> Date: Thu, 16 Jul 2026 14:23:35 +0800 Subject: [PATCH 3/7] firstboot: validate root shell credentials Validate passwd.shell.root after reading the credential so it uses the same target-root shell checks as --root-shell= and prompted input. Reproducer: CREDENTIALS_DIRECTORY=... systemd-firstboot --root=... with passwd.shell.root=/bin/nonexistentshell Before: /etc/passwd was written with the nonexistent root shell and the command exited successfully. Follow-up: 416f7b3a11e00d1a43da950fb4a00bc6c2707013 --- src/firstboot/firstboot.c | 4 ++++ test/units/TEST-74-AUX-UTILS.firstboot.sh | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index f3ea57ec92..5ebaf89764 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -998,6 +998,10 @@ static int prompt_root_shell(int rfd, sd_varlink **mute_console_link) { if (r < 0) log_debug_errno(r, "Failed to read credential passwd.shell.root, ignoring: %m"); else { + r = find_shell(rfd, arg_root_shell); + if (r < 0) + return r; + log_debug("Acquired root shell from credential."); return 0; } diff --git a/test/units/TEST-74-AUX-UTILS.firstboot.sh b/test/units/TEST-74-AUX-UTILS.firstboot.sh index 8c3c222c88..4fbbb18efa 100755 --- a/test/units/TEST-74-AUX-UTILS.firstboot.sh +++ b/test/units/TEST-74-AUX-UTILS.firstboot.sh @@ -352,6 +352,11 @@ grep -q "^root::" "$ROOT/etc/shadow" rm -fv "$ROOT/etc/passwd" "$ROOT/etc/shadow" (! systemd-firstboot --root="$ROOT" --root-shell=/bin/nonexistentshell) +WCREDS="$(mktemp -d)" +printf '/bin/nonexistentshell' >"$WCREDS/passwd.shell.root" +(! CREDENTIALS_DIRECTORY="$WCREDS" systemd-firstboot --root="$ROOT") +[[ ! -e "$ROOT/etc/passwd" ]] +rm -rf "$WCREDS" (! systemd-firstboot --root="$ROOT" --machine-id=invalidmachineid) (! systemd-firstboot --root="$ROOT" --timezone=Foo/Bar) From e7e68ec449b2f47a5cfccc77c701483783c51c96 Mon Sep 17 00:00:00 2001 From: dongshengyuan <545258830@qq.com> Date: Thu, 16 Jul 2026 14:32:14 +0800 Subject: [PATCH 4/7] repart: skip generated files during dry runs Skip generated fstab and crypttab output when --dry-run=yes is used, after confirming there is eligible content to generate. Reproducer: systemd-repart --dry-run=yes --generate-fstab=/tmp/root/etc/fstab - Before: the command exited successfully and still wrote the requested fstab file. Follow-up: 1a0541d44c78ced78a566051ec8f63417370aeaa --- src/repart/repart.c | 10 ++++++++++ test/units/TEST-58-REPART.sh | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/repart/repart.c b/src/repart/repart.c index bd8df61263..e07370805b 100644 --- a/src/repart/repart.c +++ b/src/repart/repart.c @@ -9493,6 +9493,11 @@ static int context_fstab(Context *context) { return 0; } + if (arg_dry_run) { + log_notice("Running in dry run mode, not generating %s", arg_generate_fstab); + return 0; + } + path = path_join(arg_copy_source, arg_generate_fstab); if (!path) return log_oom(); @@ -9625,6 +9630,11 @@ static int context_crypttab(Context *context, bool late) { return 0; } + if (arg_dry_run) { + log_notice("Running in dry run mode, not generating %s", arg_generate_crypttab); + return 0; + } + path = path_join(arg_copy_source, arg_generate_crypttab); if (!path) return log_oom(); diff --git a/test/units/TEST-58-REPART.sh b/test/units/TEST-58-REPART.sh index d5fe4897ea..dbc5017d28 100755 --- a/test/units/TEST-58-REPART.sh +++ b/test/units/TEST-58-REPART.sh @@ -2873,6 +2873,35 @@ EOF losetup -d "$loop" } +testcase_generate_fstab_dry_run() { + local defs root + + defs="$(mktemp --directory "/tmp/test-repart.defs.XXXXXXXXXX")" + root="$(mktemp --directory "/var/test-repart.root.XXXXXXXXXX")" + # shellcheck disable=SC2064 + trap "rm -rf '$defs' '$root'" RETURN + chmod 0755 "$defs" + + echo "*** testcase for skipping generated fstab in dry-run mode ***" + + mkdir -p "$root/etc" + tee "$defs/root.conf" < Date: Thu, 16 Jul 2026 14:37:01 +0800 Subject: [PATCH 5/7] sysupdate: keep JSON check-new exit status consistent Return failure for --json check-new when no candidate is available. Let sysupdated accept that exact JSON no-update result from workers. Reproducer: systemd-sysupdate --verify=no --json=short check-new Before: the command printed {"available":null} and exited successfully. Follow-up: 42c0b689a800b2ec7cceb1528d3834bf9b3417f8 --- src/sysupdate/sysupdate.c | 3 +++ src/sysupdate/sysupdated.c | 25 ++++++++++++++++++------- test/units/TEST-72-SYSUPDATE.sh | 5 +++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 28ea65c033..ac76d4fdba 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -2531,6 +2531,9 @@ static int verb_check_new(int argc, char *argv[], uintptr_t _data, void *userdat r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL); if (r < 0) return log_error_errno(r, "Failed to print JSON: %m"); + + if (!context.candidate) + return EXIT_FAILURE; } return EXIT_SUCCESS; diff --git a/src/sysupdate/sysupdated.c b/src/sysupdate/sysupdated.c index 13309e352e..184444a6f5 100644 --- a/src/sysupdate/sysupdated.c +++ b/src/sysupdate/sysupdated.c @@ -323,13 +323,24 @@ static int job_on_exit(sd_event_source *s, const siginfo_t *si, void *userdata) sd_bus_error_setf(&error, SD_BUS_ERROR_FAILED, "Job terminated abnormally with signal %s.", signal_to_string(si->si_status)); - } else if (si->si_status != EXIT_SUCCESS) - if (j->status_errno != 0) - sd_bus_error_set_errno(&error, j->status_errno); - else - sd_bus_error_setf(&error, SD_BUS_ERROR_FAILED, - "Job failed with exit code %i.", si->si_status); - else { + } else if (si->si_status != EXIT_SUCCESS) { + bool check_new_no_update = false; + + if (j->type == JOB_CHECK_NEW && + si->si_status == EXIT_FAILURE && + job_parse_child_output(TAKE_FD(j->stdout_fd), &json) >= 0) { + sd_json_variant *v = sd_json_variant_by_key(json, "available"); + check_new_no_update = v && sd_json_variant_is_null(v); + } + + if (!check_new_no_update) { + if (j->status_errno != 0) + sd_bus_error_set_errno(&error, j->status_errno); + else + sd_bus_error_setf(&error, SD_BUS_ERROR_FAILED, + "Job failed with exit code %i.", si->si_status); + } + } else { r = job_parse_child_output(TAKE_FD(j->stdout_fd), &json); if (r < 0) sd_bus_error_set_errnof(&error, r, "Failed to parse job worker output: %m"); diff --git a/test/units/TEST-72-SYSUPDATE.sh b/test/units/TEST-72-SYSUPDATE.sh index 84c057c372..833bd936fa 100755 --- a/test/units/TEST-72-SYSUPDATE.sh +++ b/test/units/TEST-72-SYSUPDATE.sh @@ -143,9 +143,14 @@ new_version() { check_no_new_update_available() { local client="${1:?}" + local output if [[ "$client" == "sysupdate-cli" ]]; then (! "$SYSUPDATE" --verify=no check-new) + if output="$("$SYSUPDATE" --verify=no --json=short check-new)"; then + exit 1 + fi + [[ "$output" == '{"available":null}' ]] elif [[ "$client" == "varlink" ]]; then (! varlinkctl call "$VARLINK_SOCKET" io.systemd.SysUpdate.CheckNew '{"target":{"class":"host"}}') |& grep io.systemd.SysUpdate.NoUpdateNeeded >/dev/null else From dab2a82a30355c8f1fc51c6655bdc2c5f5844755 Mon Sep 17 00:00:00 2001 From: dongshengyuan <545258830@qq.com> Date: Thu, 16 Jul 2026 14:39:42 +0800 Subject: [PATCH 6/7] dissect: include image size in JSON output Use the image metadata size when building JSON output, matching the value already shown by the text output. Reproducer: systemd-dissect --json=short image.raw Before: text output showed Size, but JSON omitted the top-level size field. Follow-up: be5bee2a132d2d3b45d79bb3f27b05bbc767cd0a --- src/dissect/dissect.c | 3 +-- test/units/TEST-50-DISSECT.dissect.sh | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 602306d9b9..c5b56b44d6 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -890,7 +890,6 @@ static int action_dissect( _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; _cleanup_(table_unrefp) Table *t = NULL; _cleanup_free_ char *bn = NULL; - uint64_t size = UINT64_MAX; int r; assert(m); @@ -1009,7 +1008,7 @@ static int action_dissect( r = sd_json_buildo( &v, SD_JSON_BUILD_PAIR_STRING("name", bn), - SD_JSON_BUILD_PAIR_CONDITION(size != UINT64_MAX, "size", SD_JSON_BUILD_INTEGER(size)), + SD_JSON_BUILD_PAIR_CONDITION(m->image_size != UINT64_MAX, "size", SD_JSON_BUILD_INTEGER(m->image_size)), SD_JSON_BUILD_PAIR_INTEGER("sectorSize", m->sector_size), SD_JSON_BUILD_PAIR_CONDITION(a >= 0, "architecture", SD_JSON_BUILD_STRING(architecture_to_string(a))), SD_JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(m->image_uuid), "imageUuid", SD_JSON_BUILD_UUID(m->image_uuid)), diff --git a/test/units/TEST-50-DISSECT.dissect.sh b/test/units/TEST-50-DISSECT.dissect.sh index 9fc15d04da..5a56da55f7 100755 --- a/test/units/TEST-50-DISSECT.dissect.sh +++ b/test/units/TEST-50-DISSECT.dissect.sh @@ -19,6 +19,7 @@ fi systemd-dissect --json=short "$MINIMAL_IMAGE.raw" | \ grep -F '{"rw":"ro","designator":"root","partition_uuid":null,"partition_label":null,"fstype":"squashfs","architecture":null,"verity":"external"' >/dev/null +systemd-dissect --json=short "$MINIMAL_IMAGE.raw" | grep -F '"size":' >/dev/null systemd-dissect "$MINIMAL_IMAGE.raw" | grep -F "MARKER=1" >/dev/null # shellcheck disable=SC2153 systemd-dissect "$MINIMAL_IMAGE.raw" | grep -F -f <(sed 's/"//g' "$OS_RELEASE") >/dev/null From 7a81ad4b6bf8b986b50edd788d597f77673e9be6 Mon Sep 17 00:00:00 2001 From: dongshengyuan <545258830@qq.com> Date: Thu, 16 Jul 2026 14:44:08 +0800 Subject: [PATCH 7/7] creds: reject empty validity intervals Reject credentials whose not-after timestamp is equal to the timestamp at each encryption entry point. Reproducer: systemd-creds --timestamp=TS --not-after=TS encrypt in out Before: encryption succeeded, but decrypt refused the credential as not in order. Follow-up: 21bc0b6fa1de44b520353b935bf14160f9f70591 --- src/creds/creds.c | 6 +++--- src/shared/creds-util.c | 4 ++-- test/units/TEST-54-CREDS.sh | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/creds/creds.c b/src/creds/creds.c index 765185dbe7..d863ed8386 100644 --- a/src/creds/creds.c +++ b/src/creds/creds.c @@ -599,8 +599,8 @@ static int verb_encrypt(int argc, char *argv[], uintptr_t _data, void *userdata) timestamp = arg_timestamp != USEC_INFINITY ? arg_timestamp : now(CLOCK_REALTIME); - if (arg_not_after != USEC_INFINITY && arg_not_after < timestamp) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential is invalidated before it is valid."); + if (arg_not_after != USEC_INFINITY && arg_not_after <= timestamp) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential is invalidated before or when it becomes valid."); if (geteuid() != 0 && !sd_id128_equal(arg_with_key, CRED_AES256_GCM_BY_NULL)) { (void) polkit_agent_open_if_enabled(BUS_TRANSPORT_LOCAL, arg_ask_password); @@ -1257,7 +1257,7 @@ static int vl_method_encrypt(sd_varlink *link, sd_json_variant *parameters, sd_v timestamp_fresh = true; } else timestamp_fresh = timestamp_is_fresh(p.timestamp); - if (p.not_after != UINT64_MAX && p.not_after < p.timestamp) + if (p.not_after != UINT64_MAX && p.not_after <= p.timestamp) return sd_varlink_error_invalid_parameter_name(link, "notAfter"); r = settle_scope(link, &p.scope, &p.uid, &cflags, /* any_scope_after_polkit= */ NULL); diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index 09bbf6bca8..bbba1c3eae 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -900,8 +900,8 @@ int encrypt_credential_and_warn( if (name && !credential_name_valid(name)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid credential name: %s", name); - if (not_after != USEC_INFINITY && timestamp != USEC_INFINITY && not_after < timestamp) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential is invalidated before it is valid (" USEC_FMT " < " USEC_FMT ").", not_after, timestamp); + if (not_after != USEC_INFINITY && timestamp != USEC_INFINITY && not_after <= timestamp) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential is invalidated before or when it becomes valid (" USEC_FMT " <= " USEC_FMT ").", not_after, timestamp); if (DEBUG_LOGGING) { char buf[FORMAT_TIMESTAMP_MAX]; diff --git a/test/units/TEST-54-CREDS.sh b/test/units/TEST-54-CREDS.sh index 7c2774fa10..75d1c41253 100755 --- a/test/units/TEST-54-CREDS.sh +++ b/test/units/TEST-54-CREDS.sh @@ -167,6 +167,8 @@ systemd-creds --not-after="+1d" encrypt /tmp/cred.orig /tmp/cred.enc systemd-creds decrypt /tmp/cred.enc /tmp/cred.dec diff /tmp/cred.orig /tmp/cred.dec rm -f /tmp/cred.{enc,dec} +ts="$(date -u '+%Y-%m-%d %H:%M:%S UTC')" +(! systemd-creds --with-key=null --timestamp="$ts" --not-after="$ts" encrypt /tmp/cred.orig /tmp/cred.enc) # Null-key credentials and the systemd.credentials_boot_policy= setting. #