xref: /linux/tools/testing/selftests/filesystems/idmapped_tmpfile.c (revision 71dfdfb0209b43dfd6f494f84f5548e4cfd18cb5)
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