]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
dissect: Introduce --copy-ownership= to configure chown behavior
authorDaanDeMeyer <daan.j.demeyer@gmail.com>
Sat, 27 Dec 2025 19:37:02 +0000 (20:37 +0100)
committerDaan De Meyer <daan@amutable.com>
Thu, 12 Feb 2026 08:45:13 +0000 (09:45 +0100)
Currently, if we're copying a file, we won't copy the owner UID/GID
from the source. If we're copying a directory, we will copy the owner
UID/GID from the source. Let's give users a bit more control over this
behavior by introducing --copy-ownership= which will default to the
current behavior but allows users to explicitly enable/disable copying
of ownership.

man/systemd-dissect.xml
src/dissect/dissect.c
src/import/import-fs.c
src/shared/btrfs-util.c
src/shared/copy.c
src/shared/copy.h
src/test/test-copy.c
src/test/test-execute.c
test/units/TEST-50-DISSECT.dissect.sh

index 669cea96f4ed07c46e2738b3b85a55342936e92f..9368d90a5283aa55bc0d5a3581a582adb8af7f93 100644 (file)
         <xi:include href="version-info.xml" xpointer="v254"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><option>--copy-ownership=</option></term>
+
+        <listitem><para>Controls whether file ownership (user and group) is preserved when copying files
+        with <option>--copy-from</option> or <option>--copy-to</option>. Takes a boolean. If
+        <literal>yes</literal>, ownership is always preserved. If <literal>no</literal>, ownership is never
+        preserved and the current user's UID/GID is used instead. If not specified, ownership is preserved
+        when copying directory trees, but not when copying individual regular files.
+        </para>
+
+        <xi:include href="version-info.xml" xpointer="v260"/></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><option>--system</option></term>
         <term><option>--user</option></term>
index deee8f6c1d1624d10f89888b61f9ab2f946ff77c..fc818bf020cbfd980f8dda63049fcf0541201387 100644 (file)
@@ -109,6 +109,7 @@ static bool arg_all = false;
 static uid_t arg_uid_base = UID_INVALID;
 static bool arg_quiet = false;
 static ImageFilter *arg_image_filter = NULL;
+static int arg_copy_ownership = -1;
 
 STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
