From 6527861039bd57718df8978e57f91cb800dbf1ed Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 7 Nov 2025 08:33:32 +0100 Subject: [PATCH] tar-util: add support for extracting OCI compatible whiteouts, and turn them into overlayfs whiteouts --- src/shared/tar-util.c | 158 ++++++++++++++++++++++++++++++++++++------ src/shared/tar-util.h | 1 + 2 files changed, 138 insertions(+), 21 deletions(-) diff --git a/src/shared/tar-util.c b/src/shared/tar-util.c index 27f3212fb32..17c03e910bb 100644 --- a/src/shared/tar-util.c +++ b/src/shared/tar-util.c @@ -147,8 +147,8 @@ static int open_inode_finalize(OpenInode *of) { /* We adjust the UID/GID right before the mode, since doing this might affect the mode (drops * suid/sgid bits). * - * We adjust the mode only when leaving a dir, because if we are unpriv we might lose the - * ability to enter it once we do this. */ + * We adjust the mode only when leaving a dir, because if we are unprivileged we might lose + * the ability to enter it once we do this. */ if (uid_is_valid(of->uid) || gid_is_valid(of->gid) || of->mode != MODE_INVALID) { k = fchmod_and_chown_with_fallback(of->fd, /* path= */ NULL, of->mode, of->uid, of->gid); @@ -277,6 +277,91 @@ static int archive_unpack_regular( return TAKE_FD(fd); } +static int overlayfs_fsetfattr( + const char *path, /* purely decorative, for log purposes */ + int fd, + const char *name, /* xattr key name */ + const char *value /* xattr value */) { + int r; + + assert(fd >= 0); + assert(path); + assert(name); + assert(value); + + /* overlayfs knows magic {user|trusted}.overlay.* xattrs for whiteouts and opaque directories. The + * 'user.overlay.*' ones are only checked if overlayfs is mounted with "userxattr". We only set that + * one because we want to operate unprivileged. Ideally, we'd set both here, to maximize the chance + * that things work both in privileged and unprivileged scenarios, but unfortunately this has the + * effect that the privileged ones are ignored (and visible in the overlayfs mount). */ + _cleanup_free_ char *n = strjoin("user.overlay.", name); + if (!n) + return log_oom(); + + r = xsetxattr(fd, /* path= */ NULL, AT_EMPTY_PATH, n, value); + if (r < 0) + return log_error_errno(r, "Failed to set '%s' xattr on file '%s': %m", n, path); + + return 0; +} + +static int archive_unpack_whiteout( + struct archive *a, + struct archive_entry *entry, + int parent_fd, + const char *parent_path, /* Full path of 'parent_fd', purely decorative for log purposes */ + const char *filename, /* Just the filename we are supposed to whiteout */ + const char *path /* Full path of the whiteout file, purely decorative for log purposes */) { + + int r; + + assert(a); + assert(entry); + assert(parent_fd >= 0); + assert(parent_path); + assert(filename); + assert(path); + + _cleanup_free_ char *tmp = NULL; + _cleanup_close_ int fd = open_tmpfile_linkable_at(parent_fd, filename, O_CLOEXEC|O_WRONLY, &tmp); + if (fd < 0) + return log_error_errno(fd, "Failed to create whiteout file for '%s': %m", path); + + CLEANUP_TMPFILE_AT(parent_fd, tmp); + + r = overlayfs_fsetfattr(path, fd, "whiteout", "y"); + if (r < 0) + return r; + + /* As per https://docs.kernel.org/filesystems/overlayfs.html also mark the parent */ + r = overlayfs_fsetfattr(parent_path, parent_fd, "opaque", "x"); + if (r < 0) + return r; + + r = link_tmpfile_at(fd, parent_fd, tmp, filename, LINK_TMPFILE_REPLACE); + if (r < 0) + return log_error_errno(r, "Failed to install regular file '%s': %m", path); + + tmp = mfree(tmp); /* disarm CLEANUP_TMPFILE_AT */ + return 0; /* we do not return an fd here, because this kills an inode, and doesn't synthesize one */ +} + +static int archive_unpack_opaque( + struct archive *a, + struct archive_entry *entry, + int parent_fd, + const char *parent_path) { + + assert(a); + assert(entry); + assert(parent_fd >= 0); + assert(parent_path); + + /* we do not return an fd here either */ + + return overlayfs_fsetfattr(parent_path, parent_fd, "opaque", "y"); +} + static int archive_unpack_directory( struct archive *a, struct archive_entry *entry, @@ -908,15 +993,45 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { switch (filetype) { case S_IFREG: + if (FLAGS_SET(flags, TAR_OCI_WHITEOUTS)) { + if (streq(e, ".wh..wh..opq")) { + r = archive_unpack_opaque(a, entry, parent_fd, empty_to_root(parent_path)); + if (r < 0) + return r; + + /* NB: this does not create an inode! */ + break; + } + + const char *w = startswith(e, ".wh."); + if (w) { + if (!filename_is_valid(w)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Invalid whiteout file entry '%s', refusing.", e); + + r = archive_unpack_whiteout(a, entry, parent_fd, empty_to_root(parent_path), w, j); + if (r < 0) + return r; + + /* NB: this does not create an inode! */ + break; + } + } + fd = archive_unpack_regular(a, entry, parent_fd, e, j, fflags); + if (fd < 0) + return fd; break; case S_IFDIR: fd = archive_unpack_directory(a, entry, parent_fd, e, j, fflags); + if (fd < 0) + return fd; break; case S_IFLNK: fd = archive_unpack_symlink(a, entry, parent_fd, e, j); + if (fd < 0) + return fd; break; case S_IFCHR: @@ -924,6 +1039,8 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { case S_IFIFO: case S_IFSOCK: fd = archive_unpack_special_inode(a, entry, parent_fd, e, j, filetype); + if (fd < 0) + return fd; break; default: @@ -931,9 +1048,6 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unexpected file type %i of '%s', refusing.", (int) filetype, j); } - if (fd < 0) - return fd; - } else { /* This is some intermediary node in the path that we haven't opened yet. Create it with default attributes */ fd = open_mkdir_at(parent_fd, e, O_CLOEXEC, 0700); @@ -949,22 +1063,24 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { * fully done with the inode (i.e. after creating further inodes inside of dir inodes * for example), due to permission problems this might create or that the mtime * changes we do might still be affected by our changes. */ - open_inodes[n_open_inodes++] = (OpenInode) { - .fd = TAKE_FD(fd), - .path = TAKE_PTR(j), - .filetype = filetype, - .mode = mode, - .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, - }; - - n_xa = 0; + if (fd >= 0) { + open_inodes[n_open_inodes++] = (OpenInode) { + .fd = TAKE_FD(fd), + .path = TAKE_PTR(j), + .filetype = filetype, + .mode = mode, + .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, + }; + + n_xa = 0; + } } } diff --git a/src/shared/tar-util.h b/src/shared/tar-util.h index 10c60be8a80..4ce8c573778 100644 --- a/src/shared/tar-util.h +++ b/src/shared/tar-util.h @@ -4,6 +4,7 @@ typedef enum TarFlags { 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 */ + TAR_OCI_WHITEOUTS = 1 << 2, /* Turn OCI/aufs whiteout inodes into overlayfs whiteouts */ } TarFlags; int tar_x(int input_fd, int tree_fd, TarFlags flags); -- 2.47.3