copy: support clamping the timestamps of copied inodes

Add a `ts_clamp` argument to `copy_tree_at_full()` and
`copy_times_full()` that clamps copied inodes to a given time stamp.
This is the recommended `$SOURCE_DATE_EPOCH` semantics [1] for
reproducible builds. Their non-`_full` APIs keep existing behaviour by
clamping to `USEC_INFINITY`.

atime and mtime are clamped separately: a file that was merely read
after the epoch has to keep its mtime and still lose its atime. E.g. FAT
(EFI partition) stores an access date.

[1] https://reproducible-builds.org/specs/source-date-epoch/
This commit is contained in:
Martin Pitt
2026-08-28 11:12:56 +02:00
parent e6b35a8f00
commit edbf043333
3 changed files with 143 additions and 23 deletions
+73 -19
View File
@@ -459,6 +459,46 @@ int copy_bytes_full(
return max_bytes <= 0; /* return 0 if we hit EOF earlier than the size limit */
}
/* Check whether *ts is within [epoch; clamp] */
static bool timespec_within(const struct timespec *ts, usec_t ts_clamp) {
assert(ts);
return ts->tv_sec < 0 || /* Date pre-1970, cannot clamp these */
timespec_load(ts) <= ts_clamp; /* Date in [epoch; clamp] */
}
/* See ts_clamp in copy.h. */
static void clamp_inode_times(const struct stat *st, usec_t ts_clamp, struct timespec ret[static 2]) {
assert(st);
assert(ret);
ret[0] = timespec_within(&st->st_atim, ts_clamp) ? st->st_atim : *TIMESPEC_STORE(ts_clamp);
ret[1] = timespec_within(&st->st_mtim, ts_clamp) ? st->st_mtim : *TIMESPEC_STORE(ts_clamp);
}
/* Put *st's clamped times on the inode referred to by fdt */
static int clamp_futimens(int fdt, const struct stat *st, usec_t ts_clamp) {
struct timespec times[2];
assert(fdt >= 0);
assert(st);
clamp_inode_times(st, ts_clamp, times);
return RET_NERRNO(futimens(fdt, times));
}
/* Same for (dt, to) */
static int clamp_utimensat(int dt, const char *to, const struct stat *st, usec_t ts_clamp) {
struct timespec times[2];
assert(dt >= 0 || dt == AT_FDCWD);
assert(to);
assert(st);
clamp_inode_times(st, ts_clamp, times);
return RET_NERRNO(utimensat(dt, to, times, AT_SYMLINK_NOFOLLOW));
}
static int fd_copy_symlink(
int df,
const char *from,
@@ -467,7 +507,8 @@ static int fd_copy_symlink(
const char *to,
uid_t override_uid,
gid_t override_gid,
CopyFlags copy_flags) {
CopyFlags copy_flags,
usec_t ts_clamp) {
_cleanup_free_ char *target = NULL;
int r;
@@ -506,7 +547,8 @@ static int fd_copy_symlink(
r = -errno;
(void) copy_xattr(df, from, dt, to, copy_flags);
(void) utimensat(dt, to, (struct timespec[]) { st->st_atim, st->st_mtim }, AT_SYMLINK_NOFOLLOW);
(void) clamp_utimensat(dt, to, st, ts_clamp);
return r;
}
@@ -798,6 +840,7 @@ static int fd_copy_tree_generic(
uid_t override_uid,
gid_t override_gid,
CopyFlags copy_flags,
usec_t ts_clamp,
Hashmap *denylist,
Hashmap *subvolumes,
HardlinkContext *hardlink_context,
@@ -815,6 +858,7 @@ static int fd_copy_regular(
uid_t override_uid,
gid_t override_gid,
CopyFlags copy_flags,
usec_t ts_clamp,
HardlinkContext *hardlink_context,
copy_progress_bytes_t progress,
void *userdata) {
@@ -862,7 +906,7 @@ static int fd_copy_regular(
if (fchmod(fdt, st->st_mode & 07777) < 0)
r = -errno;
(void) futimens(fdt, (struct timespec[]) { st->st_atim, st->st_mtim });
(void) clamp_futimens(fdt, st, ts_clamp);
(void) copy_xattr(fdf, NULL, fdt, NULL, copy_flags);
if (FLAGS_SET(copy_flags, COPY_VERIFY_LINKED)) {
@@ -910,6 +954,7 @@ static int fd_copy_fifo(
uid_t override_uid,
gid_t override_gid,
CopyFlags copy_flags,
usec_t ts_clamp,
HardlinkContext *hardlink_context) {
int r;
@@ -948,7 +993,7 @@ static int fd_copy_fifo(
if (fchmodat(dt, to, st->st_mode & 07777, AT_SYMLINK_NOFOLLOW) < 0)
r = -errno;
(void) utimensat(dt, to, (struct timespec[]) { st->st_atim, st->st_mtim }, AT_SYMLINK_NOFOLLOW);
(void) clamp_utimensat(dt, to, st, ts_clamp);
(void) memorize_hardlink(hardlink_context, st, dt, to);
return r;
@@ -963,6 +1008,7 @@ static int fd_copy_node(
uid_t override_uid,
gid_t override_gid,
CopyFlags copy_flags,
usec_t ts_clamp,
HardlinkContext *hardlink_context) {
int r;
@@ -1001,7 +1047,7 @@ static int fd_copy_node(
if (fchmodat(dt, to, st->st_mode & 07777, AT_SYMLINK_NOFOLLOW) < 0)
r = -errno;
(void) utimensat(dt, to, (struct timespec[]) { st->st_atim, st->st_mtim }, AT_SYMLINK_NOFOLLOW);
(void) clamp_utimensat(dt, to, st, ts_clamp);
(void) memorize_hardlink(hardlink_context, st, dt, to);
return r;
@@ -1018,6 +1064,7 @@ static int fd_copy_directory(
uid_t override_uid,
gid_t override_gid,
CopyFlags copy_flags,
usec_t ts_clamp,
Hashmap *denylist,
Hashmap *subvolumes,
HardlinkContext *hardlink_context,
@@ -1169,7 +1216,7 @@ static int fd_copy_directory(
r = fd_copy_tree_generic(dirfd(d), de->d_name, &buf, fdt, de->d_name, original_device,
depth_left-1, override_uid, override_gid, copy_flags & ~COPY_LOCK_BSD,
denylist, subvolumes, hardlink_context, child_display_path, progress_path,
ts_clamp, denylist, subvolumes, hardlink_context, child_display_path, progress_path,
progress_bytes, userdata);
/* Propagate SIGINT/SIGTERM, ENOSPC, and fs-verity fails up instantly */
@@ -1193,7 +1240,8 @@ finish:
/* Run hardlink context cleanup now because it potentially changes timestamps */
hardlink_context_destroy(&our_hardlink_context);
(void) copy_xattr(dirfd(d), NULL, fdt, NULL, copy_flags);
(void) futimens(fdt, (struct timespec[]) { st->st_atim, st->st_mtim });
(void) clamp_futimens(fdt, st, ts_clamp);
} else if (FLAGS_SET(copy_flags, COPY_RESTORE_DIRECTORY_TIMESTAMPS)) {
/* Run hardlink context cleanup now because it potentially changes timestamps */
hardlink_context_destroy(&our_hardlink_context);
@@ -1221,6 +1269,7 @@ static int fd_copy_leaf(
uid_t override_uid,
gid_t override_gid,
CopyFlags copy_flags,
usec_t ts_clamp,
HardlinkContext *hardlink_context,
const char *display_path,
copy_progress_bytes_t progress_bytes,
@@ -1228,13 +1277,13 @@ static int fd_copy_leaf(
int r;
if (S_ISREG(st->st_mode))
r = fd_copy_regular(df, from, st, dt, to, override_uid, override_gid, copy_flags, hardlink_context, progress_bytes, userdata);
r = fd_copy_regular(df, from, st, dt, to, override_uid, override_gid, copy_flags, ts_clamp, hardlink_context, progress_bytes, userdata);
else if (S_ISLNK(st->st_mode))
r = fd_copy_symlink(df, from, st, dt, to, override_uid, override_gid, copy_flags);
r = fd_copy_symlink(df, from, st, dt, to, override_uid, override_gid, copy_flags, ts_clamp);
else if (S_ISFIFO(st->st_mode))
r = fd_copy_fifo(df, from, st, dt, to, override_uid, override_gid, copy_flags, hardlink_context);
r = fd_copy_fifo(df, from, st, dt, to, override_uid, override_gid, copy_flags, ts_clamp, hardlink_context);
else if (S_ISBLK(st->st_mode) || S_ISCHR(st->st_mode) || S_ISSOCK(st->st_mode))
r = fd_copy_node(df, from, st, dt, to, override_uid, override_gid, copy_flags, hardlink_context);
r = fd_copy_node(df, from, st, dt, to, override_uid, override_gid, copy_flags, ts_clamp, hardlink_context);
else
r = -EOPNOTSUPP;
@@ -1252,6 +1301,7 @@ static int fd_copy_tree_generic(
uid_t override_uid,
gid_t override_gid,
CopyFlags copy_flags,
usec_t ts_clamp,
Hashmap *denylist,
Hashmap *subvolumes,
HardlinkContext *hardlink_context,
@@ -1266,7 +1316,7 @@ static int fd_copy_tree_generic(
if (S_ISDIR(st->st_mode))
return fd_copy_directory(df, from, st, dt, to, original_device, depth_left-1, override_uid,
override_gid, copy_flags, denylist, subvolumes, hardlink_context,
override_gid, copy_flags, ts_clamp, denylist, subvolumes, hardlink_context,
display_path, progress_path, progress_bytes, userdata);
/* Only if we are copying a directory we are fine if the target dir is referenced by fd only */
@@ -1280,14 +1330,14 @@ static int fd_copy_tree_generic(
} else if (t == DENY_CONTENTS)
log_debug("%s is configured to have its contents excluded, but is not a directory", from ?: "file to copy");
r = fd_copy_leaf(df, from, st, dt, to, override_uid, override_gid, copy_flags, hardlink_context, display_path, progress_bytes, userdata);
r = fd_copy_leaf(df, from, st, dt, to, override_uid, override_gid, copy_flags, ts_clamp, hardlink_context, display_path, progress_bytes, userdata);
/* We just tried to copy a leaf node of the tree. If it failed because the node already exists *and* the COPY_REPLACE flag has been provided, we should unlink the node and re-copy. */
if (r == -EEXIST && (copy_flags & COPY_REPLACE)) {
/* This codepath is us trying to address an error to copy, if the unlink fails, lets just return the original error. */
if (unlinkat(dt, to, 0) < 0)
return r;
r = fd_copy_leaf(df, from, st, dt, to, override_uid, override_gid, copy_flags, hardlink_context, display_path, progress_bytes, userdata);
r = fd_copy_leaf(df, from, st, dt, to, override_uid, override_gid, copy_flags, ts_clamp, hardlink_context, display_path, progress_bytes, userdata);
}
return r;
@@ -1301,6 +1351,7 @@ int copy_tree_at_full(
uid_t override_uid,
gid_t override_gid,
CopyFlags copy_flags,
usec_t ts_clamp,
Hashmap *denylist,
Hashmap *subvolumes,
copy_progress_path_t progress_path,
@@ -1316,7 +1367,7 @@ int copy_tree_at_full(
return -errno;
r = fd_copy_tree_generic(fdf, from, &st, fdt, to, st.st_dev, COPY_DEPTH_MAX, override_uid,
override_gid, copy_flags, denylist, subvolumes, NULL, NULL, progress_path,
override_gid, copy_flags, ts_clamp, denylist, subvolumes, NULL, NULL, progress_path,
progress_bytes, userdata);
if (r < 0)
return r;
@@ -1388,6 +1439,7 @@ int copy_directory_at_full(
override_uid,
override_gid,
copy_flags,
/* ts_clamp= */ USEC_INFINITY,
/* denylist= */ NULL,
/* subvolumes= */ NULL,
/* hardlink_context= */ NULL,
@@ -1639,8 +1691,9 @@ fail:
return r;
}
int copy_times(int fdf, int fdt, CopyFlags flags) {
int copy_times_full(int fdf, int fdt, CopyFlags flags, usec_t ts_clamp) {
struct stat st;
int r;
assert(fdf >= 0);
assert(fdt >= 0);
@@ -1648,14 +1701,15 @@ int copy_times(int fdf, int fdt, CopyFlags flags) {
if (fstat(fdf, &st) < 0)
return -errno;
if (futimens(fdt, (struct timespec[2]) { st.st_atim, st.st_mtim }) < 0)
return -errno;
r = clamp_futimens(fdt, &st, ts_clamp);
if (r < 0)
return r;
if (FLAGS_SET(flags, COPY_CRTIME)) {
usec_t crtime;
if (fd_getcrtime(fdf, &crtime) >= 0)
(void) fd_setcrtime(fdt, crtime);
(void) fd_setcrtime(fdt, MIN(crtime, ts_clamp));
}
return 0;
+11 -4
View File
@@ -81,12 +81,15 @@ static inline int copy_file_atomic(const char *from, const char *to, mode_t mode
return copy_file_atomic_full(from, to, mode, 0, 0, copy_flags, NULL, NULL);
}
int copy_tree_at_full(int fdf, const char *from, int fdt, const char *to, uid_t override_uid, gid_t override_gid, CopyFlags copy_flags, Hashmap *denylist, Hashmap *subvolumes, copy_progress_path_t progress_path, copy_progress_bytes_t progress_bytes, void *userdata);
/* ts_clamp: upper bound for the timestamps of created inodes. $SOURCE_DATE_EPOCH semantic, see
* https://reproducible-builds.org/specs/source-date-epoch/. USEC_INFINITY clamps nothing, i.e. keeps
* the source's own timestamps. */
int copy_tree_at_full(int fdf, const char *from, int fdt, const char *to, uid_t override_uid, gid_t override_gid, CopyFlags copy_flags, usec_t ts_clamp, Hashmap *denylist, Hashmap *subvolumes, copy_progress_path_t progress_path, copy_progress_bytes_t progress_bytes, void *userdata);
static inline int copy_tree_at(int fdf, const char *from, int fdt, const char *to, uid_t override_uid, gid_t override_gid, CopyFlags copy_flags, Hashmap *denylist, Hashmap *subvolumes) {
return copy_tree_at_full(fdf, from, fdt, to, override_uid, override_gid, copy_flags, denylist, subvolumes, NULL, NULL, NULL);
return copy_tree_at_full(fdf, from, fdt, to, override_uid, override_gid, copy_flags, USEC_INFINITY, denylist, subvolumes, NULL, NULL, NULL);
}
static inline int copy_tree(const char *from, const char *to, uid_t override_uid, gid_t override_gid, CopyFlags copy_flags, Hashmap *denylist, Hashmap *subvolumes) {
return copy_tree_at_full(AT_FDCWD, from, AT_FDCWD, to, override_uid, override_gid, copy_flags, denylist, subvolumes, NULL, NULL, NULL);
return copy_tree_at_full(AT_FDCWD, from, AT_FDCWD, to, override_uid, override_gid, copy_flags, USEC_INFINITY, denylist, subvolumes, NULL, NULL, NULL);
}
int copy_directory_at_full(int dir_fdf, const char *from, int dir_fdt, const char *to, uid_t override_uid, gid_t override_gid, CopyFlags copy_flags, copy_progress_path_t progress_path, copy_progress_bytes_t progress_bytes, void *userdata);
@@ -99,7 +102,11 @@ static inline int copy_bytes(int fdf, int fdt, uint64_t max_bytes, CopyFlags cop
return copy_bytes_full(fdf, fdt, max_bytes, copy_flags, NULL, NULL, NULL, NULL);
}
int copy_times(int fdf, int fdt, CopyFlags flags);
/* See ts_clamp above. Covers atime, mtime, and with COPY_CRTIME the birth time. */
int copy_times_full(int fdf, int fdt, CopyFlags flags, usec_t ts_clamp);
static inline int copy_times(int fdf, int fdt, CopyFlags flags) {
return copy_times_full(fdf, fdt, flags, USEC_INFINITY);
}
int copy_access(int fdf, int fdt);
int copy_owner(int fdf, int fdt);
int copy_rights_with_fallback(int fdf, int fdt, const char *patht);
+59
View File
@@ -26,6 +26,7 @@
#include "string-util.h"
#include "strv.h"
#include "tests.h"
#include "time-util.h"
#include "tmpfile-util.h"
#include "xattr-util.h"
@@ -798,4 +799,62 @@ TEST_RET(copy_with_verity) {
return 0;
}
static void test_copy_times_one(
const struct timespec source[static 2],
usec_t ts_clamp,
const struct timespec expect[static 2]) {
_cleanup_(unlink_tempfilep) char in_fn[] = "/tmp/test-copy-times-in-XXXXXX";
_cleanup_(unlink_tempfilep) char out_fn[] = "/tmp/test-copy-times-out-XXXXXX";
_cleanup_close_ int in_fd = -EBADF, out_fd = -EBADF;
struct stat st;
in_fd = mkostemp_safe(in_fn);
assert_se(in_fd >= 0);
out_fd = mkostemp_safe(out_fn);
assert_se(out_fd >= 0);
assert_se(futimens(in_fd, source) >= 0);
ASSERT_OK(copy_times_full(in_fd, out_fd, /* flags= */ 0, ts_clamp));
assert_se(fstat(out_fd, &st) >= 0);
ASSERT_EQ(st.st_atim.tv_sec, expect[0].tv_sec);
ASSERT_EQ(st.st_atim.tv_nsec, expect[0].tv_nsec);
ASSERT_EQ(st.st_mtim.tv_sec, expect[1].tv_sec);
ASSERT_EQ(st.st_mtim.tv_nsec, expect[1].tv_nsec);
}
#define TIMESPEC_PAIR(a, m) ((const struct timespec[2]) { { .tv_sec = (a) }, { .tv_sec = (m) } })
TEST(copy_times_clamp) {
const time_t before = 1600000000, epoch = 1700000000, after = 1750000000;
const usec_t clamp = epoch * USEC_PER_SEC;
/* Older than the clamp: kept. */
test_copy_times_one(TIMESPEC_PAIR(before, before), clamp, TIMESPEC_PAIR(before, before));
/* Newer: pulled back onto the clamp. */
test_copy_times_one(TIMESPEC_PAIR(after, after), clamp, TIMESPEC_PAIR(epoch, epoch));
/* Exactly on it: kept, the clamp is an inclusive upper bound. The comparison runs at microsecond
* resolution, so a nanosecond past the clamp still counts as "on it". */
test_copy_times_one(
(const struct timespec[2]) { { .tv_sec = epoch, .tv_nsec = 1 },
{ .tv_sec = epoch, .tv_nsec = 1 } },
clamp,
(const struct timespec[2]) { { .tv_sec = epoch, .tv_nsec = 1 },
{ .tv_sec = epoch, .tv_nsec = 1 } });
/* atime and mtime are decided independently: a file that was merely read after the epoch
* loses its atime and keeps its mtime. */
test_copy_times_one(TIMESPEC_PAIR(after, before), clamp, TIMESPEC_PAIR(epoch, before));
test_copy_times_one(TIMESPEC_PAIR(before, after), clamp, TIMESPEC_PAIR(before, epoch));
/* Before 1970: older than any epoch, so kept rather than pushed forward. */
test_copy_times_one(TIMESPEC_PAIR(-1, -1), clamp, TIMESPEC_PAIR(-1, -1));
/* No clamp requested: the source's own timestamps. */
test_copy_times_one(TIMESPEC_PAIR(after, after), USEC_INFINITY, TIMESPEC_PAIR(after, after));
}
DEFINE_TEST_MAIN(LOG_DEBUG);