]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
tar-util: add support for extracting OCI compatible whiteouts, and turn them into...
authorLennart Poettering <lennart@amutable.com>
Fri, 7 Nov 2025 07:33:32 +0000 (08:33 +0100)
committerLennart Poettering <lennart@amutable.com>
Thu, 19 Feb 2026 14:05:15 +0000 (15:05 +0100)
src/shared/tar-util.c
src/shared/tar-util.h

index 27f3212fb3269795b2e42c356423e25fbab0c1d3..17c03e910bb74ce35f5893061f7693506ae08c3a 100644 (file)
@@ -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;
+                        }
                 }
         }
 
index 10c60be8a80dc7cab3ad2a754d27111bbe014644..4ce8c573778da0fe415841b37f3d210f228c0819 100644 (file)
@@ -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);