diff --git a/man/systemd-repart.xml b/man/systemd-repart.xml
index 27e35d19da..fa7b282bc0 100644
--- a/man/systemd-repart.xml
+++ b/man/systemd-repart.xml
@@ -244,6 +244,18 @@
+
+
+
+ Controls copy-on-write for newly created image files on filesystems that support it.
+ If true, copy-on-write is enabled; if false, it is disabled. If set to auto, the
+ filesystem or parent directory default is left unchanged. Copy-on-write allows files to be reflinked
+ into the image, reducing both build time and disk usage, but may result in fragmentation for images
+ subject to frequent random writes. Defaults to auto.
+
+
+
+
diff --git a/src/basic/fs-util.c b/src/basic/fs-util.c
index 2e9a27f2a8..76c55e30b6 100644
--- a/src/basic/fs-util.c
+++ b/src/basic/fs-util.c
@@ -1183,12 +1183,13 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
/* Sockets cannot be open()ed, only pinned via O_PATH. */
assert(!FLAGS_SET(xopen_flags, XO_SOCKET) || FLAGS_SET(open_flags, O_PATH));
/* XO_TRIGGER_AUTOMOUNT requires O_PATH and does not support creating inodes. XO_SUBVOLUME
- * requires O_CREAT, and XO_NOCOW needs a writable fd for its chattr ioctl, so neither is
+ * requires O_CREAT, and XO_COW/XO_NOCOW need a writable fd for their chattr ioctl, so none are
* compatible with XO_TRIGGER_AUTOMOUNT. */
assert(!FLAGS_SET(xopen_flags, XO_TRIGGER_AUTOMOUNT) ||
(FLAGS_SET(open_flags, O_PATH) && !FLAGS_SET(open_flags, O_CREAT)));
assert(!(FLAGS_SET(xopen_flags, XO_TRIGGER_AUTOMOUNT) && FLAGS_SET(xopen_flags, XO_SUBVOLUME)));
- assert(!(FLAGS_SET(xopen_flags, XO_TRIGGER_AUTOMOUNT) && FLAGS_SET(xopen_flags, XO_NOCOW)));
+ assert(!(FLAGS_SET(xopen_flags, XO_TRIGGER_AUTOMOUNT) && (xopen_flags & (XO_COW|XO_NOCOW))));
+ assert((xopen_flags & (XO_COW|XO_NOCOW)) != (XO_COW|XO_NOCOW));
/* Don't specify an access mode if you want auto mode. */
assert(!FLAGS_SET(xopen_flags, XO_AUTO_RW_RO) || (open_flags & O_ACCMODE_STRICT) == 0);
@@ -1202,7 +1203,8 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
*
* • If the path is specified NULL or empty, behaves like fd_reopen().
*
- * • If XO_NOCOW is specified will turn on the NOCOW btrfs flag on the file, if available.
+ * • If XO_COW or XO_NOCOW is specified will turn off or on the NOCOW btrfs flag on the file, if
+ * available.
*
* • if XO_REGULAR is specified will return an error if inode is not a regular file.
*
@@ -1436,8 +1438,8 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
goto error;
}
- if (FLAGS_SET(xopen_flags, XO_NOCOW)) {
- r = chattr_fd(fd, FS_NOCOW_FL, FS_NOCOW_FL);
+ if (xopen_flags & (XO_COW|XO_NOCOW)) {
+ r = chattr_fd(fd, FLAGS_SET(xopen_flags, XO_NOCOW) ? FS_NOCOW_FL : 0, FS_NOCOW_FL);
if (r < 0 && !ERRNO_IS_IOCTL_NOT_SUPPORTED(r))
goto error;
}
diff --git a/src/basic/fs-util.h b/src/basic/fs-util.h
index 0608d713ae..bc33f2f8e9 100644
--- a/src/basic/fs-util.h
+++ b/src/basic/fs-util.h
@@ -112,10 +112,11 @@ typedef enum XOpenFlags {
XO_LABEL = 1 << 0, /* When creating: relabel */
XO_SUBVOLUME = 1 << 1, /* When creating as directory: make it a subvolume */
XO_NOCOW = 1 << 2, /* Enable NOCOW mode after opening */
- XO_REGULAR = 1 << 3, /* Fail if the inode is not a regular file */
- XO_SOCKET = 1 << 4, /* Fail if the inode is not a socket */
- XO_TRIGGER_AUTOMOUNT = 1 << 5, /* Trigger automounts via open_tree(). Requires O_PATH. */
- XO_AUTO_RW_RO = 1 << 6, /* Open in O_RDWR mode if possible, O_RDONLY if not */
+ XO_COW = 1 << 3, /* Enable COW mode after opening */
+ XO_REGULAR = 1 << 4, /* Fail if the inode is not a regular file */
+ XO_SOCKET = 1 << 5, /* Fail if the inode is not a socket */
+ XO_TRIGGER_AUTOMOUNT = 1 << 6, /* Trigger automounts via open_tree(). Requires O_PATH. */
+ XO_AUTO_RW_RO = 1 << 7, /* Open in O_RDWR mode if possible, O_RDONLY if not */
} XOpenFlags;
int open_mkdir_at_full(int dirfd, const char *path, int flags, XOpenFlags xopen_flags, mode_t mode);
diff --git a/src/repart/repart.c b/src/repart/repart.c
index 5ba535e032..e3dd9ab3f6 100644
--- a/src/repart/repart.c
+++ b/src/repart/repart.c
@@ -215,6 +215,7 @@ static char *arg_generate_crypttab = NULL;
static Set *arg_verity_settings = NULL;
static bool arg_relax_copy_block_security = false;
static bool arg_varlink = false;
+static int arg_cow = -1;
static bool arg_eltorito = false;
static char *arg_eltorito_system = NULL;
static char *arg_eltorito_volume = NULL;
@@ -10232,6 +10233,13 @@ static int parse_argv(int argc, char *argv[]) {
return r;
break;
+ OPTION_LONG("cow", "BOOL|auto",
+ "Whether to enable copy-on-write for newly created image files"):
+ r = parse_tristate_argument_with_auto("--cow=", opts.arg, &arg_cow);
+ if (r < 0)
+ return r;
+ break;
+
OPTION_LONG("sector-size", "SIZE",
"Set the logical sector size for the image"):
r = parse_sector_size(opts.arg, &arg_sector_size);
@@ -11018,7 +11026,12 @@ static int find_root(Context *context) {
if (!s)
return log_oom();
- fd = xopenat_full(AT_FDCWD, arg_node, open_flags|O_CREAT|O_EXCL|O_NOFOLLOW, XO_NOCOW, 0666);
+ fd = xopenat_full(
+ AT_FDCWD,
+ arg_node,
+ open_flags|O_CREAT|O_EXCL|O_NOFOLLOW,
+ arg_cow < 0 ? 0 : (arg_cow > 0 ? XO_COW : XO_NOCOW),
+ 0666);
if (fd < 0)
return log_error_errno(fd, "Failed to create '%s': %m", arg_node);
diff --git a/test/units/TEST-58-REPART.sh b/test/units/TEST-58-REPART.sh
index 9970dac662..551e72c53c 100755
--- a/test/units/TEST-58-REPART.sh
+++ b/test/units/TEST-58-REPART.sh
@@ -94,6 +94,108 @@ else
exit 1
fi
+testcase_cow() {
+ local attrs cow_image default_nocow defs image imgs nocow_image probe
+
+ 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
+
+ # Skip the checks entirely if the underlying filesystem does not support the attribute.
+ if ! chattr -C "$imgs"; then
+ echo "NOCOW is not supported on $imgs, skipping tests"
+ return
+ fi
+
+ probe="$imgs/probe"
+ touch "$probe"
+ if ! chattr +C "$probe"; then
+ echo "NOCOW is not supported on $imgs, skipping tests"
+ return
+ fi
+ if ! chattr -C "$probe"; then
+ echo "COW is not supported on $imgs, skipping tests"
+ return
+ fi
+ rm "$probe"
+
+ chattr +C "$imgs"
+
+ image="$imgs/inherit-nocow.raw"
+ systemd-repart --offline="$OFFLINE" \
+ --definitions="$defs" \
+ --empty=create \
+ --size=16M \
+ --cow=auto \
+ --dry-run=no \
+ "$image"
+
+ attrs="$(lsattr -d -- "$image")"
+ assert_neq "$attrs" ""
+ read -r attrs _ <<<"$attrs"
+ assert_in "C" "$attrs"
+
+ cow_image="$imgs/cow.raw"
+ systemd-repart --offline="$OFFLINE" \
+ --definitions="$defs" \
+ --empty=create \
+ --size=16M \
+ --cow=yes \
+ --dry-run=no \
+ "$cow_image"
+
+ attrs="$(lsattr -d -- "$cow_image")"
+ assert_neq "$attrs" ""
+ read -r attrs _ <<<"$attrs"
+ assert_not_in "C" "$attrs"
+
+ chattr -C "$imgs"
+
+ probe="$imgs/probe"
+ touch "$probe"
+ attrs="$(lsattr -d -- "$probe")"
+ assert_neq "$attrs" ""
+ read -r attrs _ <<<"$attrs"
+ if [[ "$attrs" == *C* ]]; then
+ default_nocow=1
+ else
+ default_nocow=0
+ fi
+ rm "$probe"
+
+ image="$imgs/inherit-cow.raw"
+ systemd-repart --offline="$OFFLINE" \
+ --definitions="$defs" \
+ --empty=create \
+ --size=16M \
+ --dry-run=no \
+ "$image"
+
+ attrs="$(lsattr -d -- "$image")"
+ assert_neq "$attrs" ""
+ read -r attrs _ <<<"$attrs"
+ if (( default_nocow )); then
+ assert_in "C" "$attrs"
+ else
+ assert_not_in "C" "$attrs"
+ fi
+
+ nocow_image="$imgs/nocow.raw"
+ systemd-repart --offline="$OFFLINE" \
+ --definitions="$defs" \
+ --empty=create \
+ --size=16M \
+ --cow=no \
+ --dry-run=no \
+ "$nocow_image"
+
+ attrs="$(lsattr -d -- "$nocow_image")"
+ assert_neq "$attrs" ""
+ read -r attrs _ <<<"$attrs"
+ assert_in "C" "$attrs"
+}
+
testcase_basic() {
local defs imgs output
local loop volume