[PATCH] erofs-utils: lib: follow symlinks on intermediate tar path components

[email protected] Sat, 25 Jul 2026 15:09:20 +0000
Newsgroups org.ozlabs.lists.linux-erofs
Message-ID <[email protected]>
From: Mikhail Malyshev <[email protected]>

mkfs.erofs --tar fails to convert layers that store files underneath a
symlinked directory, a very common "usr-merge" layout in RHEL/UBI/Fedora
based OCI images.  For example a base layer contains

    lib64 -> usr/lib64        (symlink)
    usr/lib64/                (directory)
    lib64/libc.so.6           (regular file, path goes THROUGH the symlink)

erofs_rebuild_get_dentry() walks the path component by component and, when
it hits the "lib64" component, finds an existing dentry that is a symlink
rather than a directory.  Since there are still components left to resolve
it bails out with -ENOTDIR.  In practice this surfaces as a bogus ~2 TiB
image and "Could not format the device", making the whole layer
unconvertible even though nothing is wrong with the tar.

A real filesystem (and containerd's walking differ, which applies the same
layers via fs.RootPath) resolves such a path by following the symlink to
its target directory.  Do the same during tar rebuild: when an intermediate
path component resolves to a symlink, follow it to the target directory
(relative targets from the containing directory, absolute targets from the
tree root), chasing chained symlinks up to a bounded depth to avoid loops.

Signed-off-by: Mikhail Malyshev <[email protected]>
---
 lib/rebuild.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 70 insertions(+), 2 deletions(-)

diff --git a/lib/rebuild.c b/lib/rebuild.c
index 108a464..febc95c 100644
--- a/lib/rebuild.c
+++ b/lib/rebuild.c
@@ -82,8 +82,60 @@ struct erofs_dentry *erofs_d_lookup(struct erofs_inode *dir, const char *name)
 	return NULL;
 }
 
-struct erofs_dentry *erofs_rebuild_get_dentry(struct erofs_inode *pwd,
-		char *path, bool aufs, bool *whout, bool *opq, bool to_head)
+/* Bound symlink chains during path resolution, like the kernel's MAXSYMLINKS. */
+#define EROFS_REBUILD_SYMLINK_DEPTH	40
+
+static struct erofs_dentry *__erofs_rebuild_get_dentry(struct erofs_inode *root,
+		struct erofs_inode *pwd, char *path, bool aufs,
+		bool *whout, bool *opq, bool to_head, unsigned int depth);
+
+/*
+ * Resolve the target of an intermediate symlink path component to its
+ * directory inode, following chained symlinks up to a bounded depth.  This
+ * lets tar layers that store files under a symlinked directory be converted
+ * the same way a real filesystem extracts them, e.g. usr-merge layouts where
+ * "/lib64 -> usr/lib64" coexists with entries such as "lib64/libc.so.6".
+ */
+static struct erofs_inode *erofs_rebuild_follow_link(struct erofs_inode *root,
+		struct erofs_inode *pwd, struct erofs_inode *link,
+		unsigned int depth)
+{
+	struct erofs_dentry *d;
+	struct erofs_inode *base;
+	bool dumb_wh, dumb_opq;
+	char *target;
+
+	if (depth >= EROFS_REBUILD_SYMLINK_DEPTH)
+		return ERR_PTR(-ELOOP);
+	if (!link->i_link)
+		return ERR_PTR(-ENOENT);
+
+	target = strdup(link->i_link);
+	if (!target)
+		return ERR_PTR(-ENOMEM);
+
+	/* absolute targets are resolved from the tree root */
+	base = target[0] == '/' ? root : pwd;
+	d = __erofs_rebuild_get_dentry(root, base, target, false,
+				       &dumb_wh, &dumb_opq, false, depth + 1);
+	free(target);
+	if (IS_ERR(d))
+		return ERR_CAST(d);
+	/* target referred to the root or the base directory itself ("/", ".") */
+	if (!d)
+		return base;
+	if (d->type == EROFS_FT_DIR)
+		return d->inode;
+	/* the target is itself a symlink: keep following the chain */
+	if (d->type == EROFS_FT_SYMLINK)
+		return erofs_rebuild_follow_link(root, d->inode->i_parent,
+						 d->inode, depth + 1);
+	return ERR_PTR(-ENOTDIR);
+}
+
+static struct erofs_dentry *__erofs_rebuild_get_dentry(struct erofs_inode *root,
+		struct erofs_inode *pwd, char *path, bool aufs,
+		bool *whout, bool *opq, bool to_head, unsigned int depth)
 {
 	struct erofs_dentry *d = NULL;
 	char *s = path;
@@ -121,6 +173,15 @@ struct erofs_dentry *erofs_rebuild_get_dentry(struct erofs_inode *pwd,
 			}
 
 			d = erofs_d_lookup(pwd, s);
+			if (d && slash && d->type == EROFS_FT_SYMLINK) {
+				pwd = erofs_rebuild_follow_link(root, pwd,
+							d->inode, depth);
+				if (IS_ERR(pwd))
+					return ERR_CAST(pwd);
+				*slash = '/';
+				s = slash + 1;
+				continue;
+			}
 			if (d) {
 				if (d->type != EROFS_FT_DIR) {
 					if (slash)
@@ -152,6 +213,13 @@ struct erofs_dentry *erofs_rebuild_get_dentry(struct erofs_inode *pwd,
 	return d;
 }
 
+struct erofs_dentry *erofs_rebuild_get_dentry(struct erofs_inode *pwd,
+		char *path, bool aufs, bool *whout, bool *opq, bool to_head)
+{
+	return __erofs_rebuild_get_dentry(pwd, pwd, path, aufs, whout, opq,
+					  to_head, 0);
+}
+
 static int erofs_rebuild_write_blob_index(struct erofs_sb_info *dst_sb,
 					  struct erofs_inode *inode)
 {
-- 
2.43.0