@@ -170,6 +171,8 @@ static int help(void) {
                "     --loop-ref=NAME      Set reference string for loopback device\n"
                "     --loop-ref-auto      Derive reference string from image file name\n"
                "     --mtree-hash=BOOL    Whether to include SHA256 hash in the mtree output\n"
+               "     --copy-ownership=BOOL\n"
+               "                          Whether to copy ownership when copying files\n"
                "     --user               Discover user images\n"
                "     --system             Discover system images\n"
                "     --all                Show hidden images too\n"
@@ -306,48 +309,50 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_USER,
                 ARG_ALL,
                 ARG_IMAGE_FILTER,
+                ARG_COPY_OWNERSHIP,
         };
 
         static const struct option options[] = {
-                { "help",          no_argument,       NULL, 'h'               },
-                { "version",       no_argument,       NULL, ARG_VERSION       },
-                { "no-pager",      no_argument,       NULL, ARG_NO_PAGER      },
-                { "no-legend",     no_argument,       NULL, ARG_NO_LEGEND     },
-                { "mount",         no_argument,       NULL, 'm'               },
-                { "umount",        no_argument,       NULL, 'u'               },
-                { "attach",        no_argument,       NULL, ARG_ATTACH        },
-                { "detach",        no_argument,       NULL, ARG_DETACH        },
-                { "with",          no_argument,       NULL, ARG_WITH          },
-                { "read-only",     no_argument,       NULL, 'r'               },
-                { "discard",       required_argument, NULL, ARG_DISCARD       },
-                { "fsck",          required_argument, NULL, ARG_FSCK          },
-                { "growfs",        required_argument, NULL, ARG_GROWFS        },
-                { "root-hash",     required_argument, NULL, ARG_ROOT_HASH     },
-                { "root-hash-sig", required_argument, NULL, ARG_ROOT_HASH_SIG },
-                { "usr-hash",      required_argument, NULL, ARG_USR_HASH      },
-                { "usr-hash-sig",  required_argument, NULL, ARG_USR_HASH_SIG  },
-                { "verity-data",   required_argument, NULL, ARG_VERITY_DATA   },
-                { "mkdir",         no_argument,       NULL, ARG_MKDIR         },
-                { "rmdir",         no_argument,       NULL, ARG_RMDIR         },
-                { "in-memory",     no_argument,       NULL, ARG_IN_MEMORY     },
-                { "list",          no_argument,       NULL, 'l'               },
-                { "mtree",         no_argument,       NULL, ARG_MTREE         },
-                { "copy-from",     no_argument,       NULL, 'x'               },
-                { "copy-to",       no_argument,       NULL, 'a'               },
-                { "json",          required_argument, NULL, ARG_JSON          },
-                { "discover",      no_argument,       NULL, ARG_DISCOVER      },
-                { "loop-ref",      required_argument, NULL, ARG_LOOP_REF      },
-                { "loop-ref-auto", no_argument,       NULL, ARG_LOOP_REF_AUTO },
-                { "image-policy",  required_argument, NULL, ARG_IMAGE_POLICY  },
-                { "validate",      no_argument,       NULL, ARG_VALIDATE      },
-                { "mtree-hash",    required_argument, NULL, ARG_MTREE_HASH    },
-                { "make-archive",  no_argument,       NULL, ARG_MAKE_ARCHIVE  },
-                { "shift",         no_argument,       NULL, ARG_SHIFT         },
-                { "system",        no_argument,       NULL, ARG_SYSTEM        },
-                { "user",          no_argument,       NULL, ARG_USER          },
-                { "all",           no_argument,       NULL, ARG_ALL           },
-                { "quiet",         no_argument,       NULL, 'q'               },
-                { "image-filter",  required_argument, NULL, ARG_IMAGE_FILTER  },
+                { "help",           no_argument,       NULL, 'h'                },
+                { "version",        no_argument,       NULL, ARG_VERSION        },
+                { "no-pager",       no_argument,       NULL, ARG_NO_PAGER       },
+                { "no-legend",      no_argument,       NULL, ARG_NO_LEGEND      },
+                { "mount",          no_argument,       NULL, 'm'                },
+                { "umount",         no_argument,       NULL, 'u'                },
+                { "attach",         no_argument,       NULL, ARG_ATTACH         },
+                { "detach",         no_argument,       NULL, ARG_DETACH         },
+                { "with",           no_argument,       NULL, ARG_WITH           },
+                { "read-only",      no_argument,       NULL, 'r'                },
+                { "discard",        required_argument, NULL, ARG_DISCARD        },
+                { "fsck",           required_argument, NULL, ARG_FSCK           },
+                { "growfs",         required_argument, NULL, ARG_GROWFS         },
+                { "root-hash",      required_argument, NULL, ARG_ROOT_HASH      },
+                { "root-hash-sig",  required_argument, NULL, ARG_ROOT_HASH_SIG  },
+                { "usr-hash",       required_argument, NULL, ARG_USR_HASH       },
+                { "usr-hash-sig",   required_argument, NULL, ARG_USR_HASH_SIG   },
+                { "verity-data",    required_argument, NULL, ARG_VERITY_DATA    },
+                { "mkdir",          no_argument,       NULL, ARG_MKDIR          },
+                { "rmdir",          no_argument,       NULL, ARG_RMDIR          },
+                { "in-memory",      no_argument,       NULL, ARG_IN_MEMORY      },
+                { "list",           no_argument,       NULL, 'l'                },
+                { "mtree",          no_argument,       NULL, ARG_MTREE          },
+                { "copy-from",      no_argument,       NULL, 'x'                },
+                { "copy-to",        no_argument,       NULL, 'a'                },
+                { "json",           required_argument, NULL, ARG_JSON           },
+                { "discover",       no_argument,       NULL, ARG_DISCOVER       },
+                { "loop-ref",       required_argument, NULL, ARG_LOOP_REF       },
+                { "loop-ref-auto",  no_argument,       NULL, ARG_LOOP_REF_AUTO  },
+                { "image-policy",   required_argument, NULL, ARG_IMAGE_POLICY   },
+                { "validate",       no_argument,       NULL, ARG_VALIDATE       },
+                { "mtree-hash",     required_argument, NULL, ARG_MTREE_HASH     },
+                { "make-archive",   no_argument,       NULL, ARG_MAKE_ARCHIVE   },
+                { "shift",          no_argument,       NULL, ARG_SHIFT          },
+                { "system",         no_argument,       NULL, ARG_SYSTEM         },
+                { "user",           no_argument,       NULL, ARG_USER           },
+                { "all",            no_argument,       NULL, ARG_ALL            },
+                { "quiet",          no_argument,       NULL, 'q'                },
+                { "image-filter",   required_argument, NULL, ARG_IMAGE_FILTER   },
+                { "copy-ownership", required_argument, NULL, ARG_COPY_OWNERSHIP },
                 {}
         };
 
