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


Reply via email to