From 9c735dec679e1ceaead5d745fca49051a390300e Mon Sep 17 00:00:00 2001 From: Ivan Shapovalov Date: Mon, 6 Apr 2026 22:03:36 +0200 Subject: [PATCH 1/3] time-util: add TIMESPEC_STORE_NSEC() --- src/basic/time-util.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/basic/time-util.h b/src/basic/time-util.h index 5b6a524c48..9a66a90859 100644 --- a/src/basic/time-util.h +++ b/src/basic/time-util.h @@ -114,6 +114,7 @@ struct timespec* timespec_store(struct timespec *ts, usec_t u); struct timespec* timespec_store_nsec(struct timespec *ts, nsec_t n); #define TIMESPEC_STORE(u) timespec_store(&(struct timespec) {}, (u)) +#define TIMESPEC_STORE_NSEC(n) timespec_store_nsec(&(struct timespec) {}, (n)) usec_t timeval_load(const struct timeval *tv) _pure_; struct timeval* timeval_store(struct timeval *tv, usec_t u); From e18b75c3f14960cc2a9e8bce7cce3bc3ed8c6114 Mon Sep 17 00:00:00 2001 From: Ivan Shapovalov Date: Fri, 20 Mar 2026 16:36:44 +0100 Subject: [PATCH 2/3] tmpfiles: use `NSEC_INFINITY` consistently in dir_cleanup() Correctness analysis ==================== The *time_nsec variables are used for a total of 2 or 3 times: - twice in needs_cleanup() (lines 788, 839) - once in a recursive dir_cleanup() (line 764) as self_*time_nsec In needs_cleanup(), all passed timestamps are guarded against NSEC_INFINITY (this does not fix any real bugs as a 0 value is also older than any cutoff point and thus would not cause any deletions). Recursively in dir_cleanup(), the self_* variables are used to reset the toplevel directory utimes, where they are superficially compared against NSEC_INFINITY as a guard, but subsequently mishandled in the case when only one of the times is NSEC_INFINITY: in this case, it will be a) logged as a bogus value and b) passed through directly to timespec_store_nsec(), which does special-case it, but in a way that is invalid for futimens(). This is further fixed up by explicitly mapping NSEC_INFINITY to TIMESPEC_OMIT. This constitutes a bugfix in theory, as a ~STATX_ATIME return from statx() would have previously caused the corresponding utime to be reset to 0 epoch) rather than being omitted from being set. However, in a directory with ~STATX_ATIME, attempts to set atime would likely be ignored as well. Mostly this is a self-consistency fix that establishes that dir_cleanup() should be called with NSEC_INFINITY in place of absent timestamps. --- src/tmpfiles/tmpfiles.c | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c index 17f263790e..2b1c612f5c 100644 --- a/src/tmpfiles/tmpfiles.c +++ b/src/tmpfiles/tmpfiles.c @@ -710,10 +710,10 @@ static int dir_cleanup( continue; } - atime_nsec = FLAGS_SET(sx.stx_mask, STATX_ATIME) ? statx_timestamp_load_nsec(&sx.stx_atime) : 0; - mtime_nsec = FLAGS_SET(sx.stx_mask, STATX_MTIME) ? statx_timestamp_load_nsec(&sx.stx_mtime) : 0; - ctime_nsec = FLAGS_SET(sx.stx_mask, STATX_CTIME) ? statx_timestamp_load_nsec(&sx.stx_ctime) : 0; - btime_nsec = FLAGS_SET(sx.stx_mask, STATX_BTIME) ? statx_timestamp_load_nsec(&sx.stx_btime) : 0; + 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; + ctime_nsec = FLAGS_SET(sx.stx_mask, STATX_CTIME) ? statx_timestamp_load_nsec(&sx.stx_ctime) : NSEC_INFINITY; + btime_nsec = FLAGS_SET(sx.stx_mask, STATX_BTIME) ? statx_timestamp_load_nsec(&sx.stx_btime) : NSEC_INFINITY; sub_path = path_join(p, de->d_name); if (!sub_path) { @@ -867,11 +867,19 @@ finish: log_action("Would restore", "Restoring", "%s access and modification time on \"%s\": %s, %s", p, - FORMAT_TIMESTAMP_STYLE(self_atime_nsec / NSEC_PER_USEC, TIMESTAMP_US), - FORMAT_TIMESTAMP_STYLE(self_mtime_nsec / NSEC_PER_USEC, TIMESTAMP_US)); + self_atime_nsec != NSEC_INFINITY + ? FORMAT_TIMESTAMP_STYLE(self_atime_nsec / NSEC_PER_USEC, TIMESTAMP_US) + : "(omitted)", + self_mtime_nsec != NSEC_INFINITY + ? FORMAT_TIMESTAMP_STYLE(self_mtime_nsec / NSEC_PER_USEC, TIMESTAMP_US) + : "(omitted)"); - timespec_store_nsec(ts + 0, self_atime_nsec); - timespec_store_nsec(ts + 1, self_mtime_nsec); + ts[0] = self_atime_nsec != NSEC_INFINITY + ? *TIMESPEC_STORE_NSEC(self_atime_nsec) + : TIMESPEC_OMIT; + ts[1] = self_mtime_nsec != NSEC_INFINITY + ? *TIMESPEC_STORE_NSEC(self_mtime_nsec) + : TIMESPEC_OMIT; /* Restore original directory timestamps */ if (!arg_dry_run && From 122de8d93dbf496b0013c3aa3bc49a5dc6721ff8 Mon Sep 17 00:00:00 2001 From: Ivan Shapovalov Date: Fri, 20 Mar 2026 16:45:07 +0100 Subject: [PATCH 3/3] tmpfiles: do not mandate `STATX_ATIME` and `STATX_MTIME` Timestamps are not guaranteed to be set by `statx()`, and their presence should not be asserted as a proxy to judge the kernel version. In particular, `STATX_ATIME` is omitted from the return when querying a file on a `noatime` superblock, causing spurious errors from tmpfiles. Correctness analysis ==================== The timestamps produced by the `statx()` call in `opendir_and_stat()` are only ever used once, in `clean_item_instance()` (lines 3148-3149) as inputs to `dir_cleanup()`. Convert absent timestamps into `NSEC_INFINITY` as per the previous commit. Fixes #41227. --- src/tmpfiles/tmpfiles.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c index 2b1c612f5c..baef92f7af 100644 --- a/src/tmpfiles/tmpfiles.c +++ b/src/tmpfiles/tmpfiles.c @@ -591,8 +591,8 @@ static int opendir_and_stat( /* path= */ NULL, AT_EMPTY_PATH, /* xstatx_flags= */ 0, - STATX_MODE|STATX_INO|STATX_ATIME|STATX_MTIME, - /* optional_mask= */ 0, + STATX_MODE|STATX_INO, + STATX_ATIME|STATX_MTIME, STATX_ATTR_MOUNT_ROOT, &sx); if (r < 0) @@ -3124,6 +3124,7 @@ 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; @@ -3134,6 +3135,9 @@ static int clean_item_instance( 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; + if (DEBUG_LOGGING) { _cleanup_free_ char *ab_f = NULL, *ab_d = NULL; @@ -3153,8 +3157,8 @@ static int clean_item_instance( } return dir_cleanup(c, i, instance, d, - statx_timestamp_load_nsec(&sx.stx_atime), - statx_timestamp_load_nsec(&sx.stx_mtime), + atime_nsec, + mtime_nsec, cutoff * NSEC_PER_USEC, sx.stx_dev_major, sx.stx_dev_minor, mountpoint,