@@ -631,6 +636,12 @@ static int parse_argv(int argc, char *argv[]) {
                         break;
                 }
 
+                case ARG_COPY_OWNERSHIP:
+                        r = parse_tristate_argument("--copy-ownership=", optarg, &arg_copy_ownership);
+                        if (r < 0)
+                                return r;
+                        break;
+
                 case '?':
                         return -EINVAL;
 
@@ -1447,6 +1458,13 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD
 
         assert(IN_SET(arg_action, ACTION_LIST, ACTION_MTREE, ACTION_COPY_FROM, ACTION_COPY_TO, ACTION_MAKE_ARCHIVE, ACTION_SHIFT));
 
+        /* Determine whether to copy ownership:
+         * --copy-ownership=yes: always try to preserve ownership
+         * --copy-ownership=no: never preserve ownership, use current user
+         * --copy-ownership=auto (default): preserve ownership for directory trees,
+         *                              but not for regular files (since DDI password tables are typically
+         *                              distinct from the host ones, individual file ownership is less meaningful) */
+
         if (arg_image) {
                 assert(m);
 
@@ -1509,7 +1527,12 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD
                 }
 
                 /* Try to copy as directory? */
-                r = copy_directory_at(source_fd, NULL, AT_FDCWD, arg_target, COPY_REFLINK|COPY_MERGE|COPY_REPLACE|COPY_SIGINT|COPY_HARDLINKS);
+                r = copy_directory_at(
+                                source_fd, /* from= */ NULL,
+                                AT_FDCWD, arg_target,
+                                arg_copy_ownership == 0 ? getuid() : UID_INVALID,
+                                arg_copy_ownership == 0 ? getgid() : GID_INVALID,
+                                COPY_REFLINK|COPY_MERGE|COPY_REPLACE|COPY_SIGINT|COPY_HARDLINKS);
                 if (r >= 0)
                         return 0;
                 if (r != -ENOTDIR)
@@ -1532,9 +1555,10 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD
 
                 (void) copy_xattr(source_fd, NULL, target_fd, NULL, 0);
                 (void) copy_access(source_fd, target_fd);
+                if (arg_copy_ownership > 0)
+                        (void) copy_owner(source_fd, target_fd);
                 (void) copy_times(source_fd, target_fd, 0);
 
-                /* When this is a regular file we don't copy ownership! */
                 return 0;
         }
 
@@ -1588,9 +1612,23 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD
                                 if (errno != ENOENT)
                                         return log_error_errno(errno, "Failed to open destination '%s': %m", arg_target);
 
