diff --git a/man/systemd-repart.xml b/man/systemd-repart.xml
index 096eb0c53d..75104c2abc 100644
--- a/man/systemd-repart.xml
+++ b/man/systemd-repart.xml
@@ -803,6 +803,36 @@
+
+ Environment
+
+
+
+ $SOURCE_DATE_EPOCH
+
+ Takes a UNIX timestamp in seconds. Clamps access and modification times written
+ into the image. See the
+ reproducible builds
+ specification.
+
+
+
+
+
+ Byte-for-byte reproducible images require mode. Populating a file
+ system through the kernel writes metadata that cannot be corrected from userspace afterwards.
+
+ Format=vfat images have additional noise: the
+ directory entry of the volume label is written with a wallclock timestamp by
+ mkfs.fat8
+ with dosfstools versions up to and including 4.2. A later release or a backport of the
+ upstream fix
+ sets it to $SOURCE_DATE_EPOCH.
+
+ Offline and online mode also write vfat short names differently, so images
+ built in both ways never match each other.
+
+
Exit status
diff --git a/src/repart/repart.c b/src/repart/repart.c
index c77cad685a..388f25c398 100644
--- a/src/repart/repart.c
+++ b/src/repart/repart.c
@@ -6807,6 +6807,17 @@ static int file_is_denylisted(const char *source, Hashmap *denylist) {
return 0;
}
+/* Adding an entry to a directory bumps its mtime. Clamp it back to ts. */
+static int clamp_directory_mtime(int dir_fd, usec_t ts) {
+ assert(dir_fd >= 0);
+
+ if (ts == USEC_INFINITY)
+ return 0;
+
+ /* chase() hands out O_PATH descriptors. */
+ return futimens_opath(dir_fd, (const struct timespec[2]) { TIMESPEC_OMIT, *TIMESPEC_STORE(ts) });
+}
+
static int do_copy_files(Context *context, Partition *p, const char *root) {
_cleanup_hashmap_free_ Hashmap *subvolumes = NULL;
int r;
@@ -6830,6 +6841,7 @@ static int do_copy_files(Context *context, Partition *p, const char *root) {
}
bool copy_ownership = fstype_can_ownership(p->format);
+ usec_t ts = parse_source_date_epoch();
/* copy_tree_at() automatically copies the permissions of source directories to target directories if
* it created them. However, the root directory is created by us, so we have to manually take care
@@ -6854,7 +6866,7 @@ static int do_copy_files(Context *context, Partition *p, const char *root) {
(void) copy_xattr(sfd, NULL, rfd, NULL, COPY_ALL_XATTRS);
if (copy_ownership)
(void) copy_access(sfd, rfd);
- (void) copy_times(sfd, rfd, 0);
+ (void) copy_times_full(sfd, rfd, /* flags= */ 0, ts);
break;
}
@@ -6866,7 +6878,6 @@ static int do_copy_files(Context *context, Partition *p, const char *root) {
_cleanup_hashmap_free_ Hashmap *denylist = NULL;
_cleanup_hashmap_free_ Hashmap *subvolumes_by_source_inode = NULL;
_cleanup_close_ int sfd = -EBADF, pfd = -EBADF, tfd = -EBADF;
- usec_t ts = parse_source_date_epoch();
r = make_copy_files_denylist(context, p, line->source, line->target, &denylist);
if (r < 0)
@@ -6915,19 +6926,35 @@ static int do_copy_files(Context *context, Partition *p, const char *root) {
if (pfd < 0)
return log_error_errno(pfd, "Failed to open parent directory of target: %m");
- r = copy_tree_at(
+ r = copy_tree_at_full(
sfd, ".",
pfd, fn,
uid, gid,
line->flags,
- denylist, subvolumes_by_source_inode);
+ ts,
+ denylist, subvolumes_by_source_inode,
+ /* progress_path= */ NULL,
+ /* progress_bytes= */ NULL,
+ /* userdata= */ NULL);
+ if (r >= 0) {
+ /* Creating the target bumped the parent directory's mtime,
+ * just like in the regular file case below. */
+ r = clamp_directory_mtime(pfd, ts);
+ if (r < 0)
+ return log_error_errno(
+ r, "Failed to set timestamp of '%s': %m", dn);
+ }
} else
- r = copy_tree_at(
+ r = copy_tree_at_full(
sfd, ".",
tfd, ".",
uid, gid,
line->flags,
- denylist, subvolumes_by_source_inode);
+ ts,
+ denylist, subvolumes_by_source_inode,
+ /* progress_path= */ NULL,
+ /* progress_bytes= */ NULL,
+ /* userdata= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to copy '%s%s' to '%s%s': %m",
strempty(arg_copy_source), line->source, strempty(root), line->target);
@@ -6974,15 +7001,12 @@ static int do_copy_files(Context *context, Partition *p, const char *root) {
(void) copy_xattr(sfd, NULL, tfd, NULL, COPY_ALL_XATTRS);
if (copy_ownership)
(void) copy_access(sfd, tfd);
- (void) copy_times(sfd, tfd, 0);
+ (void) copy_times_full(sfd, tfd, /* flags= */ 0, ts);
- if (ts != USEC_INFINITY) {
- struct timespec tspec;
- timespec_store(&tspec, ts);
-
- if (futimens(pfd, (const struct timespec[2]) { TIMESPEC_OMIT, tspec }) < 0)
- return -errno;
- }
+ /* Creating the file bumped the parent directory's mtime. */
+ r = clamp_directory_mtime(pfd, ts);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set timestamp of '%s': %m", dn);
}
}
@@ -7017,6 +7041,7 @@ static int do_make_directories(Partition *p, const char *root) {
}
static int do_make_symlinks(Partition *p, const char *root) {
+ usec_t ts = parse_source_date_epoch();
int r;
assert(p);
@@ -7032,6 +7057,18 @@ static int do_make_symlinks(Partition *p, const char *root) {
if (symlinkat(*target, parent_fd, f) < 0)
return log_error_errno(errno, "Failed to create symlink at %s to %s: %m", *path, *target);
+
+ /* The new symlink carries the wall clock, and creating it bumped the directory we put
+ * it into. We are the last population step, so nothing overwrites this again. */
+ if (ts != USEC_INFINITY &&
+ utimensat(parent_fd, f,
+ (const struct timespec[2]) { *TIMESPEC_STORE(ts), *TIMESPEC_STORE(ts) },
+ AT_SYMLINK_NOFOLLOW) < 0)
+ return log_error_errno(errno, "Failed to set timestamp of symlink '%s': %m", *path);
+
+ r = clamp_directory_mtime(parent_fd, ts);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set timestamp of '%s': %m", *path);
}
return 0;
@@ -7218,6 +7255,21 @@ static int partition_populate_directory(Context *context, Partition *p, char **r
if (r < 0)
return log_error_errno(errno, "Failed to create temporary directory: %m");
+ /* Nothing below touches its atime (adding an entry to a directory only moves mtime), so stamp here. */
+ usec_t ts = parse_source_date_epoch();
+ if (ts != USEC_INFINITY) {
+ _cleanup_close_ int rfd = -EBADF;
+ struct timespec tspec;
+
+ rfd = open(root, O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
+ if (rfd < 0)
+ return log_error_errno(errno, "Failed to open temporary directory: %m");
+
+ timespec_store(&tspec, ts);
+ if (futimens(rfd, (const struct timespec[2]) { tspec, tspec }) < 0)
+ return log_error_errno(errno, "Failed to set timestamp of temporary directory: %m");
+ }
+
r = do_copy_files(context, p, root);
if (r < 0)
return r;
diff --git a/src/shared/copy.c b/src/shared/copy.c
index 96a9e99a19..51dbf080ad 100644
--- a/src/shared/copy.c
+++ b/src/shared/copy.c
@@ -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,10 +1439,11 @@ int copy_directory_at_full(
override_uid,
override_gid,
copy_flags,
+ /* ts_clamp= */ USEC_INFINITY,
/* denylist= */ NULL,
/* subvolumes= */ NULL,
- /* progress_path= */ NULL,
- /* progress_bytes= */ NULL,
+ /* hardlink_context= */ NULL,
+ /* display_path= */ NULL,
progress_path,
progress_bytes,
userdata);
@@ -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;
diff --git a/src/shared/copy.h b/src/shared/copy.h
index 975b1e9942..9cc856515f 100644
--- a/src/shared/copy.h
+++ b/src/shared/copy.h
@@ -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);
diff --git a/src/shared/mkfs-util.c b/src/shared/mkfs-util.c
index a197eb28ce..cce6b6d55d 100644
--- a/src/shared/mkfs-util.c
+++ b/src/shared/mkfs-util.c
@@ -5,6 +5,7 @@
#include
#include "escape.h"
+#include "install-file.h"
#include "log.h"
#include "memory-util.h"
#include "mkfs-util.h"
@@ -17,6 +18,7 @@
#include "stdio-util.h"
#include "string-util.h"
#include "strv.h"
+#include "time-util.h"
#include "utf8.h"
int mkfs_exists(const char *fstype) {
@@ -150,6 +152,26 @@ static int mangle_fat_label(const char *s, char **ret) {
return 0;
}
+/* $SOURCE_DATE_EPOCH in seconds as *we* read it, or NULL if we have none. The tools we call parse the
+ * variable themselves, so without this they would act on values we reject (unparsable, overflowing),
+ * read differently (octal 017), or that secure_getenv() declines to look at under AT_SECURE. */
+static int source_date_epoch_seconds(char **ret) {
+ usec_t epoch;
+
+ assert(ret);
+
+ epoch = parse_source_date_epoch();
+ if (epoch == USEC_INFINITY) {
+ *ret = NULL;
+ return 0;
+ }
+
+ if (asprintf(ret, USEC_FMT, epoch / USEC_PER_SEC) < 0)
+ return -ENOMEM;
+
+ return 1;
+}
+
static int mtools_exec(char *const *argv, ForkFlags extra_fork_flags) {
int r;
@@ -161,6 +183,16 @@ static int mtools_exec(char *const *argv, ForkFlags extra_fork_flags) {
log_debug("Invoking mtools command: %s", strna(j));
}
+ _cleanup_free_ char *seconds = NULL, *source_date_epoch = NULL;
+ r = source_date_epoch_seconds(&seconds);
+ if (r < 0)
+ return log_oom();
+ if (seconds) {
+ source_date_epoch = strjoin("SOURCE_DATE_EPOCH=", seconds);
+ if (!source_date_epoch)
+ return log_oom();
+ }
+
r = pidref_safe_fork(
"(mtools)",
FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG_SIGTERM|FORK_WAIT|FORK_STDOUT_TO_STDERR|FORK_CLOSE_ALL_FDS|extra_fork_flags,
@@ -174,7 +206,7 @@ static int mtools_exec(char *const *argv, ForkFlags extra_fork_flags) {
execve(argv[0], argv,
STRV_MAKE("MTOOLS_SKIP_CHECK=1",
"TZ=UTC",
- strv_find_prefix(environ, "SOURCE_DATE_EPOCH=")));
+ source_date_epoch));
log_full_errno(FLAGS_SET(extra_fork_flags, FORK_LOG) ? LOG_ERR : LOG_DEBUG, errno, "Failed to execute %s: %m", argv[0]);
_exit(EXIT_FAILURE);
@@ -430,8 +462,11 @@ int make_filesystem(
* 0 value handling, where $E2FSPROGS_FAKE_TIME=0 is ignored and the current time is used,
* but $SOURCE_DATE_EPOCH=0 sets 1970-01-01 as the timestamp. */
if (!secure_getenv("E2FSPROGS_FAKE_TIME")) { /* honor $E2FSPROGS_FAKE_TIME if already set */
- const char *e = secure_getenv("SOURCE_DATE_EPOCH");
- if (e && strv_extend_strv(&env, STRV_MAKE("E2FSPROGS_FAKE_TIME", e), /* filter_duplicates= */ false) < 0)
+ _cleanup_free_ char *seconds = NULL;
+
+ if (source_date_epoch_seconds(&seconds) < 0)
+ return log_oom();
+ if (seconds && strv_extend_strv(&env, STRV_MAKE("E2FSPROGS_FAKE_TIME", seconds), /* filter_duplicates= */ false) < 0)
return log_oom();
}
@@ -650,6 +685,10 @@ int make_filesystem(
fork_flags |= FORK_NEW_MOUNTNS;
}
+ _cleanup_free_ char *source_date_epoch = NULL;
+ if (source_date_epoch_seconds(&source_date_epoch) < 0)
+ return log_oom();
+
log_info("Formatting %s as %s", node, fstype);
if (DEBUG_LOGGING) {
@@ -671,14 +710,21 @@ int make_filesystem(
STRV_FOREACH_PAIR(k, v, env)
if (setenv(*k, *v, /* replace= */ true) < 0) {
- log_error_errno(r, "Failed to set %s=%s environment variable: %m", *k, *v);
+ log_error_errno(errno, "Failed to set %s=%s environment variable: %m", *k, *v);
_exit(EXIT_FAILURE);
}
+ /* Same as in mtools_exec(): the mkfs tool gets our reading of the variable, or none. */
+ if (source_date_epoch ? setenv("SOURCE_DATE_EPOCH", source_date_epoch, /* replace= */ true) < 0
+ : unsetenv("SOURCE_DATE_EPOCH") < 0) {
+ log_error_errno(errno, "Failed to adjust $SOURCE_DATE_EPOCH: %m");
+ _exit(EXIT_FAILURE);
+ }
+
/* mkfs.btrfs refuses to operate on block devices with mounted partitions, even if operating
* on unformatted free space, so let's trick it and other mkfs tools into thinking no
* partitions are mounted. See https://github.com/kdave/btrfs-progs/issues/640 for more
- ° information. */
+ * information. */
if (fork_flags & FORK_NEW_MOUNTNS)
(void) mount_nofollow_verbose(LOG_DEBUG, "/dev/null", "/proc/self/mounts", NULL, MS_BIND, NULL);
diff --git a/src/test/test-copy.c b/src/test/test-copy.c
index e3e93f3642..feed013351 100644
--- a/src/test/test-copy.c
+++ b/src/test/test-copy.c
@@ -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);
diff --git a/test/units/TEST-58-REPART.sh b/test/units/TEST-58-REPART.sh
index d2b0e95970..c862c07d2e 100755
--- a/test/units/TEST-58-REPART.sh
+++ b/test/units/TEST-58-REPART.sh
@@ -2166,7 +2166,7 @@ EOF
}
testcase_make_symlinks() {
- local defs imgs output
+ local defs imgs output epoch
if systemd-detect-virt --quiet --container; then
echo "Skipping MakeSymlinks= test in container."
@@ -2191,7 +2191,13 @@ MakeSymlinks=/dir/foo-%a:/bar-%a
MakeSymlinks=/dir/bar-%a:../bar-%a
EOF
- systemd-repart --offline="$OFFLINE" \
+ # Build with an epoch, so that this also covers do_make_symlinks()' time stamping. It has to
+ # stamp the symlink itself and repair the directory it lands in, because creating an entry there
+ # bumps that directory back to the wall clock.
+ epoch=1700000000
+
+ env SOURCE_DATE_EPOCH="$epoch" \
+ systemd-repart --offline="$OFFLINE" \
--definitions="$defs" \
--empty=create \
--size=1G \
@@ -2205,6 +2211,12 @@ EOF
assert_eq "$(readlink "$imgs/mnt/dir/foo")" "/bar"
assert_eq "$(readlink "$imgs/mnt/dir/foo-${architecture}")" "/bar-${architecture}"
assert_eq "$(readlink "$imgs/mnt/dir/bar-${architecture}")" "../bar-${architecture}"
+
+ # the symlink's own mtime (no -L)
+ assert_eq "$(stat -c %Y "$imgs/mnt/dir/foo")" "$epoch"
+ # a MakeDirectories= dir, stamped with the epoch and then bumped by the symlinks below it.
+ assert_eq "$(stat -c %Y "$imgs/mnt/dir")" "$epoch"
+
systemd-dissect -U "$imgs/mnt"
}
@@ -2756,6 +2768,123 @@ EOF
cmp "$imgs/test1.img" "$imgs/test2.img"
}
+testcase_vfat_reproducibility() {
+ local defs imgs img dot_efi dot_linux
+
+ if ! command -v mdir >/dev/null; then
+ echo "Skipping vfat reproducibility test, mtools is not installed."
+ return 0
+ fi
+
+ defs="$(mktemp --directory "/tmp/test-repart.defs.XXXXXXXXXX")"
+ imgs="$(mktemp --directory "/var/tmp/test-repart.imgs.XXXXXXXXXX")"
+ # shellcheck disable=SC2064
+ trap "rm -rf '$defs' '$imgs'" RETURN
+
+ tee "$defs/esp.conf" <"$imgs/tree/EFI/Linux/old.efi"
+ echo new >"$imgs/tree/EFI/Linux/new.efi"
+ touch --date=@1600000000 "$imgs/tree/EFI/Linux/old.efi"
+ touch --date=@1750000000 "$imgs/tree/EFI/Linux/new.efi"
+
+ # The timestamps as mdir(1) reports them.
+ local -r time_epoch="2023-11-14 22:13" # $SOURCE_DATE_EPOCH below, i.e. @1700000000
+ local -r time_old="2020-09-13 12:26" # old.efi's mtime, before the epoch
+ local -r time_new="2025-06-15 15:06" # new.efi's mtime, after the epoch
+
+ # Build $1 from the tree and set $img to it, in the "file@@offset" form mdir(1) wants. The
+ # remaining arguments are passed to env(1), to control $SOURCE_DATE_EPOCH.
+ build_image() {
+ local name="$1" output offset
+ shift
+
+ output=$(env "$@" \
+ systemd-repart \
+ --offline="$OFFLINE" \
+ --definitions="$defs" \
+ --empty=create \
+ --size=auto \
+ --seed="$seed" \
+ --dry-run=no \
+ --root="$imgs/tree" \
+ --json=pretty \
+ "$imgs/$name")
+
+ offset=$(jq -r '.[0].offset' <<<"$output")
+ img="$imgs/$name@@$offset"
+
+ # dump listing for debugging
+ fat_entry -/ ::/
+ }
+
+ # Print $img's mdir(1) entry for the given path. mdir lists a directory's contents; a trailing `*`
+ # gets the directory's own entry instead. Insensitive path matching, thus works with mtools' 8.3
+ # lower case and kernel vfat's upper case names.
+ fat_entry() {
+ env MTOOLS_SKIP_CHECK=1 TZ=UTC mdir -i "$img" "$@"
+ }
+
+ # Same, for the "." entry of directory $1, which cannot be addressed directly.
+ fat_dot_entry() {
+ fat_entry "$1" | grep -E '^\. +'
+ }
+
+ build_image epoch.img SOURCE_DATE_EPOCH=1700000000
+
+ # The directories and the file newer than the epoch are clamped down to it, ...
+ assert_in "$time_epoch" "$(fat_entry '::/EFI*')"
+ assert_in "$time_epoch" "$(fat_entry '::/EFI/Linux*')"
+ assert_in "$time_epoch" "$(fat_entry '::/EFI/Linux/new.efi')"
+ # ... while the older one keeps its own mtime.
+ assert_in "$time_old" "$(fat_entry '::/EFI/Linux/old.efi')"
+
+ # "." and ".." behave differently
+ dot_efi=$(fat_dot_entry '::/EFI')
+ dot_linux=$(fat_dot_entry '::/EFI/Linux')
+ if [[ "$OFFLINE" == "yes" ]]; then
+ # mmd creates them and respects SOURCE_DATE_EPOCH
+ assert_in "$time_epoch" "$dot_efi"
+ assert_in "$time_epoch" "$dot_linux"
+ else
+ # The kernel's vfat driver creates them with wallclock mtime. Our utimensat()
+ # afterwards only rewrites the directory's entry in its *parent*, never this pair.
+ # Out of reach from userspace, remains unreproducible. Just log them.
+ echo "'.' entries carry the wall clock, as expected online: $dot_efi / $dot_linux"
+ fi
+
+ # Without an epoch, both files keep their own mtime.
+ build_image wallclock.img -u SOURCE_DATE_EPOCH
+
+ assert_in "$time_new" "$(fat_entry '::/EFI/Linux/new.efi')"
+ assert_in "$time_old" "$(fat_entry '::/EFI/Linux/old.efi')"
+
+ # A value we cannot parse is dropped rather than passed on
+ build_image bogus.img SOURCE_DATE_EPOCH=99999999999999999999
+
+ assert_in "$time_new" "$(fat_entry '::/EFI/Linux/new.efi')"
+ assert_in "$time_old" "$(fat_entry '::/EFI/Linux/old.efi')"
+
+ # SOURCE_DATE_EPOCH in hex - we do parse that, but mtools doesn't
+ build_image hex.img SOURCE_DATE_EPOCH=0x6553F100
+
+ assert_in "$time_epoch" "$(fat_entry '::/EFI*')"
+
+ # Note the images still are not reproducible byte for byte: the volume label entry carries
+ # mkfs.fat's wall clock, in *both* modes, and that needs a dosfstools with $SOURCE_DATE_EPOCH
+ # support (https://github.com/dosfstools/dosfstools/commit/8da7bc93315c, release > 4.2). Once
+ # that lands, offline mode can move to a `cmp` like in testcase_ext_reproducibility; online
+ # mode cannot, because of the "." and ".." entries above.
+}
+
testcase_luks2_keyhash() {
local defs imgs output root