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 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/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 a15790b94d..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); @@ -62,6 +64,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 diff --git a/src/shared/libarchive-util.c b/src/shared/libarchive-util.c index 961d9f6a03..09f036895c 100644 --- a/src/shared/libarchive-util.c +++ b/src/shared/libarchive-util.c @@ -7,6 +7,10 @@ #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; DLSYM_PROTOTYPE(archive_entry_gid) = NULL; @@ -26,8 +30,10 @@ 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; DLSYM_PROTOTYPE(archive_entry_set_mtime) = NULL; DLSYM_PROTOTYPE(archive_entry_set_pathname) = NULL; DLSYM_PROTOTYPE(archive_entry_set_perm) = NULL; @@ -36,11 +42,13 @@ 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 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; @@ -59,7 +67,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", @@ -71,6 +79,10 @@ 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), DLSYM_ARG(archive_entry_gid), @@ -90,8 +102,10 @@ 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), DLSYM_ARG(archive_entry_set_mtime), DLSYM_ARG(archive_entry_set_pathname), DLSYM_ARG(archive_entry_set_perm), @@ -100,11 +114,13 @@ 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 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), @@ -123,8 +139,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 83ee19d27a..615cd1ec81 100644 --- a/src/shared/libarchive-util.h +++ b/src/shared/libarchive-util.h @@ -9,6 +9,10 @@ #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); extern DLSYM_PROTOTYPE(archive_entry_gid); @@ -22,8 +26,10 @@ 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); extern DLSYM_PROTOTYPE(archive_entry_set_mtime); extern DLSYM_PROTOTYPE(archive_entry_set_pathname); extern DLSYM_PROTOTYPE(archive_entry_set_perm); @@ -32,8 +38,10 @@ 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); extern DLSYM_PROTOTYPE(archive_entry_xattr_next); extern DLSYM_PROTOTYPE(archive_entry_xattr_reset); extern DLSYM_PROTOTYPE(archive_error_string); @@ -52,7 +60,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 c75d7eb666..05dd4cd9fc 100644 --- a/src/shared/tar-util.c +++ b/src/shared/tar-util.c @@ -5,16 +5,25 @@ #include "tar-util.h" #if HAVE_LIBARCHIVE +#include #include +#include "acl-util.h" #include "alloc-util.h" #include "chase.h" +#include "chattr-util.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 "nsresource.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" @@ -23,6 +32,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; @@ -38,8 +56,10 @@ typedef struct OpenInode { struct timespec mtime; uid_t uid; gid_t gid; + unsigned fflags; XAttr *xattr; size_t n_xattr; + acl_t acl_access, acl_default; } OpenInode; static void xattr_done(XAttr *xa) { @@ -68,6 +88,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) { @@ -79,6 +103,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; @@ -99,6 +146,24 @@ 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, + /* 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) { @@ -149,7 +214,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; @@ -164,6 +230,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( @@ -204,7 +286,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); @@ -220,6 +305,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); } @@ -321,6 +420,145 @@ 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, + TarFlags flags) { + + 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; + /* 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"); + } + + 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 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, @@ -328,10 +566,15 @@ static int archive_entry_read_stat( struct timespec *mtime, uid_t *uid, gid_t *gid, + unsigned *fflags, + acl_t *acl_access, + acl_t *acl_default, XAttr **xa, 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 @@ -349,24 +592,40 @@ 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; + 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; 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(); @@ -384,6 +643,18 @@ static int archive_entry_read_stat( }; } + if (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, flags); + if (r < 0) + return r; + } + return 0; } @@ -458,6 +729,9 @@ 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].acl_access, + &open_inodes[0].acl_default, &open_inodes[0].xattr, &open_inodes[0].n_xattr, flags); @@ -528,6 +802,8 @@ 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; + _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); @@ -602,6 +878,9 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { &mtime, &uid, &gid, + &fflags, + &acl_access, + &acl_default, &xa, &n_xa, flags); @@ -611,11 +890,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: @@ -660,6 +939,9 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { .mtime = mtime, .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, }; @@ -675,6 +957,258 @@ 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_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_write_acl( + struct archive_entry *entry, + acl_type_t ntype, + acl_t acl, + TarFlags flags) { + 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; + + bool skip = false; + 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); + + /* 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)); + } + + 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); + + 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, @@ -684,7 +1218,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); @@ -702,17 +1236,31 @@ 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)) 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)) @@ -745,17 +1293,90 @@ 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)); + 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, d->flags); + + 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, d->flags); + } + } + } + } + + _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); + } + + _cleanup_close_ int data_fd = -EBADF; if (S_ISREG(sx->stx_mode)) { - _cleanup_close_ int data_fd = -EBADF; - - /* Convert the O_PATH fd in a proper fd */ + /* 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 (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)); + + if (S_ISREG(sx->stx_mode)) { + assert(data_fd >= 0); + for (;;) { char buffer[64*1024]; ssize_t l; @@ -767,15 +1388,24 @@ 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)); } } 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; @@ -789,7 +1419,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)); @@ -797,13 +1427,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, - a); + &data); if (r < 0) return log_error_errno(r, "Failed to make archive: %m"); 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); 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