From 58496809c0fcfb2364b11b36a76e0d79d4b9dec8 Mon Sep 17 00:00:00 2001 From: dongshengyuan <545258830@qq.com> Date: Wed, 15 Jul 2026 13:22:04 +0800 Subject: [PATCH 1/6] tmpfiles: keep directory specifiers rootless %t and the other directory specifiers are parsed as normal paths. They are prefixed by --root= later, so the specifier itself must stay rootless. Reproducer: tmp=$(mktemp -d /tmp/tmpfiles-root.XXXXXX) conf=$(mktemp /tmp/tmpfiles-conf.XXXXXX) printf 'd %%t/tmpfiles-repro 0755 - - -\n' >"$conf" systemd-tmpfiles --root="$tmp" --create --dry-run "$conf" Before: Would create directory $tmp/$tmp/run/tmpfiles-repro Follow-up for de61a04b188f81a85cdb5c64ddb4987dcd9d30d3. --- src/tmpfiles/tmpfiles.c | 11 +---------- test/units/TEST-22-TMPFILES.15.sh | 12 ++++++++++++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c index 82f6b8da2f..0e3244fceb 100644 --- a/src/tmpfiles/tmpfiles.c +++ b/src/tmpfiles/tmpfiles.c @@ -322,16 +322,7 @@ static int specifier_directory( if (r < 0) return r; - if (arg_root) { - _cleanup_free_ char *j = NULL; - - j = path_join(arg_root, p); - if (!j) - return -ENOMEM; - - *ret = TAKE_PTR(j); - } else - *ret = TAKE_PTR(p); + *ret = TAKE_PTR(p); return 0; } diff --git a/test/units/TEST-22-TMPFILES.15.sh b/test/units/TEST-22-TMPFILES.15.sh index 14c26b8295..68c6c301bd 100755 --- a/test/units/TEST-22-TMPFILES.15.sh +++ b/test/units/TEST-22-TMPFILES.15.sh @@ -42,3 +42,15 @@ systemd-tmpfiles --create --root="$root" - <&1 < Date: Wed, 15 Jul 2026 13:23:23 +0800 Subject: [PATCH 2/6] tmpfiles: check relative L? targets beside the link Relative symlink targets are resolved from the directory containing the link. Make the L? existence check use the same rule. Reproducer: tmp=$(mktemp -d /tmp/tmpfiles-link.XXXXXX) conf=$(mktemp /tmp/tmpfiles-conf.XXXXXX) printf data >"$tmp/target" printf 'L? %s/link - - - - target\n' "$tmp" >"$conf" systemd-tmpfiles --create --dry-run "$conf" Before: The target was checked root-relative, not beside the link. Follow-up for b5dc805583bbb019c6bf4c73bf0814a396dc0f12. --- src/tmpfiles/tmpfiles.c | 24 +++++++++++++++++----- test/units/TEST-22-TMPFILES.15.sh | 33 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c index 0e3244fceb..42512da604 100644 --- a/src/tmpfiles/tmpfiles.c +++ b/src/tmpfiles/tmpfiles.c @@ -2680,16 +2680,30 @@ static int create_symlink(Context *c, Item *i) { assert(i); if (i->ignore_if_target_missing) { - r = chase(i->argument, arg_root, CHASE_SAFE|CHASE_PREFIX_ROOT|CHASE_NOFOLLOW, /* ret_path= */ NULL, /* ret_fd= */ NULL); + _cleanup_free_ char *target = NULL; + ChaseFlags chase_flags = CHASE_SAFE|CHASE_NOFOLLOW; + + if (!path_is_absolute(i->argument)) { + _cleanup_free_ char *dn = NULL; + + r = path_extract_directory(i->path, &dn); + if (r < 0) + return log_error_errno(r, "Failed to extract directory from path '%s': %m", i->path); + + target = path_join(dn, i->argument); + if (!target) + return log_oom(); + } else + chase_flags |= CHASE_PREFIX_ROOT; + + r = chase(target ?: i->argument, arg_root, chase_flags, /* ret_path= */ NULL, /* ret_fd= */ NULL); if (r == -ENOENT) { /* Silently skip over lines where the source file is missing. */ - log_debug_errno(r, "Symlink source path '%s/%s' does not exist, skipping line.", - strempty(arg_root), skip_leading_slash(i->argument)); + log_debug_errno(r, "Symlink source path '%s' does not exist, skipping line.", i->argument); return 0; } if (r < 0) - return log_error_errno(r, "Failed to check if symlink source path '%s/%s' exists: %m", - strempty(arg_root), skip_leading_slash(i->argument)); + return log_error_errno(r, "Failed to check if symlink source path '%s' exists: %m", i->argument); } r = path_extract_filename(i->path, &bn); diff --git a/test/units/TEST-22-TMPFILES.15.sh b/test/units/TEST-22-TMPFILES.15.sh index 68c6c301bd..78be11357d 100755 --- a/test/units/TEST-22-TMPFILES.15.sh +++ b/test/units/TEST-22-TMPFILES.15.sh @@ -54,3 +54,36 @@ EOF )" [[ "$output" == *"Would create directory $root/run/test"* ]] [[ "$output" != *"$root$root"* ]] + +# Check that L? resolves relative targets from the symlink's parent directory. +root='/tmp/L/4' +rm -rf "$root" +mkdir -p "$root" +touch "$root/target" + +systemd-tmpfiles --create - < Date: Wed, 15 Jul 2026 13:25:55 +0800 Subject: [PATCH 3/6] tmpfiles: propagate copy errors after opening target Opening the destination only proves that it exists. If copy_tree_at() failed for any reason other than an existing destination, report that copy error. Reproducer: name=tmpfiles-copy-bug.$$ src=/tmp/$name.src; dst=/tmp/$name.dst; conf=/tmp/$name.conf mkdir "$src" "$dst" printf payload >"$src/file" chmod 500 "$dst" printf 'C %s - - - - %s\n' "$dst" "$src" >"$conf" systemd-tmpfiles --create "$conf" Before: exit=0 copied=no Follow-up for 8f6fb95cd069884f4ce0a24eb20efc821ae3bc5e. --- src/tmpfiles/tmpfiles.c | 3 +++ test/units/TEST-22-TMPFILES.03.sh | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c index 42512da604..9de612c1d6 100644 --- a/src/tmpfiles/tmpfiles.c +++ b/src/tmpfiles/tmpfiles.c @@ -2238,6 +2238,9 @@ static int copy_files(Context *c, Item *i) { return log_error_errno(errno, "Failed to openat(%s): %m", i->path); } + if (r < 0 && !IN_SET(r, -EEXIST, -EROFS)) + return log_error_errno(r, "Failed to copy files to %s: %m", i->path); + if (fstat(fd, &st) < 0) return log_error_errno(errno, "Failed to fstat(%s): %m", i->path); diff --git a/test/units/TEST-22-TMPFILES.03.sh b/test/units/TEST-22-TMPFILES.03.sh index d75294207a..c36f8f8790 100755 --- a/test/units/TEST-22-TMPFILES.03.sh +++ b/test/units/TEST-22-TMPFILES.03.sh @@ -5,6 +5,9 @@ set -eux set -o pipefail +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + rm -fr /tmp/{f,F,w} mkdir /tmp/{f,F,w} touch /tmp/file-owned-by-root @@ -190,6 +193,22 @@ F /tmp/F/daemon/unsafe-symlink/exploit 0644 daemon daemon - - EOF test ! -e /tmp/F/daemon/unsafe-symlink/exploit +# +# 'C' +# +rm -rf /tmp/C-copy-failure-src /tmp/C-copy-failure-dst +mkdir /tmp/C-copy-failure-src /tmp/C-copy-failure-dst +printf payload >/tmp/C-copy-failure-src/file +chmod 555 /tmp/C-copy-failure-dst + +(! runas nobody systemd-tmpfiles --create - < Date: Wed, 15 Jul 2026 13:28:57 +0800 Subject: [PATCH 4/6] tmpfiles: restore timestamps after removing directories dir_cleanup() restored parent timestamps after removing files, but not after removing child directories. Mark directory removals as changes too. Reproducer: rm -rf /tmp/tf-mtime-dir mkdir -p /tmp/tf-mtime-dir/old-child touch -d '2020-01-01 00:00:00' \ /tmp/tf-mtime-dir/old-child /tmp/tf-mtime-dir before=$(stat -c %Y /tmp/tf-mtime-dir) systemd-tmpfiles --clean - <<'EOT' d /tmp/tf-mtime-dir - - - M:1s EOT Before: equal=no gone=yes Follow-up for 3b63d2d31d0850bd7a81ab9b468218d2c4c461e8. --- src/tmpfiles/tmpfiles.c | 11 ++++++++--- test/units/TEST-22-TMPFILES.12.sh | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c index 9de612c1d6..5c520ac068 100644 --- a/src/tmpfiles/tmpfiles.c +++ b/src/tmpfiles/tmpfiles.c @@ -808,9 +808,14 @@ static int dir_cleanup( log_action("Would remove", "Removing", "%s directory \"%s\"", sub_path); if (!arg_dry_run && - unlinkat(dirfd(d), de->d_name, AT_REMOVEDIR) < 0 && - !IN_SET(errno, ENOENT, ENOTEMPTY)) - r = log_warning_errno(errno, "Failed to remove directory \"%s\", ignoring: %m", sub_path); + unlinkat(dirfd(d), de->d_name, AT_REMOVEDIR) < 0) { + if (errno == ENOTEMPTY) + continue; + if (errno != ENOENT) + r = log_warning_errno(errno, "Failed to remove directory \"%s\", ignoring: %m", sub_path); + } + + deleted = true; } else { _cleanup_close_ int fd = -EBADF; /* This file descriptor is defined here so that the diff --git a/test/units/TEST-22-TMPFILES.12.sh b/test/units/TEST-22-TMPFILES.12.sh index 44bb949825..1aa4faa0d3 100755 --- a/test/units/TEST-22-TMPFILES.12.sh +++ b/test/units/TEST-22-TMPFILES.12.sh @@ -204,3 +204,18 @@ test ! -d /tmp/ageby/d3 # Cleanup the test directory (fail if not empty). rmdir /tmp/ageby + +# Removing an old child directory should restore the parent directory mtime. +rm -rf /tmp/ageby-mtime +mkdir -p /tmp/ageby-mtime/old-child +touch --date "2020-01-01 00:00:00" /tmp/ageby-mtime/old-child /tmp/ageby-mtime +before="$(stat -c %Y /tmp/ageby-mtime)" + +systemd-tmpfiles --clean - <<-EOF +d /tmp/ageby-mtime - - - M:1s +EOF + +after="$(stat -c %Y /tmp/ageby-mtime)" +test "$before" = "$after" +test ! -e /tmp/ageby-mtime/old-child +rmdir /tmp/ageby-mtime From c56420a362fb5ec47105606bc1b7363951ef1698 Mon Sep 17 00:00:00 2001 From: dongshengyuan <545258830@qq.com> Date: Wed, 15 Jul 2026 13:29:55 +0800 Subject: [PATCH 5/6] tmpfiles: do not follow copy source symlinks C lines copy symlinks as symlinks. The post-copy type check should inspect the source symlink itself, not follow its target. Reproducer: src=/tmp/tmpfiles-src-link.$$ dst=/tmp/tmpfiles-dst-link.$$ conf=/tmp/tmpfiles-conf.$$ ln -s missing-target "$src" printf 'C %s - - - - %s\n' "$dst" "$src" >"$conf" systemd-tmpfiles --create "$conf" Before: Failed to stat($src): No such file or directory status=73 $dst -> missing-target already existed Follow-up for 8f6fb95cd069884f4ce0a24eb20efc821ae3bc5e. --- src/tmpfiles/tmpfiles.c | 4 ++-- test/units/TEST-22-TMPFILES.03.sh | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c index 5c520ac068..72781a49b7 100644 --- a/src/tmpfiles/tmpfiles.c +++ b/src/tmpfiles/tmpfiles.c @@ -2249,8 +2249,8 @@ static int copy_files(Context *c, Item *i) { if (fstat(fd, &st) < 0) return log_error_errno(errno, "Failed to fstat(%s): %m", i->path); - if (stat(i->argument, &a) < 0) - return log_error_errno(errno, "Failed to stat(%s): %m", i->argument); + if (lstat(i->argument, &a) < 0) + return log_error_errno(errno, "Failed to lstat(%s): %m", i->argument); if (((st.st_mode ^ a.st_mode) & S_IFMT) != 0) { log_debug("Can't copy to %s, file exists already and is of different type", i->path); diff --git a/test/units/TEST-22-TMPFILES.03.sh b/test/units/TEST-22-TMPFILES.03.sh index c36f8f8790..390932ec97 100755 --- a/test/units/TEST-22-TMPFILES.03.sh +++ b/test/units/TEST-22-TMPFILES.03.sh @@ -209,6 +209,15 @@ test ! -e /tmp/C-copy-failure-dst/file chmod 755 /tmp/C-copy-failure-dst rm -rf /tmp/C-copy-failure-src /tmp/C-copy-failure-dst +rm -f /tmp/C-copy-link-src /tmp/C-copy-link-dst +ln -s missing-target /tmp/C-copy-link-src +systemd-tmpfiles --create - < Date: Wed, 15 Jul 2026 13:35:57 +0800 Subject: [PATCH 6/6] tmpfiles: inherit age-by for X entries X entries inherit the cleanup age from the closest parent directory rule. Inherit the age-by fields too, so parent rules such as m:1d keep their full cleanup policy. Reproducer: mkdir -p /tmp/tmpfiles-ageby/parent/{child,other} printf old >/tmp/tmpfiles-ageby/parent/child/file printf old >/tmp/tmpfiles-ageby/parent/other/file touch -d '3 days ago' /tmp/tmpfiles-ageby/parent/{child,other}/file systemd-tmpfiles --clean - <<'EOT' d /tmp/tmpfiles-ageby/parent - - - m:1d X /tmp/tmpfiles-ageby/parent/child - - - - EOT Before: child/file remained because X used the default age-by set. other/file was removed by the parent m: rule. Follow-up for 7f7a50dd157b3bfb4282c3cc3a56671121eabe24. --- src/tmpfiles/tmpfiles.c | 2 ++ test/units/TEST-22-TMPFILES.12.sh | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c index 72781a49b7..464384b22b 100644 --- a/src/tmpfiles/tmpfiles.c +++ b/src/tmpfiles/tmpfiles.c @@ -4720,6 +4720,8 @@ static int read_config_file( if (candidate_item && candidate_item->age_set) { i->age = candidate_item->age; i->age_set = true; + i->age_by_file = candidate_item->age_by_file; + i->age_by_dir = candidate_item->age_by_dir; } } diff --git a/test/units/TEST-22-TMPFILES.12.sh b/test/units/TEST-22-TMPFILES.12.sh index 1aa4faa0d3..b2288bcf01 100755 --- a/test/units/TEST-22-TMPFILES.12.sh +++ b/test/units/TEST-22-TMPFILES.12.sh @@ -219,3 +219,19 @@ after="$(stat -c %Y /tmp/ageby-mtime)" test "$before" = "$after" test ! -e /tmp/ageby-mtime/old-child rmdir /tmp/ageby-mtime + +# X entries inherit the parent cleanup age and its age-by fields. +rm -rf /tmp/ageby-x-inherit +mkdir -p /tmp/ageby-x-inherit/parent/child /tmp/ageby-x-inherit/parent/other +printf old >/tmp/ageby-x-inherit/parent/child/file +printf old >/tmp/ageby-x-inherit/parent/other/file +touch --date "3 days ago" /tmp/ageby-x-inherit/parent/child/file /tmp/ageby-x-inherit/parent/other/file + +systemd-tmpfiles --clean - <<-EOF +d /tmp/ageby-x-inherit/parent - - - m:1d +X /tmp/ageby-x-inherit/parent/child - - - - +EOF + +test ! -e /tmp/ageby-x-inherit/parent/child/file +test ! -e /tmp/ageby-x-inherit/parent/other/file +rm -rf /tmp/ageby-x-inherit