From f789245918bc767d2ed99cfad67f217fa2c7ef0e Mon Sep 17 00:00:00 2001 From: Totoro Date: Fri, 4 Sep 2026 15:48:10 +0800 Subject: [PATCH] machine: preserve read-only clone mode for foreign UID images machinectl clone --read-only ignored the requested read-only mode when cloning a foreign-UID-owned directory because the unprivileged clone path did not receive the read_only argument. Pass the flag through and apply the immutable bit via the already-open destination directory fd after copying. Keep this best-effort, matching the ordinary directory clone fallback, so failure to set the attribute does not turn a completed copy into a reported clone failure. Add a regression test using foreign-0 rather than a fixed UID. Verify the cloned image's ReadOnly property, copied contents and ownership, and that the immutable directory rejects new entries. Run this subtest only when file attributes and the existing rootless nspawn prerequisites are available, leaving the other machined tests unchanged. --- src/shared/discover-image.c | 14 +++++++++++--- test/units/TEST-13-NSPAWN.machined.sh | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index c88fa84016..fa0fd1928f 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -1687,7 +1687,7 @@ static int get_pool_directory( return 0; } -static int unprivileged_clone(Image *i, const char *new_path) { +static int unprivileged_clone(Image *i, const char *new_path, bool read_only) { int r; assert(i); @@ -1741,7 +1741,15 @@ static int unprivileged_clone(Image *i, const char *new_path) { link = sd_varlink_unref(link); /* Fork off child that moves into userns and does the copying */ - return copy_tree_at_foreign(tree_fd, target_fd, userns_fd); + r = copy_tree_at_foreign(tree_fd, target_fd, userns_fd); + if (r < 0) + return r; + + /* Match the best-effort immutable bit handling of ordinary directory clones. */ + if (read_only) + (void) chattr_fd(new_fd, FS_IMMUTABLE_FL, FS_IMMUTABLE_FL); + + return 0; } int image_clone(Image *i, const char *new_name, bool read_only, RuntimeScope scope) { @@ -1784,7 +1792,7 @@ int image_clone(Image *i, const char *new_name, bool read_only, RuntimeScope sco return r; if (i->foreign_uid_owned) - r = unprivileged_clone(i, new_path); + r = unprivileged_clone(i, new_path, read_only); else { r = btrfs_subvol_snapshot_at( AT_FDCWD, i->path, diff --git a/test/units/TEST-13-NSPAWN.machined.sh b/test/units/TEST-13-NSPAWN.machined.sh index 2774072699..86e701fe49 100755 --- a/test/units/TEST-13-NSPAWN.machined.sh +++ b/test/units/TEST-13-NSPAWN.machined.sh @@ -181,6 +181,24 @@ if lsattr -d /var/lib/machines >/dev/null; then [[ "$(machinectl show-image --property=ReadOnly --value clone2)" == yes ]] machinectl read-only clone2 no [[ "$(machinectl show-image --property=ReadOnly --value clone2)" == no ]] + + # Foreign-UID cloning also needs mountfsd/nsresourced and user namespace delegation. + if can_do_rootless_nspawn; then + mkdir /var/lib/machines/foreign-source + echo "foreign clone contents" >/var/lib/machines/foreign-source/payload + chown foreign-0:foreign-0 /var/lib/machines/foreign-source{,/payload} + machinectl clone --read-only foreign-source foreign-clone + [[ "$(machinectl show-image --property=ReadOnly --value foreign-clone)" == yes ]] + cmp /var/lib/machines/foreign-source/payload /var/lib/machines/foreign-clone/payload + assert_eq "$(stat -c %u:%g /var/lib/machines/foreign-source)" \ + "$(stat -c %u:%g /var/lib/machines/foreign-clone)" + assert_eq "$(stat -c %u:%g /var/lib/machines/foreign-source/payload)" \ + "$(stat -c %u:%g /var/lib/machines/foreign-clone/payload)" + (! touch /var/lib/machines/foreign-clone/new-file) + machinectl remove foreign-clone foreign-source + else + echo "Skipping foreign-UID read-only clone test: rootless nspawn prerequisites not met" + fi fi machinectl remove clone2 for i in {0..4}; do