tar-util: add support for extracting OCI compatible whiteouts, and turn them into overlayfs whiteouts

This commit is contained in:
Lennart Poettering
2026-02-19 15:05:15 +01:00
parent d66e3cc79a
commit 6527861039
2 changed files with 137 additions and 20 deletions
+136 -20
View 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,
};
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;
n_xa = 0;
}
}
}
+1
View 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);