| // SPDX-License-Identifier: GPL-2.0 | |
| #define _GNU_SOURCE | |
| #include <errno.h> | |
| #include <fcntl.h> | |
| #include <limits.h> | |
| #include <sched.h> | |
| #include <stdio.h> | |
| #include <unistd.h> | |
| #include <sys/fsuid.h> | |
| #include <sys/stat.h> | |
| #include <sys/syscall.h> | |
| #include <linux/mount.h> | |
| #include <linux/types.h> | |
| #include "kselftest_harness.h" | |
| #include "wrappers.h" | |
| #include "utils.h" | |
| /* | |
| * The test mount maps caller-visible ids [0, MAP_RANGE) onto the on-disk range | |
| * [MAP_HOST, MAP_HOST + MAP_RANGE). An id outside [0, MAP_RANGE) therefore has | |
| * no mapping in the mount and is not representable in the filesystem. | |
| */ | |
| #define MAP_HOST 10000 | |
| #define MAP_RANGE 10000 | |
| #define UNMAPPED 50000 | |
| #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); | |
| } | |
| /* | |
| * Clone @path into a detached mount idmapped so that caller-visible ids | |
| * [0, MAP_RANGE) map onto the on-disk ids [MAP_HOST, MAP_HOST + MAP_RANGE). | |
| * Returns the mount fd, or -1 if idmapped mounts are not available. | |
| */ | |
| static int idmapped_clone(const char *path) | |
| { | |
| struct mount_attr attr = { | |
| .attr_set = MOUNT_ATTR_IDMAP, | |
| }; | |
| int fd_tree, userns_fd, ret; | |
| 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(MAP_HOST, 0, MAP_RANGE); | |
| if (userns_fd < 0) { | |
| close(fd_tree); | |
| return -1; | |
| } | |
| attr.userns_fd = userns_fd; | |
| ret = sys_mount_setattr(fd_tree, "", AT_EMPTY_PATH, &attr, sizeof(attr)); | |
| close(userns_fd); | |
| if (ret) { | |
| close(fd_tree); | |
| return -1; | |
| } | |
| return fd_tree; | |
| } | |
| FIXTURE(idmapped_tmpfile) { | |
| char dir[64]; /* non-idmapped path to the layer directory */ | |
| }; | |
| FIXTURE_SETUP(idmapped_tmpfile) | |
| { | |
| /* 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); | |
| ASSERT_EQ(sys_mount("tmpfs", "/tmp", "tmpfs", 0, NULL), 0); | |
| snprintf(self->dir, sizeof(self->dir), "/tmp/d"); | |
| ASSERT_EQ(mkdir(self->dir, 0777), 0); | |
| /* World-writable so an unmapped caller still passes permission(). */ | |
| ASSERT_EQ(chmod(self->dir, 0777), 0); | |
| } | |
| FIXTURE_TEARDOWN(idmapped_tmpfile) | |
| { | |
| } | |
| /* | |
| * A caller whose fsuid/fsgid have no mapping in the idmapped mount must not be | |
| * able to create an O_TMPFILE. Without the check in vfs_tmpfile() the inode | |
| * would be created owned by (uid_t)-1 and could then be linked into the | |
| * namespace. | |
| */ | |
| TEST_F(idmapped_tmpfile, unmapped_caller_is_refused) | |
| { | |
| int mfd, fd; | |
| mfd = idmapped_clone(self->dir); | |
| if (mfd < 0) | |
| SKIP(return, "idmapped mounts not supported"); | |
| /* Become a caller outside the mount's [0, MAP_RANGE) range. */ | |
| setfsgid(UNMAPPED); | |
| setfsuid(UNMAPPED); | |
| ASSERT_EQ(setfsuid(-1), UNMAPPED); | |
| fd = openat(mfd, ".", O_TMPFILE | O_WRONLY, 0644); | |
| ASSERT_LT(fd, 0); | |
| EXPECT_EQ(errno, EOVERFLOW); | |
| if (fd >= 0) | |
| close(fd); | |
| EXPECT_EQ(close(mfd), 0); | |
| } | |
| /* | |
| * A mapped caller can create an O_TMPFILE and link it into the namespace; the | |
| * ownership round-trips through the mount idmap. This is what makes refusing | |
| * the unmapped case above necessary in the first place. | |
| */ | |
| TEST_F(idmapped_tmpfile, mapped_caller_creates_and_links) | |
| { | |
| char path[PATH_MAX]; | |
| struct stat st; | |
| int mfd, fd; | |
| mfd = idmapped_clone(self->dir); | |
| if (mfd < 0) | |
| SKIP(return, "idmapped mounts not supported"); | |
| /* Caller is uid/gid 0, which maps to MAP_HOST through the mount. */ | |
| fd = openat(mfd, ".", O_TMPFILE | O_RDWR, 0600); | |
| ASSERT_GE(fd, 0); | |
| ASSERT_EQ(fstat(fd, &st), 0); | |
| EXPECT_EQ(st.st_uid, 0); | |
| EXPECT_EQ(st.st_gid, 0); | |
| /* The tmpfile is linkable: splice it into the directory. */ | |
| ASSERT_EQ(linkat(fd, "", mfd, "linked", AT_EMPTY_PATH), 0); | |
| EXPECT_EQ(close(fd), 0); | |
| ASSERT_EQ(fstatat(mfd, "linked", &st, 0), 0); | |
| EXPECT_EQ(st.st_uid, 0); | |
| EXPECT_EQ(st.st_gid, 0); | |
| /* On the underlying, non-idmapped tmpfs it is stored as MAP_HOST. */ | |
| snprintf(path, sizeof(path), "%s/linked", self->dir); | |
| ASSERT_EQ(stat(path, &st), 0); | |
| EXPECT_EQ(st.st_uid, MAP_HOST); | |
| EXPECT_EQ(st.st_gid, MAP_HOST); | |
| EXPECT_EQ(close(mfd), 0); | |
| } | |
| TEST_HARNESS_MAIN |