From f89c9149684fec4a52268bddff48ea4e37bab049 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 22 Aug 2025 11:04:56 +0200 Subject: [PATCH 1/9] acl-util: add new acl_set_perm() helper --- src/shared/acl-util.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/shared/acl-util.h b/src/shared/acl-util.h index a15790b94d..ea4aca7bdb 100644 --- a/src/shared/acl-util.h +++ b/src/shared/acl-util.h @@ -62,6 +62,10 @@ DEFINE_TRIVIAL_CLEANUP_FUNC_FULL_RENAME(char*, sym_acl_free, acl_free_charpp, NU DEFINE_TRIVIAL_CLEANUP_FUNC_FULL_RENAME(uid_t*, sym_acl_free, acl_free_uid_tpp, NULL); DEFINE_TRIVIAL_CLEANUP_FUNC_FULL_RENAME(gid_t*, sym_acl_free, acl_free_gid_tpp, NULL); +static inline int acl_set_perm(acl_permset_t ps, acl_perm_t p, bool b) { + return (b ? sym_acl_add_perm : sym_acl_delete_perm)(ps, p); +} + #else #define ACL_READ 0x04 From d4d94fcebaeee2085a4b72bf2e2f1477afebfb5b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 21 Aug 2025 12:28:06 +0200 Subject: [PATCH 2/9] tar-util: include xattrs in generated tarballs We can already unpack them, let's pack them up to. --- src/shared/libarchive-util.c | 2 ++ src/shared/libarchive-util.h | 1 + src/shared/tar-util.c | 61 +++++++++++++++++++++++++++++++----- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/shared/libarchive-util.c b/src/shared/libarchive-util.c index 961d9f6a03..65e9f5a511 100644 --- a/src/shared/libarchive-util.c +++ b/src/shared/libarchive-util.c @@ -41,6 +41,7 @@ DLSYM_PROTOTYPE(archive_entry_uid) = NULL; #if HAVE_LIBARCHIVE_UID_IS_SET DLSYM_PROTOTYPE(archive_entry_uid_is_set) = NULL; #endif +DLSYM_PROTOTYPE(archive_entry_xattr_add_entry) = NULL; DLSYM_PROTOTYPE(archive_entry_xattr_next) = NULL; DLSYM_PROTOTYPE(archive_entry_xattr_reset) = NULL; DLSYM_PROTOTYPE(archive_error_string) = NULL; @@ -105,6 +106,7 @@ int dlopen_libarchive(void) { #if HAVE_LIBARCHIVE_UID_IS_SET DLSYM_ARG(archive_entry_uid_is_set), #endif + DLSYM_ARG(archive_entry_xattr_add_entry), DLSYM_ARG(archive_entry_xattr_next), DLSYM_ARG(archive_entry_xattr_reset), DLSYM_ARG(archive_error_string), diff --git a/src/shared/libarchive-util.h b/src/shared/libarchive-util.h index 83ee19d27a..2f40c64d85 100644 --- a/src/shared/libarchive-util.h +++ b/src/shared/libarchive-util.h @@ -34,6 +34,7 @@ extern DLSYM_PROTOTYPE(archive_entry_set_symlink); extern DLSYM_PROTOTYPE(archive_entry_set_uid); extern DLSYM_PROTOTYPE(archive_entry_symlink); extern DLSYM_PROTOTYPE(archive_entry_uid); +extern DLSYM_PROTOTYPE(archive_entry_xattr_add_entry); extern DLSYM_PROTOTYPE(archive_entry_xattr_next); extern DLSYM_PROTOTYPE(archive_entry_xattr_reset); extern DLSYM_PROTOTYPE(archive_error_string); diff --git a/src/shared/tar-util.c b/src/shared/tar-util.c index c75d7eb666..95b5ba820c 100644 --- a/src/shared/tar-util.c +++ b/src/shared/tar-util.c @@ -13,6 +13,7 @@ #include "fs-util.h" #include "iovec-util.h" #include "libarchive-util.h" +#include "nulstr-util.h" #include "path-util.h" #include "recurse-dir.h" #include "stat-util.h" @@ -332,6 +333,8 @@ static int archive_entry_read_stat( size_t *n_xa, TarFlags flags) { + int r; + assert(entry); /* Fills in all fields that are present in the archive entry. Doesn't change the fields if the entry @@ -357,16 +360,26 @@ static int archive_entry_read_stat( for (;;) { const char *name = NULL; struct iovec data; - (void) sym_archive_entry_xattr_next(entry, &name, (const void**) &data.iov_base, &data.iov_len); - if (!name) + r = sym_archive_entry_xattr_next(entry, &name, (const void**) &data.iov_base, &data.iov_len); + if (r != ARCHIVE_OK) break; + assert(name); if (xattr_is_acl(name)) continue; if (!FLAGS_SET(flags, TAR_SELINUX) && xattr_is_selinux(name)) continue; + bool duplicate = false; + FOREACH_ARRAY(i, *xa, *n_xa) + if (streq(i->name, name)) { + duplicate = true; + break; + } + if (duplicate) + continue; + _cleanup_free_ char *n = strdup(name); if (!n) return log_oom(); @@ -675,6 +688,11 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { return 0; } +struct make_archive_data { + struct archive *archive; + TarFlags flags; +}; + static int archive_item( RecurseDirEvent event, const char *path, @@ -684,7 +702,7 @@ static int archive_item( const struct statx *sx, void *userdata) { - struct archive *a = ASSERT_PTR(userdata); + struct make_archive_data *d = ASSERT_PTR(userdata); int r; assert(path); @@ -745,8 +763,32 @@ static int archive_item( sym_archive_entry_set_symlink(entry, s); } - if (sym_archive_write_header(a, entry) != ARCHIVE_OK) - return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to write archive entry header: %s", sym_archive_error_string(a)); + _cleanup_free_ char *xattrs = NULL; + r = flistxattr_malloc(inode_fd, &xattrs); + if (r < 0 && !ERRNO_IS_NEG_NOT_SUPPORTED(r) && r != -ENODATA) + return log_error_errno(r, "Failed to read xattr list of '%s': %m", path); + + NULSTR_FOREACH(xa, xattrs) { + _cleanup_free_ char *buf = NULL; + size_t size; + + if (xattr_is_acl(xa)) + continue; + + if (!FLAGS_SET(d->flags, TAR_SELINUX) && xattr_is_selinux(xa)) + continue; + + r = fgetxattr_malloc(inode_fd, xa, &buf, &size); + if (r == -ENODATA) /* deleted by now? ignore... */ + continue; + if (r < 0) + return log_error_errno(r, "Failed to read xattr '%s' of '%s': %m", xa, path); + + sym_archive_entry_xattr_add_entry(entry, xa, buf, size); + } + + if (sym_archive_write_header(d->archive, entry) != ARCHIVE_OK) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to write archive entry header: %s", sym_archive_error_string(d->archive)); if (S_ISREG(sx->stx_mode)) { _cleanup_close_ int data_fd = -EBADF; @@ -767,9 +809,9 @@ static int archive_item( break; la_ssize_t k; - k = sym_archive_write_data(a, buffer, l); + k = sym_archive_write_data(d->archive, buffer, l); if (k < 0) - return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to write archive data: %s", sym_archive_error_string(a)); + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to write archive data: %s", sym_archive_error_string(d->archive)); } } @@ -803,7 +845,10 @@ int tar_c(int tree_fd, int output_fd, const char *filename, TarFlags flags) { UINT_MAX, RECURSE_DIR_SORT|RECURSE_DIR_INODE_FD|RECURSE_DIR_TOPLEVEL, archive_item, - a); + &(struct make_archive_data) { + .archive = a, + .flags = flags, + }); if (r < 0) return log_error_errno(r, "Failed to make archive: %m"); From 63bf3ca8b077f9aef0b4ed7addd4e5798e5dc483 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 21 Aug 2025 14:27:54 +0200 Subject: [PATCH 3/9] tar-util: recognize hardlinks when generating tarballs --- src/shared/libarchive-util.c | 2 + src/shared/libarchive-util.h | 1 + src/shared/tar-util.c | 147 +++++++++++++++++++++++++++++++++-- 3 files changed, 145 insertions(+), 5 deletions(-) diff --git a/src/shared/libarchive-util.c b/src/shared/libarchive-util.c index 65e9f5a511..dcd532df1d 100644 --- a/src/shared/libarchive-util.c +++ b/src/shared/libarchive-util.c @@ -28,6 +28,7 @@ DLSYM_PROTOTYPE(archive_entry_rdevminor) = NULL; DLSYM_PROTOTYPE(archive_entry_set_ctime) = NULL; DLSYM_PROTOTYPE(archive_entry_set_filetype) = NULL; DLSYM_PROTOTYPE(archive_entry_set_gid) = NULL; +DLSYM_PROTOTYPE(archive_entry_set_hardlink) = NULL; DLSYM_PROTOTYPE(archive_entry_set_mtime) = NULL; DLSYM_PROTOTYPE(archive_entry_set_pathname) = NULL; DLSYM_PROTOTYPE(archive_entry_set_perm) = NULL; @@ -93,6 +94,7 @@ int dlopen_libarchive(void) { DLSYM_ARG(archive_entry_set_ctime), DLSYM_ARG(archive_entry_set_filetype), DLSYM_ARG(archive_entry_set_gid), + DLSYM_ARG(archive_entry_set_hardlink), DLSYM_ARG(archive_entry_set_mtime), DLSYM_ARG(archive_entry_set_pathname), DLSYM_ARG(archive_entry_set_perm), diff --git a/src/shared/libarchive-util.h b/src/shared/libarchive-util.h index 2f40c64d85..e58e4d26a5 100644 --- a/src/shared/libarchive-util.h +++ b/src/shared/libarchive-util.h @@ -24,6 +24,7 @@ extern DLSYM_PROTOTYPE(archive_entry_rdevminor); extern DLSYM_PROTOTYPE(archive_entry_set_ctime); extern DLSYM_PROTOTYPE(archive_entry_set_filetype); extern DLSYM_PROTOTYPE(archive_entry_set_gid); +extern DLSYM_PROTOTYPE(archive_entry_set_hardlink); extern DLSYM_PROTOTYPE(archive_entry_set_mtime); extern DLSYM_PROTOTYPE(archive_entry_set_pathname); extern DLSYM_PROTOTYPE(archive_entry_set_perm); diff --git a/src/shared/tar-util.c b/src/shared/tar-util.c index 95b5ba820c..9d28009f71 100644 --- a/src/shared/tar-util.c +++ b/src/shared/tar-util.c @@ -5,17 +5,22 @@ #include "tar-util.h" #if HAVE_LIBARCHIVE +#include #include #include "alloc-util.h" #include "chase.h" #include "fd-util.h" #include "fs-util.h" +#include "hexdecoct.h" #include "iovec-util.h" #include "libarchive-util.h" +#include "mountpoint-util.h" #include "nulstr-util.h" #include "path-util.h" #include "recurse-dir.h" +#include "rm-rf.h" +#include "sha256.h" #include "stat-util.h" #include "string-util.h" #include "tmpfile-util.h" @@ -688,11 +693,117 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { return 0; } +static int make_tmpfs(void) { + /* Creates a tmpfs superblock to store our hardlink db in. We can do this if we run in our own + * userns, or if we are privileged. This is preferable, since it means the db is cleaned up + * automatically once we are done. Moreover, since this is a new superblock owned by us, we do not + * need to set up any uid mapping shenanigans */ + + _cleanup_close_ int superblock_fd = fsopen("tmpfs", FSOPEN_CLOEXEC); + if (superblock_fd < 0) + return log_debug_errno(errno, "Failed to allocate tmpfs superblock: %m"); + + (void) fsconfig(superblock_fd, FSCONFIG_SET_STRING, "source", "hardlink", /* aux= */ 0); + (void) fsconfig(superblock_fd, FSCONFIG_SET_STRING, "mode", "0700", /* aux= */ 0); + + if (fsconfig(superblock_fd, FSCONFIG_CMD_CREATE, /* key= */ NULL, /* value= */ NULL, /* aux= */ 0) < 0) + return log_debug_errno(errno, "Failed to finalize superblock: %m"); + + _cleanup_close_ int mount_fd = fsmount(superblock_fd, FSMOUNT_CLOEXEC, MS_NODEV|MS_NOEXEC|MS_NOSUID); + if (mount_fd < 0) + return log_debug_errno(errno, "Failed to turn tmpfs superblock into mount: %m"); + + return TAKE_FD(mount_fd); +} + struct make_archive_data { struct archive *archive; TarFlags flags; + int hardlink_db_fd; + char *hardlink_db_path; }; +static int hardlink_lookup( + struct make_archive_data *d, + int inode_fd, + const struct statx *sx, + const char *path, + char **ret) { + + _cleanup_free_ struct file_handle *handle = NULL; + _cleanup_free_ char *m = NULL, *n = NULL; + int r; + + assert(d); + assert(inode_fd >= 0); + assert(sx); + + /* If we know the hardlink count, and it's 1, then don't bother */ + if (FLAGS_SET(sx->stx_mask, STATX_NLINK) && sx->stx_nlink == 1) + goto bypass; + + /* If this is a directory, then don't bother */ + if (FLAGS_SET(sx->stx_mask, STATX_TYPE) && !inode_type_can_hardlink(sx->stx_mode)) + goto bypass; + + int mnt_id; + r = name_to_handle_at_try_fid(inode_fd, /* path= */ NULL, &handle, &mnt_id, /* flags= */ AT_EMPTY_PATH); + if (r < 0) + return log_error_errno(r, "Failed to get file handle of file: %m"); + + m = hexmem(SHA256_DIRECT(handle->f_handle, handle->handle_bytes), SHA256_DIGEST_SIZE); + if (!m) + return log_oom(); + + if (asprintf(&n, "%i:%i:%s", mnt_id, handle->handle_type, m) < 0) + return log_oom(); + + if (d->hardlink_db_fd < 0) { + assert(!d->hardlink_db_path); + + /* We first try to create our own superblock, which works if we are in a userns, and which + * doesn't require explicit clean-up */ + d->hardlink_db_fd = make_tmpfs(); + if (d->hardlink_db_fd < 0) { + log_debug_errno(d->hardlink_db_fd, "Failed to allocate tmpfs superblock for hardlink db, falling back to temporary directory: %m"); + + const char *vt; + r = var_tmp_dir(&vt); + if (r < 0) + return log_error_errno(r, "Failed to determine /var/tmp/ directory: %m"); + + _cleanup_free_ char *j = path_join(vt, "make-tar-XXXXXX"); + if (!j) + return log_oom(); + + d->hardlink_db_fd = mkdtemp_open(j, /* flags= */ 0, &d->hardlink_db_path); + if (d->hardlink_db_fd < 0) + return log_error_errno(d->hardlink_db_fd, "Failed to make hardlink database directory: %m"); + } + } else { + _cleanup_free_ char *p = NULL; + r = readlinkat_malloc(d->hardlink_db_fd, n, &p); + if (r >= 0) { + /* Found previous hit! */ + log_debug("hardlinkdb: found %s → %s", n, p); + *ret = TAKE_PTR(p); + return 1; + } + if (r != -ENOENT) + return log_error_errno(r, "Failed to read symlink '%s': %m", n); + } + + /* Store information about this inode */ + if (symlinkat(path, d->hardlink_db_fd, n) < 0) + return log_error_errno(errno, "Failed to create symlink '%s' → '%s': %m", n, path); + + log_debug("hardlinkdb: created %s → %s", n, path); + +bypass: + *ret = NULL; + return 0; +} + static int archive_item( RecurseDirEvent event, const char *path, @@ -720,8 +831,22 @@ static int archive_item( if (!entry) return log_oom(); - assert(FLAGS_SET(sx->stx_mask, STATX_TYPE|STATX_MODE)); sym_archive_entry_set_pathname(entry, path); + + _cleanup_free_ char *hardlink = NULL; + r = hardlink_lookup(d, inode_fd, sx, path, &hardlink); + if (r < 0) + return r; + if (r > 0) { + sym_archive_entry_set_hardlink(entry, hardlink); + + if (sym_archive_write_header(d->archive, entry) != ARCHIVE_OK) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to write archive entry header: %s", sym_archive_error_string(d->archive)); + + return RECURSE_DIR_CONTINUE; + } + + assert(FLAGS_SET(sx->stx_mask, STATX_TYPE|STATX_MODE)); sym_archive_entry_set_filetype(entry, sx->stx_mode); if (!S_ISLNK(sx->stx_mode)) @@ -818,6 +943,15 @@ static int archive_item( return RECURSE_DIR_CONTINUE; } +static void make_archive_data_done(struct make_archive_data *d) { + assert(d); + + if (d->hardlink_db_fd >= 0) + (void) rm_rf_children(d->hardlink_db_fd, REMOVE_PHYSICAL, /* root_dev= */ NULL); + + unlink_and_free(d->hardlink_db_path); +} + int tar_c(int tree_fd, int output_fd, const char *filename, TarFlags flags) { int r; @@ -839,16 +973,19 @@ int tar_c(int tree_fd, int output_fd, const char *filename, TarFlags flags) { if (r != ARCHIVE_OK) return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to set libarchive output file: %s", sym_archive_error_string(a)); + _cleanup_(make_archive_data_done) struct make_archive_data data = { + .archive = a, + .flags = flags, + .hardlink_db_fd = -EBADF, + }; + r = recurse_dir(tree_fd, ".", STATX_TYPE|STATX_MODE|STATX_UID|STATX_GID|STATX_SIZE|STATX_ATIME|STATX_CTIME, UINT_MAX, RECURSE_DIR_SORT|RECURSE_DIR_INODE_FD|RECURSE_DIR_TOPLEVEL, archive_item, - &(struct make_archive_data) { - .archive = a, - .flags = flags, - }); + &data); if (r < 0) return log_error_errno(r, "Failed to make archive: %m"); From a54f4520f36da05dc48322989f36b1c46ee7f01b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 21 Aug 2025 19:08:01 +0200 Subject: [PATCH 4/9] tar-util: properly deal with sparse files The extractor already deals with sparse files properly (because archive_read_data_into_fd() does). Let's also make sure the archiver also does this, and attaches the necessary sparse file metadata to each file. --- src/shared/libarchive-util.c | 7 ++-- src/shared/libarchive-util.h | 3 +- src/shared/tar-util.c | 72 ++++++++++++++++++++++++++++++++---- 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/src/shared/libarchive-util.c b/src/shared/libarchive-util.c index dcd532df1d..e3387e6e97 100644 --- a/src/shared/libarchive-util.c +++ b/src/shared/libarchive-util.c @@ -37,6 +37,7 @@ DLSYM_PROTOTYPE(archive_entry_set_rdevminor) = NULL; DLSYM_PROTOTYPE(archive_entry_set_size) = NULL; DLSYM_PROTOTYPE(archive_entry_set_symlink) = NULL; DLSYM_PROTOTYPE(archive_entry_set_uid) = NULL; +DLSYM_PROTOTYPE(archive_entry_sparse_add_entry) = NULL; DLSYM_PROTOTYPE(archive_entry_symlink) = NULL; DLSYM_PROTOTYPE(archive_entry_uid) = NULL; #if HAVE_LIBARCHIVE_UID_IS_SET @@ -61,7 +62,7 @@ DLSYM_PROTOTYPE(archive_write_new) = NULL; DLSYM_PROTOTYPE(archive_write_open_FILE) = NULL; DLSYM_PROTOTYPE(archive_write_open_fd) = NULL; DLSYM_PROTOTYPE(archive_write_set_format_filter_by_ext) = NULL; -DLSYM_PROTOTYPE(archive_write_set_format_gnutar) = NULL; +DLSYM_PROTOTYPE(archive_write_set_format_pax) = NULL; int dlopen_libarchive(void) { ELF_NOTE_DLOPEN("archive", @@ -103,6 +104,7 @@ int dlopen_libarchive(void) { DLSYM_ARG(archive_entry_set_size), DLSYM_ARG(archive_entry_set_symlink), DLSYM_ARG(archive_entry_set_uid), + DLSYM_ARG(archive_entry_sparse_add_entry), DLSYM_ARG(archive_entry_symlink), DLSYM_ARG(archive_entry_uid), #if HAVE_LIBARCHIVE_UID_IS_SET @@ -127,8 +129,7 @@ int dlopen_libarchive(void) { DLSYM_ARG(archive_write_open_FILE), DLSYM_ARG(archive_write_open_fd), DLSYM_ARG(archive_write_set_format_filter_by_ext), - DLSYM_ARG(archive_write_set_format_gnutar) - ); + DLSYM_ARG(archive_write_set_format_pax)); } /* libarchive uses its own file type macros. They happen to be defined the same way as the Linux ones, and diff --git a/src/shared/libarchive-util.h b/src/shared/libarchive-util.h index e58e4d26a5..7534b0d016 100644 --- a/src/shared/libarchive-util.h +++ b/src/shared/libarchive-util.h @@ -33,6 +33,7 @@ extern DLSYM_PROTOTYPE(archive_entry_set_rdevminor); extern DLSYM_PROTOTYPE(archive_entry_set_size); extern DLSYM_PROTOTYPE(archive_entry_set_symlink); extern DLSYM_PROTOTYPE(archive_entry_set_uid); +extern DLSYM_PROTOTYPE(archive_entry_sparse_add_entry); extern DLSYM_PROTOTYPE(archive_entry_symlink); extern DLSYM_PROTOTYPE(archive_entry_uid); extern DLSYM_PROTOTYPE(archive_entry_xattr_add_entry); @@ -54,7 +55,7 @@ extern DLSYM_PROTOTYPE(archive_write_new); extern DLSYM_PROTOTYPE(archive_write_open_FILE); extern DLSYM_PROTOTYPE(archive_write_open_fd); extern DLSYM_PROTOTYPE(archive_write_set_format_filter_by_ext); -extern DLSYM_PROTOTYPE(archive_write_set_format_gnutar); +extern DLSYM_PROTOTYPE(archive_write_set_format_pax); #if HAVE_LIBARCHIVE_UID_IS_SET extern DLSYM_PROTOTYPE(archive_entry_gid_is_set); diff --git a/src/shared/tar-util.c b/src/shared/tar-util.c index 9d28009f71..83b346d52c 100644 --- a/src/shared/tar-util.c +++ b/src/shared/tar-util.c @@ -804,6 +804,57 @@ bypass: return 0; } +static int archive_generate_sparse(struct archive_entry *entry, int fd) { + assert(entry); + assert(fd); + + off_t c = 0; + for (;;) { + /* Look for the next hole */ + off_t h = lseek(fd, c, SEEK_HOLE); + if (h < 0) { + if (errno != ENXIO) + return log_error_errno(errno, "Failed to issue SEEK_HOLE: %m"); + + /* If errno == ENXIO, that means we've reached the final data of the file and + * that data isn't followed by anything more */ + + /* Figure out where the end of the file is */ + off_t e = lseek(fd, 0, SEEK_END); + if (e < 0) + return log_error_errno(errno, "Failed to issue SEEK_END: %m"); + + /* Generate sparse entry for final block */ + if (e > c && c != 0) { + log_debug("final sparse block %" PRIu64 "…%" PRIu64, (uint64_t) c, (uint64_t) e); + sym_archive_entry_sparse_add_entry(entry, c, e - c); + } + + break; + } + + if (h > c) { + log_debug("inner sparse block %" PRIu64 "…%" PRIu64 " (%" PRIu64 ")", (uint64_t) c, (uint64_t) h, (uint64_t) h - (uint64_t) c); + sym_archive_entry_sparse_add_entry(entry, c, h - c); + } + + /* Now look for the next data after the hole */ + c = lseek(fd, h, SEEK_DATA); + if (c < 0) { + if (errno != ENXIO) + return log_error_errno(errno, "Failed to issue SEEK_DATA: %m"); + + /* No data anymore */ + break; + } + } + + if (lseek(fd, 0, SEEK_SET) < 0) + return log_error_errno(errno, "Failed to reset seek offset: %m"); + + return 0; +} + static int archive_item( RecurseDirEvent event, const char *path, @@ -912,16 +963,23 @@ static int archive_item( sym_archive_entry_xattr_add_entry(entry, xa, buf, size); } + _cleanup_close_ int data_fd = -EBADF; + if (S_ISREG(sx->stx_mode)) { + /* Convert the O_PATH fd into a proper fd */ + data_fd = fd_reopen(inode_fd, O_RDONLY|O_CLOEXEC); + if (data_fd < 0) + return log_error_errno(data_fd, "Failed to open '%s': %m", path); + + r = archive_generate_sparse(entry, data_fd); + if (r < 0) + return r; + } + if (sym_archive_write_header(d->archive, entry) != ARCHIVE_OK) return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to write archive entry header: %s", sym_archive_error_string(d->archive)); if (S_ISREG(sx->stx_mode)) { - _cleanup_close_ int data_fd = -EBADF; - - /* Convert the O_PATH fd in a proper fd */ - data_fd = fd_reopen(inode_fd, O_RDONLY|O_CLOEXEC); - if (data_fd < 0) - return log_error_errno(data_fd, "Failed to open '%s': %m", path); + assert(data_fd >= 0); for (;;) { char buffer[64*1024]; @@ -965,7 +1023,7 @@ int tar_c(int tree_fd, int output_fd, const char *filename, TarFlags flags) { if (filename) r = sym_archive_write_set_format_filter_by_ext(a, filename); else - r = sym_archive_write_set_format_gnutar(a); + r = sym_archive_write_set_format_pax(a); if (r != ARCHIVE_OK) return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to set libarchive output format: %s", sym_archive_error_string(a)); From e1e170feca99274604b7d6c4120d291343759b92 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 21 Aug 2025 22:40:59 +0200 Subject: [PATCH 5/9] tar-util: add support for file flags --- src/shared/libarchive-util.c | 4 ++ src/shared/libarchive-util.h | 2 + src/shared/tar-util.c | 90 ++++++++++++++++++++++++++++++++++-- 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/shared/libarchive-util.c b/src/shared/libarchive-util.c index e3387e6e97..8d8e775c51 100644 --- a/src/shared/libarchive-util.c +++ b/src/shared/libarchive-util.c @@ -7,6 +7,7 @@ #if HAVE_LIBARCHIVE static void *libarchive_dl = NULL; +DLSYM_PROTOTYPE(archive_entry_fflags) = NULL; DLSYM_PROTOTYPE(archive_entry_filetype) = NULL; DLSYM_PROTOTYPE(archive_entry_free) = NULL; DLSYM_PROTOTYPE(archive_entry_gid) = NULL; @@ -26,6 +27,7 @@ DLSYM_PROTOTYPE(archive_entry_pathname) = NULL; DLSYM_PROTOTYPE(archive_entry_rdevmajor) = NULL; DLSYM_PROTOTYPE(archive_entry_rdevminor) = NULL; DLSYM_PROTOTYPE(archive_entry_set_ctime) = NULL; +DLSYM_PROTOTYPE(archive_entry_set_fflags) = NULL; DLSYM_PROTOTYPE(archive_entry_set_filetype) = NULL; DLSYM_PROTOTYPE(archive_entry_set_gid) = NULL; DLSYM_PROTOTYPE(archive_entry_set_hardlink) = NULL; @@ -74,6 +76,7 @@ int dlopen_libarchive(void) { &libarchive_dl, "libarchive.so.13", LOG_DEBUG, + DLSYM_ARG(archive_entry_fflags), DLSYM_ARG(archive_entry_filetype), DLSYM_ARG(archive_entry_free), DLSYM_ARG(archive_entry_gid), @@ -93,6 +96,7 @@ int dlopen_libarchive(void) { DLSYM_ARG(archive_entry_rdevmajor), DLSYM_ARG(archive_entry_rdevminor), DLSYM_ARG(archive_entry_set_ctime), + DLSYM_ARG(archive_entry_set_fflags), DLSYM_ARG(archive_entry_set_filetype), DLSYM_ARG(archive_entry_set_gid), DLSYM_ARG(archive_entry_set_hardlink), diff --git a/src/shared/libarchive-util.h b/src/shared/libarchive-util.h index 7534b0d016..9a5d4e0b40 100644 --- a/src/shared/libarchive-util.h +++ b/src/shared/libarchive-util.h @@ -9,6 +9,7 @@ #include "dlfcn-util.h" +extern DLSYM_PROTOTYPE(archive_entry_fflags); extern DLSYM_PROTOTYPE(archive_entry_filetype); extern DLSYM_PROTOTYPE(archive_entry_free); extern DLSYM_PROTOTYPE(archive_entry_gid); @@ -22,6 +23,7 @@ extern DLSYM_PROTOTYPE(archive_entry_pathname); extern DLSYM_PROTOTYPE(archive_entry_rdevmajor); extern DLSYM_PROTOTYPE(archive_entry_rdevminor); extern DLSYM_PROTOTYPE(archive_entry_set_ctime); +extern DLSYM_PROTOTYPE(archive_entry_set_fflags); extern DLSYM_PROTOTYPE(archive_entry_set_filetype); extern DLSYM_PROTOTYPE(archive_entry_set_gid); extern DLSYM_PROTOTYPE(archive_entry_set_hardlink); diff --git a/src/shared/tar-util.c b/src/shared/tar-util.c index 83b346d52c..54f3644ad1 100644 --- a/src/shared/tar-util.c +++ b/src/shared/tar-util.c @@ -10,6 +10,7 @@ #include "alloc-util.h" #include "chase.h" +#include "chattr-util.h" #include "fd-util.h" #include "fs-util.h" #include "hexdecoct.h" @@ -29,6 +30,15 @@ #define DEPTH_MAX 128U +/* We are a bit conservative with the flags we save/restore in tar files */ +#define CHATTR_TAR_FL \ + (FS_NOATIME_FL | \ + FS_NOCOW_FL | \ + FS_PROJINHERIT_FL | \ + FS_NODUMP_FL | \ + FS_SYNC_FL | \ + FS_DIRSYNC_FL) + typedef struct XAttr { char *name; struct iovec data; @@ -44,6 +54,7 @@ typedef struct OpenInode { struct timespec mtime; uid_t uid; gid_t gid; + unsigned fflags; XAttr *xattr; size_t n_xattr; } OpenInode; @@ -105,6 +116,20 @@ static int open_inode_finalize(OpenInode *of) { RET_GATHER(r, log_error_errno(k, "Failed to adjust ownership/mode of '%s': %m", of->path)); } + if ((of->fflags & ~CHATTR_EARLY_FL) != 0 && inode_type_can_chattr(of->filetype)) { + k = chattr_full(of->fd, + /* path= */ NULL, + /* value= */ of->fflags, + /* mask= */ of->fflags & ~CHATTR_EARLY_FL, + /* ret_previous= */ NULL, + /* ret_final= */ NULL, + CHATTR_FALLBACK_BITWISE); + if (ERRNO_IS_NEG_NOT_SUPPORTED(k)) + log_warning_errno(k, "Failed to apply chattr of '%s', ignoring: %m", of->path); + else if (k < 0) + RET_GATHER(r, log_error_errno(k, "Failed to adjust chattr of '%s': %m", of->path)); + } + /* We also adjust the mtime only after leaving a dir, since it might otherwise change again * because we make modifications inside it */ if (of->mtime.tv_nsec != UTIME_OMIT) { @@ -155,7 +180,8 @@ static int archive_unpack_regular( struct archive_entry *entry, int parent_fd, const char *filename, - const char *path) { + const char *path, + unsigned fflags) { int r; @@ -170,6 +196,22 @@ static int archive_unpack_regular( if (fd < 0) return log_error_errno(fd, "Failed to create regular file '%s': %m", path); + if ((fflags & CHATTR_EARLY_FL) != 0) { + r = chattr_full(fd, + /* path= */ NULL, + /* value= */ fflags, + /* mask= */ fflags & CHATTR_EARLY_FL, + /* ret_previous= */ NULL, + /* ret_final= */ NULL, + CHATTR_FALLBACK_BITWISE); + if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) + log_warning_errno(r, "Failed to apply chattr of '%s', ignoring: %m", path); + else if (r < 0) { + log_error_errno(r, "Failed to adjust chattr of '%s': %m", path); + goto fail; + } + } + r = sym_archive_read_data_into_fd(a, fd); if (r != ARCHIVE_OK) { r = log_error_errno( @@ -210,7 +252,10 @@ static int archive_unpack_directory( struct archive_entry *entry, int parent_fd, const char *filename, - const char *path) { + const char *path, + unsigned fflags) { + + int r; assert(a); assert(entry); @@ -226,6 +271,20 @@ static int archive_unpack_directory( if (fd < 0) return log_error_errno(fd, "Failed to create directory '%s': %m", path); + if ((fflags & CHATTR_EARLY_FL) != 0) { + r = chattr_full(fd, + /* path= */ NULL, + /* value= */ fflags, + /* mask= */ fflags & CHATTR_EARLY_FL, + /* ret_previous= */ NULL, + /* ret_final= */ NULL, + CHATTR_FALLBACK_BITWISE); + if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) + log_warning_errno(r, "Failed to apply chattr of '%s', ignoring: %m", path); + else if (r < 0) + return log_error_errno(r, "Failed to adjust chattr of '%s': %m", path); + } + return TAKE_FD(fd); } @@ -334,6 +393,7 @@ static int archive_entry_read_stat( struct timespec *mtime, uid_t *uid, gid_t *gid, + unsigned *fflags, XAttr **xa, size_t *n_xa, TarFlags flags) { @@ -361,6 +421,12 @@ static int archive_entry_read_stat( if (gid && sym_archive_entry_gid_is_set(entry)) *gid = sym_archive_entry_gid(entry); + if (fflags) { + unsigned long fs = 0, fc = 0; + sym_archive_entry_fflags(entry, &fs, &fc); + *fflags = (fs & ~fc) & CHATTR_TAR_FL; + } + (void) sym_archive_entry_xattr_reset(entry); for (;;) { const char *name = NULL; @@ -476,6 +542,7 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { &open_inodes[0].mtime, &open_inodes[0].uid, &open_inodes[0].gid, + &open_inodes[0].fflags, &open_inodes[0].xattr, &open_inodes[0].n_xattr, flags); @@ -546,6 +613,7 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { uid_t uid = UID_INVALID; gid_t gid = GID_INVALID; struct timespec mtime = { .tv_nsec = UTIME_OMIT }; + unsigned fflags = 0; XAttr *xa = NULL; size_t n_xa = 0; CLEANUP_ARRAY(xa, n_xa, xattr_done_many); @@ -620,6 +688,7 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { &mtime, &uid, &gid, + &fflags, &xa, &n_xa, flags); @@ -629,11 +698,11 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { switch (filetype) { case S_IFREG: - fd = archive_unpack_regular(a, entry, parent_fd, e, j); + fd = archive_unpack_regular(a, entry, parent_fd, e, j, fflags); break; case S_IFDIR: - fd = archive_unpack_directory(a, entry, parent_fd, e, j); + fd = archive_unpack_directory(a, entry, parent_fd, e, j, fflags); break; case S_IFLNK: @@ -678,6 +747,7 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { .mtime = mtime, .uid = uid, .gid = gid, + .fflags = fflags, .xattr = TAKE_PTR(xa), .n_xattr = n_xa, }; @@ -975,6 +1045,18 @@ static int archive_item( return r; } + if (inode_type_can_chattr(sx->stx_mode)) { + unsigned f = 0; + + r = read_attr_fd(data_fd >= 0 ? data_fd : inode_fd, &f); + if (r < 0 && !ERRNO_IS_NEG_NOT_SUPPORTED(r)) + return log_error_errno(r, "Failed to read file flags of '%s': %m", path); + + f &= CHATTR_TAR_FL; + if (f != 0) + sym_archive_entry_set_fflags(entry, f, /* clear= */ 0); + } + if (sym_archive_write_header(d->archive, entry) != ARCHIVE_OK) return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to write archive entry header: %s", sym_archive_error_string(d->archive)); From 4ded7f7a434c59534f65a0f9d391c55961eb110d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 22 Aug 2025 11:05:38 +0200 Subject: [PATCH 6/9] tar-util: add support for acls --- src/shared/acl-util.c | 4 + src/shared/acl-util.h | 2 + src/shared/libarchive-util.c | 6 + src/shared/libarchive-util.h | 3 + src/shared/tar-util.c | 279 +++++++++++++++++++++++++++++++++++ 5 files changed, 294 insertions(+) diff --git a/src/shared/acl-util.c b/src/shared/acl-util.c index 554d473821..d16c165ed7 100644 --- a/src/shared/acl-util.c +++ b/src/shared/acl-util.c @@ -23,6 +23,7 @@ DLSYM_PROTOTYPE(acl_delete_entry); DLSYM_PROTOTYPE(acl_delete_perm); DLSYM_PROTOTYPE(acl_dup); DLSYM_PROTOTYPE(acl_entries); +DLSYM_PROTOTYPE(acl_extended_file); DLSYM_PROTOTYPE(acl_free); DLSYM_PROTOTYPE(acl_from_mode); DLSYM_PROTOTYPE(acl_from_text); @@ -36,6 +37,7 @@ DLSYM_PROTOTYPE(acl_get_tag_type); DLSYM_PROTOTYPE(acl_init); DLSYM_PROTOTYPE(acl_set_fd); DLSYM_PROTOTYPE(acl_set_file); +DLSYM_PROTOTYPE(acl_set_permset); DLSYM_PROTOTYPE(acl_set_qualifier); DLSYM_PROTOTYPE(acl_set_tag_type); DLSYM_PROTOTYPE(acl_to_any_text); @@ -58,6 +60,7 @@ int dlopen_libacl(void) { DLSYM_ARG(acl_delete_perm), DLSYM_ARG(acl_dup), DLSYM_ARG(acl_entries), + DLSYM_ARG(acl_extended_file), DLSYM_ARG(acl_free), DLSYM_ARG(acl_from_mode), DLSYM_ARG(acl_from_text), @@ -71,6 +74,7 @@ int dlopen_libacl(void) { DLSYM_ARG(acl_init), DLSYM_ARG(acl_set_fd), DLSYM_ARG(acl_set_file), + DLSYM_ARG(acl_set_permset), DLSYM_ARG(acl_set_qualifier), DLSYM_ARG(acl_set_tag_type), DLSYM_ARG(acl_to_any_text)); diff --git a/src/shared/acl-util.h b/src/shared/acl-util.h index ea4aca7bdb..7f6650013b 100644 --- a/src/shared/acl-util.h +++ b/src/shared/acl-util.h @@ -20,6 +20,7 @@ extern DLSYM_PROTOTYPE(acl_delete_entry); extern DLSYM_PROTOTYPE(acl_delete_perm); extern DLSYM_PROTOTYPE(acl_dup); extern DLSYM_PROTOTYPE(acl_entries); +extern DLSYM_PROTOTYPE(acl_extended_file); extern DLSYM_PROTOTYPE(acl_free); extern DLSYM_PROTOTYPE(acl_from_mode); extern DLSYM_PROTOTYPE(acl_from_text); @@ -33,6 +34,7 @@ extern DLSYM_PROTOTYPE(acl_get_tag_type); extern DLSYM_PROTOTYPE(acl_init); extern DLSYM_PROTOTYPE(acl_set_fd); extern DLSYM_PROTOTYPE(acl_set_file); +extern DLSYM_PROTOTYPE(acl_set_permset); extern DLSYM_PROTOTYPE(acl_set_qualifier); extern DLSYM_PROTOTYPE(acl_set_tag_type); extern DLSYM_PROTOTYPE(acl_to_any_text); diff --git a/src/shared/libarchive-util.c b/src/shared/libarchive-util.c index 8d8e775c51..09f036895c 100644 --- a/src/shared/libarchive-util.c +++ b/src/shared/libarchive-util.c @@ -7,6 +7,9 @@ #if HAVE_LIBARCHIVE static void *libarchive_dl = NULL; +DLSYM_PROTOTYPE(archive_entry_acl_add_entry) = NULL; +DLSYM_PROTOTYPE(archive_entry_acl_next) = NULL; +DLSYM_PROTOTYPE(archive_entry_acl_reset) = NULL; DLSYM_PROTOTYPE(archive_entry_fflags) = NULL; DLSYM_PROTOTYPE(archive_entry_filetype) = NULL; DLSYM_PROTOTYPE(archive_entry_free) = NULL; @@ -76,6 +79,9 @@ int dlopen_libarchive(void) { &libarchive_dl, "libarchive.so.13", LOG_DEBUG, + DLSYM_ARG(archive_entry_acl_add_entry), + DLSYM_ARG(archive_entry_acl_next), + DLSYM_ARG(archive_entry_acl_reset), DLSYM_ARG(archive_entry_fflags), DLSYM_ARG(archive_entry_filetype), DLSYM_ARG(archive_entry_free), diff --git a/src/shared/libarchive-util.h b/src/shared/libarchive-util.h index 9a5d4e0b40..615cd1ec81 100644 --- a/src/shared/libarchive-util.h +++ b/src/shared/libarchive-util.h @@ -9,6 +9,9 @@ #include "dlfcn-util.h" +extern DLSYM_PROTOTYPE(archive_entry_acl_add_entry); +extern DLSYM_PROTOTYPE(archive_entry_acl_next); +extern DLSYM_PROTOTYPE(archive_entry_acl_reset); extern DLSYM_PROTOTYPE(archive_entry_fflags); extern DLSYM_PROTOTYPE(archive_entry_filetype); extern DLSYM_PROTOTYPE(archive_entry_free); diff --git a/src/shared/tar-util.c b/src/shared/tar-util.c index 54f3644ad1..a4d0578b25 100644 --- a/src/shared/tar-util.c +++ b/src/shared/tar-util.c @@ -8,6 +8,7 @@ #include #include +#include "acl-util.h" #include "alloc-util.h" #include "chase.h" #include "chattr-util.h" @@ -57,6 +58,7 @@ typedef struct OpenInode { unsigned fflags; XAttr *xattr; size_t n_xattr; + acl_t acl_access, acl_default; } OpenInode; static void xattr_done(XAttr *xa) { @@ -85,6 +87,10 @@ static void open_inode_done(OpenInode *of) { of->path = mfree(of->path); } xattr_done_many(of->xattr, of->n_xattr); + if (of->acl_access) + sym_acl_free(of->acl_access); + if (of->acl_default) + sym_acl_free(of->acl_default); } static void open_inode_done_many(OpenInode *array, size_t n) { @@ -96,6 +102,29 @@ static void open_inode_done_many(OpenInode *array, size_t n) { free(array); } +static int open_inode_apply_acl(OpenInode *of) { + int r = 0; + + assert(of); + assert(of->fd >= 0); + + if (!inode_type_can_acl(of->filetype)) + return 0; + + if (of->acl_access) { + if (sym_acl_set_fd(of->fd, of->acl_access) < 0) + RET_GATHER(r, log_error_errno(errno, "Failed to adjust ACLs of '%s': %m", of->path)); + } + + if (of->filetype == S_IFDIR && of->acl_default) { + /* There's no API to set default ACLs by fd, hence go by /proc/self/fd/ path */ + if (sym_acl_set_file(FORMAT_PROC_FD_PATH(of->fd), ACL_TYPE_DEFAULT, of->acl_default) < 0) + RET_GATHER(r, log_error_errno(errno, "Failed to adjust default ACLs of '%s': %m", of->path)); + } + + return r; +} + static int open_inode_finalize(OpenInode *of) { int r = 0; @@ -116,6 +145,10 @@ static int open_inode_finalize(OpenInode *of) { RET_GATHER(r, log_error_errno(k, "Failed to adjust ownership/mode of '%s': %m", of->path)); } + k = open_inode_apply_acl(of); + if (k < 0) + RET_GATHER(r, log_error_errno(k, "Failed to adjust ACL of '%s': %m", of->path)); + if ((of->fflags & ~CHATTR_EARLY_FL) != 0 && inode_type_can_chattr(of->filetype)) { k = chattr_full(of->fd, /* path= */ NULL, @@ -386,6 +419,119 @@ static int archive_entry_pathname_safe(struct archive_entry *entry, const char * return 0; } +static int archive_entry_read_acl( + struct archive_entry *entry, + acl_type_t ntype, + acl_t *acl) { + + int r; + + assert(entry); + assert(acl); + + int type; + if (ntype == ACL_TYPE_ACCESS) + type = ARCHIVE_ENTRY_ACL_TYPE_ACCESS; + else if (ntype == ACL_TYPE_DEFAULT) + type = ARCHIVE_ENTRY_ACL_TYPE_DEFAULT; + else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unexpected ACL type"); + + int c = sym_archive_entry_acl_reset(entry, type); + if (c == 0) + return 0; + assert(c > 0); + + r = dlopen_libacl(); + if (r < 0) { + log_debug_errno(r, "Not restoring ACL data on inode as libacl is not available: %m"); + return 0; + } + + _cleanup_(acl_freep) acl_t a = NULL; + a = sym_acl_init(c); + if (!a) + return log_oom(); + + for (;;) { + int rtype, permset, tag, qual; + const char *name; + r = sym_archive_entry_acl_next( + entry, + type, + &rtype, + &permset, + &tag, + &qual, + &name); + if (r == ARCHIVE_EOF) + break; + if (r != ARCHIVE_OK) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unexpected error while iterating through ACLs."); + + assert(rtype == type); + + acl_entry_t e; + if (sym_acl_create_entry(&a, &e) < 0) + return log_error_errno(errno, "Failed to create ACL entry: %m"); + + static const struct { + int libarchive; + acl_tag_t libacl; + } tag_map[] = { + { ARCHIVE_ENTRY_ACL_USER, ACL_USER }, + { ARCHIVE_ENTRY_ACL_GROUP, ACL_GROUP }, + { ARCHIVE_ENTRY_ACL_USER_OBJ, ACL_USER_OBJ }, + { ARCHIVE_ENTRY_ACL_GROUP_OBJ, ACL_GROUP_OBJ }, + { ARCHIVE_ENTRY_ACL_MASK, ACL_MASK }, + { ARCHIVE_ENTRY_ACL_OTHER, ACL_OTHER }, + }; + + acl_tag_t ntag = ACL_UNDEFINED_TAG; + FOREACH_ELEMENT(t, tag_map) + if (t->libarchive == tag) { + ntag = t->libacl; + break; + } + if (ntag == ACL_UNDEFINED_TAG) + continue; + + if (sym_acl_set_tag_type(e, ntag) < 0) + return log_error_errno(errno, "Failed to set ACL entry tag: %m"); + + if (IN_SET(ntag, ACL_USER, ACL_GROUP)) { + id_t id = qual; + + if (sym_acl_set_qualifier(e, &id) < 0) + return log_error_errno(errno, "Failed to set ACL entry qualifier: %m"); + } + + acl_permset_t p; + if (sym_acl_get_permset(e, &p) < 0) + return log_error_errno(errno, "Failed to get ACL entry permission set: %m"); + + r = acl_set_perm(p, ACL_READ, permset & ARCHIVE_ENTRY_ACL_READ); + if (r < 0) + return log_error_errno(r, "Failed to set ACL entry read bit: %m"); + + r = acl_set_perm(p, ACL_WRITE, permset & ARCHIVE_ENTRY_ACL_WRITE); + if (r < 0) + return log_error_errno(r, "Failed to set ACL entry write bit: %m"); + + r = acl_set_perm(p, ACL_EXECUTE, permset & ARCHIVE_ENTRY_ACL_EXECUTE); + if (r < 0) + return log_error_errno(r, "Failed to set ACL entry excute bit: %m"); + + if (sym_acl_set_permset(e, p) < 0) + return log_error_errno(errno, "Failed to set ACL entry permission set: %m"); + } + + if (*acl) + sym_acl_free(*acl); + *acl = TAKE_PTR(a); + return 0; +} + static int archive_entry_read_stat( struct archive_entry *entry, mode_t *filetype, @@ -394,6 +540,8 @@ static int archive_entry_read_stat( uid_t *uid, gid_t *gid, unsigned *fflags, + acl_t *acl_access, + acl_t *acl_default, XAttr **xa, size_t *n_xa, TarFlags flags) { @@ -468,6 +616,18 @@ static int archive_entry_read_stat( }; } + if (acl_access) { + r = archive_entry_read_acl(entry, ACL_TYPE_ACCESS, acl_access); + if (r < 0) + return r; + } + + if (acl_default) { + r = archive_entry_read_acl(entry, ACL_TYPE_DEFAULT, acl_default); + if (r < 0) + return r; + } + return 0; } @@ -543,6 +703,8 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { &open_inodes[0].uid, &open_inodes[0].gid, &open_inodes[0].fflags, + &open_inodes[0].acl_access, + &open_inodes[0].acl_default, &open_inodes[0].xattr, &open_inodes[0].n_xattr, flags); @@ -614,6 +776,7 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { gid_t gid = GID_INVALID; struct timespec mtime = { .tv_nsec = UTIME_OMIT }; unsigned fflags = 0; + _cleanup_(acl_freep) acl_t acl_access = NULL, acl_default = NULL; XAttr *xa = NULL; size_t n_xa = 0; CLEANUP_ARRAY(xa, n_xa, xattr_done_many); @@ -689,6 +852,8 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { &uid, &gid, &fflags, + &acl_access, + &acl_default, &xa, &n_xa, flags); @@ -748,6 +913,8 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { .uid = uid, .gid = gid, .fflags = fflags, + .acl_access = TAKE_PTR(acl_access), + .acl_default = TAKE_PTR(acl_default), .xattr = TAKE_PTR(xa), .n_xattr = n_xa, }; @@ -925,6 +1092,88 @@ static int archive_generate_sparse(struct archive_entry *entry, int fd) { return 0; } +static int archive_write_acl( + struct archive_entry *entry, + acl_type_t ntype, + acl_t acl) { + int r; + + assert(entry); + assert(acl); + + int type; + if (ntype == ACL_TYPE_ACCESS) + type = ARCHIVE_ENTRY_ACL_TYPE_ACCESS; + else if (ntype == ACL_TYPE_DEFAULT) + type = ARCHIVE_ENTRY_ACL_TYPE_DEFAULT; + else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unexpected ACL type"); + + acl_entry_t e; + r = sym_acl_get_entry(acl, ACL_FIRST_ENTRY, &e); + for (;;) { + if (r < 0) + return log_error_errno(errno, "Failed to get ACL entry: %m"); + if (r == 0) + break; + + acl_tag_t ntag; + if (sym_acl_get_tag_type(e, &ntag) < 0) + return log_error_errno(errno, "Failed to get ACL entry tag: %m"); + + static const int tag_map[] = { + [ACL_USER] = ARCHIVE_ENTRY_ACL_USER, + [ACL_GROUP] = ARCHIVE_ENTRY_ACL_GROUP, + [ACL_USER_OBJ] = ARCHIVE_ENTRY_ACL_USER_OBJ, + [ACL_GROUP_OBJ] = ARCHIVE_ENTRY_ACL_GROUP_OBJ, + [ACL_MASK] = ARCHIVE_ENTRY_ACL_MASK, + [ACL_OTHER] = ARCHIVE_ENTRY_ACL_OTHER, + }; + assert_cc(ACL_UNDEFINED_TAG == 0); /* safety check, we assume that holes are filled with ACL_UNDEFINED_TAG */ + assert_cc(ELEMENTSOF(tag_map) <= 64); /* safety check, we assume that the tag ids are all packed and low */ + + int tag = ntag >= 0 && ntag <= (acl_tag_t) ELEMENTSOF(tag_map) ? tag_map[ntag] : ACL_UNDEFINED_TAG; + + id_t qualifier = UID_INVALID; + if (IN_SET(ntag, ACL_USER, ACL_GROUP)) { + id_t *q = sym_acl_get_qualifier(e); + if (!q) + return log_error_errno(errno, "Failed to get ACL entry qualifier: %m"); + + qualifier = *q; + sym_acl_free(q); + } + + acl_permset_t p; + if (sym_acl_get_permset(e, &p) < 0) + return log_error_errno(errno, "Failed to get ACL entry permission set: %m"); + + int permset = 0; + r = sym_acl_get_perm(p, ACL_READ); + if (r < 0) + return log_error_errno(r, "Failed to get ACL entry read bit: %m"); + SET_FLAG(permset, ARCHIVE_ENTRY_ACL_READ, r); + + r = sym_acl_get_perm(p, ACL_WRITE); + if (r < 0) + return log_error_errno(r, "Failed to get ACL entry write bit: %m"); + SET_FLAG(permset, ARCHIVE_ENTRY_ACL_WRITE, r); + + r = sym_acl_get_perm(p, ACL_EXECUTE); + if (r < 0) + return log_error_errno(r, "Failed to get ACL entry execute bit: %m"); + SET_FLAG(permset, ARCHIVE_ENTRY_ACL_EXECUTE, r); + + r = sym_archive_entry_acl_add_entry(entry, type, permset, tag, qualifier, /* name= */ NULL); + if (r != ARCHIVE_OK) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to add ACL entry."); + + r = sym_acl_get_entry(acl, ACL_NEXT_ENTRY, &e); + } + + return 0; +} + static int archive_item( RecurseDirEvent event, const char *path, @@ -1009,6 +1258,36 @@ static int archive_item( sym_archive_entry_set_symlink(entry, s); } + if (inode_type_can_acl(sx->stx_mode)) { + + r = dlopen_libacl(); + if (r < 0) + log_debug_errno(r, "No trying to read ACL off inode, as libacl support is not available: %m"); + else { + r = sym_acl_extended_file(FORMAT_PROC_FD_PATH(inode_fd)); + if (r < 0 && !ERRNO_IS_NOT_SUPPORTED(errno)) + return log_error_errno(errno, "Failed check if '%s' has ACLs: %m", path); + if (r > 0) { + _cleanup_(acl_freep) acl_t acl = NULL; + acl = sym_acl_get_file(FORMAT_PROC_FD_PATH(inode_fd), ACL_TYPE_ACCESS); + if (!acl) + return log_error_errno(errno, "Failed read access ACLs of '%s': %m", path); + + archive_write_acl(entry, ACL_TYPE_ACCESS, acl); + + if (S_ISDIR(sx->stx_mode)) { + sym_acl_free(acl); + + acl = sym_acl_get_file(FORMAT_PROC_FD_PATH(inode_fd), ACL_TYPE_DEFAULT); + if (!acl) + return log_error_errno(errno, "Failed to read default ACLs of '%s': %m", path); + + archive_write_acl(entry, ACL_TYPE_DEFAULT, acl); + } + } + } + } + _cleanup_free_ char *xattrs = NULL; r = flistxattr_malloc(inode_fd, &xattrs); if (r < 0 && !ERRNO_IS_NEG_NOT_SUPPORTED(r) && r != -ENODATA) From 78a738f4cf98ee7731b173ac0b4993ec9a88489a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 22 Aug 2025 17:28:50 +0200 Subject: [PATCH 7/9] tar-util: squash high UIDs in user mode --- src/import/import-common.c | 8 +++- src/shared/tar-util.c | 93 ++++++++++++++++++++++++++------------ src/shared/tar-util.h | 3 +- 3 files changed, 72 insertions(+), 32 deletions(-) diff --git a/src/import/import-common.c b/src/import/import-common.c index 11a6c060bb..2fb781442a 100644 --- a/src/import/import-common.c +++ b/src/import/import-common.c @@ -35,7 +35,9 @@ int import_fork_tar_x(int tree_fd, int userns_fd, PidRef *ret_pid) { if (r < 0) return r; - TarFlags flags = mac_selinux_use() ? TAR_SELINUX : 0; + TarFlags flags = + (userns_fd >= 0 ? TAR_SQUASH_UIDS_ABOVE_64K : 0) | + (mac_selinux_use() ? TAR_SELINUX : 0); _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR; if (pipe2(pipefd, O_CLOEXEC) < 0) @@ -100,7 +102,9 @@ int import_fork_tar_c(int tree_fd, int userns_fd, PidRef *ret_pid) { if (r < 0) return r; - TarFlags flags = mac_selinux_use() ? TAR_SELINUX : 0; + TarFlags flags = + (userns_fd >= 0 ? TAR_SQUASH_UIDS_ABOVE_64K : 0) | + (mac_selinux_use() ? TAR_SELINUX : 0); _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR; if (pipe2(pipefd, O_CLOEXEC) < 0) diff --git a/src/shared/tar-util.c b/src/shared/tar-util.c index a4d0578b25..05dd4cd9fc 100644 --- a/src/shared/tar-util.c +++ b/src/shared/tar-util.c @@ -18,6 +18,7 @@ #include "iovec-util.h" #include "libarchive-util.h" #include "mountpoint-util.h" +#include "nsresource.h" #include "nulstr-util.h" #include "path-util.h" #include "recurse-dir.h" @@ -422,7 +423,8 @@ static int archive_entry_pathname_safe(struct archive_entry *entry, const char * static int archive_entry_read_acl( struct archive_entry *entry, acl_type_t ntype, - acl_t *acl) { + acl_t *acl, + TarFlags flags) { int r; @@ -501,6 +503,13 @@ static int archive_entry_read_acl( if (IN_SET(ntag, ACL_USER, ACL_GROUP)) { id_t id = qual; + /* Suppress ACL entries for invalid UIDs/GIDS */ + if (!uid_is_valid(id)) + continue; + + /* Suppress ACL entries for UIDs/GIDs to squash */ + if (FLAGS_SET(flags, TAR_SQUASH_UIDS_ABOVE_64K) && id >= NSRESOURCE_UIDS_64K) + continue; if (sym_acl_set_qualifier(e, &id) < 0) return log_error_errno(errno, "Failed to set ACL entry qualifier: %m"); @@ -532,6 +541,24 @@ static int archive_entry_read_acl( return 0; } +static uid_t maybe_squash_uid(uid_t uid, TarFlags flags) { + if (FLAGS_SET(flags, TAR_SQUASH_UIDS_ABOVE_64K) && + uid_is_valid(uid) && + uid >= NSRESOURCE_UIDS_64K) + return UID_NOBODY; + + return uid; +} + +static uid_t maybe_squash_gid(uid_t gid, TarFlags flags) { + if (FLAGS_SET(flags, TAR_SQUASH_UIDS_ABOVE_64K) && + gid_is_valid(gid) && + gid >= NSRESOURCE_UIDS_64K) + return GID_NOBODY; + + return gid; +} + static int archive_entry_read_stat( struct archive_entry *entry, mode_t *filetype, @@ -565,9 +592,9 @@ static int archive_entry_read_stat( sym_archive_entry_mtime_nsec(entry), }; if (uid && sym_archive_entry_uid_is_set(entry)) - *uid = sym_archive_entry_uid(entry); + *uid = maybe_squash_uid(sym_archive_entry_uid(entry), flags); if (gid && sym_archive_entry_gid_is_set(entry)) - *gid = sym_archive_entry_gid(entry); + *gid = maybe_squash_gid(sym_archive_entry_gid(entry), flags); if (fflags) { unsigned long fs = 0, fc = 0; @@ -617,13 +644,13 @@ static int archive_entry_read_stat( } if (acl_access) { - r = archive_entry_read_acl(entry, ACL_TYPE_ACCESS, acl_access); + r = archive_entry_read_acl(entry, ACL_TYPE_ACCESS, acl_access, flags); if (r < 0) return r; } if (acl_default) { - r = archive_entry_read_acl(entry, ACL_TYPE_DEFAULT, acl_default); + r = archive_entry_read_acl(entry, ACL_TYPE_DEFAULT, acl_default, flags); if (r < 0) return r; } @@ -1095,7 +1122,8 @@ static int archive_generate_sparse(struct archive_entry *entry, int fd) { static int archive_write_acl( struct archive_entry *entry, acl_type_t ntype, - acl_t acl) { + acl_t acl, + TarFlags flags) { int r; assert(entry); @@ -1134,6 +1162,7 @@ static int archive_write_acl( int tag = ntag >= 0 && ntag <= (acl_tag_t) ELEMENTSOF(tag_map) ? tag_map[ntag] : ACL_UNDEFINED_TAG; + bool skip = false; id_t qualifier = UID_INVALID; if (IN_SET(ntag, ACL_USER, ACL_GROUP)) { id_t *q = sym_acl_get_qualifier(e); @@ -1142,31 +1171,37 @@ static int archive_write_acl( qualifier = *q; sym_acl_free(q); + + /* Suppress invalid UIDs or those that shall be squashed */ + skip = !(uid_is_valid(qualifier) && + (!FLAGS_SET(flags, TAR_SQUASH_UIDS_ABOVE_64K) || qualifier < NSRESOURCE_UIDS_64K)); } - acl_permset_t p; - if (sym_acl_get_permset(e, &p) < 0) - return log_error_errno(errno, "Failed to get ACL entry permission set: %m"); + if (!skip) { + acl_permset_t p; + if (sym_acl_get_permset(e, &p) < 0) + return log_error_errno(errno, "Failed to get ACL entry permission set: %m"); - int permset = 0; - r = sym_acl_get_perm(p, ACL_READ); - if (r < 0) - return log_error_errno(r, "Failed to get ACL entry read bit: %m"); - SET_FLAG(permset, ARCHIVE_ENTRY_ACL_READ, r); + int permset = 0; + r = sym_acl_get_perm(p, ACL_READ); + if (r < 0) + return log_error_errno(r, "Failed to get ACL entry read bit: %m"); + SET_FLAG(permset, ARCHIVE_ENTRY_ACL_READ, r); - r = sym_acl_get_perm(p, ACL_WRITE); - if (r < 0) - return log_error_errno(r, "Failed to get ACL entry write bit: %m"); - SET_FLAG(permset, ARCHIVE_ENTRY_ACL_WRITE, r); + r = sym_acl_get_perm(p, ACL_WRITE); + if (r < 0) + return log_error_errno(r, "Failed to get ACL entry write bit: %m"); + SET_FLAG(permset, ARCHIVE_ENTRY_ACL_WRITE, r); - r = sym_acl_get_perm(p, ACL_EXECUTE); - if (r < 0) - return log_error_errno(r, "Failed to get ACL entry execute bit: %m"); - SET_FLAG(permset, ARCHIVE_ENTRY_ACL_EXECUTE, r); + r = sym_acl_get_perm(p, ACL_EXECUTE); + if (r < 0) + return log_error_errno(r, "Failed to get ACL entry execute bit: %m"); + SET_FLAG(permset, ARCHIVE_ENTRY_ACL_EXECUTE, r); - r = sym_archive_entry_acl_add_entry(entry, type, permset, tag, qualifier, /* name= */ NULL); - if (r != ARCHIVE_OK) - return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to add ACL entry."); + r = sym_archive_entry_acl_add_entry(entry, type, permset, tag, qualifier, /* name= */ NULL); + if (r != ARCHIVE_OK) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to add ACL entry."); + } r = sym_acl_get_entry(acl, ACL_NEXT_ENTRY, &e); } @@ -1223,9 +1258,9 @@ static int archive_item( sym_archive_entry_set_perm(entry, sx->stx_mode); if (FLAGS_SET(sx->stx_mask, STATX_UID)) - sym_archive_entry_set_uid(entry, sx->stx_uid); + sym_archive_entry_set_uid(entry, maybe_squash_uid(sx->stx_uid, d->flags)); if (FLAGS_SET(sx->stx_mask, STATX_GID)) - sym_archive_entry_set_gid(entry, sx->stx_gid); + sym_archive_entry_set_gid(entry, maybe_squash_gid(sx->stx_gid, d->flags)); if (S_ISREG(sx->stx_mode)) { if (!FLAGS_SET(sx->stx_mask, STATX_SIZE)) @@ -1273,7 +1308,7 @@ static int archive_item( if (!acl) return log_error_errno(errno, "Failed read access ACLs of '%s': %m", path); - archive_write_acl(entry, ACL_TYPE_ACCESS, acl); + archive_write_acl(entry, ACL_TYPE_ACCESS, acl, d->flags); if (S_ISDIR(sx->stx_mode)) { sym_acl_free(acl); @@ -1282,7 +1317,7 @@ static int archive_item( if (!acl) return log_error_errno(errno, "Failed to read default ACLs of '%s': %m", path); - archive_write_acl(entry, ACL_TYPE_DEFAULT, acl); + archive_write_acl(entry, ACL_TYPE_DEFAULT, acl, d->flags); } } } diff --git a/src/shared/tar-util.h b/src/shared/tar-util.h index 06e2dc88fe..10c60be8a8 100644 --- a/src/shared/tar-util.h +++ b/src/shared/tar-util.h @@ -2,7 +2,8 @@ #pragma once typedef enum TarFlags { - TAR_SELINUX = 1 << 0, + TAR_SELINUX = 1 << 0, /* Include SELinux xattr in tarball, or unpack it */ + TAR_SQUASH_UIDS_ABOVE_64K = 1 << 1, /* Squash UIDs/GIDs above 64K when packing/unpacking to the nobody user */ } TarFlags; int tar_x(int input_fd, int tree_fd, TarFlags flags); From 4248b02c443de2981b2e8da199fdc63dc3121658 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 25 Aug 2025 17:07:20 +0200 Subject: [PATCH 8/9] test: add test case --- test/units/TEST-13-NSPAWN.unpriv.sh | 125 ++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/test/units/TEST-13-NSPAWN.unpriv.sh b/test/units/TEST-13-NSPAWN.unpriv.sh index d412a7fd9a..02faeb1796 100755 --- a/test/units/TEST-13-NSPAWN.unpriv.sh +++ b/test/units/TEST-13-NSPAWN.unpriv.sh @@ -14,8 +14,21 @@ fi at_exit() { rm -rf /home/testuser/.local/state/machines/zurps ||: + rm -rf /home/testuser/.local/state/machines/nurps ||: + rm -rf /home/testuser/.local/state/machines/kurps ||: + rm -rf /home/testuser/.local/state/machines/wumms ||: + rm -rf /home/testuser/.local/state/machines/wamms ||: + rm -rf /home/testuser/.local/state/machines/inodetest ||: + rm -rf /home/testuser/.local/state/machines/inodetest2 ||: machinectl terminate zurps ||: rm -f /etc/polkit-1/rules.d/registermachinetest.rules + machinectl terminate nurps ||: + machinectl terminate kurps ||: + machinectl terminate wumms ||: + machinectl terminate wamms ||: + rm -f /usr/share/polkit-1/rules.d/registermachinetest.rules + rm -rf /var/tmp/mangletest + rm -f /var/tmp/mangletest.tar.gz } trap at_exit EXIT @@ -45,6 +58,8 @@ EOF loginctl enable-linger testuser +run0 -u testuser mkdir -p .config/systemd/nspawn/ +run0 -u testuser -i "echo -e \"[Exec]\nKillSignal=SIGKILL\n\" > .config/systemd/nspawn/zurps.nspawn" run0 -u testuser systemctl start --user systemd-nspawn@zurps.service machinectl status zurps @@ -87,4 +102,114 @@ run0 -u testuser \ (! run0 -u testuser machinectl shell 0@shouldnotwork2 /usr/bin/id -u) (! run0 -u testuser machinectl shell testuser@shouldnotwork2 /usr/bin/id -u) +run0 -u testuser mkdir /var/tmp/image-tar +run0 -u testuser importctl --user export-tar zurps /var/tmp/image-tar/kurps.tar.gz -m +run0 -u testuser importctl --user import-tar /var/tmp/image-tar/kurps.tar.gz -m + +run0 -u testuser -i "echo -e \"[Exec]\nKillSignal=SIGKILL\n\" > .config/systemd/nspawn/kurps.nspawn" +run0 -u testuser systemctl start --user systemd-nspawn@kurps.service +machinectl terminate kurps + +run0 -u testuser -D /var/tmp/image-tar/ bash -c 'sha256sum kurps.tar.gz > SHA256SUMS' +run0 -u testuser importctl --user pull-tar file:///var/tmp/image-tar/kurps.tar.gz nurps --verify=checksum -m + +run0 -u testuser -i "echo -e \"[Exec]\nKillSignal=SIGKILL\n\" > .config/systemd/nspawn/nurps.nspawn" +run0 -u testuser systemctl start --user systemd-nspawn@nurps.service +machinectl terminate nurps + +run0 -u testuser rm -r /var/tmp/image-tar + +run0 -u testuser importctl --user list-images +run0 -u testuser machinectl --user list-images + +assert_in 'zurps' "$(run0 --pipe -u testuser machinectl --user list-images)" +assert_in 'nurps' "$(run0 --pipe -u testuser machinectl --user list-images)" +assert_in 'kurps' "$(run0 --pipe -u testuser machinectl --user list-images)" + +run0 -u testuser machinectl --user image-status zurps +run0 -u testuser machinectl --user image-status nurps +run0 -u testuser machinectl --user image-status kurps + +run0 -u testuser machinectl --user show-image zurps +run0 -u testuser machinectl --user show-image nurps +run0 -u testuser machinectl --user show-image kurps + +run0 -u testuser machinectl --user clone zurps wumms + +assert_in 'wumms' "$(run0 -u testuser machinectl --user list-images)" +run0 -u testuser machinectl --user image-status wumms +run0 -u testuser machinectl --user show-image wumms + +run0 -u testuser machinectl --user rename wumms wamms +assert_not_in 'wumms' "$(run0 -u testuser machinectl --user list-images)" +assert_in 'wamms' "$(run0 -u testuser machinectl --user list-images)" +run0 -u testuser machinectl --user image-status wamms +run0 -u testuser machinectl --user show-image wamms + +run0 -u testuser -i "echo -e \"[Exec]\nKillSignal=SIGKILL\n\" > .config/systemd/nspawn/wamms.nspawn" +run0 -u testuser systemctl start --user systemd-nspawn@wamms.service + +run0 -u testuser systemctl stop --user systemd-nspawn@zurps.service +run0 -u testuser systemctl stop --user systemd-nspawn@nurps.service +run0 -u testuser systemctl stop --user systemd-nspawn@kurps.service +run0 -u testuser systemctl stop --user systemd-nspawn@wamms.service + +run0 -u testuser machinectl --user remove zurps +run0 -u testuser machinectl --user remove kurps +run0 -u testuser machinectl --user remove nurps +run0 -u testuser machinectl --user remove wamms + +assert_not_in 'zurps' "$(run0 --pipe -u testuser machinectl --user list-images)" +assert_not_in 'nurps' "$(run0 --pipe -u testuser machinectl --user list-images)" +assert_not_in 'kurps' "$(run0 --pipe -u testuser machinectl --user list-images)" + +mkdir /home/testuser/.local/state/machines/inodetest +echo hallo > /home/testuser/.local/state/machines/inodetest/testfile + +# Make the file sparse, set an xattr, set an ACL, set a chattr flag, and make it hardlink +ln /home/testuser/.local/state/machines/inodetest/testfile /home/testuser/.local/state/machines/inodetest/testfile.hard +truncate -s 1M /home/testuser/.local/state/machines/inodetest/testfile +setfattr -n "user.piff" -v "paff" /home/testuser/.local/state/machines/inodetest/testfile +setfacl -m g:foreign-47:rwx /home/testuser/.local/state/machines/inodetest/testfile +chattr +A /home/testuser/.local/state/machines/inodetest/testfile +chown foreign-0:foreign-0 /home/testuser/.local/state/machines/inodetest/testfile.hard /home/testuser/.local/state/machines/inodetest +ls -al /home/testuser/.local/state/machines/inodetest + +echo gaga > /home/testuser/.local/state/machines/inodetest/squashtest +chown 1000:1000 /home/testuser/.local/state/machines/inodetest/squashtest + +run0 --pipe -u testuser importctl -m --user export-tar inodetest | + run0 --pipe -u testuser importctl -m --user import-tar - inodetest2 + +ls -al /home/testuser/.local/state/machines/inodetest2 + +cmp /home/testuser/.local/state/machines/inodetest/testfile /home/testuser/.local/state/machines/inodetest2/testfile.hard +cmp <(stat -c"%s %b %B" /home/testuser/.local/state/machines/inodetest/testfile) <(stat -c"%s %b %B" /home/testuser/.local/state/machines/inodetest2/testfile) +cmp <(stat -c"%i" /home/testuser/.local/state/machines/inodetest2/testfile) <(stat -c"%i" /home/testuser/.local/state/machines/inodetest2/testfile.hard) +getfattr -d /home/testuser/.local/state/machines/inodetest/testfile +getfattr -d /home/testuser/.local/state/machines/inodetest2/testfile +getfacl /home/testuser/.local/state/machines/inodetest/testfile +getfacl /home/testuser/.local/state/machines/inodetest2/testfile +lsattr /home/testuser/.local/state/machines/inodetest/testfile +lsattr /home/testuser/.local/state/machines/inodetest2/testfile +cmp <(getfattr -d /home/testuser/.local/state/machines/inodetest/testfile | grep -v ^#) <(getfattr -d /home/testuser/.local/state/machines/inodetest2/testfile | grep -v ^#) +cmp <(getfacl /home/testuser/.local/state/machines/inodetest/testfile | grep -v ^#) <(getfacl /home/testuser/.local/state/machines/inodetest2/testfile | grep -v ^#) +cmp <(lsattr /home/testuser/.local/state/machines/inodetest/testfile | cut -d " " -f1) <(lsattr /home/testuser/.local/state/machines/inodetest2/testfile | cut -d " " -f1) + +# verify that squashing outside of 64K works +test "$(stat -c'%U:%G' /home/testuser/.local/state/machines/inodetest2/squashtest)" = "foreign-65534:foreign-65534" + +# chown to foreing UID range, so that removal works +chown foreign-4711:foreign-4711 /home/testuser/.local/state/machines/inodetest/squashtest + +run0 -u testuser machinectl --user remove inodetest +run0 -u testuser machinectl --user remove inodetest2 + +# Test tree mangling (i.e. moving the root dir one level up on extract) +mkdir -p /var/tmp/mangletest/mangletest-0.1/usr/lib +echo "ID=brumm" > /var/tmp/mangletest/mangletest-0.1/usr/lib/os-release +tar -C /var/tmp/mangletest/ -cvzf /var/tmp/mangletest.tar.gz mangletest-0.1 +run0 --pipe -u testuser importctl -m --user import-tar /var/tmp/mangletest.tar.gz +cmp /var/tmp/mangletest/mangletest-0.1/usr/lib/os-release /home/testuser/.local/state/machines/mangletest/usr/lib/os-release + loginctl disable-linger testuser From 9a1d72fe996f85988cae8841416d06512702408c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 27 Aug 2025 11:29:43 +0200 Subject: [PATCH 9/9] update TODO --- TODO | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/TODO b/TODO index ca4cab4654..5820260612 100644 --- a/TODO +++ b/TODO @@ -63,6 +63,12 @@ Regularly: Janitorial Clean-ups: +* machined: make remaining machine bus calls compatible with unpriv machined + + unpriv npsawn: GetAddresses(), GetSSHInfo(), GetOSRelease(), OpenPTY(), + OpenLogin(), OpenShell(), BindMount(), CopyFrom(), CopyTo(), + OpenRootDirectory(). Similar for images: GetHostname(), GetMachineID(), + GetMachineInfo(), GetOSRelease(). + * rework mount.c and swap.c to follow proper state enumeration/deserialization semantics, like we do for device.c now @@ -469,18 +475,10 @@ Features: * allow dynamic modifications of ConcurrencyHardMax= and ConcurrencySoftMax= via DBus (and with that also by daemon-reload) -* importd: introduce a per-user instance, that downloads into per-user DDI dirs - -* sysupdated: similar +* sysupdated: introduce per-user version that can update per-user installed dDIs * portabled: similar -* machined: implement a per-user instance, that manages per-user DDI dirs for - images. systemd-nspawn/systemd-vmspawn should probably register with both the - system and the user scoped machined instance. The former to get the machine - name registered as hostname, and the latter so that the image stuff is nicely - per-user managed. - * resolved: make resolved process DNR DHCP info * maybe introduce an OSC sequence that signals when we ask for a password, so @@ -490,14 +488,8 @@ Features: * start using STATX_SUBVOL in btrfs_is_subvol(). Also, make use of it generically, so that image discovery recognizes bcachefs subvols too. -* "systemd-export tar" should reuse the libarchive export code from systemd-dissect - --archive. - -* "systemd-import tar" should be moved to libarchive - * foreign uid: - - add support to export-fs, import-fs, import-tar, export-tar - - add tool for deleting foreign UID held container images + - add support to export-fs, import-fs - systemd-dissect should learn mappings, too, when doing mtree and such * format-table: introduce new cell type for strings with ansi sequences in @@ -720,7 +712,6 @@ Features: from, tracing through overlayfs, DM, loopback block device. * importd/importctl - - port tar handling to libarchive - complete varlink interface - download images into .v/ dirs