From e347f63b0f04fc71b545ef10dfcda2df95f0c757 Mon Sep 17 00:00:00 2001 From: dongshengyuan <545258830@qq.com> Date: Tue, 28 Jul 2026 16:40:24 +0800 Subject: [PATCH 1/2] tmpfiles: reject unused argument fields Line types which do not use the argument field used to warn and ignore a non-empty field. Treat that as invalid configuration instead, so typos are not silently accepted. Follow-up for: 614cc34f3a2a7c64a21c3f5256f2e2b2c1de1d51 --- NEWS | 6 ++++++ TODO.md | 1 - man/tmpfiles.d.xml | 7 +++++-- src/tmpfiles/tmpfiles.c | 8 +++++--- test/units/TEST-22-TMPFILES.01.sh | 7 +++++++ test/units/TEST-22-TMPFILES.12.sh | 2 +- 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/NEWS b/NEWS index 1c6e644137..60fcc0a518 100644 --- a/NEWS +++ b/NEWS @@ -73,6 +73,12 @@ CHANGES WITH 262: * bootctl, systemd-oomd, systemd-repart, systemd-sysusers, and systemd-tmpfiles now accept -n as a short option for --dry-run. + Changes in systemd-tmpfiles: + + * systemd-tmpfiles now rejects non-empty argument fields for tmpfiles.d + line types that do not use the argument field. Previously, such + fields were silently ignored after a warning. + Changes in the TPM Subsystem: * The way that NvPCRs are anchored has been improved. They are now diff --git a/TODO.md b/TODO.md index 689bcf8549..d456d3117d 100644 --- a/TODO.md +++ b/TODO.md @@ -2730,7 +2730,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later - **tmpfiles:** - allow time-based cleanup in r and R too - - instead of ignoring unknown fields, reject them. - creating new directories/subvolumes/fifos/device nodes should not follow symlinks. None of the other adjustment or creation calls follow symlinks. diff --git a/man/tmpfiles.d.xml b/man/tmpfiles.d.xml index 00c6ecb399..d19ae86297 100644 --- a/man/tmpfiles.d.xml +++ b/man/tmpfiles.d.xml @@ -164,7 +164,9 @@ L /tmp/foobar - - - - /dev/null Fields may contain C-style escapes. With the exception of the seventh field (the "argument") all fields may be enclosed in quotes. Note that any whitespace found in the line after the beginning of the argument field will be considered part of the argument field. To begin the argument field with a - whitespace character, use C-style escapes (e.g. \x20). + whitespace character, use C-style escapes (e.g. \x20). Line types which do not + document use of the argument field reject a non-empty argument field; use - to leave + the argument field empty. Type @@ -743,7 +745,8 @@ d /tmp/foo/bar - - - bmA:1h - and T, determines extended attributes to be set. For a and A, determines ACL attributes to be set. For h and H, determines the file attributes to set. For k and K, determines - file capabilities to be set. Ignored for all other lines. + file capabilities to be set. For line types that do not document use of this field, a non-empty + argument is rejected; use - to leave it unset. This field can contain specifiers, see below. diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c index 28a4c9a987..2e4b3e2989 100644 --- a/src/tmpfiles/tmpfiles.c +++ b/src/tmpfiles/tmpfiles.c @@ -4043,9 +4043,11 @@ static int parse_line( case ADJUST_MODE: case RELABEL_PATH: case RECURSIVE_RELABEL_PATH: - if (i.argument) - log_syntax(NULL, LOG_WARNING, fname, line, 0, - "%c lines don't take argument fields, ignoring.", (char) i.type); + if (i.argument) { + *invalid_config = true; + return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG), + "%c lines don't take argument fields.", (char) i.type); + } break; case CREATE_FILE: diff --git a/test/units/TEST-22-TMPFILES.01.sh b/test/units/TEST-22-TMPFILES.01.sh index 4062c838dc..132bfed59a 100755 --- a/test/units/TEST-22-TMPFILES.01.sh +++ b/test/units/TEST-22-TMPFILES.01.sh @@ -21,12 +21,19 @@ test ! -e /tmp/fifo test ! -e /tmp/test # Test invalid config +ret=0 systemd-tmpfiles --inline --remove 'garbage' || ret=$? test "$ret" -eq 65 # EX_DATAERR +ret=0 +systemd-tmpfiles --inline --create 'd /tmp/test - - - - unexpected' || ret=$? +test "$ret" -eq 65 # EX_DATAERR + echo 'garbage' >/tmp/config.conf +ret=0 systemd-tmpfiles --remove /tmp/config.conf || ret=$? test "$ret" -eq 65 # EX_DATAERR +ret=0 systemd-tmpfiles --remove /tmp/config-missing.conf || ret=$? test "$ret" -eq 1 diff --git a/test/units/TEST-22-TMPFILES.12.sh b/test/units/TEST-22-TMPFILES.12.sh index b2288bcf01..97cb974ef4 100755 --- a/test/units/TEST-22-TMPFILES.12.sh +++ b/test/units/TEST-22-TMPFILES.12.sh @@ -84,7 +84,7 @@ else fi # Check for an invalid "age" and "age-by" arguments. -for a in ':' ':1s' '2:1h' 'nope:42h' '" :7m"' 'm:' '::' '"+r^w-x:2/h"' 'b ar::64'; do +for a in ':' ':1s' '2:1h' 'nope:42h' '" :7m"' 'm:' '::' '"+r^w-x:2/h"' '"b ar::64"'; do systemd-tmpfiles --clean - <&1 | grep -F 'Invalid age' >/dev/null d /tmp/ageby - - - ${a} - EOF From 249421e75e389e78bfaaac6837561f449e462a76 Mon Sep 17 00:00:00 2001 From: dongshengyuan <545258830@qq.com> Date: Tue, 28 Jul 2026 16:41:10 +0800 Subject: [PATCH 2/2] tmpfiles: honor age for r/R cleanup Let r and R lines participate in --clean when they specify an age. The target itself is removed only after its selected file or directory timestamps have aged enough; --remove remains unconditional. Follow-up for: beca6b6e6b64cebfe9fc2c89117f6abd3c1b5701 --- NEWS | 9 +- TODO.md | 1 - man/tmpfiles.d.xml | 15 +- src/tmpfiles/tmpfiles.c | 471 ++++++++++++++++++++++++++++-- test/units/TEST-22-TMPFILES.12.sh | 148 ++++++++++ 5 files changed, 618 insertions(+), 26 deletions(-) diff --git a/NEWS b/NEWS index 60fcc0a518..b8dd61d110 100644 --- a/NEWS +++ b/NEWS @@ -76,8 +76,13 @@ CHANGES WITH 262: Changes in systemd-tmpfiles: * systemd-tmpfiles now rejects non-empty argument fields for tmpfiles.d - line types that do not use the argument field. Previously, such - fields were silently ignored after a warning. + line types that do not use the argument field. This is a compatibility + change: fields that were previously ignored after a warning will now + cause the tmpfiles.d configuration to be rejected, so downstreams + should audit shipped tmpfiles.d snippets before upgrading. + + * The r and R tmpfiles.d line types now honor the age field when + systemd-tmpfiles is invoked with --clean. Changes in the TPM Subsystem: diff --git a/TODO.md b/TODO.md index d456d3117d..db817f33c3 100644 --- a/TODO.md +++ b/TODO.md @@ -2729,7 +2729,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later DHCP/HTTP base EFI boot. - **tmpfiles:** - - allow time-based cleanup in r and R too - creating new directories/subvolumes/fifos/device nodes should not follow symlinks. None of the other adjustment or creation calls follow symlinks. diff --git a/man/tmpfiles.d.xml b/man/tmpfiles.d.xml index d19ae86297..5ebace1ac7 100644 --- a/man/tmpfiles.d.xml +++ b/man/tmpfiles.d.xml @@ -382,7 +382,8 @@ L /tmp/foobar - - - - /dev/null This may not be used to remove non-empty directories, use R for that. Lines of this type accept shell-style globs in place of normal path - names. Does not follow symlinks. + names. Does not follow symlinks. When is used, the file or directory is subject + to time-based cleanup if the age argument is specified. This age handling was added in v262. @@ -390,7 +391,8 @@ L /tmp/foobar - - - - /dev/null Recursively remove a path and all its subdirectories (if it is a directory). Lines of this type accept shell-style globs in place of normal path - names. Does not follow symlinks. + names. Does not follow symlinks. When is used, the path is subject to + time-based cleanup if the age argument is specified. This age handling was added in v262. @@ -676,13 +678,14 @@ w- /proc/sys/vm/swappiness - - - - 10 The age field only applies to lines starting with d, D, e, v, q, - Q, C, x - and X. If omitted or set to - -, no automatic clean-up is done. + Q, C, r, + R, x and X. + If omitted or set to -, no automatic clean-up is done. If the age field starts with a tilde character ~, clean-up is only applied to files and directories one level inside the directory specified, but not the files and directories - immediately inside it. + immediately inside it. For R lines, this also skips removing the specified + top-level directory itself. This modifier is not supported by r lines. The age of a file system entry is determined from its last modification timestamp (mtime), its last access timestamp (atime), diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c index 2e4b3e2989..9531762ee9 100644 --- a/src/tmpfiles/tmpfiles.c +++ b/src/tmpfiles/tmpfiles.c @@ -3390,6 +3390,352 @@ static int remove_item_instance( } } +static bool item_instance_needs_cleanup( + Item *i, + const char *instance, + const struct statx *sx) { + + assert(i); + assert(instance); + assert(sx); + + if (!i->age_set) + return false; + + usec_t n = now(CLOCK_REALTIME); + if (n < i->age) + return false; + + usec_t cutoff = n - i->age; + nsec_t atime_nsec = FLAGS_SET(sx->stx_mask, STATX_ATIME) ? statx_timestamp_load_nsec(&sx->stx_atime) : NSEC_INFINITY; + nsec_t mtime_nsec = FLAGS_SET(sx->stx_mask, STATX_MTIME) ? statx_timestamp_load_nsec(&sx->stx_mtime) : NSEC_INFINITY; + nsec_t ctime_nsec = FLAGS_SET(sx->stx_mask, STATX_CTIME) ? statx_timestamp_load_nsec(&sx->stx_ctime) : NSEC_INFINITY; + nsec_t btime_nsec = FLAGS_SET(sx->stx_mask, STATX_BTIME) ? statx_timestamp_load_nsec(&sx->stx_btime) : NSEC_INFINITY; + bool is_dir = S_ISDIR(sx->stx_mode); + + return needs_cleanup(atime_nsec, btime_nsec, ctime_nsec, mtime_nsec, + cutoff * NSEC_PER_USEC, instance, + is_dir ? i->age_by_dir : i->age_by_file, is_dir); +} + +static int item_instance_open_parent( + Item *i, + const char *instance, + char **ret_name, + bool *ret_must_be_directory) { + + int r; + + assert(i); + assert(instance); + assert(ret_name); + assert(ret_must_be_directory); + + _cleanup_free_ char *name = NULL; + r = path_extract_filename(instance, &name); + if (r < 0) + return log_error_errno(r, "Failed to extract filename from path '%s': %m", instance); + + _cleanup_close_ int parent_fd = path_open_parent_safe(instance, i->allow_failure); + if (parent_fd < 0) + return parent_fd; + + *ret_name = TAKE_PTR(name); + *ret_must_be_directory = r == O_DIRECTORY; + return TAKE_FD(parent_fd); +} + +static int item_instance_open_opath( + int parent_fd, + const char *name, + const char *instance, + int *ret_fd, + struct statx *ret_sx) { + + int r; + + assert(parent_fd >= 0); + assert(name); + assert(instance); + assert(ret_fd); + assert(ret_sx); + + _cleanup_close_ int fd = RET_NERRNO(openat(parent_fd, name, O_PATH|O_CLOEXEC|O_NOFOLLOW)); + if (IN_SET(fd, -ENOENT, -ENOTDIR)) + return 0; + if (fd < 0) + return log_error_errno(fd, "Failed to open \"%s\": %m", instance); + + struct statx sx; + r = xstatx_full(fd, + /* path= */ NULL, + AT_EMPTY_PATH, + /* xstatx_flags= */ 0, + STATX_TYPE|STATX_MODE, + STATX_ATIME|STATX_MTIME|STATX_CTIME|STATX_BTIME, + /* mandatory_attributes= */ 0, + &sx); + if (r < 0) + return log_error_errno(r, "statx(%s) failed: %m", instance); + + *ret_fd = TAKE_FD(fd); + *ret_sx = sx; + return 1; +} + +static int item_instance_still_current( + int fd, + int parent_fd, + const char *name, + const char *instance) { + + int r; + + assert(fd >= 0); + assert(parent_fd >= 0); + assert(name); + assert(instance); + + r = inode_same_at(fd, /* filea= */ NULL, + parent_fd, name, + AT_EMPTY_PATH|AT_SYMLINK_NOFOLLOW|AT_NO_AUTOMOUNT); + if (IN_SET(r, -ENOENT, -ENOTDIR)) + return 0; + if (r < 0) { + log_debug_errno(r, "Failed to verify that \"%s\" still refers to the same inode, skipping: %m", instance); + return 0; + } + if (r == 0) { + log_debug("Skipping \"%s\": inode changed.", instance); + return 0; + } + + return 1; +} + +static int item_instance_fd_still_current( + int fd, + int other_fd, + const char *instance) { + + int r; + + assert(fd >= 0); + assert(other_fd >= 0); + assert(instance); + + r = fd_inode_same(fd, other_fd); + if (r < 0) { + log_debug_errno(r, "Failed to verify that \"%s\" still refers to the same inode, skipping: %m", instance); + return 0; + } + if (r == 0) { + log_debug("Skipping \"%s\": inode changed.", instance); + return 0; + } + + return 1; +} + +static int item_instance_open_directory_and_lock( + int parent_fd, + const char *name, + int fd, + const char *instance, + DIR **ret) { + + _cleanup_closedir_ DIR *d = NULL; + int r; + + assert(parent_fd >= 0); + assert(name); + assert(fd >= 0); + assert(instance); + assert(ret); + + d = xopendirat_nomod(parent_fd, name); + if (!d) { + if (IN_SET(errno, ENOENT, ENOTDIR, ELOOP)) + return 0; + + return log_error_errno(errno, "Failed to open directory \"%s\": %m", instance); + } + + r = item_instance_fd_still_current(fd, dirfd(d), instance); + if (r <= 0) + return r; + + if (!arg_dry_run && + flock(dirfd(d), LOCK_EX|LOCK_NB) < 0) { + log_debug_errno(errno, "Couldn't acquire shared BSD lock on directory \"%s\", skipping: %m", instance); + return 0; + } + + *ret = TAKE_PTR(d); + return 1; +} + +static int item_instance_remove_empty_directory( + int parent_fd, + const char *name, + const char *instance) { + + int r; + + assert(parent_fd >= 0); + assert(name); + assert(instance); + + if (arg_dry_run) + return 0; + + r = RET_NERRNO(unlinkat(parent_fd, name, AT_REMOVEDIR)); + if (r < 0) { + bool fatal = !IN_SET(r, -ENOENT, -ENOTEMPTY, -EBUSY); + + log_full_errno(fatal ? LOG_ERR : LOG_DEBUG, r, "Failed to remove %s: %m", instance); + if (fatal) + return r; + } + + return 0; +} + +static int item_instance_remove_directory_if_empty( + int parent_fd, + const char *name, + DIR *d, + const char *instance, + bool assume_empty) { + + int r; + + assert(parent_fd >= 0); + assert(name); + assert(d); + assert(instance); + + if (!assume_empty) { + r = dir_is_empty_at(dirfd(d), /* path= */ NULL, /* ignore_hidden_or_backup= */ false); + if (IN_SET(r, -ENOENT, -ENOTDIR)) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to determine whether \"%s\" is empty: %m", instance); + if (r == 0) + return 0; + } + + r = item_instance_still_current(dirfd(d), parent_fd, name, instance); + if (r <= 0) + return r; + + log_action("Would remove", "Removing", "%s directory \"%s\".", instance); + r = item_instance_remove_empty_directory(parent_fd, name, instance); + if (r < 0) + return r; + + return 0; +} + +static int clean_remove_item_instance_at( + Item *i, + int parent_fd, + const char *name, + int fd, + const char *instance, + const struct statx *sx, + bool must_be_directory) { + + int r; + + assert(i); + assert(parent_fd >= 0); + assert(name); + assert(fd >= 0); + assert(instance); + assert(sx); + + if (must_be_directory && !S_ISDIR(sx->stx_mode)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "\"%s\" is not a directory.", instance); + + if (!item_instance_needs_cleanup(i, instance, sx)) + return 0; + + if (S_ISDIR(sx->stx_mode)) { + _cleanup_closedir_ DIR *d = NULL; + + r = item_instance_open_directory_and_lock(parent_fd, name, fd, instance, &d); + if (r <= 0) + return r; + + return item_instance_remove_directory_if_empty(parent_fd, name, d, instance, /* assume_empty= */ false); + } + + _cleanup_close_ int lock_fd = -EBADF; + if (!arg_dry_run) { + lock_fd = xopenat(parent_fd, name, O_RDONLY|O_CLOEXEC|O_NOFOLLOW|O_NOATIME|O_NONBLOCK|O_NOCTTY); + if (lock_fd < 0 && !IN_SET(lock_fd, -ENOENT, -ELOOP)) + log_warning_errno(lock_fd, "Opening file \"%s\" failed, proceeding without lock: %m", instance); + + if (lock_fd >= 0) { + r = item_instance_fd_still_current(fd, lock_fd, instance); + if (r <= 0) + return r; + + if (flock(lock_fd, LOCK_EX|LOCK_NB) < 0 && errno == EAGAIN) { + log_debug_errno(errno, "Couldn't acquire shared BSD lock on file \"%s\", skipping: %m", instance); + return 0; + } + } + + r = item_instance_still_current(fd, parent_fd, name, instance); + if (r <= 0) + return r; + } + + if (i->type == RECURSIVE_REMOVE_PATH) + log_action("Would remove", "Removing", "%s file \"%s\".", instance); + else + log_action("Would remove", "Removing", "%s \"%s\".", instance); + + if (!arg_dry_run && + unlinkat(parent_fd, name, 0) < 0 && + errno != ENOENT) + log_warning_errno(errno, "Failed to remove \"%s\", ignoring: %m", instance); + + return 0; +} + +static int clean_remove_item_instance( + Context *c, + Item *i, + const char *instance, + CreationMode creation) { + + int r; + + assert(c); + assert(i); + assert(instance); + + if (!i->age_set) + return 0; + + _cleanup_free_ char *name = NULL; + bool must_be_directory; + _cleanup_close_ int parent_fd = item_instance_open_parent(i, instance, &name, &must_be_directory); + if (parent_fd < 0) + return parent_fd; + + _cleanup_close_ int fd = -EBADF; + struct statx sx; + r = item_instance_open_opath(parent_fd, name, instance, &fd, &sx); + if (r <= 0) + return r; + + return clean_remove_item_instance_at(i, parent_fd, name, fd, instance, &sx, must_be_directory); +} + static int remove_item(Context *c, Item *i) { assert(c); assert(i); @@ -3427,13 +3773,19 @@ static char *age_by_to_string(AgeBy ab, bool is_dir) { return ret; } -static int clean_item_instance( +static int clean_item_instance_from_dir( Context *c, Item *i, - const char* instance, - CreationMode creation) { + const char *instance, + DIR *d, + const struct statx *sx, + bool mountpoint) { + assert(c); assert(i); + assert(instance); + assert(d); + assert(sx); if (!i->age_set) return 0; @@ -3443,19 +3795,8 @@ static int clean_item_instance( return 0; usec_t cutoff = n - i->age; - nsec_t atime_nsec, mtime_nsec; - - _cleanup_closedir_ DIR *d = NULL; - struct statx sx; - bool mountpoint; - int r; - - r = opendir_and_stat(instance, &d, &sx, &mountpoint); - if (r <= 0) - return r; - - atime_nsec = FLAGS_SET(sx.stx_mask, STATX_ATIME) ? statx_timestamp_load_nsec(&sx.stx_atime) : NSEC_INFINITY; - mtime_nsec = FLAGS_SET(sx.stx_mask, STATX_MTIME) ? statx_timestamp_load_nsec(&sx.stx_mtime) : NSEC_INFINITY; + nsec_t atime_nsec = FLAGS_SET(sx->stx_mask, STATX_ATIME) ? statx_timestamp_load_nsec(&sx->stx_atime) : NSEC_INFINITY; + nsec_t mtime_nsec = FLAGS_SET(sx->stx_mask, STATX_MTIME) ? statx_timestamp_load_nsec(&sx->stx_mtime) : NSEC_INFINITY; if (DEBUG_LOGGING) { _cleanup_free_ char *ab_f = NULL, *ab_d = NULL; @@ -3479,12 +3820,96 @@ static int clean_item_instance( atime_nsec, mtime_nsec, cutoff * NSEC_PER_USEC, - sx.stx_dev_major, sx.stx_dev_minor, + sx->stx_dev_major, sx->stx_dev_minor, mountpoint, MAX_DEPTH, i->keep_first_level, i->age_by_file, i->age_by_dir); } +static int clean_item_instance( + Context *c, + Item *i, + const char *instance, + CreationMode creation) { + + int r; + + assert(c); + assert(i); + assert(instance); + + _cleanup_closedir_ DIR *d = NULL; + struct statx sx; + bool mountpoint; + + r = opendir_and_stat(instance, &d, &sx, &mountpoint); + if (r <= 0) + return r; + + return clean_item_instance_from_dir(c, i, instance, d, &sx, mountpoint); +} + +static int clean_recursive_remove_item_instance( + Context *c, + Item *i, + const char *instance, + CreationMode creation) { + + int r; + + assert(c); + assert(i); + assert(instance); + + if (!i->age_set) + return 0; + + _cleanup_free_ char *name = NULL; + bool must_be_directory; + _cleanup_close_ int parent_fd = item_instance_open_parent(i, instance, &name, &must_be_directory); + if (parent_fd < 0) + return parent_fd; + + _cleanup_close_ int fd = -EBADF; + struct statx sx; + r = item_instance_open_opath(parent_fd, name, instance, &fd, &sx); + if (r <= 0) + return r; + if (!S_ISDIR(sx.stx_mode)) + return clean_remove_item_instance_at(i, parent_fd, name, fd, instance, &sx, must_be_directory); + + _cleanup_closedir_ DIR *d = NULL; + r = item_instance_open_directory_and_lock(parent_fd, name, fd, instance, &d); + if (r <= 0) + return r; + + struct statx dir_sx; + r = xstatx_full(dirfd(d), + /* path= */ NULL, + AT_EMPTY_PATH, + /* xstatx_flags= */ 0, + STATX_TYPE|STATX_MODE|STATX_INO, + STATX_ATIME|STATX_MTIME|STATX_CTIME|STATX_BTIME, + STATX_ATTR_MOUNT_ROOT, + &dir_sx); + if (r < 0) + return log_error_errno(r, "statx(%s) failed: %m", instance); + + bool cleanup_needed = item_instance_needs_cleanup(i, instance, &dir_sx); + + r = clean_item_instance_from_dir(c, i, instance, d, &dir_sx, FLAGS_SET(dir_sx.stx_attributes, STATX_ATTR_MOUNT_ROOT)); + if (r < 0) + return r; + + if (i->keep_first_level) + return 0; + + if (!cleanup_needed) + return 0; + + return item_instance_remove_directory_if_empty(parent_fd, name, d, instance, /* assume_empty= */ arg_dry_run); +} + static int clean_item(Context *c, Item *i) { assert(c); assert(i); @@ -3506,6 +3931,12 @@ static int clean_item(Context *c, Item *i) { case IGNORE_DIRECTORY_PATH: return glob_item(c, i, clean_item_instance); + case REMOVE_PATH: + return glob_item(c, i, clean_remove_item_instance); + + case RECURSIVE_REMOVE_PATH: + return glob_item(c, i, clean_recursive_remove_item_instance); + default: return 0; } @@ -4378,6 +4809,12 @@ static int parse_line( _cleanup_free_ char *seconds = NULL, *age_by = NULL; if (*a == '~') { + if (i.type == REMOVE_PATH) { + *invalid_config = true; + return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG), + "Age modifier '~' is not supported for %c lines.", (char) i.type); + } + i.keep_first_level = true; a++; } diff --git a/test/units/TEST-22-TMPFILES.12.sh b/test/units/TEST-22-TMPFILES.12.sh index 97cb974ef4..adaa55a88f 100755 --- a/test/units/TEST-22-TMPFILES.12.sh +++ b/test/units/TEST-22-TMPFILES.12.sh @@ -220,6 +220,154 @@ test "$before" = "$after" test ! -e /tmp/ageby-mtime/old-child rmdir /tmp/ageby-mtime +# r/R entries honor age when cleaning. +rm -rf /tmp/ageby-remove +mkdir -p /tmp/ageby-remove/old-dir/child /tmp/ageby-remove/new-dir/child \ + /tmp/ageby-remove/old-empty-dir /tmp/ageby-remove/new-empty-dir +touch /tmp/ageby-remove/old-file /tmp/ageby-remove/new-file \ + /tmp/ageby-remove/old-recursive-file /tmp/ageby-remove/new-recursive-file +touch --date "3 minutes ago" \ + /tmp/ageby-remove/old-file \ + /tmp/ageby-remove/old-recursive-file \ + /tmp/ageby-remove/old-dir/child \ + /tmp/ageby-remove/old-dir \ + /tmp/ageby-remove/old-empty-dir + +output="$(systemd-tmpfiles --dry-run --clean - 2>&1 <<-EOF +r /tmp/ageby-remove/old-file - - - m:1m - +r /tmp/ageby-remove/new-file - - - m:1m - +R /tmp/ageby-remove/old-recursive-file - - - m:1m - +R /tmp/ageby-remove/new-recursive-file - - - m:1m - +R /tmp/ageby-remove/old-dir - - - M:1m - +R /tmp/ageby-remove/new-dir - - - M:1m - +R /tmp/ageby-remove/old-empty-dir - - - M:1m - +R /tmp/ageby-remove/new-empty-dir - - - M:1m - +EOF +)" +[[ "$output" == *"Would remove directory \"/tmp/ageby-remove/old-dir\""* ]] +[[ "$output" != *"Would remove directory \"/tmp/ageby-remove/new-dir\""* ]] +[[ "$output" == *"Would remove directory \"/tmp/ageby-remove/old-empty-dir\""* ]] +[[ "$output" != *"Would remove directory \"/tmp/ageby-remove/new-empty-dir\""* ]] + +test -e /tmp/ageby-remove/old-file +test -e /tmp/ageby-remove/new-file +test -e /tmp/ageby-remove/old-recursive-file +test -e /tmp/ageby-remove/new-recursive-file +test -d /tmp/ageby-remove/old-dir +test -d /tmp/ageby-remove/old-dir/child +test -d /tmp/ageby-remove/new-dir +test -d /tmp/ageby-remove/new-dir/child +test -d /tmp/ageby-remove/old-empty-dir +test -d /tmp/ageby-remove/new-empty-dir + +systemd-tmpfiles --clean - <<-EOF +r /tmp/ageby-remove/old-file - - - m:1m - +r /tmp/ageby-remove/new-file - - - m:1m - +R /tmp/ageby-remove/old-recursive-file - - - m:1m - +R /tmp/ageby-remove/new-recursive-file - - - m:1m - +R /tmp/ageby-remove/old-dir - - - M:1m - +R /tmp/ageby-remove/new-dir - - - M:1m - +R /tmp/ageby-remove/old-empty-dir - - - M:1m - +R /tmp/ageby-remove/new-empty-dir - - - M:1m - +EOF + +test ! -e /tmp/ageby-remove/old-file +test -e /tmp/ageby-remove/new-file +test ! -e /tmp/ageby-remove/old-recursive-file +test -e /tmp/ageby-remove/new-recursive-file +test ! -e /tmp/ageby-remove/old-dir +test -d /tmp/ageby-remove/new-dir +test -d /tmp/ageby-remove/new-dir/child +test ! -e /tmp/ageby-remove/old-empty-dir +test -d /tmp/ageby-remove/new-empty-dir + +# Aged r cleanup removes empty directories but not non-empty or too-new directories. +mkdir -p /tmp/ageby-remove/old-r-empty-dir \ + /tmp/ageby-remove/new-r-empty-dir \ + /tmp/ageby-remove/old-r-nonempty-dir/child +touch --date "3 minutes ago" \ + /tmp/ageby-remove/old-r-empty-dir \ + /tmp/ageby-remove/old-r-nonempty-dir +output="$(systemd-tmpfiles --dry-run --clean - 2>&1 <<-EOF +r /tmp/ageby-remove/old-r-empty-dir - - - M:1m - +r /tmp/ageby-remove/new-r-empty-dir - - - M:1m - +r /tmp/ageby-remove/old-r-nonempty-dir - - - M:1m - +EOF +)" +[[ "$output" == *"Would remove directory \"/tmp/ageby-remove/old-r-empty-dir\""* ]] +[[ "$output" != *"Would remove directory \"/tmp/ageby-remove/new-r-empty-dir\""* ]] +[[ "$output" != *"Would remove directory \"/tmp/ageby-remove/old-r-nonempty-dir\""* ]] + +systemd-tmpfiles --clean - <<-EOF +r /tmp/ageby-remove/old-r-empty-dir - - - M:1m - +r /tmp/ageby-remove/new-r-empty-dir - - - M:1m - +r /tmp/ageby-remove/old-r-nonempty-dir - - - M:1m - +EOF +test ! -e /tmp/ageby-remove/old-r-empty-dir +test -d /tmp/ageby-remove/new-r-empty-dir +test -d /tmp/ageby-remove/old-r-nonempty-dir/child + +# Aged R cleanup keeps the top-level directory if a child is too new. +mkdir -p /tmp/ageby-remove/old-dir-new-child/new-child +touch --date "3 minutes ago" /tmp/ageby-remove/old-dir-new-child +systemd-tmpfiles --clean --inline "R /tmp/ageby-remove/old-dir-new-child - - - M:1m -" +test -d /tmp/ageby-remove/old-dir-new-child/new-child + +(! systemd-tmpfiles --clean --inline "r /tmp/ageby-remove/new-file - - - ~1m -") + +# The ~ age modifier on R keeps the target and its immediate children. +mkdir -p /tmp/ageby-remove/keep-root/old-child/grandchild +touch --date "3 minutes ago" \ + /tmp/ageby-remove/keep-root \ + /tmp/ageby-remove/keep-root/old-child \ + /tmp/ageby-remove/keep-root/old-child/grandchild +systemd-tmpfiles --clean --inline "R /tmp/ageby-remove/keep-root - - - ~M:1m -" +test -d /tmp/ageby-remove/keep-root +test -d /tmp/ageby-remove/keep-root/old-child +test ! -e /tmp/ageby-remove/keep-root/old-child/grandchild + +# Aged r/R cleanup skips locked files. +touch /tmp/ageby-remove/locked-file /tmp/ageby-remove/locked-recursive-file +touch --date "3 minutes ago" /tmp/ageby-remove/locked-file /tmp/ageby-remove/locked-recursive-file +exec {lock_fd}<>/tmp/ageby-remove/locked-file +exec {recursive_lock_fd}<>/tmp/ageby-remove/locked-recursive-file +flock --exclusive "$lock_fd" +flock --exclusive "$recursive_lock_fd" +systemd-tmpfiles --clean - <<-EOF +r /tmp/ageby-remove/locked-file - - - m:1m - +R /tmp/ageby-remove/locked-recursive-file - - - m:1m - +EOF +test -e /tmp/ageby-remove/locked-file +test -e /tmp/ageby-remove/locked-recursive-file +exec {lock_fd}>&- +exec {recursive_lock_fd}>&- +systemd-tmpfiles --clean - <<-EOF +r /tmp/ageby-remove/locked-file - - - m:1m - +R /tmp/ageby-remove/locked-recursive-file - - - m:1m - +EOF +test ! -e /tmp/ageby-remove/locked-file +test ! -e /tmp/ageby-remove/locked-recursive-file + +# Aged r cleanup skips locked directories. +mkdir -p /tmp/ageby-remove/locked-r-dir +touch --date "3 minutes ago" /tmp/ageby-remove/locked-r-dir +flock --exclusive /tmp/ageby-remove/locked-r-dir systemd-tmpfiles --clean --inline \ + "r /tmp/ageby-remove/locked-r-dir - - - M:1m -" +test -d /tmp/ageby-remove/locked-r-dir +systemd-tmpfiles --clean --inline "r /tmp/ageby-remove/locked-r-dir - - - M:1m -" +test ! -e /tmp/ageby-remove/locked-r-dir + +# Aged R cleanup skips locked directories and everything below them. +mkdir -p /tmp/ageby-remove/locked-dir/child +touch --date "3 minutes ago" /tmp/ageby-remove/locked-dir /tmp/ageby-remove/locked-dir/child +flock --exclusive /tmp/ageby-remove/locked-dir systemd-tmpfiles --clean --inline \ + "R /tmp/ageby-remove/locked-dir - - - M:1m -" +test -d /tmp/ageby-remove/locked-dir/child +systemd-tmpfiles --clean --inline "R /tmp/ageby-remove/locked-dir - - - M:1m -" +test ! -e /tmp/ageby-remove/locked-dir + +rm -rf /tmp/ageby-remove + # 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