Test the *_r system calls (malformed ids, switching to a UUID user and back through the saved uid, GUID groups, sorted supplementary groups, chown_r() and statx() on tmpfs, creating files as a UUID fsuid, -EOVERFLOW on sysfs) and the extended interfaces: SO_PEERCRED_R, SO_PEERGROUPS_R, SCM_CREDENTIALS_R, ssi_uid_r, IPC_STAT_R/IPC_SET_R, version 3 POSIX ACL xattrs, the Uid_r: line of /proc//status, and that the root of a user namespace has no privilege over UUID owners. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Jan Koester --- tools/testing/selftests/Makefile | 1 + tools/testing/selftests/uid_r/.gitignore | 1 + tools/testing/selftests/uid_r/Makefile | 6 + tools/testing/selftests/uid_r/config | 4 + tools/testing/selftests/uid_r/uid_r_test.c | 535 +++++++++++++++++++++ 5 files changed, 547 insertions(+) create mode 100644 tools/testing/selftests/uid_r/.gitignore create mode 100644 tools/testing/selftests/uid_r/Makefile create mode 100644 tools/testing/selftests/uid_r/config create mode 100644 tools/testing/selftests/uid_r/uid_r_test.c diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile index bc4fdb6f53..393eb18119 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -140,6 +140,7 @@ TARGETS += tpm2 TARGETS += tty TARGETS += ublk TARGETS += uevent +TARGETS += uid_r TARGETS += user_events TARGETS += vDSO TARGETS += mm diff --git a/tools/testing/selftests/uid_r/.gitignore b/tools/testing/selftests/uid_r/.gitignore new file mode 100644 index 0000000000..5855e85e33 --- /dev/null +++ b/tools/testing/selftests/uid_r/.gitignore @@ -0,0 +1 @@ +uid_r_test diff --git a/tools/testing/selftests/uid_r/Makefile b/tools/testing/selftests/uid_r/Makefile new file mode 100644 index 0000000000..d5dc3e6aa5 --- /dev/null +++ b/tools/testing/selftests/uid_r/Makefile @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0 +CFLAGS += -g -Wall $(KHDR_INCLUDES) $(TOOLS_INCLUDES) + +TEST_GEN_PROGS := uid_r_test + +include ../lib.mk diff --git a/tools/testing/selftests/uid_r/config b/tools/testing/selftests/uid_r/config new file mode 100644 index 0000000000..424ac0a2ca --- /dev/null +++ b/tools/testing/selftests/uid_r/config @@ -0,0 +1,4 @@ +CONFIG_TMPFS=y +CONFIG_TMPFS_POSIX_ACL=y +CONFIG_SYSVIPC=y +CONFIG_SIGNALFD=y diff --git a/tools/testing/selftests/uid_r/uid_r_test.c b/tools/testing/selftests/uid_r/uid_r_test.c new file mode 100644 index 0000000000..88397bf3da --- /dev/null +++ b/tools/testing/selftests/uid_r/uid_r_test.c @@ -0,0 +1,535 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Tests of UUID and GUID user and group ids: the *_r system calls and the + * interfaces extended for them. Needs root in the initial user namespace; + * every test runs in a child process, file tests on a private tmpfs. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../kselftest_harness.h" + +#ifndef __NR_getuid_r +#define __NR_getuid_r 473 +#define __NR_geteuid_r 474 +#define __NR_getgid_r 475 +#define __NR_getegid_r 476 +#define __NR_setuid_r 477 +#define __NR_setgid_r 478 +#define __NR_setreuid_r 479 +#define __NR_setregid_r 480 +#define __NR_setresuid_r 481 +#define __NR_getresuid_r 482 +#define __NR_setresgid_r 483 +#define __NR_getresgid_r 484 +#define __NR_setfsuid_r 485 +#define __NR_setfsgid_r 486 +#define __NR_getgroups_r 487 +#define __NR_setgroups_r 488 +#define __NR_chown_r 489 +#define __NR_lchown_r 490 +#define __NR_fchown_r 491 +#define __NR_fchownat_r 492 +#endif + +#ifndef STATX_UID_R +#define STATX_UID_R 0x00040000U +#define STATX_GID_R 0x00080000U +#endif +#ifndef SO_PEERCRED_R +#define SO_PEERCRED_R 86 +#define SO_PEERGROUPS_R 87 +#define SO_PASSCRED_R 88 +#endif +#ifndef SCM_CREDENTIALS_R +#define SCM_CREDENTIALS_R 0x05 +#endif +#ifndef IPC_STAT_R +#define IPC_SET_R 21 +#define IPC_STAT_R 22 +#endif + +typedef __kernel_uid_r_t id_r; + +struct ucred_r { + __u32 pid; + id_r uid, gid; +}; + +struct ipc_perm_r_t { + int key; + __u32 mode; + id_r uid, gid, cuid, cgid; +}; + +/* + * Reserved bytes must be zero, and an initializer of one union member + * leaves the others undefined: always clear the whole id first. + */ +static id_r legacy(unsigned int v) +{ + id_r id; + + memset(&id, 0, sizeof(id)); + id.type = UID_R_LEGACY; + id.legacy = v; + return id; +} + +/* a version 4 UUID, made distinct by @tag */ +static id_r uuid(unsigned char tag) +{ + static const unsigned char b[16] = { + 0x6f, 0x1c, 0x2a, 0x3b, 0x4d, 0x5e, 0x4f, 0x60, + 0x8a, 0x7b, 0x0c, 0x1d, 0x2e, 0x3f, 0x4a, 0x00 }; + id_r id; + + memset(&id, 0, sizeof(id)); + id.type = UID_R_UUID; + memcpy(id.uuid, b, sizeof(b)); + id.uuid[15] = tag; + return id; +} + +static id_r guid(unsigned char tag) +{ + id_r id; + + memset(&id, 0, sizeof(id)); + id.type = UID_R_GUID; + for (int i = 0; i < 16; i++) + id.guid[i] = 0x10 + i; + id.guid[15] = tag; + return id; +} + +static int ideq(const id_r *a, const id_r *b) +{ + return !memcmp(a, b, sizeof(*a)); +} + +static long sys(long nr, long a, long b, long c, long d, long e) +{ + long ret = syscall(nr, a, b, c, d, e); + + return ret < 0 ? -errno : ret; +} + +/* a private tmpfs at a fresh directory, for file tests */ +static void private_tmpfs(struct __test_metadata *_metadata, char *dir) +{ + strcpy(dir, "/tmp/uid_r.XXXXXX"); + ASSERT_EQ(0, unshare(CLONE_NEWNS)); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)); + ASSERT_NE(NULL, mkdtemp(dir)); + ASSERT_EQ(0, mount("tmpfs", dir, "tmpfs", 0, "mode=1777")); +} + +static int statx_owner(const char *path, id_r *uid, id_r *gid) +{ + struct statx stx; + + memset(&stx, 0, sizeof(stx)); + if (statx(AT_FDCWD, path, 0, STATX_UID_R | STATX_GID_R, &stx)) + return -errno; + if ((stx.stx_mask & (STATX_UID_R | STATX_GID_R)) != + (STATX_UID_R | STATX_GID_R)) + return -ENODATA; + /* stx_uid_r and stx_gid_r at 0xc0 */ + memcpy(uid, (char *)&stx + 0xc0, sizeof(*uid)); + memcpy(gid, (char *)&stx + 0xc0 + sizeof(*uid), sizeof(*gid)); + return 0; +} + +FIXTURE(uid_r) { + char dir[64]; +}; + +FIXTURE_SETUP(uid_r) +{ + id_r id; + + if (geteuid() != 0) + SKIP(return, "needs root"); + if (sys(__NR_getuid_r, (long)&id, 0, 0, 0, 0) == -ENOSYS) + SKIP(return, "no *_r system calls"); + self->dir[0] = 0; +} + +FIXTURE_TEARDOWN(uid_r) +{ +} + +TEST_F(uid_r, malformed_ids) +{ + id_r id; + + id = uuid(1); + id.uuid[6] = 0x14; /* version 1 */ + EXPECT_EQ(-EINVAL, sys(__NR_setuid_r, (long)&id, 0, 0, 0, 0)); + id = uuid(1); + id.type = 9; + EXPECT_EQ(-EINVAL, sys(__NR_setuid_r, (long)&id, 0, 0, 0, 0)); + id = legacy(5); + id.uuid[10] = 1; /* reserved byte */ + EXPECT_EQ(-EINVAL, sys(__NR_setuid_r, (long)&id, 0, 0, 0, 0)); + memset(&id, 0, sizeof(id)); + id.type = UID_R_GUID; /* nil GUID */ + EXPECT_EQ(-EINVAL, sys(__NR_setuid_r, (long)&id, 0, 0, 0, 0)); + EXPECT_EQ(-EINVAL, sys(__NR_setuid_r, 0, 0, 0, 0, 0)); + EXPECT_EQ(-EFAULT, sys(__NR_getuid_r, 1, 0, 0, 0, 0)); +} + +TEST_F(uid_r, setresuid_r) +{ + id_r u = uuid(1), s0 = legacy(0), r, e, s; + + ASSERT_EQ(0, sys(__NR_setresuid_r, (long)&u, (long)&u, (long)&s0, 0, 0)); + EXPECT_EQ(65534, getuid()); + EXPECT_EQ(65534, geteuid()); + ASSERT_EQ(0, sys(__NR_getresuid_r, (long)&r, (long)&e, (long)&s, 0, 0)); + EXPECT_TRUE(ideq(&r, &u)); + EXPECT_TRUE(ideq(&e, &u)); + EXPECT_TRUE(ideq(&s, &s0)); + + /* without euid 0 no capabilities: no other UUID */ + r = uuid(2); + EXPECT_EQ(-EPERM, sys(__NR_setuid_r, (long)&r, 0, 0, 0, 0)); + + /* back through the saved uid */ + ASSERT_EQ(0, sys(__NR_setresuid_r, 0, (long)&s0, 0, 0, 0)); + EXPECT_EQ(0, geteuid()); +} + +TEST_F(uid_r, guid_group) +{ + id_r g = guid(7), e; + + ASSERT_EQ(0, sys(__NR_setresgid_r, 0, (long)&g, 0, 0, 0)); + EXPECT_EQ(65534, getegid()); + ASSERT_EQ(0, sys(__NR_getegid_r, (long)&e, 0, 0, 0, 0)); + EXPECT_TRUE(ideq(&e, &g)); +} + +TEST_F(uid_r, groups_r) +{ + id_r in[3] = { guid(9), legacy(10), uuid(4) }, out[4]; + gid_t classic[4]; + + ASSERT_EQ(0, sys(__NR_setgroups_r, 3, (long)in, 0, 0, 0)); + ASSERT_EQ(3, sys(__NR_getgroups_r, 4, (long)out, 0, 0, 0)); + /* sorted: legacy, UUID, GUID */ + EXPECT_TRUE(ideq(&out[0], &in[1])); + EXPECT_TRUE(ideq(&out[1], &in[2])); + EXPECT_TRUE(ideq(&out[2], &in[0])); + ASSERT_EQ(3, getgroups(4, classic)); + EXPECT_EQ(10, classic[0]); + EXPECT_EQ(65534, classic[1]); + memset(&in[0], 0, sizeof(in[0])); + EXPECT_EQ(-EINVAL, sys(__NR_setgroups_r, 1, (long)in, 0, 0, 0)); +} + +TEST_F(uid_r, chown_r_statx) +{ + id_r u = uuid(5), g = guid(6), r, rg; + char path[96]; + struct stat st; + int fd; + + private_tmpfs(_metadata, self->dir); + snprintf(path, sizeof(path), "%s/f", self->dir); + fd = open(path, O_CREAT | O_WRONLY, 0644); + ASSERT_LE(0, fd); + ASSERT_EQ(0, sys(__NR_fchown_r, fd, (long)&u, (long)&g, 0, 0)); + close(fd); + ASSERT_EQ(0, stat(path, &st)); + EXPECT_EQ(65534, st.st_uid); + ASSERT_EQ(0, statx_owner(path, &r, &rg)); + EXPECT_TRUE(ideq(&r, &u)); + EXPECT_TRUE(ideq(&rg, &g)); + + /* NULL leaves the owner, UID_R_NONE too */ + r = legacy(42); + memset(&rg, 0, sizeof(rg)); + ASSERT_EQ(0, sys(__NR_chown_r, (long)path, 0, (long)&r, 0, 0)); + ASSERT_EQ(0, sys(__NR_fchownat_r, AT_FDCWD, (long)path, (long)&rg, 0, 0)); + ASSERT_EQ(0, statx_owner(path, &r, &rg)); + EXPECT_TRUE(ideq(&r, &u)); + EXPECT_EQ(UID_R_LEGACY, rg.type); + EXPECT_EQ(42, rg.legacy); +} + +TEST_F(uid_r, create_as_uuid_fsuid) +{ + id_r u = uuid(3), s0 = legacy(0), old, r, rg; + char path[96]; + int fd; + + private_tmpfs(_metadata, self->dir); + snprintf(path, sizeof(path), "%s/byuuid", self->dir); + ASSERT_EQ(0, sys(__NR_setfsuid_r, (long)&u, (long)&old, 0, 0, 0)); + EXPECT_TRUE(ideq(&old, &s0)); + fd = open(path, O_CREAT | O_WRONLY, 0644); + ASSERT_EQ(0, sys(__NR_setfsuid_r, (long)&s0, 0, 0, 0, 0)); + ASSERT_LE(0, fd); + close(fd); + ASSERT_EQ(0, statx_owner(path, &r, &rg)); + EXPECT_TRUE(ideq(&r, &u)); +} + +TEST_F(uid_r, classic_only_fs) +{ + id_r u = uuid(5); + + /* sysfs (kernfs) can't store UUID owners */ + EXPECT_EQ(-EOVERFLOW, sys(__NR_chown_r, (long)"/sys/kernel", + (long)&u, 0, 0, 0)); +} + +TEST_F(uid_r, peercred_r) +{ + id_r u = uuid(8), s0 = legacy(0), g = guid(10), groups[2]; + struct ucred_r pc; + struct ucred lc; + socklen_t len; + int sv[2]; + + groups[0] = g; + groups[1] = legacy(7); + ASSERT_EQ(0, sys(__NR_setgroups_r, 2, (long)groups, 0, 0, 0)); + ASSERT_EQ(0, sys(__NR_setresuid_r, 0, (long)&u, (long)&s0, 0, 0)); + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, sv)); + ASSERT_EQ(0, sys(__NR_setresuid_r, 0, (long)&s0, 0, 0, 0)); + + len = sizeof(pc); + ASSERT_EQ(0, getsockopt(sv[1], SOL_SOCKET, SO_PEERCRED_R, &pc, &len)); + EXPECT_TRUE(ideq(&pc.uid, &u)); + len = sizeof(lc); + ASSERT_EQ(0, getsockopt(sv[1], SOL_SOCKET, SO_PEERCRED, &lc, &len)); + EXPECT_EQ(65534, lc.uid); + len = sizeof(groups); + ASSERT_EQ(0, getsockopt(sv[1], SOL_SOCKET, SO_PEERGROUPS_R, groups, &len)); + EXPECT_EQ(2 * sizeof(id_r), len); + EXPECT_TRUE(ideq(&groups[1], &g)); +} + +TEST_F(uid_r, scm_credentials_r) +{ + id_r u = uuid(9), s0 = legacy(0); + char data = 'x', cbuf[256]; + struct iovec iov = { &data, 1 }; + struct msghdr mh = { 0 }; + struct cmsghdr *c; + int sv[2], one = 1, got_r = 0, got_l = 0; + + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, sv)); + ASSERT_EQ(0, setsockopt(sv[1], SOL_SOCKET, SO_PASSCRED, &one, sizeof(one))); + ASSERT_EQ(0, setsockopt(sv[1], SOL_SOCKET, SO_PASSCRED_R, &one, sizeof(one))); + /* SCM credentials carry the real ids */ + ASSERT_EQ(0, sys(__NR_setresuid_r, (long)&u, 0, (long)&s0, 0, 0)); + ASSERT_EQ(1, write(sv[0], &data, 1)); + ASSERT_EQ(0, sys(__NR_setresuid_r, (long)&s0, 0, 0, 0, 0)); + + mh.msg_iov = &iov; + mh.msg_iovlen = 1; + mh.msg_control = cbuf; + mh.msg_controllen = sizeof(cbuf); + ASSERT_EQ(1, recvmsg(sv[1], &mh, 0)); + EXPECT_FALSE(mh.msg_flags & MSG_CTRUNC); + for (c = CMSG_FIRSTHDR(&mh); c; c = CMSG_NXTHDR(&mh, c)) { + if (c->cmsg_level != SOL_SOCKET) + continue; + if (c->cmsg_type == SCM_CREDENTIALS_R) { + struct ucred_r pc; + + memcpy(&pc, CMSG_DATA(c), sizeof(pc)); + got_r = ideq(&pc.uid, &u); + } else if (c->cmsg_type == SCM_CREDENTIALS) { + struct ucred lc; + + memcpy(&lc, CMSG_DATA(c), sizeof(lc)); + got_l = lc.uid == 65534; + } + } + EXPECT_TRUE(got_r); + EXPECT_TRUE(got_l); +} + +TEST_F(uid_r, signalfd_uid_r) +{ + id_r u = uuid(11), s0 = legacy(0), got; + struct signalfd_siginfo ssi; + sigset_t mask; + int sfd; + + sigemptyset(&mask); + sigaddset(&mask, SIGUSR1); + ASSERT_EQ(0, sigprocmask(SIG_BLOCK, &mask, NULL)); + sfd = signalfd(-1, &mask, 0); + ASSERT_LE(0, sfd); + ASSERT_EQ(0, sys(__NR_setresuid_r, (long)&u, 0, 0, 0, 0)); + ASSERT_EQ(0, kill(getpid(), SIGUSR1)); + ASSERT_EQ(0, sys(__NR_setresuid_r, (long)&s0, 0, 0, 0, 0)); + ASSERT_EQ(sizeof(ssi), read(sfd, &ssi, sizeof(ssi))); + EXPECT_EQ(65534, ssi.ssi_uid); + /* ssi_uid_r follows ssi_arch */ + memcpy(&got, (char *)&ssi + 100, sizeof(got)); + EXPECT_TRUE(ideq(&got, &u)); +} + +TEST_F(uid_r, ipc_stat_r) +{ + id_r u = uuid(12), g = guid(13); + struct ipc_perm_r_t pr; + struct shmid_ds ds; + int id; + + memset(&pr, 0, sizeof(pr)); + pr.mode = 0640; + pr.uid = u; + pr.gid = g; + + id = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600); + ASSERT_LE(0, id); + ASSERT_EQ(0, shmctl(id, IPC_SET_R, (void *)&pr)); + memset(&pr, 0, sizeof(pr)); + ASSERT_EQ(0, shmctl(id, IPC_STAT_R, (void *)&pr)); + EXPECT_TRUE(ideq(&pr.uid, &u)); + EXPECT_TRUE(ideq(&pr.gid, &g)); + EXPECT_EQ(0640, pr.mode & 0777); + ASSERT_EQ(0, shmctl(id, IPC_STAT, &ds)); + EXPECT_EQ(65534, ds.shm_perm.uid); + shmctl(id, IPC_RMID, NULL); +} + +/* user::rw- user::r-- group::--- mask::r-- other::--- in version 3 */ +static size_t acl_v3(unsigned char *buf, const id_r *u) +{ + static const unsigned short ent[5][2] = { + { 0x01, 6 }, { 0x02, 4 }, { 0x04, 0 }, { 0x10, 4 }, { 0x20, 0 } }; + unsigned char *e = buf + 4; + unsigned int ver = 3; + + memcpy(buf, &ver, 4); + for (int i = 0; i < 5; i++, e += 24) { + unsigned int type = 0; + + memset(e, 0, 24); + memcpy(e, &ent[i][0], 2); + memcpy(e + 2, &ent[i][1], 2); + if (ent[i][0] == 0x02) { + type = u->type; + memcpy(e + 8, u->uuid, 16); + } + memcpy(e + 4, &type, 4); + } + return 4 + 5 * 24; +} + +TEST_F(uid_r, posix_acl_v3) +{ + unsigned char buf[256], got[256]; + id_r u = uuid(20), s0 = legacy(0); + char path[96]; + size_t len; + int fd; + + private_tmpfs(_metadata, self->dir); + snprintf(path, sizeof(path), "%s/acl", self->dir); + fd = open(path, O_CREAT | O_WRONLY, 0600); + ASSERT_LE(0, fd); + close(fd); + len = acl_v3(buf, &u); + if (setxattr(path, "system.posix_acl_access", buf, len, 0) && + errno == EOPNOTSUPP) + SKIP(return, "tmpfs without POSIX ACLs"); + ASSERT_EQ(len, getxattr(path, "system.posix_acl_access", got, sizeof(got))); + EXPECT_EQ(3, got[0]); + EXPECT_EQ(0, memcmp(got + 4 + 24 + 8, u.uuid, 16)); + + /* the ACL lets the UUID user read the 0600 root file */ + ASSERT_EQ(0, sys(__NR_setfsuid_r, (long)&u, 0, 0, 0, 0)); + fd = open(path, O_RDONLY); + ASSERT_EQ(0, sys(__NR_setfsuid_r, (long)&s0, 0, 0, 0, 0)); + EXPECT_LE(0, fd); +} + +TEST_F(uid_r, proc_status) +{ + id_r u = uuid(6), s0 = legacy(0); + char buf[4096], line[160]; + int fd, n; + + ASSERT_EQ(0, sys(__NR_setresuid_r, 0, (long)&u, (long)&s0, 0, 0)); + fd = open("/proc/self/status", O_RDONLY); + n = fd < 0 ? -1 : read(fd, buf, sizeof(buf) - 1); + ASSERT_EQ(0, sys(__NR_setresuid_r, 0, (long)&s0, 0, 0, 0)); + ASSERT_LT(0, n); + buf[n] = 0; + close(fd); + EXPECT_NE(NULL, strstr(buf, "Uid:\t0\t65534\t0\t65534\n")); + snprintf(line, sizeof(line), + "Uid_r:\t0\t6f1c2a3b-4d5e-4f60-8a7b-0c1d2e3f4a06\t0\t" + "6f1c2a3b-4d5e-4f60-8a7b-0c1d2e3f4a06\n"); + EXPECT_NE(NULL, strstr(buf, line)); +} + +TEST_F(uid_r, user_namespace) +{ + id_r u = uuid(21), r; + char path[96]; + int fd, status; + pid_t pid; + + private_tmpfs(_metadata, self->dir); + snprintf(path, sizeof(path), "%s/nsfile", self->dir); + fd = open(path, O_CREAT | O_WRONLY, 0644); + ASSERT_LE(0, fd); + close(fd); + ASSERT_EQ(0, sys(__NR_chown_r, (long)path, (long)&u, 0, 0, 0)); + + pid = fork(); + ASSERT_LE(0, pid); + if (pid == 0) { + int bad = 0; + + if (unshare(CLONE_NEWUSER)) + _exit(100); + /* from inside, only the own id can be mapped */ + fd = open("/proc/self/uid_map", O_WRONLY); + if (fd < 0 || write(fd, "0 0 1", 5) != 5) + bad |= 1; + close(fd); + if (sys(__NR_setuid_r, (long)&u, 0, 0, 0, 0) != -EINVAL) + bad |= 2; + /* CAP_FOWNER in the namespace gives nothing over a UUID */ + if (chmod(path, 0600) == 0 || errno != EPERM) + bad |= 4; + (void)r; + _exit(bad); + } + ASSERT_EQ(pid, waitpid(pid, &status, 0)); + if (WEXITSTATUS(status) == 100) + SKIP(return, "no user namespaces"); + EXPECT_EQ(0, WEXITSTATUS(status)); +} + +TEST_HARNESS_MAIN -- 2.56.0