[PATCH v2 3/3] selftests: overlayfs: keep index copy up with mixed lower idmaps

From: Jérémy Jean

Date: Sun Sep 20 2026 - 16:33:23 EST


Add a copy-up test for mixed-idmap lower layers with
index=on,nfs_export=off.

Write through one hardlink alias and read through the other.
Check that both aliases still point to the same copied-up inode.
Check that the copied-up inode keeps the expected mapped owner.

Signed-off-by: Jérémy Jean <Jeremy.Jean@xxxxxxxxxxxxxxxxx>
---
.../filesystems/overlayfs/idmapped_mounts.c | 103 ++++++++++++++++++
1 file changed, 103 insertions(+)

diff --git a/tools/testing/selftests/filesystems/overlayfs/idmapped_mounts.c b/tools/testing/selftests/filesystems/overlayfs/idmapped_mounts.c
index 0e94bd464f1c..ae99698c280a 100644
--- a/tools/testing/selftests/filesystems/overlayfs/idmapped_mounts.c
+++ b/tools/testing/selftests/filesystems/overlayfs/idmapped_mounts.c
@@ -219,6 +219,42 @@ static int setup_extra_layer(const char *base, const char *name)
return 0;
}

+static int write_path(const char *path, const char *buf)
+{
+ size_t len = strlen(buf);
+ int fd;
+
+ fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0644);
+ if (fd < 0)
+ return -1;
+ if (write(fd, buf, len) != len) {
+ close(fd);
+ return -1;
+ }
+ if (close(fd))
+ return -1;
+ return 0;
+}
+
+static int read_fd_contents(int dirfd, const char *name, char *buf, size_t len)
+{
+ ssize_t n;
+ int fd;
+
+ fd = openat(dirfd, name, O_RDONLY);
+ if (fd < 0)
+ return -1;
+ n = read(fd, buf, len - 1);
+ if (n < 0) {
+ close(fd);
+ return -1;
+ }
+ buf[n] = '\0';
+ if (close(fd))
+ return -1;
+ return 0;
+}
+
/*
* Mount an overlay inside user namespace @u1 (so the overlay sb's s_user_ns is
* not the initial namespace) and idmap that overlay mount with @u2. Runs in a
@@ -588,4 +624,71 @@ TEST_F(idmapped_overlay, nfs_export_same_sb_mixed_idmaps)
EXPECT_EQ(close(ovl), 0);
}

+/*
+ * index=on without nfs_export still preserves lower hardlinks on copy up when
+ * same-superblock lower layers use different idmaps. The copied-up alias must
+ * keep the same contents, inode identity, and owner as the path-selected lower.
+ */
+TEST_F(idmapped_overlay, index_copy_up_same_sb_mixed_idmaps)
+{
+ static const char lower_data[] = "lower\n";
+ static const char upper_data[] = "upper\n";
+ char lower1[PATH_MAX], lower2[PATH_MAX], upper[PATH_MAX], work[PATH_MAX];
+ char path[PATH_MAX], alias[PATH_MAX], buf[32];
+ struct stat st_file, st_alias, st_upper;
+ int fd_lower1, fd_lower2, ovl, fd;
+
+ if (!ovl_supported())
+ SKIP(return, "overlayfs not supported");
+
+ snprintf(lower1, sizeof(lower1), "%s/l", self->base);
+ snprintf(lower2, sizeof(lower2), "%s/l2", self->base);
+ snprintf(upper, sizeof(upper), "%s/u", self->base);
+ snprintf(work, sizeof(work), "%s/w", self->base);
+ ASSERT_EQ(setup_extra_layer(self->base, "l2"), 0);
+
+ snprintf(path, sizeof(path), "%s/l2/file", self->base);
+ snprintf(alias, sizeof(alias), "%s/l2/alias", self->base);
+ ASSERT_EQ(write_path(path, lower_data), 0);
+ ASSERT_EQ(chown(path, ID_HOST + 7, ID_HOST + 7), 0);
+ ASSERT_EQ(link(path, alias), 0);
+
+ fd_lower1 = idmapped_layer_fd(lower1, ID_HOST, ID_NS, ID_RANGE);
+ ASSERT_GE(fd_lower1, 0);
+ fd_lower2 = layer_fd(lower2);
+ ASSERT_GE(fd_lower2, 0);
+
+ ovl = ovl_mount_lower_fds(upper, work, fd_lower1, fd_lower2, "on", "off");
+ ASSERT_GE(ovl, 0);
+ EXPECT_EQ(close(fd_lower1), 0);
+ EXPECT_EQ(close(fd_lower2), 0);
+ ASSERT_EQ(ovl_idmap(ovl), 0);
+
+ fd = openat(ovl, "file", O_WRONLY | O_TRUNC);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(write(fd, upper_data, sizeof(upper_data) - 1),
+ sizeof(upper_data) - 1);
+ EXPECT_EQ(close(fd), 0);
+
+ ASSERT_EQ(read_fd_contents(ovl, "alias", buf, sizeof(buf)), 0);
+ EXPECT_STREQ(upper_data, buf);
+
+ ASSERT_EQ(fstatat(ovl, "file", &st_file, 0), 0);
+ ASSERT_EQ(fstatat(ovl, "alias", &st_alias, 0), 0);
+ EXPECT_EQ(st_file.st_ino, st_alias.st_ino);
+ EXPECT_EQ(st_file.st_nlink, 2);
+ EXPECT_EQ(st_alias.st_nlink, 2);
+ EXPECT_EQ(st_file.st_uid, ID_NS + 7);
+ EXPECT_EQ(st_file.st_gid, ID_NS + 7);
+ EXPECT_EQ(st_alias.st_uid, ID_NS + 7);
+ EXPECT_EQ(st_alias.st_gid, ID_NS + 7);
+
+ snprintf(path, sizeof(path), "%s/u/file", self->base);
+ ASSERT_EQ(stat(path, &st_upper), 0);
+ EXPECT_EQ(st_upper.st_uid, ID_HOST + 7);
+ EXPECT_EQ(st_upper.st_gid, ID_HOST + 7);
+
+ EXPECT_EQ(close(ovl), 0);
+}
+
TEST_HARNESS_MAIN
--
2.47.3