-                                r = copy_tree_at(source_fd, ".", dfd, bn, UID_INVALID, GID_INVALID, COPY_REFLINK|COPY_MERGE|COPY_REPLACE|COPY_SIGINT|COPY_HARDLINKS, NULL, NULL);
+                                r = copy_tree_at(
+                                                source_fd, ".",
+                                                dfd, bn,
+                                                arg_copy_ownership == 0 ? getuid() : UID_INVALID,
+                                                arg_copy_ownership == 0 ? getgid() : GID_INVALID,
+                                                COPY_REFLINK|COPY_MERGE|COPY_REPLACE|COPY_SIGINT|COPY_HARDLINKS,
+                                                /* denylist= */ NULL,
+                                                /* subvolumes= */ NULL);
                         } else
-                                r = copy_tree_at(source_fd, ".", target_fd, ".", UID_INVALID, GID_INVALID, COPY_REFLINK|COPY_MERGE|COPY_REPLACE|COPY_SIGINT|COPY_HARDLINKS, NULL, NULL);
+                                r = copy_tree_at(
+                                                source_fd, ".",
+                                                target_fd, ".",
+                                                arg_copy_ownership == 0 ? getuid() : UID_INVALID,
+                                                arg_copy_ownership == 0 ? getgid() : GID_INVALID,
+                                                COPY_REFLINK|COPY_MERGE|COPY_REPLACE|COPY_SIGINT|COPY_HARDLINKS,
+                                                /* denylist= */ NULL,
+                                                /* subvolumes= */ NULL);
                         if (r < 0)
                                 return log_error_errno(r, "Failed to copy '%s' to '%s' in image '%s': %m", arg_source, arg_target, arg_image);
 
@@ -1611,9 +1649,10 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD
 
                 (void) copy_xattr(source_fd, NULL, target_fd, NULL, 0);
                 (void) copy_access(source_fd, target_fd);
+                if (arg_copy_ownership > 0)
+                        (void) copy_owner(source_fd, target_fd);
                 (void) copy_times(source_fd, target_fd, 0);
 
-                /* When this is a regular file we don't copy ownership! */
                 return 0;
         }
 
