// SPDX-License-Identifier: GPL-2.0 #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include "kselftest_harness.h" #include "../wrappers.h" #include "../utils.h" /* * An idmapping that maps the mount-visible id range [0, ID_RANGE) onto the * host/overlay-final id range [ID_HOST, ID_HOST + ID_RANGE). Through such an * idmapped overlay mount, an overlay-final id of ID_HOST + n is reported as n, * and an id of n requested through the mount is stored as ID_HOST + n. */ #define ID_NS 0 #define ID_HOST 10000 #define ID_RANGE 10000 /* * For the composition test the lower layer's on-disk ids live in a * separate range and are mapped by an idmapped lower layer onto the * overlay-final range [ID_HOST, ID_HOST + ID_RANGE). */ #define LAYER_HOST 20000 #ifndef MOUNT_ATTR_IDMAP #define MOUNT_ATTR_IDMAP 0x00100000 #endif #ifndef __NR_mount_setattr #define __NR_mount_setattr 442 #endif static inline int sys_mount_setattr(int dfd, const char *path, unsigned int flags, struct mount_attr *attr, size_t size) { return syscall(__NR_mount_setattr, dfd, path, flags, attr, size); } static bool ovl_supported(void) { int fd = sys_fsopen("overlay", 0); if (fd < 0) return false; close(fd); return true; } /* base/{l,u,w} owned by ID_HOST so they map to ID_NS through the idmap. */ static int setup_layers(const char *base) { static const char *sub[] = { "", "/l", "/u", "/w" }; char path[PATH_MAX]; for (size_t i = 0; i < ARRAY_SIZE(sub); i++) { snprintf(path, sizeof(path), "%s%s", base, sub[i]); if (mkdir(path, 0755) && errno != EEXIST) return -1; if (i && chown(path, ID_HOST, ID_HOST)) return -1; } return 0; } static int ovl_mount(const char *base, bool nfs_export) { char lower[PATH_MAX], upper[PATH_MAX], work[PATH_MAX]; int fsfd, ovl; snprintf(lower, sizeof(lower), "%s/l", base); snprintf(upper, sizeof(upper), "%s/u", base); snprintf(work, sizeof(work), "%s/w", base); fsfd = sys_fsopen("overlay", 0); if (fsfd < 0) return -1; if (sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "source", "test", 0) || sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "lowerdir", lower, 0) || sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "upperdir", upper, 0) || sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "workdir", work, 0)) goto err; if (nfs_export && (sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "index", "on", 0) || sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "nfs_export", "on", 0))) goto err; if (sys_fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) goto err; ovl = sys_fsmount(fsfd, 0, 0); close(fsfd); return ovl; err: close(fsfd); return -1; } /* Idmap the (still detached, not yet visible) overlay mount @mfd. */ static int ovl_idmap(int mfd) { struct mount_attr attr = { .attr_set = MOUNT_ATTR_IDMAP, }; int ret, userns_fd; /* * get_userns_fd(fs_id, mount_id, range): a file whose filesystem id * is fs_id + n is shown through the idmapped mount as mount_id + n. * Here the overlay-final (fs side) range is [ID_HOST, ..) and the * caller-visible (mount side) range is [ID_NS, ..). */ userns_fd = get_userns_fd(ID_HOST, ID_NS, ID_RANGE); if (userns_fd < 0) return -1; attr.userns_fd = userns_fd; ret = sys_mount_setattr(mfd, "", AT_EMPTY_PATH, &attr, sizeof(attr)); close(userns_fd); return ret; } /* Clone @path into a detached, idmapped mount usable as an overlay layer. */ static int idmapped_layer_fd(const char *path, int nsid, int hostid, int range) { struct mount_attr attr = { .attr_set = MOUNT_ATTR_IDMAP, }; int fd_tree, userns_fd; fd_tree = sys_open_tree(AT_FDCWD, path, OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); if (fd_tree < 0) return -1; userns_fd = get_userns_fd(nsid, hostid, range); if (userns_fd < 0) { close(fd_tree); return -1; } attr.userns_fd = userns_fd; if (sys_mount_setattr(fd_tree, "", AT_EMPTY_PATH, &attr, sizeof(attr))) { close(userns_fd); close(fd_tree); return -1; } close(userns_fd); return fd_tree; } /* Overlay with a layer passed by fd (idmapped) plus a plain upper/work. */ static int ovl_mount_lower_fd(const char *upper, const char *work, int fd_lower) { int fsfd, ovl; fsfd = sys_fsopen("overlay", 0); if (fsfd < 0) return -1; if (sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "source", "test", 0) || sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "upperdir", upper, 0) || sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "workdir", work, 0) || sys_fsconfig(fsfd, FSCONFIG_SET_FD, "lowerdir+", NULL, fd_lower) || sys_fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) goto err; ovl = sys_fsmount(fsfd, 0, 0); close(fsfd); return ovl; err: close(fsfd); return -1; } /* * 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 * child that joins @u1; returns 0 on success. */ static int userns_overlay_child(int u1) { struct mount_attr attr = { .attr_set = MOUNT_ATTR_IDMAP, }; struct stat st; int ovl, u2; /* Become root in the overlay sb's user namespace u1. */ if (!switch_userns(u1, 0, 0, false)) return fprintf(stderr, "userns: switch_userns: %m\n"), -1; if (unshare(CLONE_NEWNS) || sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL)) return fprintf(stderr, "userns: unshare/slave: %m\n"), -1; if (sys_mount("tmpfs", "/tmp", "tmpfs", 0, NULL)) return fprintf(stderr, "userns: mount tmpfs: %m\n"), -1; if (setup_layers("/tmp/ovl")) return fprintf(stderr, "userns: setup_layers: %m\n"), -1; if (mknod("/tmp/ovl/l/file", S_IFREG | 0644, 0) || chown("/tmp/ovl/l/file", ID_HOST + 5, ID_HOST + 5)) return fprintf(stderr, "userns: lower file: %m\n"), -1; ovl = ovl_mount("/tmp/ovl", false); if (ovl < 0) return fprintf(stderr, "userns: ovl_mount: %m\n"), -1; /* * mount_setattr() requires CAP_SYS_ADMIN over the idmap user * namespace, so it must be a child of u1. Create it now, from * inside u1. */ u2 = get_userns_fd(ID_HOST, ID_NS, ID_RANGE); if (u2 < 0) return fprintf(stderr, "userns: get_userns_fd: %m\n"), -1; attr.userns_fd = u2; if (sys_mount_setattr(ovl, "", AT_EMPTY_PATH, &attr, sizeof(attr))) return fprintf(stderr, "userns: mount_setattr: %m\n"), -1; close(u2); if (fstatat(ovl, "file", &st, 0)) return fprintf(stderr, "userns: fstatat: %m\n"), -1; if (st.st_uid != ID_NS + 5 || st.st_gid != ID_NS + 5) { fprintf(stderr, "userns: got %u:%u expected %u:%u\n", st.st_uid, st.st_gid, ID_NS + 5, ID_NS + 5); return -1; } return 0; } FIXTURE(idmapped_overlay) { char base[64]; }; FIXTURE_SETUP(idmapped_overlay) { /* Private mount namespace so test mounts need no cleanup. */ ASSERT_EQ(unshare(CLONE_NEWNS), 0); ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0); /* tmpfs for the layers so we can chown them to arbitrary ids. */ ASSERT_EQ(sys_mount("tmpfs", "/tmp", "tmpfs", 0, NULL), 0); snprintf(self->base, sizeof(self->base), "/tmp/ovl"); ASSERT_EQ(setup_layers(self->base), 0); } FIXTURE_TEARDOWN(idmapped_overlay) { } /* A file owned by ID_HOST + 5 is reported as ID_NS + 5 through the idmap. */ TEST_F(idmapped_overlay, getattr) { char path[PATH_MAX]; struct stat st; int ovl; if (!ovl_supported()) SKIP(return, "overlayfs not supported"); snprintf(path, sizeof(path), "%s/l/file", self->base); ASSERT_EQ(mknod(path, S_IFREG | 0644, 0), 0); ASSERT_EQ(chown(path, ID_HOST + 5, ID_HOST + 5), 0); ovl = ovl_mount(self->base, false); ASSERT_GE(ovl, 0); ASSERT_EQ(ovl_idmap(ovl), 0); ASSERT_EQ(fstatat(ovl, "file", &st, 0), 0); EXPECT_EQ(st.st_uid, ID_NS + 5); EXPECT_EQ(st.st_gid, ID_NS + 5); EXPECT_EQ(close(ovl), 0); } /* * Every creation path initializes the new owner through the mount idmap: * created as caller id ID_NS, stored on the upper layer as overlay-final * ID_HOST. Covers ovl_create() (regular file), ovl_mkdir(), ovl_mknod() * and ovl_symlink() (which share ovl_create_object()), plus the separate * ovl_tmpfile() path. */ TEST_F(idmapped_overlay, create) { static const char *names[] = { "reg", "dir", "fifo", "lnk" }; char path[PATH_MAX]; struct stat st; int ovl, fd; if (!ovl_supported()) SKIP(return, "overlayfs not supported"); ovl = ovl_mount(self->base, false); ASSERT_GE(ovl, 0); ASSERT_EQ(ovl_idmap(ovl), 0); /* One object per creation operation, all as caller id ID_NS. */ fd = openat(ovl, "reg", O_CREAT | O_WRONLY | O_EXCL, 0644); ASSERT_GE(fd, 0); EXPECT_EQ(close(fd), 0); ASSERT_EQ(mkdirat(ovl, "dir", 0755), 0); ASSERT_EQ(mknodat(ovl, "fifo", S_IFIFO | 0644, 0), 0); ASSERT_EQ(symlinkat("target", ovl, "lnk"), 0); for (size_t i = 0; i < ARRAY_SIZE(names); i++) { /* Reported as ID_NS through the idmapped mount ... */ ASSERT_EQ(fstatat(ovl, names[i], &st, AT_SYMLINK_NOFOLLOW), 0); EXPECT_EQ(st.st_uid, ID_NS); EXPECT_EQ(st.st_gid, ID_NS); /* ... and stored as ID_HOST on the upper layer. */ snprintf(path, sizeof(path), "%s/u/%s", self->base, names[i]); ASSERT_EQ(lstat(path, &st), 0); EXPECT_EQ(st.st_uid, ID_HOST); EXPECT_EQ(st.st_gid, ID_HOST); } /* O_TMPFILE goes through the separate ovl_tmpfile() path. */ fd = openat(ovl, ".", O_TMPFILE | O_WRONLY, 0644); ASSERT_GE(fd, 0); /* Inside the mount: caller id ID_NS. */ ASSERT_EQ(fstat(fd, &st), 0); EXPECT_EQ(st.st_uid, ID_NS); EXPECT_EQ(st.st_gid, ID_NS); /* Link it in so the upper backing file can be inspected too. */ ASSERT_EQ(linkat(fd, "", ovl, "tmp", AT_EMPTY_PATH), 0); EXPECT_EQ(close(fd), 0); snprintf(path, sizeof(path), "%s/u/tmp", self->base); ASSERT_EQ(lstat(path, &st), 0); EXPECT_EQ(st.st_uid, ID_HOST); EXPECT_EQ(st.st_gid, ID_HOST); EXPECT_EQ(close(ovl), 0); } /* chown through the idmapped mount round-trips: ID_NS + 5 <-> ID_HOST + 5. */ TEST_F(idmapped_overlay, chown) { char path[PATH_MAX]; struct stat st; int ovl, fd; if (!ovl_supported()) SKIP(return, "overlayfs not supported"); ovl = ovl_mount(self->base, false); ASSERT_GE(ovl, 0); ASSERT_EQ(ovl_idmap(ovl), 0); fd = openat(ovl, "f", O_CREAT | O_WRONLY | O_EXCL, 0644); ASSERT_GE(fd, 0); EXPECT_EQ(close(fd), 0); ASSERT_EQ(fchownat(ovl, "f", ID_NS + 5, ID_NS + 5, 0), 0); ASSERT_EQ(fstatat(ovl, "f", &st, 0), 0); EXPECT_EQ(st.st_uid, ID_NS + 5); EXPECT_EQ(st.st_gid, ID_NS + 5); snprintf(path, sizeof(path), "%s/u/f", self->base); ASSERT_EQ(stat(path, &st), 0); EXPECT_EQ(st.st_uid, ID_HOST + 5); EXPECT_EQ(st.st_gid, ID_HOST + 5); EXPECT_EQ(close(ovl), 0); } /* * Composition: an idmapped lower layer underneath an idmapped overlay mount. * An on-disk id is mapped by the layer idmap into the overlay-final range and * then by the mount idmap into the caller's range: * * on-disk LAYER_HOST+7 --layer--> ID_HOST+7 --mount--> ID_NS+7 */ TEST_F(idmapped_overlay, composition) { char lower[PATH_MAX], upper[PATH_MAX], work[PATH_MAX], path[PATH_MAX]; struct stat st; int ovl, fd_lower; if (!ovl_supported()) SKIP(return, "overlayfs not supported"); snprintf(lower, sizeof(lower), "%s/l", self->base); snprintf(upper, sizeof(upper), "%s/u", self->base); snprintf(work, sizeof(work), "%s/w", self->base); /* Put the lower layer's ids in the on-disk [LAYER_HOST, ..) range. */ ASSERT_EQ(chown(lower, LAYER_HOST, LAYER_HOST), 0); snprintf(path, sizeof(path), "%s/l/file", self->base); ASSERT_EQ(mknod(path, S_IFREG | 0644, 0), 0); ASSERT_EQ(chown(path, LAYER_HOST + 7, LAYER_HOST + 7), 0); /* Idmapped lower: on-disk LAYER_HOST <-> overlay-final ID_HOST. */ fd_lower = idmapped_layer_fd(lower, LAYER_HOST, ID_HOST, ID_RANGE); ASSERT_GE(fd_lower, 0); ovl = ovl_mount_lower_fd(upper, work, fd_lower); ASSERT_GE(ovl, 0); EXPECT_EQ(close(fd_lower), 0); /* Idmap the overlay mount: overlay-final ID_HOST <-> caller ID_NS. */ ASSERT_EQ(ovl_idmap(ovl), 0); ASSERT_EQ(fstatat(ovl, "file", &st, 0), 0); EXPECT_EQ(st.st_uid, ID_NS + 7); EXPECT_EQ(st.st_gid, ID_NS + 7); EXPECT_EQ(close(ovl), 0); } /* An idmapped overlay mount whose sb lives inside a user namespace. */ TEST_F(idmapped_overlay, userns) { int u1; pid_t pid; if (!ovl_supported()) SKIP(return, "overlayfs not supported"); /* u1 backs the overlay sb: identity-mapped, but not the init ns. */ u1 = get_userns_fd(0, 0, 65536); if (u1 < 0) SKIP(return, "user namespaces not available"); pid = fork(); ASSERT_GE(pid, 0); if (pid == 0) { int ret = userns_overlay_child(u1); _exit(ret ? EXIT_FAILURE : EXIT_SUCCESS); } EXPECT_EQ(wait_for_pid(pid), 0); EXPECT_EQ(close(u1), 0); } /* * An nfs_export overlay can be idmapped, and decodable file handles round-trip * through the idmapped mount with correctly mapped ownership. Overlay file * handles encode object identity, not ownership, so the mount idmap does not * affect them; it only maps the owner reported once a handle is reopened. */ TEST_F(idmapped_overlay, nfs_export_handles) { char path[PATH_MAX], mnt[128]; union { struct file_handle fh; char buf[sizeof(struct file_handle) + MAX_HANDLE_SZ]; } fhu; struct file_handle *fh = &fhu.fh; struct stat st; int ovl, mfd, fd, mount_id; if (!ovl_supported()) SKIP(return, "overlayfs not supported"); snprintf(path, sizeof(path), "%s/l/file", self->base); ASSERT_EQ(mknod(path, S_IFREG | 0644, 0), 0); ASSERT_EQ(chown(path, ID_HOST + 7, ID_HOST + 7), 0); /* nfs_export=on gives decodable overlay file handles. */ ovl = ovl_mount(self->base, true); if (ovl < 0) SKIP(return, "overlayfs nfs_export not supported"); ASSERT_EQ(ovl_idmap(ovl), 0); /* Attach the idmapped mount so handles can be resolved against it. */ snprintf(mnt, sizeof(mnt), "%s/mnt", self->base); ASSERT_EQ(mkdir(mnt, 0755), 0); ASSERT_EQ(sys_move_mount(ovl, "", AT_FDCWD, mnt, MOVE_MOUNT_F_EMPTY_PATH), 0); snprintf(path, sizeof(path), "%s/file", mnt); fh->handle_bytes = MAX_HANDLE_SZ; ASSERT_EQ(name_to_handle_at(AT_FDCWD, path, fh, &mount_id, 0), 0); mfd = open(mnt, O_RDONLY | O_DIRECTORY); ASSERT_GE(mfd, 0); fd = open_by_handle_at(mfd, fh, O_RDONLY); EXPECT_EQ(close(mfd), 0); ASSERT_GE(fd, 0); ASSERT_EQ(fstat(fd, &st), 0); EXPECT_EQ(st.st_uid, ID_NS + 7); EXPECT_EQ(st.st_gid, ID_NS + 7); EXPECT_EQ(close(fd), 0); EXPECT_EQ(close(ovl), 0); } TEST_HARNESS_MAIN