mirror of
https://github.com/openwrt/openwrt.git
synced 2026-10-12 04:47:17 +04:00
tools/mtd-utils: make mkfs.ubifs images reproducible
UBIFS root file systems differ between builds, e.g. the one of the Microchip SAMA7G5-EK: the superblock gets a random UUID, the inodes get the ctime of the build host, which can't be reset with touch, and the entries of each directory are added in readdir() order, which depends on the host file system and decides inode numbers and layout. Add a patch that, when SOURCE_DATE_EPOCH is set, uses a time-based (version 1) UUID with that time and clamps all inode times to it. The node ID of the UUID is taken from the new --uuid-node option, which the image build sets from IMG_PART_DISKGUID, itself derived from SOURCE_DATE_EPOCH and the kernel vermagic. Directory entries are added sorted by name. Two trees with the same contents but different ctimes and readdir() order now give identical images; without SOURCE_DATE_EPOCH the UUID stays random. Link: https://github.com/openwrt/openwrt/pull/25430 Signed-off-by: Paul Spooren <mail@aparcar.org>
This commit is contained in:
3 files changed
+295
-1
No files matched your search
@@ -314,6 +314,7 @@ define Image/mkfs/ubifs
|
||||
$(if $(CONFIG_TARGET_UBIFS_COMPRESSION_LZO),--compr=lzo) \
|
||||
$(if $(CONFIG_TARGET_UBIFS_COMPRESSION_ZLIB),--compr=zlib) \
|
||||
$(if $(shell echo $(CONFIG_TARGET_UBIFS_JOURNAL_SIZE)),--jrn-size=$(CONFIG_TARGET_UBIFS_JOURNAL_SIZE)) \
|
||||
$(if $(IMG_PART_DISKGUID),--uuid-node=$(subst -,,$(IMG_PART_DISKGUID))) \
|
||||
--squash-uids \
|
||||
-o $@ -d $(call mkfs_target_dir,$(1))
|
||||
endef
|
||||
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=mtd-utils
|
||||
PKG_VERSION:=2.3.1
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.bz2
|
||||
PKG_SOURCE_URL:=https://infraroot.at/pub/mtd/
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Paul Spooren <mail@aparcar.org>
|
||||
Date: Sat, 3 Oct 2026 14:00:00 +0200
|
||||
Subject: [PATCH] mkfs.ubifs: make images reproducible
|
||||
|
||||
Two builds of the same root file system give different UBIFS images:
|
||||
|
||||
- the superblock gets a random UUID
|
||||
- the inodes get the ctime of the host, which can't be set, so it is
|
||||
the time the files were created or last changed
|
||||
- the entries of each directory are added in readdir() order, which
|
||||
depends on the host file system and decides the inode numbers and the
|
||||
layout of the image
|
||||
|
||||
When SOURCE_DATE_EPOCH is set, use a time-based (version 1) UUID with
|
||||
that time instead of a random one. Its node ID can be given with the new
|
||||
--uuid-node option, e.g. from a hash, otherwise it is zero. Either way
|
||||
the multicast bit is set, as for node IDs that are not a MAC address.
|
||||
|
||||
Also clamp atime, ctime and mtime of all inodes to SOURCE_DATE_EPOCH,
|
||||
and add the entries of each directory sorted by name, comparing bytes so
|
||||
the order does not depend on the locale either.
|
||||
|
||||
Signed-off-by: Paul Spooren <mail@aparcar.org>
|
||||
---
|
||||
ubifs-utils/mkfs.ubifs/mkfs.ubifs.c | 134 ++++++++++++++++++++++++----
|
||||
1 file changed, 117 insertions(+), 17 deletions(-)
|
||||
|
||||
--- a/ubifs-utils/mkfs.ubifs/mkfs.ubifs.c
|
||||
+++ b/ubifs-utils/mkfs.ubifs/mkfs.ubifs.c
|
||||
@@ -159,6 +159,9 @@ static int squash_owner;
|
||||
static int do_create_inum_attr;
|
||||
static char *context;
|
||||
static int context_len;
|
||||
+static long long source_date_epoch = -1;
|
||||
+static int uuid_node_set;
|
||||
+static unsigned char uuid_node[6];
|
||||
static struct stat context_st;
|
||||
|
||||
/* The 'head' (position) which nodes are written */
|
||||
@@ -188,6 +191,7 @@ enum {
|
||||
HASH_ALGO_OPTION = CHAR_MAX + 1,
|
||||
AUTH_KEY_OPTION,
|
||||
AUTH_CERT_OPTION,
|
||||
+ UUID_NODE_OPTION,
|
||||
};
|
||||
|
||||
static const struct option longopts[] = {
|
||||
@@ -221,6 +225,7 @@ static const struct option longopts[] =
|
||||
{"hash-algo", 1, NULL, HASH_ALGO_OPTION},
|
||||
{"auth-key", 1, NULL, AUTH_KEY_OPTION},
|
||||
{"auth-cert", 1, NULL, AUTH_CERT_OPTION},
|
||||
+ {"uuid-node", 1, NULL, UUID_NODE_OPTION},
|
||||
{NULL, 0, NULL, 0}
|
||||
};
|
||||
|
||||
@@ -277,6 +282,8 @@ static const char *helptext =
|
||||
" for signing\n"
|
||||
" --auth-cert=FILE Authentication certificate filename for signing. Unused\n"
|
||||
" when certificate is provided via PKCS #11\n"
|
||||
+" --uuid-node=HEX node ID of the time-based UUID used if SOURCE_DATE_EPOCH\n"
|
||||
+" is set, from the first 12 of at least 12 hex digits\n"
|
||||
"-h, --help display this help text\n\n"
|
||||
"Note, SIZE is specified in bytes, but it may also be specified in Kilobytes,\n"
|
||||
"Megabytes, and Gigabytes if a KiB, MiB, or GiB suffix is used.\n\n"
|
||||
@@ -540,17 +547,39 @@ static void select_default_compr(void)
|
||||
#endif
|
||||
}
|
||||
|
||||
+/* Take the node ID from the first 12 of at least 12 hex digits */
|
||||
+static int parse_uuid_node(const char *str)
|
||||
+{
|
||||
+ int i;
|
||||
+
|
||||
+ if (strlen(str) < 12 || str[strspn(str, "0123456789abcdefABCDEF")])
|
||||
+ return -1;
|
||||
+ for (i = 0; i < 6; i++)
|
||||
+ if (sscanf(str + 2 * i, "%2hhx", &uuid_node[i]) != 1)
|
||||
+ return -1;
|
||||
+ uuid_node_set = 1;
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
static int get_options(int argc, char**argv)
|
||||
{
|
||||
int opt, i, fscrypt_flags = FS_POLICY_FLAGS_PAD_4;
|
||||
const char *key_file = NULL, *key_desc = NULL;
|
||||
const char *tbl_file = NULL;
|
||||
struct stat st;
|
||||
- char *endp;
|
||||
+ char *endp, *env;
|
||||
#ifdef WITH_CRYPTO
|
||||
const char *cipher_name = NULL;
|
||||
#endif
|
||||
|
||||
+ env = getenv("SOURCE_DATE_EPOCH");
|
||||
+ if (env && *env) {
|
||||
+ errno = 0;
|
||||
+ source_date_epoch = strtoll(env, &endp, 10);
|
||||
+ if (errno || *endp || source_date_epoch < 0)
|
||||
+ return errmsg("bad SOURCE_DATE_EPOCH '%s'", env);
|
||||
+ }
|
||||
+
|
||||
c->fanout = 8;
|
||||
c->orph_lebs = 1;
|
||||
c->key_hash = key_r5_hash;
|
||||
@@ -778,6 +807,10 @@ static int get_options(int argc, char**a
|
||||
}
|
||||
break;
|
||||
}
|
||||
+ case UUID_NODE_OPTION:
|
||||
+ if (parse_uuid_node(optarg))
|
||||
+ return errmsg("bad UUID node '%s'", optarg);
|
||||
+ break;
|
||||
#ifdef WITH_CRYPTO
|
||||
case 'C':
|
||||
cipher_name = optarg;
|
||||
@@ -1090,6 +1123,14 @@ static int reserve_space(int len, int *l
|
||||
return 0;
|
||||
}
|
||||
|
||||
+/* Clamp times to SOURCE_DATE_EPOCH, as ctime can't be set on the host */
|
||||
+static time_t clamp_time(time_t t)
|
||||
+{
|
||||
+ if (source_date_epoch >= 0 && t > source_date_epoch)
|
||||
+ return source_date_epoch;
|
||||
+ return t;
|
||||
+}
|
||||
+
|
||||
/**
|
||||
* add_node - write a node to the head.
|
||||
* @key: node key
|
||||
@@ -1178,9 +1219,9 @@ static int add_xattr(struct ubifs_ino_no
|
||||
* The time fields are updated assuming the default time granularity
|
||||
* of 1 second. To support finer granularities, utime() would be needed.
|
||||
*/
|
||||
- ino->atime_sec = cpu_to_le64(st->st_atime);
|
||||
- ino->ctime_sec = cpu_to_le64(st->st_ctime);
|
||||
- ino->mtime_sec = cpu_to_le64(st->st_mtime);
|
||||
+ ino->atime_sec = cpu_to_le64(clamp_time(st->st_atime));
|
||||
+ ino->ctime_sec = cpu_to_le64(clamp_time(st->st_ctime));
|
||||
+ ino->mtime_sec = cpu_to_le64(clamp_time(st->st_mtime));
|
||||
ino->atime_nsec = 0;
|
||||
ino->ctime_nsec = 0;
|
||||
ino->mtime_nsec = 0;
|
||||
@@ -1495,9 +1536,9 @@ static int add_inode(struct stat *st, in
|
||||
* The time fields are updated assuming the default time granularity
|
||||
* of 1 second. To support finer granularities, utime() would be needed.
|
||||
*/
|
||||
- ino->atime_sec = cpu_to_le64(st->st_atime);
|
||||
- ino->ctime_sec = cpu_to_le64(st->st_ctime);
|
||||
- ino->mtime_sec = cpu_to_le64(st->st_mtime);
|
||||
+ ino->atime_sec = cpu_to_le64(clamp_time(st->st_atime));
|
||||
+ ino->ctime_sec = cpu_to_le64(clamp_time(st->st_ctime));
|
||||
+ ino->mtime_sec = cpu_to_le64(clamp_time(st->st_mtime));
|
||||
ino->atime_nsec = 0;
|
||||
ino->ctime_nsec = 0;
|
||||
ino->mtime_nsec = 0;
|
||||
@@ -1928,6 +1969,54 @@ static int add_non_dir(const char *path_
|
||||
return errmsg("file '%s' has unknown inode type", path_name);
|
||||
}
|
||||
|
||||
+/*
|
||||
+ * With SOURCE_DATE_EPOCH set, use a time-based (version 1) UUID with that
|
||||
+ * time instead of a random one, so that the image is reproducible. The node
|
||||
+ * ID comes from --uuid-node, e.g. a hash, otherwise it is zero. Either way the
|
||||
+ * multicast bit is set, which marks it as not being a MAC address. Returns 1
|
||||
+ * if it did so.
|
||||
+ */
|
||||
+static int set_reproducible_uuid(unsigned char *uuid)
|
||||
+{
|
||||
+ unsigned long long t;
|
||||
+
|
||||
+ if (source_date_epoch < 0)
|
||||
+ return 0;
|
||||
+
|
||||
+ /* 100 ns intervals from 1582-10-15, the start of UUID time */
|
||||
+ t = source_date_epoch * 10000000ULL + 0x01b21dd213814000ULL;
|
||||
+ uuid[0] = t >> 24; /* time_low */
|
||||
+ uuid[1] = t >> 16;
|
||||
+ uuid[2] = t >> 8;
|
||||
+ uuid[3] = t;
|
||||
+ uuid[4] = t >> 40; /* time_mid */
|
||||
+ uuid[5] = t >> 32;
|
||||
+ uuid[6] = ((t >> 56) & 0x0f) | 0x10; /* version 1, time_hi */
|
||||
+ uuid[7] = t >> 48;
|
||||
+ uuid[8] = 0x80; /* RFC 4122 variant, clock_seq 0 */
|
||||
+ uuid[9] = 0;
|
||||
+ if (uuid_node_set)
|
||||
+ memcpy(uuid + 10, uuid_node, 6);
|
||||
+ else
|
||||
+ memset(uuid + 10, 0, 6);
|
||||
+ /* multicast bit, as the node ID is not a MAC address */
|
||||
+ uuid[10] |= 0x01;
|
||||
+ return 1;
|
||||
+}
|
||||
+
|
||||
+/* Byte-wise order, independent of the locale unlike alphasort() */
|
||||
+static int dirent_cmp(const struct dirent **a, const struct dirent **b)
|
||||
+{
|
||||
+ return strcmp((*a)->d_name, (*b)->d_name);
|
||||
+}
|
||||
+
|
||||
+static void free_dirents(struct dirent **entries, int cnt)
|
||||
+{
|
||||
+ while (cnt-- > 0)
|
||||
+ free(entries[cnt]);
|
||||
+ free(entries);
|
||||
+}
|
||||
+
|
||||
/**
|
||||
* add_directory - write a directory tree to the output file.
|
||||
* @dir_name: directory path name
|
||||
@@ -1941,6 +2030,8 @@ static int add_directory(const char *dir
|
||||
int existing, struct fscrypt_context *fctx)
|
||||
{
|
||||
struct dirent *entry;
|
||||
+ struct dirent **entries = NULL;
|
||||
+ int entry_cnt = 0, entry_idx = 0;
|
||||
DIR *dir = NULL;
|
||||
int kname_len, err = 0;
|
||||
loff_t size = UBIFS_INO_NODE_SZ;
|
||||
@@ -1959,6 +2050,17 @@ static int add_directory(const char *dir
|
||||
if (dir == NULL)
|
||||
return sys_errmsg("cannot open directory '%s'",
|
||||
dir_name);
|
||||
+ /*
|
||||
+ * Add the entries in a fixed order rather than in the one of
|
||||
+ * readdir(), which depends on the host file system. The order
|
||||
+ * decides the inode numbers and the layout of the image.
|
||||
+ */
|
||||
+ entry_cnt = scandir(dir_name, &entries, NULL, dirent_cmp);
|
||||
+ if (entry_cnt < 0) {
|
||||
+ closedir(dir);
|
||||
+ return sys_errmsg("error reading directory '%s'",
|
||||
+ dir_name);
|
||||
+ }
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1976,14 +2078,9 @@ static int add_directory(const char *dir
|
||||
struct stat dent_st;
|
||||
struct fscrypt_context *new_fctx = NULL;
|
||||
|
||||
- errno = 0;
|
||||
- entry = readdir(dir);
|
||||
- if (!entry) {
|
||||
- if (errno == 0)
|
||||
- break;
|
||||
- sys_errmsg("error reading directory '%s'", dir_name);
|
||||
- goto out_free;
|
||||
- }
|
||||
+ if (entry_idx >= entry_cnt)
|
||||
+ break;
|
||||
+ entry = entries[entry_idx++];
|
||||
|
||||
if (strcmp(".", entry->d_name) == 0)
|
||||
continue;
|
||||
@@ -2148,6 +2245,7 @@ static int add_directory(const char *dir
|
||||
goto out_free;
|
||||
|
||||
free(name);
|
||||
+ free_dirents(entries, entry_cnt);
|
||||
if (existing && closedir(dir) == -1)
|
||||
return sys_errmsg("error closing directory '%s'", dir_name);
|
||||
|
||||
@@ -2156,6 +2254,7 @@ static int add_directory(const char *dir
|
||||
out_free:
|
||||
free(itr);
|
||||
free(name);
|
||||
+ free_dirents(entries, entry_cnt);
|
||||
if (existing)
|
||||
closedir(dir);
|
||||
return -1;
|
||||
@@ -2201,7 +2300,7 @@ static int write_data(void)
|
||||
if (squash_owner)
|
||||
root_st.st_uid = root_st.st_gid = 0;
|
||||
} else {
|
||||
- root_st.st_mtime = time(NULL);
|
||||
+ root_st.st_mtime = clamp_time(time(NULL));
|
||||
root_st.st_atime = root_st.st_ctime = root_st.st_mtime;
|
||||
root_st.st_mode = mode;
|
||||
}
|
||||
@@ -2554,7 +2653,8 @@ static int write_super(void)
|
||||
sup->rp_size = cpu_to_le64(c->rp_size);
|
||||
sup->time_gran = cpu_to_le32(DEFAULT_TIME_GRAN);
|
||||
sup->hash_algo = cpu_to_le16(c->hash_algo);
|
||||
- uuid_generate_random(sup->uuid);
|
||||
+ if (!set_reproducible_uuid(sup->uuid))
|
||||
+ uuid_generate_random(sup->uuid);
|
||||
|
||||
if (verbose) {
|
||||
char s[40];
|
||||
Reference in new issue
Block a user