index 947bc7c9b56c1a981eb7e7a3ac59fdd5e428c0a6..46daf3acc05f8efcff9ab81eb094a09ee3ce719d 100644 (file)
@@ -223,6 +223,8 @@ static int import_fs(int argc, char *argv[], void *userdata) {
                         r = copy_directory_at_full(
                                         fd, NULL,
                                         AT_FDCWD, dest,
+                                        /* override_uid= */ UID_INVALID,
+                                        /* override_gid= */ GID_INVALID,
                                         COPY_REFLINK|
                                         COPY_SAME_MOUNT|
                                         COPY_HARDLINKS|
index fffe9f2b34f2c6593f2865875a8f5590e2f8416a..9bd0a74c5fe83469f2d0c50f0e7ac1e7e77d8603 100644 (file)
@@ -1480,6 +1480,8 @@ int btrfs_subvol_snapshot_at_full(
                 r = copy_directory_at_full(
                                 dir_fdf, from,
                                 new_fd, subvolume,
+                                /* override_uid= */ UID_INVALID,
+                                /* override_gid= */ GID_INVALID,
                                 COPY_MERGE_EMPTY|
                                 COPY_REFLINK|
                                 COPY_SAME_MOUNT|
index 0598759313734ac720ac608c3e3a3587e040d5af..445c246359a7abb472f3768d4421423f766d8a66 100644 (file)
@@ -1442,6 +1442,8 @@ int copy_directory_at_full(
                 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,
@@ -1468,9 +1470,13 @@ int copy_directory_at_full(
                         dir_fdt, to,
                         st.st_dev,
                         COPY_DEPTH_MAX,
-                        UID_INVALID, GID_INVALID,
+                        override_uid,
+                        override_gid,
                         copy_flags,
-                        NULL, NULL, NULL, NULL,
+                        /* denylist= */ NULL,
+                        /* subvolumes= */ NULL,
+                        /* progress_path= */ NULL,
+                        /* progress_bytes= */ NULL,
                         progress_path,
                         progress_bytes,
                         userdata);
@@ -1750,6 +1756,18 @@ int copy_access(int fdf, int fdt) {
         return RET_NERRNO(fchmod(fdt, st.st_mode & 07777));
 }
 
+int copy_owner(int fdf, int fdt) {
+        struct stat st;
+
+        assert(fdf >= 0);
+        assert(fdt >= 0);
+
+        if (fstat(fdf, &st) < 0)
+                return -errno;
+
+        return RET_NERRNO(fchown(fdt, st.st_uid, st.st_gid));
+}
+
 int copy_rights_with_fallback(int fdf, int fdt, const char *patht) {
         struct stat st;
 
index 704abdb84b72b9b2c5f9929c92f3b65c917ebd56..6e4a3b177b337a6ea55b3e96457297ec2e00187c 100644 (file)
@@ -88,9 +88,9 @@ static inline int copy_tree(const char *from, const char *to, uid_t override_uid
         return copy_tree_at_full(AT_FDCWD, from, AT_FDCWD, to, override_uid, override_gid, copy_flags, denylist, subvolumes, NULL, NULL, NULL);
 }
 
-int copy_directory_at_full(int dir_fdf, const char *from, int dir_fdt, const char *to, CopyFlags copy_flags, copy_progress_path_t progress_path, copy_progress_bytes_t progress_bytes, void *userdata);
-static inline int copy_directory_at(int dir_fdf, const char *from, int dir_fdt, const char *to, CopyFlags copy_flags) {
-        return copy_directory_at_full(dir_fdf, from, dir_fdt, to, copy_flags, 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);
+static inline int copy_directory_at(int dir_fdf, const char *from, int dir_fdt, const char *to, uid_t override_uid, gid_t override_gid, CopyFlags copy_flags) {
+        return copy_directory_at_full(dir_fdf, from, dir_fdt, to, override_uid, override_gid, copy_flags, NULL, NULL, NULL);
 }
 
 int copy_bytes_full(int fdf, int fdt, uint64_t max_bytes, CopyFlags copy_flags, void **ret_remains, size_t *ret_remains_size, copy_progress_bytes_t progress, void *userdata);
@@ -100,6 +100,7 @@ static inline int copy_bytes(int fdf, int fdt, uint64_t max_bytes, CopyFlags cop
 
 int copy_times(int fdf, int fdt, CopyFlags flags);
 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);
 static inline int copy_rights(int fdf, int fdt) {
         return copy_rights_with_fallback(fdf, fdt, NULL); /* no fallback */
index e1bb7ae049378f2fd3102c552e2d3e8a2639cdcd..758a597fc539a8391b3fdf13b4bcdc8af363f3b2 100644 (file)
@@ -560,7 +560,7 @@ TEST(copy_lock) {
         assert_se(mkdirat(tfd, "abc", 0755) >= 0);
         assert_se(write_string_file_at(tfd, "abc/def", "abc", WRITE_STRING_FILE_CREATE) >= 0);
 
-        assert_se((fd = copy_directory_at(tfd, "abc", tfd, "qed", COPY_LOCK_BSD)) >= 0);
+        assert_se((fd = copy_directory_at(tfd, "abc", tfd, "qed", UID_INVALID, GID_INVALID, COPY_LOCK_BSD)) >= 0);
         assert_se(faccessat(tfd, "qed", F_OK, 0) >= 0);
         assert_se(faccessat(tfd, "qed/def", F_OK, 0) >= 0);
         assert_se(xopenat_lock(tfd, "qed", 0, LOCK_BSD, LOCK_EX|LOCK_NB) == -EAGAIN);
index 55b7c08924d116e05cf2453b83e3082e326776ca..ebd0260892bfd563180753c4e119d0dc600b1a69 100644 (file)
@@ -1497,7 +1497,7 @@ static int prepare_ns(const char *process_name) {
 
                 /* Copy unit files to make them accessible even when unprivileged. */
                 ASSERT_OK(get_testdata_dir("test-execute/", &unit_dir));
-                ASSERT_OK(copy_directory_at(AT_FDCWD, unit_dir, AT_FDCWD, PRIVATE_UNIT_DIR, COPY_MERGE_EMPTY));
+                ASSERT_OK(copy_directory_at(AT_FDCWD, unit_dir, AT_FDCWD, PRIVATE_UNIT_DIR, UID_INVALID, GID_INVALID, COPY_MERGE_EMPTY));
 
                 /* Mount tmpfs on the following directories to make not StateDirectory= or friends disturb the host. */
                 ASSERT_OK_OR(get_build_exec_dir(&build_dir), -ENOEXEC);
index ad8b14139c85ac08131d775f5401ab68ad6a7b41..07e0d1a8a04884fe7ba1bc52fd88e0926ff2d1b1 100755 (executable)
@@ -1050,6 +1050,45 @@ echo abc >abc
 systemd-dissect --copy-to /tmp/img abc /abc
 test -f /tmp/img/abc
 
+# Test --copy-ownership= option
+rm -rf /tmp/copychown-test
+mkdir -p /tmp/copychown-test/srcdir
+echo "test file" >/tmp/copychown-test/srcdir/testfile
+chown 1234:5678 /tmp/copychown-test/srcdir/testfile
+chown 1234:5678 /tmp/copychown-test/srcdir
+
+# Test --copy-ownership=yes preserves ownership for regular files
+systemd-dissect --copy-from /tmp/img etc/os-release /tmp/copychown-test/os-release-chown-yes --copy-ownership=yes
+test "$(stat -c %u:%g /tmp/copychown-test/os-release-chown-yes)" = "0:0"
+
+# Test --copy-ownership=no uses current user for regular files
+systemd-dissect --copy-from /tmp/img etc/os-release /tmp/copychown-test/os-release-chown-no --copy-ownership=no
+test "$(stat -c %u:%g /tmp/copychown-test/os-release-chown-no)" = "0:0"
+
+# Test --copy-ownership=auto (default) does not preserve ownership for regular files
+systemd-dissect --copy-from /tmp/img etc/os-release /tmp/copychown-test/os-release-chown-auto
+test "$(stat -c %u:%g /tmp/copychown-test/os-release-chown-auto)" = "0:0"
+
+# Test --copy-ownership=yes preserves ownership for directories
+systemd-dissect --copy-to /tmp/img /tmp/copychown-test/srcdir /copychown-dir-yes --copy-ownership=yes
+test "$(stat -c %u:%g /tmp/img/copychown-dir-yes)" = "1234:5678"
+test "$(stat -c %u:%g /tmp/img/copychown-dir-yes/testfile)" = "1234:5678"
+rm -rf /tmp/img/copychown-dir-yes
+
+# Test --copy-ownership=no overrides ownership for directories
+systemd-dissect --copy-to /tmp/img /tmp/copychown-test/srcdir /copychown-dir-no --copy-ownership=no
+test "$(stat -c %u:%g /tmp/img/copychown-dir-no)" = "0:0"
+test "$(stat -c %u:%g /tmp/img/copychown-dir-no/testfile)" = "0:0"
+rm -rf /tmp/img/copychown-dir-no
+
+# Test --copy-ownership=auto (default) preserves ownership for directories
+systemd-dissect --copy-to /tmp/img /tmp/copychown-test/srcdir /copychown-dir-auto
+test "$(stat -c %u:%g /tmp/img/copychown-dir-auto)" = "1234:5678"
+test "$(stat -c %u:%g /tmp/img/copychown-dir-auto/testfile)" = "1234:5678"
+rm -rf /tmp/img/copychown-dir-auto
+
+rm -rf /tmp/copychown-test
+
 # Test for dissect tool support with systemd-sysext
 mkdir -p /run/extensions/ testkit/usr/lib/extension-release.d/
 echo "ID=_any" >testkit/usr/lib/extension-release.d/extension-release.testkit