1*d943e68eSChristian Brauner // SPDX-License-Identifier: GPL-2.0
2*d943e68eSChristian Brauner #define _GNU_SOURCE
3*d943e68eSChristian Brauner
4*d943e68eSChristian Brauner #include <errno.h>
5*d943e68eSChristian Brauner #include <fcntl.h>
6*d943e68eSChristian Brauner #include <limits.h>
7*d943e68eSChristian Brauner #include <sched.h>
8*d943e68eSChristian Brauner #include <stdio.h>
9*d943e68eSChristian Brauner #include <unistd.h>
10*d943e68eSChristian Brauner #include <sys/fsuid.h>
11*d943e68eSChristian Brauner #include <sys/stat.h>
12*d943e68eSChristian Brauner #include <sys/syscall.h>
13*d943e68eSChristian Brauner
14*d943e68eSChristian Brauner #include <linux/mount.h>
15*d943e68eSChristian Brauner #include <linux/types.h>
16*d943e68eSChristian Brauner
17*d943e68eSChristian Brauner #include "kselftest_harness.h"
18*d943e68eSChristian Brauner #include "wrappers.h"
19*d943e68eSChristian Brauner #include "utils.h"
20*d943e68eSChristian Brauner
21*d943e68eSChristian Brauner /*
22*d943e68eSChristian Brauner * The test mount maps caller-visible ids [0, MAP_RANGE) onto the on-disk range
23*d943e68eSChristian Brauner * [MAP_HOST, MAP_HOST + MAP_RANGE). An id outside [0, MAP_RANGE) therefore has
24*d943e68eSChristian Brauner * no mapping in the mount and is not representable in the filesystem.
25*d943e68eSChristian Brauner */
26*d943e68eSChristian Brauner #define MAP_HOST 10000
27*d943e68eSChristian Brauner #define MAP_RANGE 10000
28*d943e68eSChristian Brauner #define UNMAPPED 50000
29*d943e68eSChristian Brauner
30*d943e68eSChristian Brauner #ifndef MOUNT_ATTR_IDMAP
31*d943e68eSChristian Brauner #define MOUNT_ATTR_IDMAP 0x00100000
32*d943e68eSChristian Brauner #endif
33*d943e68eSChristian Brauner
34*d943e68eSChristian Brauner #ifndef __NR_mount_setattr
35*d943e68eSChristian Brauner #define __NR_mount_setattr 442
36*d943e68eSChristian Brauner #endif
37*d943e68eSChristian Brauner
sys_mount_setattr(int dfd,const char * path,unsigned int flags,struct mount_attr * attr,size_t size)38*d943e68eSChristian Brauner static inline int sys_mount_setattr(int dfd, const char *path,
39*d943e68eSChristian Brauner unsigned int flags,
40*d943e68eSChristian Brauner struct mount_attr *attr, size_t size)
41*d943e68eSChristian Brauner {
42*d943e68eSChristian Brauner return syscall(__NR_mount_setattr, dfd, path, flags, attr, size);
43*d943e68eSChristian Brauner }
44*d943e68eSChristian Brauner
45*d943e68eSChristian Brauner /*
46*d943e68eSChristian Brauner * Clone @path into a detached mount idmapped so that caller-visible ids
47*d943e68eSChristian Brauner * [0, MAP_RANGE) map onto the on-disk ids [MAP_HOST, MAP_HOST + MAP_RANGE).
48*d943e68eSChristian Brauner * Returns the mount fd, or -1 if idmapped mounts are not available.
49*d943e68eSChristian Brauner */
idmapped_clone(const char * path)50*d943e68eSChristian Brauner static int idmapped_clone(const char *path)
51*d943e68eSChristian Brauner {
52*d943e68eSChristian Brauner struct mount_attr attr = {
53*d943e68eSChristian Brauner .attr_set = MOUNT_ATTR_IDMAP,
54*d943e68eSChristian Brauner };
55*d943e68eSChristian Brauner int fd_tree, userns_fd, ret;
56*d943e68eSChristian Brauner
57*d943e68eSChristian Brauner fd_tree = sys_open_tree(AT_FDCWD, path,
58*d943e68eSChristian Brauner OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
59*d943e68eSChristian Brauner if (fd_tree < 0)
60*d943e68eSChristian Brauner return -1;
61*d943e68eSChristian Brauner
62*d943e68eSChristian Brauner userns_fd = get_userns_fd(MAP_HOST, 0, MAP_RANGE);
63*d943e68eSChristian Brauner if (userns_fd < 0) {
64*d943e68eSChristian Brauner close(fd_tree);
65*d943e68eSChristian Brauner return -1;
66*d943e68eSChristian Brauner }
67*d943e68eSChristian Brauner
68*d943e68eSChristian Brauner attr.userns_fd = userns_fd;
69*d943e68eSChristian Brauner ret = sys_mount_setattr(fd_tree, "", AT_EMPTY_PATH, &attr, sizeof(attr));
70*d943e68eSChristian Brauner close(userns_fd);
71*d943e68eSChristian Brauner if (ret) {
72*d943e68eSChristian Brauner close(fd_tree);
73*d943e68eSChristian Brauner return -1;
74*d943e68eSChristian Brauner }
75*d943e68eSChristian Brauner
76*d943e68eSChristian Brauner return fd_tree;
77*d943e68eSChristian Brauner }
78*d943e68eSChristian Brauner
FIXTURE(idmapped_tmpfile)79*d943e68eSChristian Brauner FIXTURE(idmapped_tmpfile) {
80*d943e68eSChristian Brauner char dir[64]; /* non-idmapped path to the layer directory */
81*d943e68eSChristian Brauner };
82*d943e68eSChristian Brauner
FIXTURE_SETUP(idmapped_tmpfile)83*d943e68eSChristian Brauner FIXTURE_SETUP(idmapped_tmpfile)
84*d943e68eSChristian Brauner {
85*d943e68eSChristian Brauner /* Private mount namespace so test mounts need no cleanup. */
86*d943e68eSChristian Brauner ASSERT_EQ(unshare(CLONE_NEWNS), 0);
87*d943e68eSChristian Brauner ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0);
88*d943e68eSChristian Brauner ASSERT_EQ(sys_mount("tmpfs", "/tmp", "tmpfs", 0, NULL), 0);
89*d943e68eSChristian Brauner
90*d943e68eSChristian Brauner snprintf(self->dir, sizeof(self->dir), "/tmp/d");
91*d943e68eSChristian Brauner ASSERT_EQ(mkdir(self->dir, 0777), 0);
92*d943e68eSChristian Brauner /* World-writable so an unmapped caller still passes permission(). */
93*d943e68eSChristian Brauner ASSERT_EQ(chmod(self->dir, 0777), 0);
94*d943e68eSChristian Brauner }
95*d943e68eSChristian Brauner
FIXTURE_TEARDOWN(idmapped_tmpfile)96*d943e68eSChristian Brauner FIXTURE_TEARDOWN(idmapped_tmpfile)
97*d943e68eSChristian Brauner {
98*d943e68eSChristian Brauner }
99*d943e68eSChristian Brauner
100*d943e68eSChristian Brauner /*
101*d943e68eSChristian Brauner * A caller whose fsuid/fsgid have no mapping in the idmapped mount must not be
102*d943e68eSChristian Brauner * able to create an O_TMPFILE. Without the check in vfs_tmpfile() the inode
103*d943e68eSChristian Brauner * would be created owned by (uid_t)-1 and could then be linked into the
104*d943e68eSChristian Brauner * namespace.
105*d943e68eSChristian Brauner */
TEST_F(idmapped_tmpfile,unmapped_caller_is_refused)106*d943e68eSChristian Brauner TEST_F(idmapped_tmpfile, unmapped_caller_is_refused)
107*d943e68eSChristian Brauner {
108*d943e68eSChristian Brauner int mfd, fd;
109*d943e68eSChristian Brauner
110*d943e68eSChristian Brauner mfd = idmapped_clone(self->dir);
111*d943e68eSChristian Brauner if (mfd < 0)
112*d943e68eSChristian Brauner SKIP(return, "idmapped mounts not supported");
113*d943e68eSChristian Brauner
114*d943e68eSChristian Brauner /* Become a caller outside the mount's [0, MAP_RANGE) range. */
115*d943e68eSChristian Brauner setfsgid(UNMAPPED);
116*d943e68eSChristian Brauner setfsuid(UNMAPPED);
117*d943e68eSChristian Brauner ASSERT_EQ(setfsuid(-1), UNMAPPED);
118*d943e68eSChristian Brauner
119*d943e68eSChristian Brauner fd = openat(mfd, ".", O_TMPFILE | O_WRONLY, 0644);
120*d943e68eSChristian Brauner ASSERT_LT(fd, 0);
121*d943e68eSChristian Brauner EXPECT_EQ(errno, EOVERFLOW);
122*d943e68eSChristian Brauner if (fd >= 0)
123*d943e68eSChristian Brauner close(fd);
124*d943e68eSChristian Brauner
125*d943e68eSChristian Brauner EXPECT_EQ(close(mfd), 0);
126*d943e68eSChristian Brauner }
127*d943e68eSChristian Brauner
128*d943e68eSChristian Brauner /*
129*d943e68eSChristian Brauner * A mapped caller can create an O_TMPFILE and link it into the namespace; the
130*d943e68eSChristian Brauner * ownership round-trips through the mount idmap. This is what makes refusing
131*d943e68eSChristian Brauner * the unmapped case above necessary in the first place.
132*d943e68eSChristian Brauner */
TEST_F(idmapped_tmpfile,mapped_caller_creates_and_links)133*d943e68eSChristian Brauner TEST_F(idmapped_tmpfile, mapped_caller_creates_and_links)
134*d943e68eSChristian Brauner {
135*d943e68eSChristian Brauner char path[PATH_MAX];
136*d943e68eSChristian Brauner struct stat st;
137*d943e68eSChristian Brauner int mfd, fd;
138*d943e68eSChristian Brauner
139*d943e68eSChristian Brauner mfd = idmapped_clone(self->dir);
140*d943e68eSChristian Brauner if (mfd < 0)
141*d943e68eSChristian Brauner SKIP(return, "idmapped mounts not supported");
142*d943e68eSChristian Brauner
143*d943e68eSChristian Brauner /* Caller is uid/gid 0, which maps to MAP_HOST through the mount. */
144*d943e68eSChristian Brauner fd = openat(mfd, ".", O_TMPFILE | O_RDWR, 0600);
145*d943e68eSChristian Brauner ASSERT_GE(fd, 0);
146*d943e68eSChristian Brauner
147*d943e68eSChristian Brauner ASSERT_EQ(fstat(fd, &st), 0);
148*d943e68eSChristian Brauner EXPECT_EQ(st.st_uid, 0);
149*d943e68eSChristian Brauner EXPECT_EQ(st.st_gid, 0);
150*d943e68eSChristian Brauner
151*d943e68eSChristian Brauner /* The tmpfile is linkable: splice it into the directory. */
152*d943e68eSChristian Brauner ASSERT_EQ(linkat(fd, "", mfd, "linked", AT_EMPTY_PATH), 0);
153*d943e68eSChristian Brauner EXPECT_EQ(close(fd), 0);
154*d943e68eSChristian Brauner
155*d943e68eSChristian Brauner ASSERT_EQ(fstatat(mfd, "linked", &st, 0), 0);
156*d943e68eSChristian Brauner EXPECT_EQ(st.st_uid, 0);
157*d943e68eSChristian Brauner EXPECT_EQ(st.st_gid, 0);
158*d943e68eSChristian Brauner
159*d943e68eSChristian Brauner /* On the underlying, non-idmapped tmpfs it is stored as MAP_HOST. */
160*d943e68eSChristian Brauner snprintf(path, sizeof(path), "%s/linked", self->dir);
161*d943e68eSChristian Brauner ASSERT_EQ(stat(path, &st), 0);
162*d943e68eSChristian Brauner EXPECT_EQ(st.st_uid, MAP_HOST);
163*d943e68eSChristian Brauner EXPECT_EQ(st.st_gid, MAP_HOST);
164*d943e68eSChristian Brauner
165*d943e68eSChristian Brauner EXPECT_EQ(close(mfd), 0);
166*d943e68eSChristian Brauner }
167*d943e68eSChristian Brauner
168*d943e68eSChristian Brauner TEST_HARNESS_MAIN
169