Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: GPL-2.0-only
dnotify_test
devpts_pts
fclog
file_stressor
anon_inode_test
kernfs_test
idmapped_tmpfile
@@ -0,0 +1,11 @@
# SPDX-License-Identifier: GPL-2.0
CFLAGS += $(KHDR_INCLUDES)
TEST_GEN_PROGS := devpts_pts file_stressor anon_inode_test kernfs_test fclog
TEST_GEN_PROGS += idmapped_tmpfile
TEST_GEN_PROGS_EXTENDED := dnotify_test
include ../lib.mk
$(OUTPUT)/idmapped_tmpfile: LDLIBS += -lcap
$(OUTPUT)/idmapped_tmpfile: utils.c
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#define __SANE_USERSPACE_TYPES__
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include "kselftest_harness.h"
#include "wrappers.h"
TEST(anon_inode_no_chown)
{
int fd_context;
fd_context = sys_fsopen("tmpfs", 0);
ASSERT_GE(fd_context, 0);
ASSERT_LT(fchown(fd_context, 1234, 5678), 0);
ASSERT_EQ(errno, EOPNOTSUPP);
EXPECT_EQ(close(fd_context), 0);
}
TEST(anon_inode_no_chmod)
{
int fd_context;
fd_context = sys_fsopen("tmpfs", 0);
ASSERT_GE(fd_context, 0);
ASSERT_LT(fchmod(fd_context, 0777), 0);
ASSERT_EQ(errno, EOPNOTSUPP);
EXPECT_EQ(close(fd_context), 0);
}
TEST(anon_inode_no_exec)
{
int fd_context;
fd_context = sys_fsopen("tmpfs", 0);
ASSERT_GE(fd_context, 0);
char *const empty_argv[] = {NULL};
char *const empty_envp[] = {NULL};
ASSERT_LT(execveat(fd_context, "", empty_argv, empty_envp, AT_EMPTY_PATH), 0);
ASSERT_EQ(errno, EACCES);
EXPECT_EQ(close(fd_context), 0);
}
TEST(anon_inode_no_open)
{
int fd_context;
fd_context = sys_fsopen("tmpfs", 0);
ASSERT_GE(fd_context, 0);
ASSERT_GE(dup2(fd_context, 500), 0);
ASSERT_EQ(close(fd_context), 0);
fd_context = 500;
ASSERT_LT(open("/proc/self/fd/500", 0), 0);
ASSERT_EQ(errno, ENXIO);
EXPECT_EQ(close(fd_context), 0);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0-only
binderfs_test
@@ -0,0 +1,6 @@
# SPDX-License-Identifier: GPL-2.0
CFLAGS += $(KHDR_INCLUDES) -pthread
TEST_GEN_PROGS := binderfs_test
include ../../lib.mk
@@ -0,0 +1,541 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <sched.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/fsuid.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/sysinfo.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/android/binder.h>
#include <linux/android/binderfs.h>
#include "kselftest_harness.h"
#define DEFAULT_THREADS 4
#define PTR_TO_INT(p) ((int)((intptr_t)(p)))
#define INT_TO_PTR(u) ((void *)((intptr_t)(u)))
#define close_prot_errno_disarm(fd) \
if (fd >= 0) { \
int _e_ = errno; \
close(fd); \
errno = _e_; \
fd = -EBADF; \
}
static void change_mountns(struct __test_metadata *_metadata)
{
int ret;
ret = unshare(CLONE_NEWNS);
ASSERT_EQ(ret, 0) {
TH_LOG("%s - Failed to unshare mount namespace",
strerror(errno));
}
ret = mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, 0);
ASSERT_EQ(ret, 0) {
TH_LOG("%s - Failed to mount / as private",
strerror(errno));
}
}
static int __do_binderfs_test(struct __test_metadata *_metadata)
{
int fd, ret, saved_errno, result = 1;
size_t len;
struct binderfs_device device = { 0 };
struct binder_version version = { 0 };
char binderfs_mntpt[] = P_tmpdir "/binderfs_XXXXXX",
device_path[sizeof(P_tmpdir "/binderfs_XXXXXX/") + BINDERFS_MAX_NAME];
static const char * const binder_features[] = {
"oneway_spam_detection",
"extended_error",
"freeze_notification",
"transaction_report",
};
change_mountns(_metadata);
EXPECT_NE(mkdtemp(binderfs_mntpt), NULL) {
TH_LOG("%s - Failed to create binderfs mountpoint",
strerror(errno));
goto out;
}
ret = mount(NULL, binderfs_mntpt, "binder", 0, 0);
EXPECT_EQ(ret, 0) {
if (errno == ENODEV)
SKIP(goto out, "binderfs missing");
TH_LOG("%s - Failed to mount binderfs", strerror(errno));
goto rmdir;
}
/* success: binderfs mounted */
memcpy(device.name, "my-binder", strlen("my-binder"));
snprintf(device_path, sizeof(device_path), "%s/binder-control", binderfs_mntpt);
fd = open(device_path, O_RDONLY | O_CLOEXEC);
EXPECT_GE(fd, 0) {
TH_LOG("%s - Failed to open binder-control device",
strerror(errno));
goto umount;
}
ret = ioctl(fd, BINDER_CTL_ADD, &device);
saved_errno = errno;
close(fd);
errno = saved_errno;
EXPECT_GE(ret, 0) {
TH_LOG("%s - Failed to allocate new binder device",
strerror(errno));
goto umount;
}
TH_LOG("Allocated new binder device with major %d, minor %d, and name %s",
device.major, device.minor, device.name);
/* success: binder device allocation */
snprintf(device_path, sizeof(device_path), "%s/my-binder", binderfs_mntpt);
fd = open(device_path, O_CLOEXEC | O_RDONLY);
EXPECT_GE(fd, 0) {
TH_LOG("%s - Failed to open my-binder device",
strerror(errno));
goto umount;
}
ret = ioctl(fd, BINDER_VERSION, &version);
saved_errno = errno;
close(fd);
errno = saved_errno;
EXPECT_GE(ret, 0) {
TH_LOG("%s - Failed to open perform BINDER_VERSION request",
strerror(errno));
goto umount;
}
TH_LOG("Detected binder version: %d", version.protocol_version);
/* success: binder transaction with binderfs binder device */
ret = unlink(device_path);
EXPECT_EQ(ret, 0) {
TH_LOG("%s - Failed to delete binder device",
strerror(errno));
goto umount;
}
/* success: binder device removal */
snprintf(device_path, sizeof(device_path), "%s/binder-control", binderfs_mntpt);
ret = unlink(device_path);
EXPECT_NE(ret, 0) {
TH_LOG("Managed to delete binder-control device");
goto umount;
}
EXPECT_EQ(errno, EPERM) {
TH_LOG("%s - Failed to delete binder-control device but exited with unexpected error code",
strerror(errno));
goto umount;
}
/* success: binder-control device removal failed as expected */
for (int i = 0; i < ARRAY_SIZE(binder_features); i++) {
snprintf(device_path, sizeof(device_path), "%s/features/%s",
binderfs_mntpt, binder_features[i]);
fd = open(device_path, O_CLOEXEC | O_RDONLY);
EXPECT_GE(fd, 0) {
TH_LOG("%s - Failed to open binder feature: %s",
strerror(errno), binder_features[i]);
goto umount;
}
close(fd);
}
/* success: binder feature files found */
result = 0;
umount:
ret = umount2(binderfs_mntpt, MNT_DETACH);
EXPECT_EQ(ret, 0) {
TH_LOG("%s - Failed to unmount binderfs", strerror(errno));
}
rmdir:
ret = rmdir(binderfs_mntpt);
EXPECT_EQ(ret, 0) {
TH_LOG("%s - Failed to rmdir binderfs mount", strerror(errno));
}
out:
return result;
}
static int wait_for_pid(pid_t pid)
{
int status, ret;
again:
ret = waitpid(pid, &status, 0);
if (ret == -1) {
if (errno == EINTR)
goto again;
return -1;
}
if (!WIFEXITED(status))
return -1;
return WEXITSTATUS(status);
}
static int setid_userns_root(void)
{
if (setuid(0))
return -1;
if (setgid(0))
return -1;
setfsuid(0);
setfsgid(0);
return 0;
}
enum idmap_type {
UID_MAP,
GID_MAP,
};
static ssize_t read_nointr(int fd, void *buf, size_t count)
{
ssize_t ret;
again:
ret = read(fd, buf, count);
if (ret < 0 && errno == EINTR)
goto again;
return ret;
}
static ssize_t write_nointr(int fd, const void *buf, size_t count)
{
ssize_t ret;
again:
ret = write(fd, buf, count);
if (ret < 0 && errno == EINTR)
goto again;
return ret;
}
static int write_id_mapping(enum idmap_type type, pid_t pid, const char *buf,
size_t buf_size)
{
int fd;
int ret;
char path[4096];
if (type == GID_MAP) {
int setgroups_fd;
snprintf(path, sizeof(path), "/proc/%d/setgroups", pid);
setgroups_fd = open(path, O_WRONLY | O_CLOEXEC | O_NOFOLLOW);
if (setgroups_fd < 0 && errno != ENOENT)
return -1;
if (setgroups_fd >= 0) {
ret = write_nointr(setgroups_fd, "deny", sizeof("deny") - 1);
close_prot_errno_disarm(setgroups_fd);
if (ret != sizeof("deny") - 1)
return -1;
}
}
switch (type) {
case UID_MAP:
ret = snprintf(path, sizeof(path), "/proc/%d/uid_map", pid);
break;
case GID_MAP:
ret = snprintf(path, sizeof(path), "/proc/%d/gid_map", pid);
break;
default:
return -1;
}
if (ret < 0 || ret >= sizeof(path))
return -E2BIG;
fd = open(path, O_WRONLY | O_CLOEXEC | O_NOFOLLOW);
if (fd < 0)
return -1;
ret = write_nointr(fd, buf, buf_size);
close_prot_errno_disarm(fd);
if (ret != buf_size)
return -1;
return 0;
}
static void change_userns(struct __test_metadata *_metadata, int syncfds[2])
{
int ret;
char buf;
close_prot_errno_disarm(syncfds[1]);
ret = unshare(CLONE_NEWUSER);
ASSERT_EQ(ret, 0) {
TH_LOG("%s - Failed to unshare user namespace",
strerror(errno));
}
ret = write_nointr(syncfds[0], "1", 1);
ASSERT_EQ(ret, 1) {
TH_LOG("write_nointr() failed");
}
ret = read_nointr(syncfds[0], &buf, 1);
ASSERT_EQ(ret, 1) {
TH_LOG("read_nointr() failed");
}
close_prot_errno_disarm(syncfds[0]);
ASSERT_EQ(setid_userns_root(), 0) {
TH_LOG("setid_userns_root() failed");
}
}
static void change_idmaps(struct __test_metadata *_metadata, int syncfds[2], pid_t pid)
{
int ret;
char buf;
char id_map[4096];
close_prot_errno_disarm(syncfds[0]);
ret = read_nointr(syncfds[1], &buf, 1);
ASSERT_EQ(ret, 1) {
TH_LOG("read_nointr() failed");
}
snprintf(id_map, sizeof(id_map), "0 %d 1\n", getuid());
ret = write_id_mapping(UID_MAP, pid, id_map, strlen(id_map));
ASSERT_EQ(ret, 0) {
TH_LOG("write_id_mapping(UID_MAP) failed");
}
snprintf(id_map, sizeof(id_map), "0 %d 1\n", getgid());
ret = write_id_mapping(GID_MAP, pid, id_map, strlen(id_map));
ASSERT_EQ(ret, 0) {
TH_LOG("write_id_mapping(GID_MAP) failed");
}
ret = write_nointr(syncfds[1], "1", 1);
ASSERT_EQ(ret, 1) {
TH_LOG("write_nointr() failed");
}
close_prot_errno_disarm(syncfds[1]);
}
struct __test_metadata *_thread_metadata;
static void *binder_version_thread(void *data)
{
struct __test_metadata *_metadata = _thread_metadata;
int fd = PTR_TO_INT(data);
struct binder_version version = { 0 };
int ret;
ret = ioctl(fd, BINDER_VERSION, &version);
if (ret < 0)
TH_LOG("%s - Failed to open perform BINDER_VERSION request\n",
strerror(errno));
pthread_exit(data);
}
/*
* Regression test:
* 2669b8b0c798 ("binder: prevent UAF for binderfs devices")
* f0fe2c0f050d ("binder: prevent UAF for binderfs devices II")
* 211b64e4b5b6 ("binderfs: use refcount for binder control devices too")
*/
TEST(binderfs_stress)
{
int fds[1000];
int syncfds[2];
pid_t pid;
int fd, ret;
size_t len;
struct binderfs_device device = { 0 };
char binderfs_mntpt[] = P_tmpdir "/binderfs_XXXXXX",
device_path[sizeof(P_tmpdir "/binderfs_XXXXXX/") + BINDERFS_MAX_NAME];
ret = socketpair(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, syncfds);
ASSERT_EQ(ret, 0) {
TH_LOG("%s - Failed to create socket pair", strerror(errno));
}
pid = fork();
ASSERT_GE(pid, 0) {
TH_LOG("%s - Failed to fork", strerror(errno));
close_prot_errno_disarm(syncfds[0]);
close_prot_errno_disarm(syncfds[1]);
}
if (pid == 0) {
int i, j, k, nthreads;
pthread_attr_t attr;
pthread_t threads[DEFAULT_THREADS];
change_userns(_metadata, syncfds);
change_mountns(_metadata);
ASSERT_NE(mkdtemp(binderfs_mntpt), NULL) {
TH_LOG("%s - Failed to create binderfs mountpoint",
strerror(errno));
}
ret = mount(NULL, binderfs_mntpt, "binder", 0, 0);
ASSERT_EQ(ret, 0) {
TH_LOG("%s - Failed to mount binderfs, check if CONFIG_ANDROID_BINDERFS is enabled in the running kernel",
strerror(errno));
}
for (int i = 0; i < ARRAY_SIZE(fds); i++) {
snprintf(device_path, sizeof(device_path),
"%s/binder-control", binderfs_mntpt);
fd = open(device_path, O_RDONLY | O_CLOEXEC);
ASSERT_GE(fd, 0) {
TH_LOG("%s - Failed to open binder-control device",
strerror(errno));
}
memset(&device, 0, sizeof(device));
snprintf(device.name, sizeof(device.name), "%d", i);
ret = ioctl(fd, BINDER_CTL_ADD, &device);
close_prot_errno_disarm(fd);
ASSERT_EQ(ret, 0) {
TH_LOG("%s - Failed to allocate new binder device",
strerror(errno));
}
snprintf(device_path, sizeof(device_path), "%s/%d",
binderfs_mntpt, i);
fds[i] = open(device_path, O_RDONLY | O_CLOEXEC);
ASSERT_GE(fds[i], 0) {
TH_LOG("%s - Failed to open binder device", strerror(errno));
}
}
ret = umount2(binderfs_mntpt, MNT_DETACH);
ASSERT_EQ(ret, 0) {
TH_LOG("%s - Failed to unmount binderfs", strerror(errno));
rmdir(binderfs_mntpt);
}
nthreads = get_nprocs_conf();
if (nthreads > DEFAULT_THREADS)
nthreads = DEFAULT_THREADS;
_thread_metadata = _metadata;
pthread_attr_init(&attr);
for (k = 0; k < ARRAY_SIZE(fds); k++) {
for (i = 0; i < nthreads; i++) {
ret = pthread_create(&threads[i], &attr, binder_version_thread, INT_TO_PTR(fds[k]));
if (ret) {
TH_LOG("%s - Failed to create thread %d",
strerror(errno), i);
break;
}
}
for (j = 0; j < i; j++) {
void *fdptr = NULL;
ret = pthread_join(threads[j], &fdptr);
if (ret)
TH_LOG("%s - Failed to join thread %d for fd %d",
strerror(errno), j, PTR_TO_INT(fdptr));
}
}
pthread_attr_destroy(&attr);
for (k = 0; k < ARRAY_SIZE(fds); k++)
close(fds[k]);
exit(EXIT_SUCCESS);
}
change_idmaps(_metadata, syncfds, pid);
ret = wait_for_pid(pid);
ASSERT_EQ(ret, 0) {
TH_LOG("wait_for_pid() failed");
}
}
TEST(binderfs_test_privileged)
{
if (geteuid() != 0)
SKIP(return, "Tests are not run as root. Skipping privileged tests");
if (__do_binderfs_test(_metadata))
SKIP(return, "The Android binderfs filesystem is not available");
}
TEST(binderfs_test_unprivileged)
{
int ret;
int syncfds[2];
pid_t pid;
ret = socketpair(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, syncfds);
ASSERT_EQ(ret, 0) {
TH_LOG("%s - Failed to create socket pair", strerror(errno));
}
pid = fork();
ASSERT_GE(pid, 0) {
close_prot_errno_disarm(syncfds[0]);
close_prot_errno_disarm(syncfds[1]);
TH_LOG("%s - Failed to fork", strerror(errno));
}
if (pid == 0) {
change_userns(_metadata, syncfds);
if (__do_binderfs_test(_metadata))
exit(2);
exit(EXIT_SUCCESS);
}
change_idmaps(_metadata, syncfds, pid);
ret = wait_for_pid(pid);
if (ret) {
if (ret == 2)
SKIP(return, "The Android binderfs filesystem is not available");
ASSERT_EQ(ret, 0) {
TH_LOG("wait_for_pid() failed");
}
}
}
TEST_HARNESS_MAIN
@@ -0,0 +1,2 @@
CONFIG_ANDROID_BINDERFS=y
CONFIG_ANDROID_BINDER_IPC=y
@@ -0,0 +1,314 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <asm/ioctls.h>
#include <sys/mount.h>
#include <sys/wait.h>
#include "kselftest.h"
static bool terminal_dup2(int duplicate, int original)
{
int ret;
ret = dup2(duplicate, original);
if (ret < 0)
return false;
return true;
}
static int terminal_set_stdfds(int fd)
{
int i;
if (fd < 0)
return 0;
for (i = 0; i < 3; i++)
if (!terminal_dup2(fd, (int[]){STDIN_FILENO, STDOUT_FILENO,
STDERR_FILENO}[i]))
return -1;
return 0;
}
static int login_pty(int fd)
{
int ret;
setsid();
ret = ioctl(fd, TIOCSCTTY, NULL);
if (ret < 0)
return -1;
ret = terminal_set_stdfds(fd);
if (ret < 0)
return -1;
if (fd > STDERR_FILENO)
close(fd);
return 0;
}
static int wait_for_pid(pid_t pid)
{
int status, ret;
again:
ret = waitpid(pid, &status, 0);
if (ret == -1) {
if (errno == EINTR)
goto again;
return -1;
}
if (ret != pid)
goto again;
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
return -1;
return 0;
}
static int resolve_procfd_symlink(int fd, char *buf, size_t buflen)
{
int ret;
char procfd[4096];
ret = snprintf(procfd, 4096, "/proc/self/fd/%d", fd);
if (ret < 0 || ret >= 4096)
return -1;
ret = readlink(procfd, buf, buflen);
if (ret < 0 || (size_t)ret >= buflen)
return -1;
buf[ret] = '\0';
return 0;
}
static int do_tiocgptpeer(char *ptmx, char *expected_procfd_contents)
{
int ret;
int master = -1, slave = -1, fret = -1;
master = open(ptmx, O_RDWR | O_NOCTTY | O_CLOEXEC);
if (master < 0) {
fprintf(stderr, "Failed to open \"%s\": %s\n", ptmx,
strerror(errno));
return -1;
}
/*
* grantpt() makes assumptions about /dev/pts/ so ignore it. It's also
* not really needed.
*/
ret = unlockpt(master);
if (ret < 0) {
fprintf(stderr, "Failed to unlock terminal\n");
goto do_cleanup;
}
slave = ioctl(master, TIOCGPTPEER, O_RDWR | O_NOCTTY | O_CLOEXEC);
if (slave < 0) {
if (errno == EINVAL) {
fprintf(stderr, "TIOCGPTPEER is not supported. "
"Skipping test.\n");
fret = KSFT_SKIP;
} else {
fprintf(stderr,
"Failed to perform TIOCGPTPEER ioctl\n");
fret = EXIT_FAILURE;
}
goto do_cleanup;
}
pid_t pid = fork();
if (pid < 0)
goto do_cleanup;
if (pid == 0) {
char buf[4096];
ret = login_pty(slave);
if (ret < 0) {
fprintf(stderr, "Failed to setup terminal\n");
_exit(EXIT_FAILURE);
}
ret = resolve_procfd_symlink(STDIN_FILENO, buf, sizeof(buf));
if (ret < 0) {
fprintf(stderr, "Failed to retrieve pathname of pts "
"slave file descriptor\n");
_exit(EXIT_FAILURE);
}
if (strncmp(expected_procfd_contents, buf,
strlen(expected_procfd_contents)) != 0) {
fprintf(stderr, "Received invalid contents for "
"\"/proc/<pid>/fd/%d\" symlink: %s\n",
STDIN_FILENO, buf);
_exit(-1);
}
fprintf(stderr, "Contents of \"/proc/<pid>/fd/%d\" "
"symlink are valid: %s\n", STDIN_FILENO, buf);
_exit(EXIT_SUCCESS);
}
ret = wait_for_pid(pid);
if (ret < 0)
goto do_cleanup;
fret = EXIT_SUCCESS;
do_cleanup:
if (master >= 0)
close(master);
if (slave >= 0)
close(slave);
return fret;
}
static int verify_non_standard_devpts_mount(void)
{
char *mntpoint;
int ret = -1;
char devpts[] = P_tmpdir "/devpts_fs_XXXXXX";
char ptmx[] = P_tmpdir "/devpts_fs_XXXXXX/ptmx";
ret = umount("/dev/pts");
if (ret < 0) {
fprintf(stderr, "Failed to unmount \"/dev/pts\": %s\n",
strerror(errno));
return -1;
}
(void)umount("/dev/ptmx");
mntpoint = mkdtemp(devpts);
if (!mntpoint) {
fprintf(stderr, "Failed to create temporary mountpoint: %s\n",
strerror(errno));
return -1;
}
ret = mount("devpts", mntpoint, "devpts", MS_NOSUID | MS_NOEXEC,
"newinstance,ptmxmode=0666,mode=0620,gid=5");
if (ret < 0) {
fprintf(stderr, "Failed to mount devpts fs to \"%s\" in new "
"mount namespace: %s\n", mntpoint,
strerror(errno));
unlink(mntpoint);
return -1;
}
ret = snprintf(ptmx, sizeof(ptmx), "%s/ptmx", devpts);
if (ret < 0 || (size_t)ret >= sizeof(ptmx)) {
unlink(mntpoint);
return -1;
}
ret = do_tiocgptpeer(ptmx, mntpoint);
unlink(mntpoint);
if (ret < 0)
return -1;
return 0;
}
static int verify_ptmx_bind_mount(void)
{
int ret;
ret = mount("/dev/pts/ptmx", "/dev/ptmx", NULL, MS_BIND, NULL);
if (ret < 0) {
fprintf(stderr, "Failed to bind mount \"/dev/pts/ptmx\" to "
"\"/dev/ptmx\" mount namespace\n");
return -1;
}
ret = do_tiocgptpeer("/dev/ptmx", "/dev/pts/");
if (ret < 0)
return -1;
return 0;
}
static int verify_invalid_ptmx_bind_mount(void)
{
int ret;
char mntpoint_fd;
char ptmx[] = P_tmpdir "/devpts_ptmx_XXXXXX";
mntpoint_fd = mkstemp(ptmx);
if (mntpoint_fd < 0) {
fprintf(stderr, "Failed to create temporary directory: %s\n",
strerror(errno));
return -1;
}
ret = mount("/dev/pts/ptmx", ptmx, NULL, MS_BIND, NULL);
close(mntpoint_fd);
if (ret < 0) {
fprintf(stderr, "Failed to bind mount \"/dev/pts/ptmx\" to "
"\"%s\" mount namespace\n", ptmx);
return -1;
}
ret = do_tiocgptpeer(ptmx, "/dev/pts/");
if (ret == 0)
return -1;
return 0;
}
int main(int argc, char *argv[])
{
int ret;
if (!isatty(STDIN_FILENO)) {
fprintf(stderr, "Standard input file descriptor is not attached "
"to a terminal. Skipping test\n");
exit(KSFT_SKIP);
}
ret = unshare(CLONE_NEWNS);
if (ret < 0) {
fprintf(stderr, "Failed to unshare mount namespace\n");
exit(EXIT_FAILURE);
}
ret = mount("", "/", NULL, MS_PRIVATE | MS_REC, 0);
if (ret < 0) {
fprintf(stderr, "Failed to make \"/\" MS_PRIVATE in new mount "
"namespace\n");
exit(EXIT_FAILURE);
}
ret = verify_ptmx_bind_mount();
if (ret < 0)
exit(EXIT_FAILURE);
ret = verify_invalid_ptmx_bind_mount();
if (ret < 0)
exit(EXIT_FAILURE);
ret = verify_non_standard_devpts_mount();
if (ret < 0)
exit(EXIT_FAILURE);
exit(EXIT_SUCCESS);
}
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE /* needed to get the defines */
#include <fcntl.h> /* in glibc 2.2 this has the needed
values defined */
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
static volatile int event_fd;
static void handler(int sig, siginfo_t *si, void *data)
{
event_fd = si->si_fd;
}
int main(void)
{
struct sigaction act;
int fd;
act.sa_sigaction = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_SIGINFO;
sigaction(SIGRTMIN + 1, &act, NULL);
fd = open(".", O_RDONLY);
fcntl(fd, F_SETSIG, SIGRTMIN + 1);
fcntl(fd, F_NOTIFY, DN_MODIFY|DN_CREATE|DN_MULTISHOT);
/* we will now be notified if any of the files
in "." is modified or new files are created */
while (1) {
pause();
printf("Got event on fd=%d\n", event_fd);
}
}
@@ -0,0 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
clone3_empty_mntns_test
empty_mntns_test
overmount_chroot_test
@@ -0,0 +1,12 @@
# SPDX-License-Identifier: GPL-2.0-or-later
CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES)
LDLIBS += -lcap
TEST_GEN_PROGS := empty_mntns_test overmount_chroot_test clone3_empty_mntns_test
include ../../lib.mk
$(OUTPUT)/empty_mntns_test: ../utils.c
$(OUTPUT)/overmount_chroot_test: ../utils.c
$(OUTPUT)/clone3_empty_mntns_test: ../utils.c
@@ -0,0 +1,938 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Tests for empty mount namespace creation via clone3() CLONE_EMPTY_MNTNS
*
* These tests exercise the clone3() code path for creating empty mount
* namespaces, which is distinct from the unshare() path tested in
* empty_mntns_test.c. With clone3(), CLONE_EMPTY_MNTNS (0x2000000000ULL)
* is a 64-bit flag that implies CLONE_NEWNS. The implication happens in
* kernel_clone() before copy_process(), unlike unshare() where it goes
* through UNSHARE_EMPTY_MNTNS -> CLONE_EMPTY_MNTNS conversion in
* unshare_nsproxy_namespaces().
*
* Copyright (c) 2024 Christian Brauner <brauner@kernel.org>
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <linux/mount.h>
#include <linux/stat.h>
#include <stdio.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "../utils.h"
#include "../wrappers.h"
#include "clone3/clone3_selftests.h"
#include "empty_mntns.h"
#include "kselftest_harness.h"
static pid_t clone3_empty_mntns(uint64_t extra_flags)
{
struct __clone_args args = {
.flags = CLONE_EMPTY_MNTNS | extra_flags,
.exit_signal = SIGCHLD,
};
return sys_clone3(&args, sizeof(args));
}
static bool clone3_empty_mntns_supported(void)
{
pid_t pid;
int status;
pid = fork();
if (pid < 0)
return false;
if (pid == 0) {
if (enter_userns())
_exit(1);
pid = clone3_empty_mntns(0);
if (pid < 0)
_exit(1);
if (pid == 0)
_exit(0);
_exit(wait_for_pid(pid) != 0);
}
if (waitpid(pid, &status, 0) != pid)
return false;
if (!WIFEXITED(status))
return false;
return WEXITSTATUS(status) == 0;
}
FIXTURE(clone3_empty_mntns) {};
FIXTURE_SETUP(clone3_empty_mntns)
{
if (!clone3_empty_mntns_supported())
SKIP(return, "CLONE_EMPTY_MNTNS via clone3 not supported");
}
FIXTURE_TEARDOWN(clone3_empty_mntns) {}
/*
* Basic clone3() with CLONE_EMPTY_MNTNS: child gets empty mount namespace
* with exactly 1 mount and root == cwd.
*/
TEST_F(clone3_empty_mntns, basic)
{
pid_t pid, inner;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
if (enter_userns())
_exit(1);
inner = clone3_empty_mntns(0);
if (inner < 0)
_exit(2);
if (inner == 0) {
uint64_t root_id, cwd_id;
if (count_mounts() != 1)
_exit(3);
root_id = get_unique_mnt_id("/");
cwd_id = get_unique_mnt_id(".");
if (root_id == 0 || cwd_id == 0)
_exit(4);
if (root_id != cwd_id)
_exit(5);
_exit(0);
}
_exit(wait_for_pid(inner));
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* CLONE_EMPTY_MNTNS implies CLONE_NEWNS. Verify that it works without
* explicitly setting CLONE_NEWNS (tests fork.c:2627-2630).
*/
TEST_F(clone3_empty_mntns, implies_newns)
{
pid_t pid, inner;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
ssize_t parent_mounts;
if (enter_userns())
_exit(1);
/* Verify we have mounts in our current namespace. */
parent_mounts = count_mounts();
if (parent_mounts < 1)
_exit(2);
/* Only CLONE_EMPTY_MNTNS, no explicit CLONE_NEWNS. */
inner = clone3_empty_mntns(0);
if (inner < 0)
_exit(3);
if (inner == 0) {
if (count_mounts() != 1)
_exit(4);
_exit(0);
}
/* Parent still has its mounts. */
if (count_mounts() != parent_mounts)
_exit(5);
_exit(wait_for_pid(inner));
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* Helper macro: generate a test that clones with CLONE_EMPTY_MNTNS |
* @extra_flags and verifies the child has exactly one mount.
*/
#define TEST_CLONE3_FLAGS(test_name, extra_flags) \
TEST_F(clone3_empty_mntns, test_name) \
{ \
pid_t pid, inner; \
\
pid = fork(); \
ASSERT_GE(pid, 0); \
\
if (pid == 0) { \
if (enter_userns()) \
_exit(1); \
\
inner = clone3_empty_mntns(extra_flags); \
if (inner < 0) \
_exit(2); \
\
if (inner == 0) { \
if (count_mounts() != 1) \
_exit(3); \
_exit(0); \
} \
\
_exit(wait_for_pid(inner)); \
} \
\
ASSERT_EQ(wait_for_pid(pid), 0); \
}
/* Redundant CLONE_NEWNS | CLONE_EMPTY_MNTNS should succeed. */
TEST_CLONE3_FLAGS(with_explicit_newns, CLONE_NEWNS)
/* CLONE_EMPTY_MNTNS combined with CLONE_NEWUSER. */
TEST_CLONE3_FLAGS(with_newuser, CLONE_NEWUSER)
/* CLONE_EMPTY_MNTNS combined with other namespace flags. */
TEST_CLONE3_FLAGS(with_other_ns_flags, CLONE_NEWUTS | CLONE_NEWIPC)
/*
* CLONE_EMPTY_MNTNS combined with CLONE_NEWPID.
*/
TEST_F(clone3_empty_mntns, with_newpid)
{
pid_t pid, inner;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
if (enter_userns())
_exit(1);
inner = clone3_empty_mntns(CLONE_NEWPID);
if (inner < 0)
_exit(2);
if (inner == 0) {
if (count_mounts() != 1)
_exit(3);
/* In a new PID namespace, getpid() returns 1. */
if (getpid() != 1)
_exit(4);
_exit(0);
}
_exit(wait_for_pid(inner));
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* CLONE_EMPTY_MNTNS | CLONE_FS must fail because the implied CLONE_NEWNS
* and CLONE_FS are mutually exclusive (fork.c:1981).
*/
TEST_F(clone3_empty_mntns, with_clone_fs_fails)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
struct __clone_args args = {
.flags = CLONE_EMPTY_MNTNS | CLONE_FS,
.exit_signal = SIGCHLD,
};
pid_t ret;
if (enter_userns())
_exit(1);
ret = sys_clone3(&args, sizeof(args));
if (ret >= 0) {
if (ret == 0)
_exit(0);
wait_for_pid(ret);
_exit(2);
}
if (errno != EINVAL)
_exit(3);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* CLONE_EMPTY_MNTNS combined with CLONE_PIDFD returns a valid pidfd.
*/
TEST_F(clone3_empty_mntns, with_pidfd)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
struct __clone_args args = {
.flags = CLONE_EMPTY_MNTNS | CLONE_PIDFD,
.exit_signal = SIGCHLD,
};
int pidfd = -1;
pid_t inner;
if (enter_userns())
_exit(1);
args.pidfd = (uintptr_t)&pidfd;
inner = sys_clone3(&args, sizeof(args));
if (inner < 0)
_exit(2);
if (inner == 0) {
if (count_mounts() != 1)
_exit(3);
_exit(0);
}
/* Verify we got a valid pidfd. */
if (pidfd < 0)
_exit(4);
close(pidfd);
_exit(wait_for_pid(inner));
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* clone3 without CAP_SYS_ADMIN must fail with EPERM.
*/
TEST_F(clone3_empty_mntns, eperm_without_caps)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
pid_t ret;
/* Skip if already root. */
if (getuid() == 0)
_exit(0);
ret = clone3_empty_mntns(0);
if (ret >= 0) {
if (ret == 0)
_exit(0);
wait_for_pid(ret);
_exit(1);
}
if (errno != EPERM)
_exit(2);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* Parent's mount namespace is unaffected after clone3 with CLONE_EMPTY_MNTNS.
*/
TEST_F(clone3_empty_mntns, parent_unchanged)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
ssize_t nr_before, nr_after;
pid_t inner;
if (enter_userns())
_exit(1);
nr_before = count_mounts();
if (nr_before < 1)
_exit(2);
inner = clone3_empty_mntns(0);
if (inner < 0)
_exit(3);
if (inner == 0)
_exit(0);
if (wait_for_pid(inner) != 0)
_exit(4);
nr_after = count_mounts();
if (nr_after != nr_before)
_exit(5);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* Parent with many mounts: child still gets exactly 1 mount.
*/
TEST_F(clone3_empty_mntns, many_parent_mounts)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
char tmpdir[] = "/tmp/clone3_mntns_test.XXXXXX";
pid_t inner;
int i;
if (enter_userns())
_exit(1);
if (unshare(CLONE_NEWNS))
_exit(2);
if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL))
_exit(3);
if (!mkdtemp(tmpdir))
_exit(4);
if (mount("tmpfs", tmpdir, "tmpfs", 0, "size=1M"))
_exit(5);
for (i = 0; i < 5; i++) {
char subdir[256];
snprintf(subdir, sizeof(subdir), "%s/sub%d", tmpdir, i);
if (mkdir(subdir, 0755) && errno != EEXIST)
_exit(6);
if (mount(subdir, subdir, NULL, MS_BIND, NULL))
_exit(7);
}
if (count_mounts() < 5)
_exit(8);
inner = clone3_empty_mntns(0);
if (inner < 0)
_exit(9);
if (inner == 0) {
if (count_mounts() != 1)
_exit(10);
_exit(0);
}
_exit(wait_for_pid(inner));
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* Verify the child's root mount is nullfs with expected statmount properties.
*/
TEST_F(clone3_empty_mntns, mount_properties)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
pid_t inner;
if (enter_userns())
_exit(1);
inner = clone3_empty_mntns(0);
if (inner < 0)
_exit(2);
if (inner == 0) {
struct statmount *sm;
uint64_t root_id;
root_id = get_unique_mnt_id("/");
if (!root_id)
_exit(3);
sm = statmount_alloc(root_id, 0,
STATMOUNT_MNT_BASIC |
STATMOUNT_MNT_POINT |
STATMOUNT_FS_TYPE, 0);
if (!sm)
_exit(4);
/* Root mount point is "/". */
if (!(sm->mask & STATMOUNT_MNT_POINT))
_exit(5);
if (strcmp(sm->str + sm->mnt_point, "/") != 0)
_exit(6);
/* Filesystem type is nullfs. */
if (!(sm->mask & STATMOUNT_FS_TYPE))
_exit(7);
if (strcmp(sm->str + sm->fs_type, "nullfs") != 0)
_exit(8);
/* Root mount is its own parent. */
if (!(sm->mask & STATMOUNT_MNT_BASIC))
_exit(9);
if (sm->mnt_parent_id != sm->mnt_id)
_exit(10);
free(sm);
_exit(0);
}
_exit(wait_for_pid(inner));
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* Listmount returns only the root mount in the child's empty namespace.
*/
TEST_F(clone3_empty_mntns, listmount_single_entry)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
pid_t inner;
if (enter_userns())
_exit(1);
inner = clone3_empty_mntns(0);
if (inner < 0)
_exit(2);
if (inner == 0) {
uint64_t list[16];
ssize_t nr_mounts;
uint64_t root_id;
nr_mounts = listmount(LSMT_ROOT, 0, 0, list, 16, 0);
if (nr_mounts != 1)
_exit(3);
root_id = get_unique_mnt_id("/");
if (!root_id)
_exit(4);
if (list[0] != root_id)
_exit(5);
_exit(0);
}
_exit(wait_for_pid(inner));
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* Child can mount tmpfs over nullfs root (the primary container use case).
*
* Uses the new mount API (fsopen/fsmount/move_mount) because resolving
* "/" returns the process root directly without following overmounts.
* The mount fd from fsmount lets us fchdir + chroot into the new tmpfs.
*/
TEST_F(clone3_empty_mntns, child_overmount_tmpfs)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
pid_t inner;
if (enter_userns())
_exit(1);
inner = clone3_empty_mntns(0);
if (inner < 0)
_exit(2);
if (inner == 0) {
struct statmount *sm;
uint64_t root_id;
int fd, fsfd, mntfd;
if (count_mounts() != 1)
_exit(3);
/* Verify root is nullfs. */
root_id = get_unique_mnt_id("/");
if (!root_id)
_exit(4);
sm = statmount_alloc(root_id, 0, STATMOUNT_FS_TYPE, 0);
if (!sm)
_exit(5);
if (!(sm->mask & STATMOUNT_FS_TYPE))
_exit(6);
if (strcmp(sm->str + sm->fs_type, "nullfs") != 0)
_exit(7);
free(sm);
/* Create tmpfs via the new mount API. */
fsfd = sys_fsopen("tmpfs", 0);
if (fsfd < 0)
_exit(8);
if (sys_fsconfig(fsfd, FSCONFIG_SET_STRING,
"size", "1M", 0)) {
close(fsfd);
_exit(9);
}
if (sys_fsconfig(fsfd, FSCONFIG_CMD_CREATE,
NULL, NULL, 0)) {
close(fsfd);
_exit(10);
}
mntfd = sys_fsmount(fsfd, 0, 0);
close(fsfd);
if (mntfd < 0)
_exit(11);
/* Attach tmpfs to "/". */
if (sys_move_mount(mntfd, "", AT_FDCWD, "/",
MOVE_MOUNT_F_EMPTY_PATH)) {
close(mntfd);
_exit(12);
}
if (count_mounts() != 2) {
close(mntfd);
_exit(13);
}
/* Enter the tmpfs. */
if (fchdir(mntfd)) {
close(mntfd);
_exit(14);
}
if (chroot(".")) {
close(mntfd);
_exit(15);
}
close(mntfd);
/* Verify "/" is now tmpfs. */
root_id = get_unique_mnt_id("/");
if (!root_id)
_exit(16);
sm = statmount_alloc(root_id, 0, STATMOUNT_FS_TYPE, 0);
if (!sm)
_exit(17);
if (!(sm->mask & STATMOUNT_FS_TYPE))
_exit(18);
if (strcmp(sm->str + sm->fs_type, "tmpfs") != 0)
_exit(19);
free(sm);
/* Verify tmpfs is writable. */
fd = open("/testfile", O_CREAT | O_RDWR, 0644);
if (fd < 0)
_exit(20);
if (write(fd, "test", 4) != 4) {
close(fd);
_exit(21);
}
close(fd);
if (access("/testfile", F_OK))
_exit(22);
_exit(0);
}
_exit(wait_for_pid(inner));
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* Multiple clone3 calls with CLONE_EMPTY_MNTNS produce children with
* distinct mount namespace root mount IDs.
*/
TEST_F(clone3_empty_mntns, repeated)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
int pipe1[2], pipe2[2];
uint64_t id1 = 0, id2 = 0;
pid_t inner1, inner2;
if (enter_userns())
_exit(1);
if (pipe(pipe1) || pipe(pipe2))
_exit(2);
inner1 = clone3_empty_mntns(0);
if (inner1 < 0)
_exit(3);
if (inner1 == 0) {
uint64_t root_id;
close(pipe1[0]);
root_id = get_unique_mnt_id("/");
if (write(pipe1[1], &root_id, sizeof(root_id)) != sizeof(root_id))
_exit(1);
close(pipe1[1]);
_exit(0);
}
inner2 = clone3_empty_mntns(0);
if (inner2 < 0)
_exit(4);
if (inner2 == 0) {
uint64_t root_id;
close(pipe2[0]);
root_id = get_unique_mnt_id("/");
if (write(pipe2[1], &root_id, sizeof(root_id)) != sizeof(root_id))
_exit(1);
close(pipe2[1]);
_exit(0);
}
close(pipe1[1]);
close(pipe2[1]);
if (read(pipe1[0], &id1, sizeof(id1)) != sizeof(id1))
_exit(5);
if (read(pipe2[0], &id2, sizeof(id2)) != sizeof(id2))
_exit(6);
close(pipe1[0]);
close(pipe2[0]);
if (wait_for_pid(inner1) || wait_for_pid(inner2))
_exit(7);
/* Each child must have a distinct root mount ID. */
if (id1 == 0 || id2 == 0)
_exit(8);
if (id1 == id2)
_exit(9);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* Verify setns() into a child's empty mount namespace works.
*/
TEST_F(clone3_empty_mntns, setns_into_child_mntns)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
int pipe_fd[2];
pid_t inner;
char c;
if (enter_userns())
_exit(1);
if (pipe(pipe_fd))
_exit(2);
inner = clone3_empty_mntns(0);
if (inner < 0)
_exit(3);
if (inner == 0) {
/* Signal parent we're ready. */
close(pipe_fd[0]);
if (write(pipe_fd[1], "r", 1) != 1)
_exit(1);
/*
* Wait for parent to finish. Reading from our
* write end will block until the parent closes
* its read end, giving us an implicit barrier.
*/
if (read(pipe_fd[1], &c, 1) < 0)
;
close(pipe_fd[1]);
_exit(0);
}
close(pipe_fd[1]);
/* Wait for child to be ready. */
if (read(pipe_fd[0], &c, 1) != 1)
_exit(4);
/* Open child's mount namespace. */
{
char path[64];
int mntns_fd;
snprintf(path, sizeof(path), "/proc/%d/ns/mnt", inner);
mntns_fd = open(path, O_RDONLY);
if (mntns_fd < 0)
_exit(5);
if (setns(mntns_fd, CLONE_NEWNS))
_exit(6);
close(mntns_fd);
}
/* Now we should be in the child's empty mntns. */
if (count_mounts() != 1)
_exit(7);
close(pipe_fd[0]);
_exit(wait_for_pid(inner));
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* Tests below do not require CLONE_EMPTY_MNTNS support.
*/
/*
* Unknown 64-bit flags beyond the known set are rejected.
*/
TEST(unknown_flags_rejected)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
struct __clone_args args = {
.flags = 0x800000000ULL,
.exit_signal = SIGCHLD,
};
pid_t ret;
ret = sys_clone3(&args, sizeof(args));
if (ret >= 0) {
if (ret == 0)
_exit(0);
wait_for_pid(ret);
_exit(1);
}
if (errno != EINVAL)
_exit(2);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* Regular clone3 with CLONE_NEWNS (without CLONE_EMPTY_MNTNS) still
* copies the full mount tree.
*/
TEST(clone3_newns_full_copy)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
struct __clone_args args = {
.flags = CLONE_NEWNS,
.exit_signal = SIGCHLD,
};
ssize_t parent_mounts;
pid_t inner;
if (enter_userns())
_exit(1);
parent_mounts = count_mounts();
if (parent_mounts < 1)
_exit(2);
inner = sys_clone3(&args, sizeof(args));
if (inner < 0)
_exit(3);
if (inner == 0) {
/* Full copy should have at least as many mounts. */
if (count_mounts() < parent_mounts)
_exit(1);
_exit(0);
}
_exit(wait_for_pid(inner));
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,25 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef EMPTY_MNTNS_H
#define EMPTY_MNTNS_H
#include <errno.h>
#include <stdlib.h>
#include "../statmount/statmount.h"
#ifndef UNSHARE_EMPTY_MNTNS
#define UNSHARE_EMPTY_MNTNS 0x00100000
#endif
#ifndef CLONE_EMPTY_MNTNS
#define CLONE_EMPTY_MNTNS (1ULL << 37)
#endif
static inline ssize_t count_mounts(void)
{
uint64_t list[4096];
return listmount(LSMT_ROOT, 0, 0, list, sizeof(list) / sizeof(list[0]), 0);
}
#endif /* EMPTY_MNTNS_H */
@@ -0,0 +1,725 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Tests for empty mount namespace creation via UNSHARE_EMPTY_MNTNS
*
* Copyright (c) 2024 Christian Brauner <brauner@kernel.org>
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <linux/mount.h>
#include <linux/stat.h>
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "../utils.h"
#include "../wrappers.h"
#include "empty_mntns.h"
#include "kselftest_harness.h"
static bool unshare_empty_mntns_supported(void)
{
pid_t pid;
int status;
pid = fork();
if (pid < 0)
return false;
if (pid == 0) {
if (enter_userns())
_exit(1);
if (unshare(UNSHARE_EMPTY_MNTNS) && errno == EINVAL)
_exit(1);
_exit(0);
}
if (waitpid(pid, &status, 0) != pid)
return false;
if (!WIFEXITED(status))
return false;
return WEXITSTATUS(status) == 0;
}
FIXTURE(empty_mntns) {};
FIXTURE_SETUP(empty_mntns)
{
if (!unshare_empty_mntns_supported())
SKIP(return, "UNSHARE_EMPTY_MNTNS not supported");
}
FIXTURE_TEARDOWN(empty_mntns) {}
/* Verify unshare succeeds, produces exactly 1 mount, and root == cwd */
TEST_F(empty_mntns, basic)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
uint64_t root_id, cwd_id;
if (enter_userns())
_exit(1);
if (unshare(UNSHARE_EMPTY_MNTNS))
_exit(2);
if (count_mounts() != 1)
_exit(3);
root_id = get_unique_mnt_id("/");
cwd_id = get_unique_mnt_id(".");
if (root_id == 0 || cwd_id == 0)
_exit(4);
if (root_id != cwd_id)
_exit(5);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* UNSHARE_EMPTY_MNTNS combined with CLONE_NEWUSER.
*
* The user namespace must be created first so /proc is still accessible
* for writing uid_map/gid_map. The empty mount namespace is created
* afterwards.
*/
TEST_F(empty_mntns, with_clone_newuser)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
uid_t uid = getuid();
gid_t gid = getgid();
char map[100];
if (unshare(CLONE_NEWUSER))
_exit(1);
snprintf(map, sizeof(map), "0 %d 1", uid);
if (write_file("/proc/self/uid_map", map))
_exit(2);
if (write_file("/proc/self/setgroups", "deny"))
_exit(3);
snprintf(map, sizeof(map), "0 %d 1", gid);
if (write_file("/proc/self/gid_map", map))
_exit(4);
if (unshare(UNSHARE_EMPTY_MNTNS))
_exit(5);
if (count_mounts() != 1)
_exit(6);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/* UNSHARE_EMPTY_MNTNS combined with other namespace flags */
TEST_F(empty_mntns, with_other_ns_flags)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
if (enter_userns())
_exit(1);
if (unshare(UNSHARE_EMPTY_MNTNS | CLONE_NEWUTS | CLONE_NEWIPC))
_exit(2);
if (count_mounts() != 1)
_exit(3);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/* EPERM without proper capabilities */
TEST_F(empty_mntns, eperm_without_caps)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
/* Skip if already root */
if (getuid() == 0)
_exit(0);
if (unshare(UNSHARE_EMPTY_MNTNS) == 0)
_exit(1);
if (errno != EPERM)
_exit(2);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/* Many source mounts still result in exactly 1 mount */
TEST_F(empty_mntns, many_source_mounts)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
char tmpdir[] = "/tmp/empty_mntns_test.XXXXXX";
int i;
if (enter_userns())
_exit(1);
if (unshare(CLONE_NEWNS))
_exit(2);
if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL))
_exit(3);
if (!mkdtemp(tmpdir))
_exit(4);
if (mount("tmpfs", tmpdir, "tmpfs", 0, "size=1M"))
_exit(5);
for (i = 0; i < 5; i++) {
char subdir[256];
snprintf(subdir, sizeof(subdir), "%s/sub%d", tmpdir, i);
if (mkdir(subdir, 0755) && errno != EEXIST)
_exit(6);
if (mount(subdir, subdir, NULL, MS_BIND, NULL))
_exit(7);
}
if (count_mounts() < 5)
_exit(8);
if (unshare(UNSHARE_EMPTY_MNTNS))
_exit(9);
if (count_mounts() != 1)
_exit(10);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/* CWD on a different mount gets reset to root */
TEST_F(empty_mntns, cwd_reset)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
char tmpdir[] = "/tmp/empty_mntns_cwd.XXXXXX";
uint64_t root_id, cwd_id;
struct statmount *sm;
if (enter_userns())
_exit(1);
if (unshare(CLONE_NEWNS))
_exit(2);
if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL))
_exit(3);
if (!mkdtemp(tmpdir))
_exit(4);
if (mount("tmpfs", tmpdir, "tmpfs", 0, "size=1M"))
_exit(5);
if (chdir(tmpdir))
_exit(6);
if (unshare(UNSHARE_EMPTY_MNTNS))
_exit(7);
root_id = get_unique_mnt_id("/");
cwd_id = get_unique_mnt_id(".");
if (root_id == 0 || cwd_id == 0)
_exit(8);
if (root_id != cwd_id)
_exit(9);
sm = statmount_alloc(root_id, 0, STATMOUNT_MNT_ROOT | STATMOUNT_MNT_POINT, 0);
if (!sm)
_exit(10);
if (strcmp(sm->str + sm->mnt_point, "/") != 0)
_exit(11);
free(sm);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/* Verify statmount properties of the root mount */
TEST_F(empty_mntns, mount_properties)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
struct statmount *sm;
uint64_t root_id;
if (enter_userns())
_exit(1);
if (unshare(UNSHARE_EMPTY_MNTNS))
_exit(2);
root_id = get_unique_mnt_id("/");
if (!root_id)
_exit(3);
sm = statmount_alloc(root_id, 0, STATMOUNT_MNT_BASIC | STATMOUNT_MNT_ROOT |
STATMOUNT_MNT_POINT | STATMOUNT_FS_TYPE, 0);
if (!sm)
_exit(4);
if (!(sm->mask & STATMOUNT_MNT_POINT))
_exit(5);
if (strcmp(sm->str + sm->mnt_point, "/") != 0)
_exit(6);
if (!(sm->mask & STATMOUNT_MNT_BASIC))
_exit(7);
if (sm->mnt_id != root_id)
_exit(8);
free(sm);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/* Consecutive UNSHARE_EMPTY_MNTNS calls produce new namespaces */
TEST_F(empty_mntns, repeated_unshare)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
uint64_t first_root_id, second_root_id;
if (enter_userns())
_exit(1);
if (unshare(UNSHARE_EMPTY_MNTNS))
_exit(2);
if (count_mounts() != 1)
_exit(3);
first_root_id = get_unique_mnt_id("/");
if (unshare(UNSHARE_EMPTY_MNTNS))
_exit(4);
if (count_mounts() != 1)
_exit(5);
second_root_id = get_unique_mnt_id("/");
if (first_root_id == second_root_id)
_exit(6);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/* Root mount's parent is itself */
TEST_F(empty_mntns, root_is_own_parent)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
struct statmount sm;
uint64_t root_id;
if (enter_userns())
_exit(1);
if (unshare(UNSHARE_EMPTY_MNTNS))
_exit(2);
root_id = get_unique_mnt_id("/");
if (!root_id)
_exit(3);
if (statmount(root_id, 0, 0, STATMOUNT_MNT_BASIC, &sm, sizeof(sm), 0) < 0)
_exit(4);
if (!(sm.mask & STATMOUNT_MNT_BASIC))
_exit(5);
if (sm.mnt_parent_id != sm.mnt_id)
_exit(6);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/* Listmount returns only the root mount */
TEST_F(empty_mntns, listmount_single_entry)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
uint64_t list[16];
ssize_t nr_mounts;
uint64_t root_id;
if (enter_userns())
_exit(1);
if (unshare(UNSHARE_EMPTY_MNTNS))
_exit(2);
nr_mounts = listmount(LSMT_ROOT, 0, 0, list, 16, 0);
if (nr_mounts != 1)
_exit(3);
root_id = get_unique_mnt_id("/");
if (!root_id)
_exit(4);
if (list[0] != root_id)
_exit(5);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* Mount tmpfs over nullfs root to build a writable filesystem from scratch.
* This exercises the intended usage pattern: create an empty mount namespace
* (which has a nullfs root), then mount a real filesystem over it.
*
* Because resolving "/" returns the process root directly (via nd_jump_root)
* without following overmounts, we use the new mount API (fsopen/fsmount)
* to obtain a mount fd, then fchdir + chroot to enter the new filesystem.
*/
TEST_F(empty_mntns, overmount_tmpfs)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
struct statmount *sm;
uint64_t root_id, cwd_id;
int fd, fsfd, mntfd;
if (enter_userns())
_exit(1);
if (unshare(UNSHARE_EMPTY_MNTNS))
_exit(2);
if (count_mounts() != 1)
_exit(3);
root_id = get_unique_mnt_id("/");
if (!root_id)
_exit(4);
/* Verify root is nullfs */
sm = statmount_alloc(root_id, 0, STATMOUNT_FS_TYPE, 0);
if (!sm)
_exit(5);
if (!(sm->mask & STATMOUNT_FS_TYPE))
_exit(6);
if (strcmp(sm->str + sm->fs_type, "nullfs") != 0)
_exit(7);
free(sm);
cwd_id = get_unique_mnt_id(".");
if (!cwd_id || root_id != cwd_id)
_exit(8);
/*
* nullfs root is immutable. open(O_CREAT) returns ENOENT
* because empty_dir_lookup() returns -ENOENT before the
* IS_IMMUTABLE permission check in may_o_create() is reached.
*/
fd = open("/test", O_CREAT | O_RDWR, 0644);
if (fd >= 0) {
close(fd);
_exit(9);
}
if (errno != ENOENT)
_exit(10);
/*
* Use the new mount API to create tmpfs and get a mount fd.
* We need the fd because after attaching the tmpfs on top of
* "/", path resolution of "/" still returns the process root
* (nullfs) without following the overmount. The mount fd
* lets us fchdir + chroot into the tmpfs.
*/
fsfd = sys_fsopen("tmpfs", 0);
if (fsfd < 0)
_exit(11);
if (sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "size", "1M", 0)) {
close(fsfd);
_exit(12);
}
if (sys_fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) {
close(fsfd);
_exit(13);
}
mntfd = sys_fsmount(fsfd, 0, 0);
close(fsfd);
if (mntfd < 0)
_exit(14);
if (sys_move_mount(mntfd, "", AT_FDCWD, "/",
MOVE_MOUNT_F_EMPTY_PATH)) {
close(mntfd);
_exit(15);
}
if (count_mounts() != 2) {
close(mntfd);
_exit(16);
}
/* Enter the tmpfs via the mount fd */
if (fchdir(mntfd)) {
close(mntfd);
_exit(17);
}
if (chroot(".")) {
close(mntfd);
_exit(18);
}
close(mntfd);
/* Verify "/" now resolves to tmpfs */
root_id = get_unique_mnt_id("/");
if (!root_id)
_exit(19);
sm = statmount_alloc(root_id, 0, STATMOUNT_FS_TYPE, 0);
if (!sm)
_exit(20);
if (!(sm->mask & STATMOUNT_FS_TYPE))
_exit(21);
if (strcmp(sm->str + sm->fs_type, "tmpfs") != 0)
_exit(22);
free(sm);
/* Verify tmpfs is writable */
fd = open("/testfile", O_CREAT | O_RDWR, 0644);
if (fd < 0)
_exit(23);
if (write(fd, "test", 4) != 4) {
close(fd);
_exit(24);
}
close(fd);
if (access("/testfile", F_OK))
_exit(25);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/*
* Tests below do not require UNSHARE_EMPTY_MNTNS support.
*/
/* Invalid unshare flags return EINVAL */
TEST(invalid_flags)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
if (enter_userns())
_exit(1);
if (unshare(0x80000000) == 0)
_exit(2);
if (errno != EINVAL)
_exit(3);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/* Regular CLONE_NEWNS still copies the full mount tree */
TEST(clone_newns_full_copy)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
ssize_t nr_mounts_before, nr_mounts_after;
char tmpdir[] = "/tmp/empty_mntns_regr.XXXXXX";
int i;
if (enter_userns())
_exit(1);
if (unshare(CLONE_NEWNS))
_exit(2);
if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL))
_exit(3);
if (!mkdtemp(tmpdir))
_exit(4);
if (mount("tmpfs", tmpdir, "tmpfs", 0, "size=1M"))
_exit(5);
for (i = 0; i < 3; i++) {
char subdir[256];
snprintf(subdir, sizeof(subdir), "%s/sub%d", tmpdir, i);
if (mkdir(subdir, 0755) && errno != EEXIST)
_exit(6);
if (mount(subdir, subdir, NULL, MS_BIND, NULL))
_exit(7);
}
nr_mounts_before = count_mounts();
if (nr_mounts_before < 3)
_exit(8);
if (unshare(CLONE_NEWNS))
_exit(9);
nr_mounts_after = count_mounts();
if (nr_mounts_after < nr_mounts_before)
_exit(10);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
/* Other namespace unshares are unaffected */
TEST(other_ns_unaffected)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
char hostname[256];
if (enter_userns())
_exit(1);
if (unshare(CLONE_NEWUTS))
_exit(2);
if (sethostname("test-empty-mntns", 16))
_exit(3);
if (gethostname(hostname, sizeof(hostname)))
_exit(4);
if (strcmp(hostname, "test-empty-mntns") != 0)
_exit(5);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,225 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Test: rootfs overmounted multiple times with chroot into topmost
*
* This test creates a scenario where:
* 1. A new mount namespace is created with a tmpfs root (via pivot_root)
* 2. A mountpoint is created and overmounted multiple times
* 3. The caller chroots into the topmost mount layer
*
* The test verifies that:
* - Multiple overmounts create separate mount layers
* - Each layer's files are isolated
* - chroot correctly sets the process's root to the topmost layer
* - After chroot, only the topmost layer's files are visible
*
* Copyright (c) 2024 Christian Brauner <brauner@kernel.org>
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <linux/mount.h>
#include <linux/stat.h>
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "../utils.h"
#include "empty_mntns.h"
#include "kselftest_harness.h"
#define NR_OVERMOUNTS 5
/*
* Setup a proper root filesystem using pivot_root.
* This ensures we own the root directory in our user namespace.
*/
static int setup_root(void)
{
char tmpdir[] = "/tmp/overmount_test.XXXXXX";
char oldroot[256];
if (!mkdtemp(tmpdir))
return -1;
/* Mount tmpfs at the temporary directory */
if (mount("tmpfs", tmpdir, "tmpfs", 0, "size=10M"))
return -1;
/* Create directory for old root */
snprintf(oldroot, sizeof(oldroot), "%s/oldroot", tmpdir);
if (mkdir(oldroot, 0755))
return -1;
/* pivot_root to use the tmpfs as new root */
if (syscall(SYS_pivot_root, tmpdir, oldroot))
return -1;
if (chdir("/"))
return -1;
/* Unmount old root */
if (umount2("/oldroot", MNT_DETACH))
return -1;
/* Remove oldroot directory */
if (rmdir("/oldroot"))
return -1;
return 0;
}
/*
* Test scenario:
* 1. Enter a user namespace to gain CAP_SYS_ADMIN
* 2. Create a new mount namespace
* 3. Setup a tmpfs root via pivot_root
* 4. Create a mountpoint /newroot and overmount it multiple times
* 5. Create a marker file in each layer
* 6. Chroot into /newroot (the topmost overmount)
* 7. Verify we're in the topmost layer (only topmost marker visible)
*/
TEST(overmount_chroot)
{
pid_t pid;
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
ssize_t nr_mounts;
uint64_t mnt_ids[NR_OVERMOUNTS + 1];
uint64_t root_id_before, root_id_after;
struct statmount *sm;
char marker[64];
int fd, i;
/* Step 1: Enter user namespace for privileges */
if (enter_userns())
_exit(1);
/* Step 2: Create a new mount namespace */
if (unshare(CLONE_NEWNS))
_exit(2);
/* Step 3: Make the mount tree private */
if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL))
_exit(3);
/* Step 4: Setup a proper tmpfs root via pivot_root */
if (setup_root())
_exit(4);
/* Create the base mount point for overmounting */
if (mkdir("/newroot", 0755))
_exit(5);
/* Mount base tmpfs on /newroot */
if (mount("tmpfs", "/newroot", "tmpfs", 0, "size=1M"))
_exit(6);
/* Record base mount ID */
mnt_ids[0] = get_unique_mnt_id("/newroot");
if (!mnt_ids[0])
_exit(7);
/* Create marker in base layer */
fd = open("/newroot/layer_0", O_CREAT | O_RDWR, 0644);
if (fd < 0)
_exit(8);
if (write(fd, "layer_0", 7) != 7) {
close(fd);
_exit(9);
}
close(fd);
/* Step 5: Overmount /newroot multiple times with tmpfs */
for (i = 0; i < NR_OVERMOUNTS; i++) {
if (mount("tmpfs", "/newroot", "tmpfs", 0, "size=1M"))
_exit(10);
/* Record mount ID for this layer */
mnt_ids[i + 1] = get_unique_mnt_id("/newroot");
if (!mnt_ids[i + 1])
_exit(11);
/* Create a marker file in each layer */
snprintf(marker, sizeof(marker), "/newroot/layer_%d", i + 1);
fd = open(marker, O_CREAT | O_RDWR, 0644);
if (fd < 0)
_exit(12);
if (write(fd, marker, strlen(marker)) != (ssize_t)strlen(marker)) {
close(fd);
_exit(13);
}
close(fd);
}
/* Verify mount count increased */
nr_mounts = count_mounts();
if (nr_mounts < NR_OVERMOUNTS + 2)
_exit(14);
/* Record root mount ID before chroot */
root_id_before = get_unique_mnt_id("/newroot");
/* Verify this is the topmost layer's mount */
if (root_id_before != mnt_ids[NR_OVERMOUNTS])
_exit(15);
/* Step 6: Chroot into /newroot (the topmost overmount) */
if (chroot("/newroot"))
_exit(16);
/* Change to root directory within the chroot */
if (chdir("/"))
_exit(17);
/* Step 7: Verify we're in the topmost layer */
root_id_after = get_unique_mnt_id("/");
/* The mount ID should be the same as the topmost layer */
if (root_id_after != mnt_ids[NR_OVERMOUNTS])
_exit(18);
/* Verify the topmost layer's marker file exists */
snprintf(marker, sizeof(marker), "/layer_%d", NR_OVERMOUNTS);
if (access(marker, F_OK))
_exit(19);
/* Verify we cannot see markers from lower layers (they're hidden) */
for (i = 0; i < NR_OVERMOUNTS; i++) {
snprintf(marker, sizeof(marker), "/layer_%d", i);
if (access(marker, F_OK) == 0)
_exit(20);
}
/* Verify the root mount is tmpfs */
sm = statmount_alloc(root_id_after, 0,
STATMOUNT_MNT_BASIC | STATMOUNT_MNT_ROOT |
STATMOUNT_MNT_POINT | STATMOUNT_FS_TYPE, 0);
if (!sm)
_exit(21);
if (sm->mask & STATMOUNT_FS_TYPE) {
if (strcmp(sm->str + sm->fs_type, "tmpfs") != 0) {
free(sm);
_exit(22);
}
}
free(sm);
_exit(0);
}
ASSERT_EQ(wait_for_pid(pid), 0);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0-only
epoll_wakeup_test
@@ -0,0 +1,7 @@
# SPDX-License-Identifier: GPL-2.0
CFLAGS += $(KHDR_INCLUDES)
LDLIBS += -lpthread
TEST_GEN_PROGS := epoll_wakeup_test
include ../../lib.mk
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0-only
eventfd_test
@@ -0,0 +1,7 @@
# SPDX-License-Identifier: GPL-2.0
CFLAGS += $(KHDR_INCLUDES)
LDLIBS += -lpthread
TEST_GEN_PROGS := eventfd_test
include ../../lib.mk
@@ -0,0 +1,311 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <asm/unistd.h>
#include <linux/time_types.h>
#include <unistd.h>
#include <assert.h>
#include <signal.h>
#include <pthread.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include "kselftest_harness.h"
#define EVENTFD_TEST_ITERATIONS 100000UL
struct error {
int code;
char msg[512];
};
static int error_set(struct error *err, int code, const char *fmt, ...)
{
va_list args;
int r;
if (code == 0 || !err || err->code != 0)
return code;
err->code = code;
va_start(args, fmt);
r = vsnprintf(err->msg, sizeof(err->msg), fmt, args);
assert((size_t)r < sizeof(err->msg));
va_end(args);
return code;
}
static inline int sys_eventfd2(unsigned int count, int flags)
{
return syscall(__NR_eventfd2, count, flags);
}
TEST(eventfd_check_flag_rdwr)
{
int fd, flags;
fd = sys_eventfd2(0, 0);
ASSERT_GE(fd, 0);
flags = fcntl(fd, F_GETFL);
// The kernel automatically adds the O_RDWR flag.
EXPECT_EQ(flags, O_RDWR);
close(fd);
}
TEST(eventfd_check_flag_cloexec)
{
int fd, flags;
fd = sys_eventfd2(0, EFD_CLOEXEC);
ASSERT_GE(fd, 0);
flags = fcntl(fd, F_GETFD);
ASSERT_GT(flags, -1);
EXPECT_EQ(flags, FD_CLOEXEC);
close(fd);
}
TEST(eventfd_check_flag_nonblock)
{
int fd, flags;
fd = sys_eventfd2(0, EFD_NONBLOCK);
ASSERT_GE(fd, 0);
flags = fcntl(fd, F_GETFL);
ASSERT_GT(flags, -1);
EXPECT_EQ(flags & EFD_NONBLOCK, EFD_NONBLOCK);
EXPECT_EQ(flags & O_RDWR, O_RDWR);
close(fd);
}
TEST(eventfd_check_flag_cloexec_and_nonblock)
{
int fd, flags;
fd = sys_eventfd2(0, EFD_CLOEXEC|EFD_NONBLOCK);
ASSERT_GE(fd, 0);
flags = fcntl(fd, F_GETFL);
ASSERT_GT(flags, -1);
EXPECT_EQ(flags & EFD_NONBLOCK, EFD_NONBLOCK);
EXPECT_EQ(flags & O_RDWR, O_RDWR);
flags = fcntl(fd, F_GETFD);
ASSERT_GT(flags, -1);
EXPECT_EQ(flags, FD_CLOEXEC);
close(fd);
}
static inline void trim_newline(char *str)
{
char *pos = strrchr(str, '\n');
if (pos)
*pos = '\0';
}
static int verify_fdinfo(int fd, struct error *err, const char *prefix,
size_t prefix_len, const char *expect, ...)
{
char buffer[512] = {0, };
char path[512] = {0, };
va_list args;
FILE *f;
char *line = NULL;
size_t n = 0;
int found = 0;
int r;
va_start(args, expect);
r = vsnprintf(buffer, sizeof(buffer), expect, args);
assert((size_t)r < sizeof(buffer));
va_end(args);
snprintf(path, sizeof(path), "/proc/self/fdinfo/%d", fd);
f = fopen(path, "re");
if (!f)
return error_set(err, -1, "fdinfo open failed for %d", fd);
while (getline(&line, &n, f) != -1) {
char *val;
if (strncmp(line, prefix, prefix_len))
continue;
found = 1;
val = line + prefix_len;
r = strcmp(val, buffer);
if (r != 0) {
trim_newline(line);
trim_newline(buffer);
error_set(err, -1, "%s '%s' != '%s'",
prefix, val, buffer);
}
break;
}
free(line);
fclose(f);
if (found == 0)
return error_set(err, -1, "%s not found for fd %d",
prefix, fd);
return 0;
}
TEST(eventfd_check_flag_semaphore)
{
struct error err = {0};
int fd, ret;
fd = sys_eventfd2(0, EFD_SEMAPHORE);
ASSERT_GE(fd, 0);
ret = fcntl(fd, F_GETFL);
ASSERT_GT(ret, -1);
EXPECT_EQ(ret & O_RDWR, O_RDWR);
// The semaphore could only be obtained from fdinfo.
ret = verify_fdinfo(fd, &err, "eventfd-semaphore: ", 19, "1\n");
if (ret != 0)
ksft_print_msg("eventfd semaphore flag check failed: %s\n", err.msg);
EXPECT_EQ(ret, 0);
close(fd);
}
/*
* A write(2) fails with the error EINVAL if the size of the supplied buffer
* is less than 8 bytes, or if an attempt is made to write the value
* 0xffffffffffffffff.
*/
TEST(eventfd_check_write)
{
uint64_t value = 1;
ssize_t size;
int fd;
fd = sys_eventfd2(0, 0);
ASSERT_GE(fd, 0);
size = write(fd, &value, sizeof(int));
EXPECT_EQ(size, -1);
EXPECT_EQ(errno, EINVAL);
size = write(fd, &value, sizeof(value));
EXPECT_EQ(size, sizeof(value));
value = (uint64_t)-1;
size = write(fd, &value, sizeof(value));
EXPECT_EQ(size, -1);
EXPECT_EQ(errno, EINVAL);
close(fd);
}
/*
* A read(2) fails with the error EINVAL if the size of the supplied buffer is
* less than 8 bytes.
*/
TEST(eventfd_check_read)
{
uint64_t value;
ssize_t size;
int fd;
fd = sys_eventfd2(1, 0);
ASSERT_GE(fd, 0);
size = read(fd, &value, sizeof(int));
EXPECT_EQ(size, -1);
EXPECT_EQ(errno, EINVAL);
size = read(fd, &value, sizeof(value));
EXPECT_EQ(size, sizeof(value));
EXPECT_EQ(value, 1);
close(fd);
}
/*
* If EFD_SEMAPHORE was not specified and the eventfd counter has a nonzero
* value, then a read(2) returns 8 bytes containing that value, and the
* counter's value is reset to zero.
* If the eventfd counter is zero at the time of the call to read(2), then the
* call fails with the error EAGAIN if the file descriptor has been made nonblocking.
*/
TEST(eventfd_check_read_with_nonsemaphore)
{
uint64_t value;
ssize_t size;
int fd;
int i;
fd = sys_eventfd2(0, EFD_NONBLOCK);
ASSERT_GE(fd, 0);
value = 1;
for (i = 0; i < EVENTFD_TEST_ITERATIONS; i++) {
size = write(fd, &value, sizeof(value));
EXPECT_EQ(size, sizeof(value));
}
size = read(fd, &value, sizeof(value));
EXPECT_EQ(size, sizeof(uint64_t));
EXPECT_EQ(value, EVENTFD_TEST_ITERATIONS);
size = read(fd, &value, sizeof(value));
EXPECT_EQ(size, -1);
EXPECT_EQ(errno, EAGAIN);
close(fd);
}
/*
* If EFD_SEMAPHORE was specified and the eventfd counter has a nonzero value,
* then a read(2) returns 8 bytes containing the value 1, and the counter's
* value is decremented by 1.
* If the eventfd counter is zero at the time of the call to read(2), then the
* call fails with the error EAGAIN if the file descriptor has been made nonblocking.
*/
TEST(eventfd_check_read_with_semaphore)
{
uint64_t value;
ssize_t size;
int fd;
int i;
fd = sys_eventfd2(0, EFD_SEMAPHORE|EFD_NONBLOCK);
ASSERT_GE(fd, 0);
value = 1;
for (i = 0; i < EVENTFD_TEST_ITERATIONS; i++) {
size = write(fd, &value, sizeof(value));
EXPECT_EQ(size, sizeof(value));
}
for (i = 0; i < EVENTFD_TEST_ITERATIONS; i++) {
size = read(fd, &value, sizeof(value));
EXPECT_EQ(size, sizeof(value));
EXPECT_EQ(value, 1);
}
size = read(fd, &value, sizeof(value));
EXPECT_EQ(size, -1);
EXPECT_EQ(errno, EAGAIN);
close(fd);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0-only
rename_exchange
@@ -0,0 +1,7 @@
# SPDX-License-Identifier: GPL-2.0
TEST_PROGS := run_fat_tests.sh
TEST_GEN_PROGS_EXTENDED := rename_exchange
CFLAGS += -O2 -g -Wall $(KHDR_INCLUDES)
include ../../lib.mk
@@ -0,0 +1,2 @@
CONFIG_BLK_DEV_LOOP=y
CONFIG_VFAT_FS=y
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Program that atomically exchanges two paths using
* the renameat2() system call RENAME_EXCHANGE flag.
*
* Copyright 2022 Red Hat Inc.
* Author: Javier Martinez Canillas <javierm@redhat.com>
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
void print_usage(const char *program)
{
printf("Usage: %s [oldpath] [newpath]\n", program);
printf("Atomically exchange oldpath and newpath\n");
}
int main(int argc, char *argv[])
{
int ret;
if (argc != 3) {
print_usage(argv[0]);
exit(EXIT_FAILURE);
}
ret = renameat2(AT_FDCWD, argv[1], AT_FDCWD, argv[2], RENAME_EXCHANGE);
if (ret) {
perror("rename exchange failed");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
@@ -0,0 +1,82 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Run filesystem operations tests on an 1 MiB disk image that is formatted with
# a vfat filesystem and mounted in a temporary directory using a loop device.
#
# Copyright 2022 Red Hat Inc.
# Author: Javier Martinez Canillas <javierm@redhat.com>
set -e
set -u
set -o pipefail
BASE_DIR="$(dirname $0)"
TMP_DIR="$(mktemp -d /tmp/fat_tests_tmp.XXXXXX)"
IMG_PATH="${TMP_DIR}/fat.img"
MNT_PATH="${TMP_DIR}/mnt"
cleanup()
{
mountpoint -q "${MNT_PATH}" && unmount_image
rm -rf "${TMP_DIR}"
}
trap cleanup SIGINT SIGTERM EXIT
create_loopback()
{
touch "${IMG_PATH}"
chattr +C "${IMG_PATH}" >/dev/null 2>&1 || true
truncate -s 1M "${IMG_PATH}"
mkfs.vfat "${IMG_PATH}" >/dev/null 2>&1
}
mount_image()
{
mkdir -p "${MNT_PATH}"
sudo mount -o loop "${IMG_PATH}" "${MNT_PATH}"
}
rename_exchange_test()
{
local rename_exchange="${BASE_DIR}/rename_exchange"
local old_path="${MNT_PATH}/old_file"
local new_path="${MNT_PATH}/new_file"
echo old | sudo tee "${old_path}" >/dev/null 2>&1
echo new | sudo tee "${new_path}" >/dev/null 2>&1
sudo "${rename_exchange}" "${old_path}" "${new_path}" >/dev/null 2>&1
sudo sync -f "${MNT_PATH}"
grep new "${old_path}" >/dev/null 2>&1
grep old "${new_path}" >/dev/null 2>&1
}
rename_exchange_subdir_test()
{
local rename_exchange="${BASE_DIR}/rename_exchange"
local dir_path="${MNT_PATH}/subdir"
local old_path="${MNT_PATH}/old_file"
local new_path="${dir_path}/new_file"
sudo mkdir -p "${dir_path}"
echo old | sudo tee "${old_path}" >/dev/null 2>&1
echo new | sudo tee "${new_path}" >/dev/null 2>&1
sudo "${rename_exchange}" "${old_path}" "${new_path}" >/dev/null 2>&1
sudo sync -f "${MNT_PATH}"
grep new "${old_path}" >/dev/null 2>&1
grep old "${new_path}" >/dev/null 2>&1
}
unmount_image()
{
sudo umount "${MNT_PATH}" &> /dev/null
}
create_loopback
mount_image
rename_exchange_test
rename_exchange_subdir_test
unmount_image
exit 0
@@ -0,0 +1,128 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Author: Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2025 SUSE LLC.
*/
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <sys/mount.h>
#include "kselftest_harness.h"
#define ASSERT_ERRNO(expected, _t, seen) \
__EXPECT(expected, #expected, \
({__typeof__(seen) _tmp_seen = (seen); \
_tmp_seen >= 0 ? _tmp_seen : -errno; }), #seen, _t, 1)
#define ASSERT_ERRNO_EQ(expected, seen) \
ASSERT_ERRNO(expected, ==, seen)
#define ASSERT_SUCCESS(seen) \
ASSERT_ERRNO(0, <=, seen)
FIXTURE(ns)
{
int host_mntns;
};
FIXTURE_SETUP(ns)
{
/* Stash the old mntns. */
self->host_mntns = open("/proc/self/ns/mnt", O_RDONLY|O_CLOEXEC);
ASSERT_SUCCESS(self->host_mntns);
/* Create a new mount namespace and make it private. */
ASSERT_SUCCESS(unshare(CLONE_NEWNS));
ASSERT_SUCCESS(mount(NULL, "/", NULL, MS_PRIVATE|MS_REC, NULL));
}
FIXTURE_TEARDOWN(ns)
{
ASSERT_SUCCESS(setns(self->host_mntns, CLONE_NEWNS));
ASSERT_SUCCESS(close(self->host_mntns));
}
TEST_F(ns, fscontext_log_enodata)
{
int fsfd = fsopen("tmpfs", FSOPEN_CLOEXEC);
ASSERT_SUCCESS(fsfd);
/* A brand new fscontext has no log entries. */
char buf[128] = {};
for (int i = 0; i < 16; i++)
ASSERT_ERRNO_EQ(-ENODATA, read(fsfd, buf, sizeof(buf)));
ASSERT_SUCCESS(close(fsfd));
}
TEST_F(ns, fscontext_log_errorfc)
{
int fsfd = fsopen("tmpfs", FSOPEN_CLOEXEC);
ASSERT_SUCCESS(fsfd);
ASSERT_ERRNO_EQ(-EINVAL, fsconfig(fsfd, FSCONFIG_SET_STRING, "invalid-arg", "123", 0));
char buf[128] = {};
ASSERT_SUCCESS(read(fsfd, buf, sizeof(buf)));
EXPECT_STREQ("e tmpfs: Unknown parameter 'invalid-arg'\n", buf);
/* The message has been consumed. */
ASSERT_ERRNO_EQ(-ENODATA, read(fsfd, buf, sizeof(buf)));
ASSERT_SUCCESS(close(fsfd));
}
TEST_F(ns, fscontext_log_errorfc_after_fsmount)
{
int fsfd = fsopen("tmpfs", FSOPEN_CLOEXEC);
ASSERT_SUCCESS(fsfd);
ASSERT_ERRNO_EQ(-EINVAL, fsconfig(fsfd, FSCONFIG_SET_STRING, "invalid-arg", "123", 0));
ASSERT_SUCCESS(fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0));
int mfd = fsmount(fsfd, FSMOUNT_CLOEXEC, MOUNT_ATTR_NOEXEC | MOUNT_ATTR_NOSUID);
ASSERT_SUCCESS(mfd);
ASSERT_SUCCESS(move_mount(mfd, "", AT_FDCWD, "/tmp", MOVE_MOUNT_F_EMPTY_PATH));
/*
* The fscontext log should still contain data even after
* FSCONFIG_CMD_CREATE and fsmount().
*/
char buf[128] = {};
ASSERT_SUCCESS(read(fsfd, buf, sizeof(buf)));
EXPECT_STREQ("e tmpfs: Unknown parameter 'invalid-arg'\n", buf);
/* The message has been consumed. */
ASSERT_ERRNO_EQ(-ENODATA, read(fsfd, buf, sizeof(buf)));
ASSERT_SUCCESS(close(fsfd));
}
TEST_F(ns, fscontext_log_emsgsize)
{
int fsfd = fsopen("tmpfs", FSOPEN_CLOEXEC);
ASSERT_SUCCESS(fsfd);
ASSERT_ERRNO_EQ(-EINVAL, fsconfig(fsfd, FSCONFIG_SET_STRING, "invalid-arg", "123", 0));
char buf[128] = {};
/*
* Attempting to read a message with too small a buffer should not
* result in the message getting consumed.
*/
ASSERT_ERRNO_EQ(-EMSGSIZE, read(fsfd, buf, 0));
ASSERT_ERRNO_EQ(-EMSGSIZE, read(fsfd, buf, 1));
for (int i = 0; i < 16; i++)
ASSERT_ERRNO_EQ(-EMSGSIZE, read(fsfd, buf, 16));
ASSERT_SUCCESS(read(fsfd, buf, sizeof(buf)));
EXPECT_STREQ("e tmpfs: Unknown parameter 'invalid-arg'\n", buf);
/* The message has been consumed. */
ASSERT_ERRNO_EQ(-ENODATA, read(fsfd, buf, sizeof(buf)));
ASSERT_SUCCESS(close(fsfd));
}
TEST_HARNESS_MAIN
@@ -0,0 +1,194 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#define __SANE_USERSPACE_TYPES__
#include <fcntl.h>
#include <limits.h>
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <unistd.h>
#include "kselftest_harness.h"
#include <linux/types.h>
#include <linux/mount.h>
#include <sys/syscall.h>
static inline int sys_fsopen(const char *fsname, unsigned int flags)
{
return syscall(__NR_fsopen, fsname, flags);
}
static inline int sys_fsconfig(int fd, unsigned int cmd, const char *key,
const char *value, int aux)
{
return syscall(__NR_fsconfig, fd, cmd, key, value, aux);
}
static inline int sys_fsmount(int fd, unsigned int flags,
unsigned int attr_flags)
{
return syscall(__NR_fsmount, fd, flags, attr_flags);
}
#ifndef MOVE_MOUNT_F_EMPTY_PATH
#define MOVE_MOUNT_F_EMPTY_PATH 0x00000004 /* Empty from path permitted */
#endif
static inline int sys_move_mount(int from_dfd, const char *from_pathname,
int to_dfd, const char *to_pathname,
unsigned int flags)
{
return syscall(__NR_move_mount, from_dfd, from_pathname, to_dfd,
to_pathname, flags);
}
FIXTURE(file_stressor) {
int fd_tmpfs;
int nr_procs;
int max_fds;
pid_t *pids_openers;
pid_t *pids_getdents;
int *fd_proc_pid;
};
FIXTURE_SETUP(file_stressor)
{
int fd_context;
ASSERT_EQ(unshare(CLONE_NEWNS), 0);
ASSERT_EQ(mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0);
ASSERT_EQ(mkdir("/slab_typesafe_by_rcu", 0755), 0);
fd_context = sys_fsopen("tmpfs", 0);
ASSERT_GE(fd_context, 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
self->fd_tmpfs = sys_fsmount(fd_context, 0, 0);
ASSERT_GE(self->fd_tmpfs, 0);
ASSERT_EQ(close(fd_context), 0);
ASSERT_EQ(sys_move_mount(self->fd_tmpfs, "", -EBADF, "/slab_typesafe_by_rcu", MOVE_MOUNT_F_EMPTY_PATH), 0);
self->nr_procs = sysconf(_SC_NPROCESSORS_ONLN);
self->pids_openers = malloc(sizeof(pid_t) * self->nr_procs);
ASSERT_NE(self->pids_openers, NULL);
self->pids_getdents = malloc(sizeof(pid_t) * self->nr_procs);
ASSERT_NE(self->pids_getdents, NULL);
self->fd_proc_pid = malloc(sizeof(int) * self->nr_procs);
ASSERT_NE(self->fd_proc_pid, NULL);
self->max_fds = 500;
}
FIXTURE_TEARDOWN(file_stressor)
{
for (int i = 0; i < self->nr_procs; i++) {
int wstatus;
pid_t pid;
pid = waitpid(self->pids_openers[i], &wstatus, 0);
ASSERT_EQ(pid, self->pids_openers[i]);
ASSERT_TRUE(!WIFEXITED(wstatus) || !WIFSIGNALED(wstatus));
pid = waitpid(self->pids_getdents[i], &wstatus, 0);
ASSERT_EQ(pid, self->pids_getdents[i]);
ASSERT_TRUE(!WIFEXITED(wstatus) || !WIFSIGNALED(wstatus));
}
free(self->pids_openers);
free(self->pids_getdents);
ASSERT_EQ(close(self->fd_tmpfs), 0);
umount2("/slab_typesafe_by_rcu", 0);
ASSERT_EQ(rmdir("/slab_typesafe_by_rcu"), 0);
}
TEST_F_TIMEOUT(file_stressor, slab_typesafe_by_rcu, 900 * 2)
{
for (int i = 0; i < self->nr_procs; i++) {
pid_t pid_self;
self->pids_openers[i] = fork();
ASSERT_GE(self->pids_openers[i], 0);
if (self->pids_openers[i] != 0)
continue;
self->pids_openers[i] = getpid();
for (;;) {
for (int i = 0; i < self->max_fds; i++) {
char path[PATH_MAX];
int fd;
sprintf(path, "/slab_typesafe_by_rcu/file-%d-%d", self->pids_openers[i], i);
fd = open(path, O_CREAT | O_RDONLY | O_CLOEXEC, 0644);
if (fd < 0)
continue;
}
close_range(3, ~0U, 0);
}
exit(0);
}
for (int i = 0; i < self->nr_procs; i++) {
char path[PATH_MAX];
sprintf(path, "/proc/%d/fd/", self->pids_openers[i]);
self->fd_proc_pid[i] = open(path, O_DIRECTORY | O_RDONLY | O_CLOEXEC);
ASSERT_GE(self->fd_proc_pid[i], 0);
}
for (int i = 0; i < self->nr_procs; i++) {
self->pids_getdents[i] = fork();
ASSERT_GE(self->pids_getdents[i], 0);
if (self->pids_getdents[i] != 0)
continue;
self->pids_getdents[i] = getpid();
for (;;) {
char ents[1024];
ssize_t nr_read;
/*
* Concurrently read /proc/<pid>/fd/ which roughly does:
*
* f = fget_task_next(p, &fd);
* if (!f)
* break;
* data.mode = f->f_mode;
* fput(f);
*
* Which means that it'll try to get a reference to a
* file in another task's file descriptor table.
*
* Under heavy file load it is increasingly likely that
* the other task will manage to close @file and @file
* is being recycled due to SLAB_TYPEAFE_BY_RCU
* concurrently. This will trigger various warnings in
* the file reference counting code.
*/
do {
nr_read = syscall(SYS_getdents64, self->fd_proc_pid[i], ents, sizeof(ents));
} while (nr_read >= 0);
lseek(self->fd_proc_pid[i], 0, SEEK_SET);
}
exit(0);
}
ASSERT_EQ(clock_nanosleep(CLOCK_MONOTONIC, 0, &(struct timespec){ .tv_sec = 900 /* 15 min */ }, NULL), 0);
for (int i = 0; i < self->nr_procs; i++) {
kill(self->pids_openers[i], SIGKILL);
kill(self->pids_getdents[i], SIGKILL);
}
}
TEST_HARNESS_MAIN
@@ -0,0 +1,10 @@
# SPDX-License-Identifier: GPL-2.0
TEST_GEN_PROGS := fscontext_ns_test
CFLAGS += -Wall -O0 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES)
LDLIBS := -lcap
include ../../lib.mk
$(OUTPUT)/fscontext_ns_test: fscontext_ns_test.c ../utils.c
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
@@ -0,0 +1,239 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2026 Christian Brauner <brauner@kernel.org>
*
* Test that completing a filesystem context from another user namespace
* doesn't warn.
*
* fsopen() records the caller's user namespace in fc->user_ns and hands
* back an ordinary file descriptor. The task that issues
* FSCONFIG_CMD_CREATE need not be the one that created the context: the fd
* is inherited across fork() and exec() and it can be passed over a unix
* socket. vfs_cmd_create() authorizes the create with mount_capable(),
* which for FS_USERNS_MOUNT checks ns_capable(fc->user_ns, CAP_SYS_ADMIN),
* and that succeeds for a task holding CAP_SYS_ADMIN in an ancestor of
* fc->user_ns.
*
* binfmt_misc and overlayfs used to WARN_ON() that mismatch, which let an
* unprivileged user taint the kernel, flood the log and panic a kernel
* booted with panic_on_warn. The mount must still be refused, but it must
* not warn.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include "../wrappers.h"
#include "../utils.h"
#include "../../kselftest_harness.h"
#ifndef FSCONFIG_CMD_CREATE
#define FSCONFIG_CMD_CREATE 6
#endif
/* TAINT_WARN, i.e. bit 9 of /proc/sys/kernel/tainted. */
#define TAINT_WARN_BIT 9
static bool taint_warn_set(void)
{
unsigned long taint = 0;
FILE *f;
f = fopen("/proc/sys/kernel/tainted", "r");
if (!f)
return false;
if (fscanf(f, "%lu", &taint) != 1)
taint = 0;
fclose(f);
return taint & (1UL << TAINT_WARN_BIT);
}
static int send_fd(int sock, int fd)
{
char cmsgbuf[CMSG_SPACE(sizeof(int))] = {};
char b[1] = { 'x' };
struct iovec iov = { .iov_base = b, .iov_len = sizeof(b) };
struct msghdr msg = {
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = cmsgbuf,
.msg_controllen = sizeof(cmsgbuf),
};
struct cmsghdr *cmsg;
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(cmsg), &fd, sizeof(int));
return sendmsg(sock, &msg, 0) < 0 ? -1 : 0;
}
static int recv_fd(int sock)
{
char cmsgbuf[CMSG_SPACE(sizeof(int))] = {};
char b[1];
struct iovec iov = { .iov_base = b, .iov_len = sizeof(b) };
struct msghdr msg = {
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = cmsgbuf,
.msg_controllen = sizeof(cmsgbuf),
};
struct cmsghdr *cmsg;
int fd = -1;
if (recvmsg(sock, &msg, 0) <= 0)
return -1;
cmsg = CMSG_FIRSTHDR(&msg);
if (!cmsg || cmsg->cmsg_type != SCM_RIGHTS)
return -1;
memcpy(&fd, CMSG_DATA(cmsg), sizeof(int));
return fd;
}
/*
* Create a context for @fsname in a child and complete it here. With @nest
* the child first creates its own user namespace, so that the context is
* created in a descendant of the namespace completing it. The child needs a
* mount namespace of its own as well: fsopen() gates on may_mount(), which
* asks for CAP_SYS_ADMIN in the user namespace owning the caller's mount
* namespace.
*
* Returns the result of FSCONFIG_CMD_CREATE with errno set, or -ENODATA if
* the child could not create the context at all.
*/
static int create_from_child(const char *fsname, bool nest)
{
int sock[2], fd, ret, status;
pid_t pid;
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sock))
return -ENODATA;
pid = fork();
if (pid < 0) {
close(sock[0]);
close(sock[1]);
return -ENODATA;
}
if (pid == 0) {
close(sock[0]);
if (nest && unshare(CLONE_NEWUSER | CLONE_NEWNS))
_exit(1);
fd = sys_fsopen(fsname, 0);
if (fd < 0)
_exit(1);
if (send_fd(sock[1], fd))
_exit(1);
_exit(0);
}
close(sock[1]);
fd = recv_fd(sock[0]);
close(sock[0]);
wait_for_pid(pid);
waitpid(pid, &status, WNOHANG);
if (fd < 0)
return -ENODATA;
errno = 0;
ret = sys_fsconfig(fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0);
status = errno;
close(fd);
errno = status;
return ret;
}
FIXTURE(fscontext_ns) {
bool warn_before;
};
FIXTURE_SETUP(fscontext_ns)
{
self->warn_before = taint_warn_set();
if (setup_userns() != 0)
SKIP(return, "setup_userns failed");
}
FIXTURE_TEARDOWN(fscontext_ns)
{
}
/*
* The condition the kernel used to WARN about. It has to be refused, and it
* has to be refused quietly: an unprivileged task reaches this.
*/
FIXTURE_VARIANT(fscontext_ns) {
const char *fsname;
int expected_errno;
};
FIXTURE_VARIANT_ADD(fscontext_ns, binfmt_misc) {
.fsname = "binfmt_misc",
.expected_errno = EINVAL,
};
FIXTURE_VARIANT_ADD(fscontext_ns, overlay) {
.fsname = "overlay",
.expected_errno = EIO,
};
TEST_F(fscontext_ns, create_from_descendant_userns)
{
int ret;
ret = create_from_child(variant->fsname, true);
if (ret == -ENODATA)
SKIP(return, "%s unavailable", variant->fsname);
ASSERT_EQ(-1, ret);
ASSERT_EQ(variant->expected_errno, errno);
/*
* Only meaningful if nothing had warned before us. Note that an
* unrelated warning racing this test would look like a failure.
*/
if (self->warn_before)
TH_LOG("TAINT_WARN already set, not checking for a new warning");
else
ASSERT_FALSE(taint_warn_set());
}
/*
* The same handover within one user namespace is a supported thing to do and
* has to keep working. binfmt_misc takes no options, so the create succeeds
* outright and this also shows the test really drives the create path.
*/
TEST(create_from_same_userns)
{
int ret;
if (setup_userns() != 0)
SKIP(return, "setup_userns failed");
ret = create_from_child("binfmt_misc", false);
if (ret == -ENODATA)
SKIP(return, "binfmt_misc unavailable");
ASSERT_EQ(0, ret);
}
TEST_HARNESS_MAIN
@@ -0,0 +1 @@
fsmount_ns_test
@@ -0,0 +1,10 @@
# SPDX-License-Identifier: GPL-2.0
TEST_GEN_PROGS := fsmount_ns_test
CFLAGS += -Wall -O0 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES)
LDLIBS := -lcap
include ../../lib.mk
$(OUTPUT)/fsmount_ns_test: fsmount_ns_test.c ../utils.c
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,3 @@
# SPDX-License-Identifier: GPL-2.0-only
fuse_mnt
fusectl_test
@@ -0,0 +1,31 @@
# SPDX-License-Identifier: GPL-2.0-or-later
CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
TEST_GEN_PROGS := fusectl_test
TEST_GEN_FILES := fuse_mnt
# fuse_acl_cache_test requires libfuse3; add it only when the library is present.
ACL_CFLAGS := $(shell pkg-config fuse3 --cflags 2>/dev/null)
ACL_LDLIBS := $(shell pkg-config fuse3 --libs 2>/dev/null)
ifneq ($(ACL_CFLAGS),)
TEST_GEN_PROGS += fuse_acl_cache_test
endif
include ../../lib.mk
VAR_CFLAGS := $(shell pkg-config fuse --cflags 2>/dev/null)
ifeq ($(VAR_CFLAGS),)
VAR_CFLAGS := -D_FILE_OFFSET_BITS=64 -I/usr/include/fuse
endif
VAR_LDLIBS := $(shell pkg-config fuse --libs 2>/dev/null)
ifeq ($(VAR_LDLIBS),)
VAR_LDLIBS := -lfuse -pthread
endif
$(OUTPUT)/fuse_mnt: CFLAGS += $(VAR_CFLAGS)
$(OUTPUT)/fuse_mnt: LDLIBS += $(VAR_LDLIBS)
$(OUTPUT)/fuse_acl_cache_test: CFLAGS += $(ACL_CFLAGS)
$(OUTPUT)/fuse_acl_cache_test: LDLIBS += $(ACL_LDLIBS)
@@ -0,0 +1,347 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Test: FUSE ACL caching bug triggered by AT_STATX_FORCE_SYNC
*
* A FUSE mount that does not negotiate FUSE_POSIX_ACL initialises every inode
* with i_acl = i_default_acl = ACL_DONT_CACHE. When a fresh stat is needed
* (e.g. AT_STATX_FORCE_SYNC), fuse_update_get_attr() calls
* forget_all_cached_acls() before issuing FUSE_GETATTR. On an unfixed kernel,
* __forget_cached_acl() replaces ACL_DONT_CACHE with ACL_NOT_CACHED,
* inadvertently enabling the kernel ACL cache for that inode. The next
* getxattr populates the cache. Because fuse_set_acl() skips
* forget_all_cached_acls() for !fc->posix_acl mounts, any subsequent change to
* the ACL leaves the stale kernel entry in place, and the next getxattr returns
* wrong data without ever reaching the FUSE daemon.
*
* Fix (fs/posix_acl.c): __forget_cached_acl() returns early when *p is
* ACL_DONT_CACHE, preserving the "never cache" invariant for the inode's
* lifetime.
*
* Test outline:
* 1. Mount a minimal FUSE fs (no FUSE_POSIX_ACL negotiated).
* 2. lgetxattr -> daemon called, ACL_A returned, NOT cached (ACL_DONT_CACHE).
* 3. statx(AT_STATX_FORCE_SYNC) -> forget_all_cached_acls() called.
* Buggy: ACL_DONT_CACHE -> ACL_NOT_CACHED (cache enabled).
* Fixed: ACL_DONT_CACHE preserved.
* 4. lgetxattr -> daemon called, ACL_A returned.
* Buggy: result now cached (ACL_NOT_CACHED -> cached).
* Fixed: result still not cached.
* 5. Daemon switches to ACL_B internally (different size).
* 6. lgetxattr -> should return ACL_B (44 bytes).
* Buggy: cache hit, returns stale ACL_A (28 bytes). FAIL.
* Fixed: no cache, daemon called, returns ACL_B (44 bytes). PASS.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/limits.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/xattr.h>
#include <unistd.h>
#define FUSE_USE_VERSION 31
#include <fuse_lowlevel.h>
#include "kselftest_harness.h"
/* ---- ACL binary encoding ------------------------------------------------ */
/*
* POSIX ACL v2 xattr format (little-endian):
* header: u32 version (= 0x00000002)
* entry: u16 tag | u16 perm | u32 id
*
* Entries must appear in tag-ascending order; named USER/GROUP entries
* require a MASK entry. Both ACLs pass posix_acl_from_xattr() validation.
*/
/* ACL_A: 3 entries (USER_OBJ:rwx, GROUP_OBJ:r-x, OTHER:r-x) = 28 bytes */
static const uint8_t acl_a[] = {
0x02, 0x00, 0x00, 0x00, /* v2 header */
0x01, 0x00, 0x07, 0x00, 0xff, 0xff, 0xff, 0xff, /* USER_OBJ rwx */
0x04, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff, /* GROUP_OBJ r-x */
0x20, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff, /* OTHER r-x */
};
/*
* ACL_B: 5 entries — adds USER uid=1 and MASK = 44 bytes.
* A named USER entry requires a MASK; all tags in ascending order.
*/
static const uint8_t acl_b[] = {
0x02, 0x00, 0x00, 0x00, /* v2 header */
0x01, 0x00, 0x07, 0x00, 0xff, 0xff, 0xff, 0xff, /* USER_OBJ rwx */
0x02, 0x00, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00, /* USER uid=1 rwx */
0x04, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff, /* GROUP_OBJ r-x */
0x10, 0x00, 0x07, 0x00, 0xff, 0xff, 0xff, 0xff, /* MASK rwx */
0x20, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff, /* OTHER r-x */
};
/* ---- Shared state (daemon thread <-> test thread) ----------------------- */
#define FILE_INO 2
#define FILE_NAME "testfile"
struct daemon_state {
pthread_mutex_t lock;
const uint8_t *acl;
size_t acl_size;
int getxattr_count;
};
/*
* Global: callbacks are stateless fns so we use a single global.
* Safe because only one test instance runs at a time.
*/
static struct daemon_state g_ds = {
.lock = PTHREAD_MUTEX_INITIALIZER,
};
/* ---- FUSE lowlevel callbacks -------------------------------------------- */
static void fs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name)
{
if (parent != FUSE_ROOT_ID || strcmp(name, FILE_NAME)) {
fuse_reply_err(req, ENOENT);
return;
}
struct fuse_entry_param e = {};
/*
* Long attr/entry timeouts so that normal stat() calls do not
* expire and trigger forget_all_cached_acls() on their own;
* only the explicit AT_STATX_FORCE_SYNC should trigger it.
*/
e.ino = FILE_INO;
e.generation = 1;
e.attr_timeout = 10.0;
e.entry_timeout = 10.0;
e.attr.st_ino = FILE_INO;
e.attr.st_mode = S_IFREG | 0644;
e.attr.st_nlink = 1;
fuse_reply_entry(req, &e);
}
static void fs_getattr(fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi)
{
struct stat st = {};
(void)fi;
if (ino == FUSE_ROOT_ID) {
st.st_ino = FUSE_ROOT_ID;
st.st_mode = S_IFDIR | 0755;
st.st_nlink = 2;
} else if (ino == FILE_INO) {
st.st_ino = FILE_INO;
st.st_mode = S_IFREG | 0644;
st.st_nlink = 1;
} else {
fuse_reply_err(req, ENOENT);
return;
}
fuse_reply_attr(req, &st, 10);
}
static void fs_getxattr(fuse_req_t req, fuse_ino_t ino, const char *name,
size_t size)
{
if (ino != FILE_INO ||
strcmp(name, "system.posix_acl_access") != 0) {
fuse_reply_err(req, ENODATA);
return;
}
pthread_mutex_lock(&g_ds.lock);
const uint8_t *acl = g_ds.acl;
size_t acl_size = g_ds.acl_size;
g_ds.getxattr_count++;
pthread_mutex_unlock(&g_ds.lock);
if (size == 0)
fuse_reply_xattr(req, acl_size);
else if (size < acl_size)
fuse_reply_err(req, ERANGE);
else
fuse_reply_buf(req, (const char *)acl, acl_size);
}
static const struct fuse_lowlevel_ops fs_ops = {
.lookup = fs_lookup,
.getattr = fs_getattr,
.getxattr = fs_getxattr,
};
/* ---- Daemon thread ------------------------------------------------------- */
static void *run_daemon(void *arg)
{
fuse_session_loop((struct fuse_session *)arg);
return NULL;
}
/* ---- kselftest harness --------------------------------------------------- */
FIXTURE(acl_cache) {
struct fuse_session *se;
char mountpoint[PATH_MAX];
char file_path[PATH_MAX];
pthread_t thread;
};
FIXTURE_SETUP(acl_cache)
{
char *fuse_argv[] = { "fuse_acl_cache_test", NULL };
struct fuse_args args = FUSE_ARGS_INIT(1, fuse_argv);
g_ds.acl = acl_a;
g_ds.acl_size = sizeof(acl_a);
g_ds.getxattr_count = 0;
strcpy(self->mountpoint, "/tmp/acl_cache_test_XXXXXX");
if (!mkdtemp(self->mountpoint))
SKIP(return, "mkdtemp: %s", strerror(errno));
snprintf(self->file_path, sizeof(self->file_path),
"%s/" FILE_NAME, self->mountpoint);
self->se = fuse_session_new(&args, &fs_ops, sizeof(fs_ops), NULL);
if (!self->se) {
rmdir(self->mountpoint);
SKIP(return, "fuse_session_new failed");
}
if (fuse_session_mount(self->se, self->mountpoint)) {
fuse_session_destroy(self->se);
rmdir(self->mountpoint);
SKIP(return, "fuse_session_mount failed "
"(missing fusermount3 or insufficient privileges)");
}
if (pthread_create(&self->thread, NULL, run_daemon, self->se)) {
fuse_session_unmount(self->se);
fuse_session_destroy(self->se);
rmdir(self->mountpoint);
SKIP(return, "pthread_create: %s", strerror(errno));
}
fuse_opt_free_args(&args);
}
FIXTURE_TEARDOWN(acl_cache)
{
fuse_session_exit(self->se);
fuse_session_unmount(self->se);
pthread_join(self->thread, NULL);
fuse_session_destroy(self->se);
rmdir(self->mountpoint);
}
static int do_force_statx(const char *path)
{
struct statx stx;
return statx(AT_FDCWD, path, AT_STATX_FORCE_SYNC, STATX_BASIC_STATS,
&stx);
}
TEST_F(acl_cache, stale_after_force_sync)
{
char buf[512];
ssize_t sz;
int count;
/*
* Step 1: two getxattr calls before any statx(FORCE_SYNC).
* i_acl == ACL_DONT_CACHE. __get_acl's cmpxchg(p, ACL_NOT_CACHED,
* sentinel) finds *p != ACL_NOT_CACHED on every call, so the sentinel
* is never placed and the result is never cached. Both calls must
* reach the daemon, proving ACL_DONT_CACHE suppresses caching.
*/
sz = lgetxattr(self->file_path, "system.posix_acl_access",
buf, sizeof(buf));
ASSERT_EQ(sz, (ssize_t)sizeof(acl_a));
sz = lgetxattr(self->file_path, "system.posix_acl_access",
buf, sizeof(buf));
ASSERT_EQ(sz, (ssize_t)sizeof(acl_a));
pthread_mutex_lock(&g_ds.lock);
count = g_ds.getxattr_count;
pthread_mutex_unlock(&g_ds.lock);
ASSERT_EQ(count, 2);
TH_LOG("step 1 OK: both pre-trigger getxattrs reached daemon (count=%d), "
"ACL_DONT_CACHE is working", count);
/*
* Step 2: statx(AT_STATX_FORCE_SYNC).
* fuse_update_get_attr() calls forget_all_cached_acls() before sending
* FUSE_GETATTR.
* Buggy kernel: ACL_DONT_CACHE -> ACL_NOT_CACHED (cache enabled)
* Fixed kernel: ACL_DONT_CACHE preserved (no effect)
*/
ASSERT_EQ(do_force_statx(self->file_path), 0);
TH_LOG("step 2 OK: statx(AT_STATX_FORCE_SYNC) succeeded");
/*
* Step 3: getxattr — cache population attempt after the trigger.
* Buggy: *p == ACL_NOT_CACHED -> sentinel placed -> fuse_get_inode_acl
* called -> ACL_A parsed and stored in the kernel cache.
* Fixed: *p == ACL_DONT_CACHE -> sentinel placement skipped ->
* fuse_get_inode_acl called but result not cached.
* Either way the correct ACL_A is returned here.
*/
sz = lgetxattr(self->file_path, "system.posix_acl_access",
buf, sizeof(buf));
ASSERT_EQ(sz, (ssize_t)sizeof(acl_a));
pthread_mutex_lock(&g_ds.lock);
count = g_ds.getxattr_count;
pthread_mutex_unlock(&g_ds.lock);
ASSERT_EQ(count, 3);
TH_LOG("step 3 OK: post-trigger getxattr reached daemon (count=%d), "
"returned correct ACL_A (%zd bytes)", count, sz);
/*
* Step 4: switch daemon to ACL_B (different size: 44 vs 28 bytes).
* Simulates an ACL change that fuse_set_acl() would NOT invalidate for
* !fc->posix_acl mounts (it skips forget_all_cached_acls in that case).
* On a fixed kernel the ACL was never cached, so this is moot.
*/
pthread_mutex_lock(&g_ds.lock);
g_ds.acl = acl_b;
g_ds.acl_size = sizeof(acl_b);
pthread_mutex_unlock(&g_ds.lock);
TH_LOG("step 4: daemon switched to ACL_B (%zu bytes)", sizeof(acl_b));
/*
* Step 5: getxattr — the decisive check.
* Buggy kernel: cache hit -> stale ACL_A (28 bytes), count stays 3.
* Fixed kernel: no cache -> daemon called -> ACL_B (44 bytes), count 4.
*/
sz = lgetxattr(self->file_path, "system.posix_acl_access",
buf, sizeof(buf));
pthread_mutex_lock(&g_ds.lock);
count = g_ds.getxattr_count;
pthread_mutex_unlock(&g_ds.lock);
if (sz == (ssize_t)sizeof(acl_a))
TH_LOG("step 5 BUG: stale ACL_A (%zd bytes) from kernel cache "
"(count=%d); ACL_DONT_CACHE corrupted by "
"forget_all_cached_acls()", sz, count);
else
TH_LOG("step 5 OK: daemon reached (count=%d), "
"fresh ACL_B (%zd bytes)", count, sz);
EXPECT_EQ(sz, (ssize_t)sizeof(acl_b));
EXPECT_EQ(count, 4);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,146 @@
// SPDX-License-Identifier: GPL-2.0
/*
* fusectl test file-system
* Creates a simple FUSE filesystem with a single read-write file (/test)
*/
#define FUSE_USE_VERSION 26
#include <fuse.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
static char *content;
static size_t content_size = 0;
static const char test_path[] = "/test";
static int test_getattr(const char *path, struct stat *st)
{
memset(st, 0, sizeof(*st));
if (!strcmp(path, "/")) {
st->st_mode = S_IFDIR | 0755;
st->st_nlink = 2;
return 0;
}
if (!strcmp(path, test_path)) {
st->st_mode = S_IFREG | 0664;
st->st_nlink = 1;
st->st_size = content_size;
return 0;
}
return -ENOENT;
}
static int test_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
off_t offset, struct fuse_file_info *fi)
{
if (strcmp(path, "/"))
return -ENOENT;
filler(buf, ".", NULL, 0);
filler(buf, "..", NULL, 0);
filler(buf, test_path + 1, NULL, 0);
return 0;
}
static int test_open(const char *path, struct fuse_file_info *fi)
{
if (strcmp(path, test_path))
return -ENOENT;
return 0;
}
static int test_read(const char *path, char *buf, size_t size, off_t offset,
struct fuse_file_info *fi)
{
if (strcmp(path, test_path) != 0)
return -ENOENT;
if (!content || content_size == 0)
return 0;
if (offset >= content_size)
return 0;
if (offset + size > content_size)
size = content_size - offset;
memcpy(buf, content + offset, size);
return size;
}
static int test_write(const char *path, const char *buf, size_t size,
off_t offset, struct fuse_file_info *fi)
{
size_t new_size;
if (strcmp(path, test_path) != 0)
return -ENOENT;
if(offset > content_size)
return -EINVAL;
new_size = MAX(offset + size, content_size);
if (new_size > content_size)
content = realloc(content, new_size);
content_size = new_size;
if (!content)
return -ENOMEM;
memcpy(content + offset, buf, size);
return size;
}
static int test_truncate(const char *path, off_t size)
{
if (strcmp(path, test_path) != 0)
return -ENOENT;
if (size == 0) {
free(content);
content = NULL;
content_size = 0;
return 0;
}
content = realloc(content, size);
if (!content)
return -ENOMEM;
if (size > content_size)
memset(content + content_size, 0, size - content_size);
content_size = size;
return 0;
}
static struct fuse_operations memfd_ops = {
.getattr = test_getattr,
.readdir = test_readdir,
.open = test_open,
.read = test_read,
.write = test_write,
.truncate = test_truncate,
};
int main(int argc, char *argv[])
{
return fuse_main(argc, argv, &memfd_ops, NULL);
}
@@ -0,0 +1,140 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2025 Chen Linxuan <chenlinxuan@uniontech.com>
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <dirent.h>
#include <sched.h>
#include <linux/limits.h>
#include "kselftest_harness.h"
#define FUSECTL_MOUNTPOINT "/sys/fs/fuse/connections"
#define FUSE_MOUNTPOINT "/tmp/fuse_mnt_XXXXXX"
#define FUSE_DEVICE "/dev/fuse"
#define FUSECTL_TEST_VALUE "1"
static void write_file(struct __test_metadata *const _metadata,
const char *path, const char *val)
{
int fd = open(path, O_WRONLY);
size_t len = strlen(val);
ASSERT_GE(fd, 0);
ASSERT_EQ(write(fd, val, len), len);
ASSERT_EQ(close(fd), 0);
}
FIXTURE(fusectl){
char fuse_mountpoint[sizeof(FUSE_MOUNTPOINT)];
int connection;
};
FIXTURE_SETUP(fusectl)
{
const char *fuse_mnt_prog = "./fuse_mnt";
int status, pid;
struct stat statbuf;
uid_t uid = getuid();
gid_t gid = getgid();
char buf[32];
/* Setup userns */
ASSERT_EQ(unshare(CLONE_NEWNS|CLONE_NEWUSER), 0);
sprintf(buf, "0 %d 1", uid);
write_file(_metadata, "/proc/self/uid_map", buf);
write_file(_metadata, "/proc/self/setgroups", "deny");
sprintf(buf, "0 %d 1", gid);
write_file(_metadata, "/proc/self/gid_map", buf);
ASSERT_EQ(mount("", "/", NULL, MS_REC|MS_PRIVATE, NULL), 0);
strcpy(self->fuse_mountpoint, FUSE_MOUNTPOINT);
if (!mkdtemp(self->fuse_mountpoint))
SKIP(return,
"Failed to create FUSE mountpoint %s",
strerror(errno));
if (access(FUSECTL_MOUNTPOINT, F_OK))
SKIP(return,
"FUSE control filesystem not mounted");
pid = fork();
if (pid < 0)
SKIP(return,
"Failed to fork FUSE daemon process: %s",
strerror(errno));
if (pid == 0) {
execlp(fuse_mnt_prog, fuse_mnt_prog, self->fuse_mountpoint, NULL);
exit(errno);
}
waitpid(pid, &status, 0);
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
SKIP(return,
"Failed to start FUSE daemon %s",
strerror(WEXITSTATUS(status)));
}
if (stat(self->fuse_mountpoint, &statbuf))
SKIP(return,
"Failed to stat FUSE mountpoint %s",
strerror(errno));
self->connection = statbuf.st_dev;
}
FIXTURE_TEARDOWN(fusectl)
{
umount2(self->fuse_mountpoint, MNT_DETACH);
rmdir(self->fuse_mountpoint);
}
TEST_F(fusectl, abort)
{
char path_buf[PATH_MAX];
int abort_fd, test_fd, ret;
sprintf(path_buf, "/sys/fs/fuse/connections/%d/abort", self->connection);
ASSERT_EQ(0, access(path_buf, F_OK));
abort_fd = open(path_buf, O_WRONLY);
ASSERT_GE(abort_fd, 0);
sprintf(path_buf, "%s/test", self->fuse_mountpoint);
test_fd = open(path_buf, O_RDWR);
ASSERT_GE(test_fd, 0);
ret = read(test_fd, path_buf, sizeof(path_buf));
ASSERT_EQ(ret, 0);
ret = write(test_fd, "test", sizeof("test"));
ASSERT_EQ(ret, sizeof("test"));
ret = lseek(test_fd, 0, SEEK_SET);
ASSERT_GE(ret, 0);
ret = write(abort_fd, FUSECTL_TEST_VALUE, sizeof(FUSECTL_TEST_VALUE));
ASSERT_GT(ret, 0);
close(abort_fd);
ret = read(test_fd, path_buf, sizeof(path_buf));
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, ENOTCONN);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,168 @@
// 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
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#define __SANE_USERSPACE_TYPES__
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/xattr.h>
#include "kselftest_harness.h"
#include "wrappers.h"
TEST(kernfs_listxattr)
{
int fd;
/* Read-only file that can never have any extended attributes set. */
fd = open("/sys/kernel/warn_count", O_RDONLY | O_CLOEXEC);
ASSERT_GE(fd, 0);
ASSERT_EQ(flistxattr(fd, NULL, 0), 0);
EXPECT_EQ(close(fd), 0);
}
TEST(kernfs_getxattr)
{
int fd;
char buf[1];
/* Read-only file that can never have any extended attributes set. */
fd = open("/sys/kernel/warn_count", O_RDONLY | O_CLOEXEC);
ASSERT_GE(fd, 0);
ASSERT_LT(fgetxattr(fd, "user.foo", buf, sizeof(buf)), 0);
ASSERT_EQ(errno, ENODATA);
EXPECT_EQ(close(fd), 0);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,3 @@
# SPDX-License-Identifier: GPL-2.0-only
/*_test
/*_test_ns
@@ -0,0 +1,11 @@
# SPDX-License-Identifier: GPL-2.0-or-later
CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES)
LDLIBS += -lcap
TEST_GEN_PROGS := mount-notify_test mount-notify_test_ns
include ../../lib.mk
$(OUTPUT)/mount-notify_test: ../utils.c
$(OUTPUT)/mount-notify_test_ns: ../utils.c
@@ -0,0 +1,528 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2025 Miklos Szeredi <miklos@szeredi.hu>
#define _GNU_SOURCE
// Needed for linux/fanotify.h
typedef struct {
int val[2];
} __kernel_fsid_t;
#define __kernel_fsid_t __kernel_fsid_t
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/fanotify.h>
#include "kselftest_harness.h"
#include "../statmount/statmount.h"
#include "../utils.h"
static const char root_mntpoint_templ[] = "/tmp/mount-notify_test_root.XXXXXX";
static const int mark_cmds[] = {
FAN_MARK_ADD,
FAN_MARK_REMOVE,
FAN_MARK_FLUSH
};
#define NUM_FAN_FDS ARRAY_SIZE(mark_cmds)
FIXTURE(fanotify) {
int fan_fd[NUM_FAN_FDS];
char buf[256];
unsigned int rem;
void *next;
char root_mntpoint[sizeof(root_mntpoint_templ)];
int orig_root;
int ns_fd;
uint64_t root_id;
};
FIXTURE_SETUP(fanotify)
{
int i, ret;
ASSERT_EQ(unshare(CLONE_NEWNS), 0);
self->ns_fd = open("/proc/self/ns/mnt", O_RDONLY);
ASSERT_GE(self->ns_fd, 0);
ASSERT_EQ(mount("", "/", NULL, MS_REC|MS_PRIVATE, NULL), 0);
strcpy(self->root_mntpoint, root_mntpoint_templ);
ASSERT_NE(mkdtemp(self->root_mntpoint), NULL);
self->orig_root = open("/", O_PATH | O_CLOEXEC);
ASSERT_GE(self->orig_root, 0);
ASSERT_EQ(mount("tmpfs", self->root_mntpoint, "tmpfs", 0, NULL), 0);
ASSERT_EQ(chroot(self->root_mntpoint), 0);
ASSERT_EQ(chdir("/"), 0);
ASSERT_EQ(mkdir("a", 0700), 0);
ASSERT_EQ(mkdir("b", 0700), 0);
self->root_id = get_unique_mnt_id("/");
ASSERT_NE(self->root_id, 0);
for (i = 0; i < NUM_FAN_FDS; i++) {
self->fan_fd[i] = fanotify_init(FAN_REPORT_MNT | FAN_NONBLOCK,
0);
ASSERT_GE(self->fan_fd[i], 0);
ret = fanotify_mark(self->fan_fd[i], FAN_MARK_ADD |
FAN_MARK_MNTNS,
FAN_MNT_ATTACH | FAN_MNT_DETACH,
self->ns_fd, NULL);
ASSERT_EQ(ret, 0);
// On fd[0] we do an extra ADD that changes nothing.
// On fd[1]/fd[2] we REMOVE/FLUSH which removes the mark.
ret = fanotify_mark(self->fan_fd[i], mark_cmds[i] |
FAN_MARK_MNTNS,
FAN_MNT_ATTACH | FAN_MNT_DETACH,
self->ns_fd, NULL);
ASSERT_EQ(ret, 0);
}
self->rem = 0;
}
FIXTURE_TEARDOWN(fanotify)
{
int i;
ASSERT_EQ(self->rem, 0);
for (i = 0; i < NUM_FAN_FDS; i++)
close(self->fan_fd[i]);
ASSERT_EQ(fchdir(self->orig_root), 0);
ASSERT_EQ(chroot("."), 0);
EXPECT_EQ(umount2(self->root_mntpoint, MNT_DETACH), 0);
EXPECT_EQ(chdir(self->root_mntpoint), 0);
EXPECT_EQ(chdir("/"), 0);
EXPECT_EQ(rmdir(self->root_mntpoint), 0);
}
static uint64_t expect_notify(struct __test_metadata *const _metadata,
FIXTURE_DATA(fanotify) *self,
uint64_t *mask)
{
struct fanotify_event_metadata *meta;
struct fanotify_event_info_mnt *mnt;
unsigned int thislen;
if (!self->rem) {
ssize_t len;
int i;
for (i = NUM_FAN_FDS - 1; i >= 0; i--) {
len = read(self->fan_fd[i], self->buf,
sizeof(self->buf));
if (i > 0) {
// Groups 1,2 should get EAGAIN
ASSERT_EQ(len, -1);
ASSERT_EQ(errno, EAGAIN);
} else {
// Group 0 should get events
ASSERT_GT(len, 0);
}
}
self->rem = len;
self->next = (void *) self->buf;
}
meta = self->next;
ASSERT_TRUE(FAN_EVENT_OK(meta, self->rem));
thislen = meta->event_len;
self->rem -= thislen;
self->next += thislen;
*mask = meta->mask;
thislen -= sizeof(*meta);
mnt = ((void *) meta) + meta->event_len - thislen;
ASSERT_EQ(thislen, sizeof(*mnt));
return mnt->mnt_id;
}
static void expect_notify_n(struct __test_metadata *const _metadata,
FIXTURE_DATA(fanotify) *self,
unsigned int n, uint64_t mask[], uint64_t mnts[])
{
unsigned int i;
for (i = 0; i < n; i++)
mnts[i] = expect_notify(_metadata, self, &mask[i]);
}
static uint64_t expect_notify_mask(struct __test_metadata *const _metadata,
FIXTURE_DATA(fanotify) *self,
uint64_t expect_mask)
{
uint64_t mntid, mask;
mntid = expect_notify(_metadata, self, &mask);
ASSERT_EQ(expect_mask, mask);
return mntid;
}
static void expect_notify_mask_n(struct __test_metadata *const _metadata,
FIXTURE_DATA(fanotify) *self,
uint64_t mask, unsigned int n, uint64_t mnts[])
{
unsigned int i;
for (i = 0; i < n; i++)
mnts[i] = expect_notify_mask(_metadata, self, mask);
}
static void verify_mount_ids(struct __test_metadata *const _metadata,
const uint64_t list1[], const uint64_t list2[],
size_t num)
{
unsigned int i, j;
// Check that neither list has any duplicates
for (i = 0; i < num; i++) {
for (j = 0; j < num; j++) {
if (i != j) {
ASSERT_NE(list1[i], list1[j]);
ASSERT_NE(list2[i], list2[j]);
}
}
}
// Check that all list1 memebers can be found in list2. Together with
// the above it means that the list1 and list2 represent the same sets.
for (i = 0; i < num; i++) {
for (j = 0; j < num; j++) {
if (list1[i] == list2[j])
break;
}
ASSERT_NE(j, num);
}
}
static void check_mounted(struct __test_metadata *const _metadata,
const uint64_t mnts[], size_t num)
{
ssize_t ret;
uint64_t *list;
list = malloc((num + 1) * sizeof(list[0]));
ASSERT_NE(list, NULL);
ret = listmount(LSMT_ROOT, 0, 0, list, num + 1, 0);
ASSERT_EQ(ret, num);
verify_mount_ids(_metadata, mnts, list, num);
free(list);
}
static void setup_mount_tree(struct __test_metadata *const _metadata,
int log2_num)
{
int ret, i;
ret = mount("", "/", NULL, MS_SHARED, NULL);
ASSERT_EQ(ret, 0);
for (i = 0; i < log2_num; i++) {
ret = mount("/", "/", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
}
}
TEST_F(fanotify, bind)
{
int ret;
uint64_t mnts[2] = { self->root_id };
ret = mount("/", "/", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH);
ASSERT_NE(mnts[0], mnts[1]);
check_mounted(_metadata, mnts, 2);
// Cleanup
uint64_t detach_id;
ret = umount("/");
ASSERT_EQ(ret, 0);
detach_id = expect_notify_mask(_metadata, self, FAN_MNT_DETACH);
ASSERT_EQ(detach_id, mnts[1]);
check_mounted(_metadata, mnts, 1);
}
TEST_F(fanotify, move)
{
int ret;
uint64_t mnts[2] = { self->root_id };
uint64_t move_id;
ret = mount("/", "/a", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH);
ASSERT_NE(mnts[0], mnts[1]);
check_mounted(_metadata, mnts, 2);
ret = move_mount(AT_FDCWD, "/a", AT_FDCWD, "/b", 0);
ASSERT_EQ(ret, 0);
move_id = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH | FAN_MNT_DETACH);
ASSERT_EQ(move_id, mnts[1]);
// Cleanup
ret = umount("/b");
ASSERT_EQ(ret, 0);
check_mounted(_metadata, mnts, 1);
}
TEST_F(fanotify, propagate)
{
const unsigned int log2_num = 4;
const unsigned int num = (1 << log2_num);
uint64_t mnts[num];
setup_mount_tree(_metadata, log2_num);
expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, num - 1, mnts + 1);
mnts[0] = self->root_id;
check_mounted(_metadata, mnts, num);
// Cleanup
int ret;
uint64_t mnts2[num];
ret = umount2("/", MNT_DETACH);
ASSERT_EQ(ret, 0);
ret = mount("", "/", NULL, MS_PRIVATE, NULL);
ASSERT_EQ(ret, 0);
mnts2[0] = self->root_id;
expect_notify_mask_n(_metadata, self, FAN_MNT_DETACH, num - 1, mnts2 + 1);
verify_mount_ids(_metadata, mnts, mnts2, num);
check_mounted(_metadata, mnts, 1);
}
TEST_F(fanotify, fsmount)
{
int ret, fs, mnt;
uint64_t mnts[2] = { self->root_id };
fs = fsopen("tmpfs", 0);
ASSERT_GE(fs, 0);
ret = fsconfig(fs, FSCONFIG_CMD_CREATE, 0, 0, 0);
ASSERT_EQ(ret, 0);
mnt = fsmount(fs, 0, 0);
ASSERT_GE(mnt, 0);
close(fs);
ret = move_mount(mnt, "", AT_FDCWD, "/a", MOVE_MOUNT_F_EMPTY_PATH);
ASSERT_EQ(ret, 0);
close(mnt);
mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH);
ASSERT_NE(mnts[0], mnts[1]);
check_mounted(_metadata, mnts, 2);
// Cleanup
uint64_t detach_id;
ret = umount("/a");
ASSERT_EQ(ret, 0);
detach_id = expect_notify_mask(_metadata, self, FAN_MNT_DETACH);
ASSERT_EQ(detach_id, mnts[1]);
check_mounted(_metadata, mnts, 1);
}
TEST_F(fanotify, reparent)
{
uint64_t mnts[6] = { self->root_id };
uint64_t dmnts[3];
uint64_t masks[3];
unsigned int i;
int ret;
// Create setup with a[1] -> b[2] propagation
ret = mount("/", "/a", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
ret = mount("", "/a", NULL, MS_SHARED, NULL);
ASSERT_EQ(ret, 0);
ret = mount("/a", "/b", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
ret = mount("", "/b", NULL, MS_SLAVE, NULL);
ASSERT_EQ(ret, 0);
expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, 2, mnts + 1);
check_mounted(_metadata, mnts, 3);
// Mount on a[3], which is propagated to b[4]
ret = mount("/", "/a", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, 2, mnts + 3);
check_mounted(_metadata, mnts, 5);
// Mount on b[5], not propagated
ret = mount("/", "/b", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
mnts[5] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH);
check_mounted(_metadata, mnts, 6);
// Umount a[3], which is propagated to b[4], but not b[5]
// This will result in b[5] "falling" on b[2]
ret = umount("/a");
ASSERT_EQ(ret, 0);
expect_notify_n(_metadata, self, 3, masks, dmnts);
verify_mount_ids(_metadata, mnts + 3, dmnts, 3);
for (i = 0; i < 3; i++) {
if (dmnts[i] == mnts[5]) {
ASSERT_EQ(masks[i], FAN_MNT_ATTACH | FAN_MNT_DETACH);
} else {
ASSERT_EQ(masks[i], FAN_MNT_DETACH);
}
}
mnts[3] = mnts[5];
check_mounted(_metadata, mnts, 4);
// Cleanup
ret = umount("/b");
ASSERT_EQ(ret, 0);
ret = umount("/a");
ASSERT_EQ(ret, 0);
ret = umount("/b");
ASSERT_EQ(ret, 0);
expect_notify_mask_n(_metadata, self, FAN_MNT_DETACH, 3, dmnts);
verify_mount_ids(_metadata, mnts + 1, dmnts, 3);
check_mounted(_metadata, mnts, 1);
}
TEST_F(fanotify, rmdir)
{
uint64_t mnts[3] = { self->root_id };
int ret;
ret = mount("/", "/a", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
ret = mount("/", "/a/b", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, 2, mnts + 1);
check_mounted(_metadata, mnts, 3);
ret = chdir("/a");
ASSERT_EQ(ret, 0);
ret = fork();
ASSERT_GE(ret, 0);
if (ret == 0) {
chdir("/");
unshare(CLONE_NEWNS);
mount("", "/", NULL, MS_REC|MS_PRIVATE, NULL);
umount2("/a", MNT_DETACH);
// This triggers a detach in the other namespace
rmdir("/a");
exit(0);
}
wait(NULL);
expect_notify_mask_n(_metadata, self, FAN_MNT_DETACH, 2, mnts + 1);
check_mounted(_metadata, mnts, 1);
// Cleanup
ret = chdir("/");
ASSERT_EQ(ret, 0);
}
TEST_F(fanotify, pivot_root)
{
uint64_t mnts[3] = { self->root_id };
uint64_t mnts2[3];
int ret;
ret = mount("tmpfs", "/a", "tmpfs", 0, NULL);
ASSERT_EQ(ret, 0);
mnts[2] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH);
ret = mkdir("/a/new", 0700);
ASSERT_EQ(ret, 0);
ret = mkdir("/a/old", 0700);
ASSERT_EQ(ret, 0);
ret = mount("/a", "/a/new", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH);
check_mounted(_metadata, mnts, 3);
ret = syscall(SYS_pivot_root, "/a/new", "/a/new/old");
ASSERT_EQ(ret, 0);
expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH | FAN_MNT_DETACH, 2, mnts2);
verify_mount_ids(_metadata, mnts, mnts2, 2);
check_mounted(_metadata, mnts, 3);
// Cleanup
ret = syscall(SYS_pivot_root, "/old", "/old/a/new");
ASSERT_EQ(ret, 0);
ret = umount("/a/new");
ASSERT_EQ(ret, 0);
ret = umount("/a");
ASSERT_EQ(ret, 0);
check_mounted(_metadata, mnts, 1);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,555 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2025 Miklos Szeredi <miklos@szeredi.hu>
#define _GNU_SOURCE
// Needed for linux/fanotify.h
typedef struct {
int val[2];
} __kernel_fsid_t;
#define __kernel_fsid_t __kernel_fsid_t
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/fanotify.h>
#include "kselftest_harness.h"
#include "../statmount/statmount.h"
#include "../utils.h"
static const char root_mntpoint_templ[] = "/tmp/mount-notify_test_root.XXXXXX";
static const int mark_types[] = {
FAN_MARK_FILESYSTEM,
FAN_MARK_MOUNT,
FAN_MARK_INODE
};
static const int mark_cmds[] = {
FAN_MARK_ADD,
FAN_MARK_REMOVE,
FAN_MARK_FLUSH
};
#define NUM_FAN_FDS ARRAY_SIZE(mark_cmds)
FIXTURE(fanotify) {
int fan_fd[NUM_FAN_FDS];
char buf[256];
unsigned int rem;
void *next;
char root_mntpoint[sizeof(root_mntpoint_templ)];
int orig_root;
int orig_ns_fd;
int ns_fd;
uint64_t root_id;
};
FIXTURE_SETUP(fanotify)
{
int i, ret;
self->orig_ns_fd = open("/proc/self/ns/mnt", O_RDONLY);
ASSERT_GE(self->orig_ns_fd, 0);
ret = setup_userns();
ASSERT_EQ(ret, 0);
self->ns_fd = open("/proc/self/ns/mnt", O_RDONLY);
ASSERT_GE(self->ns_fd, 0);
strcpy(self->root_mntpoint, root_mntpoint_templ);
ASSERT_NE(mkdtemp(self->root_mntpoint), NULL);
self->orig_root = open("/", O_PATH | O_CLOEXEC);
ASSERT_GE(self->orig_root, 0);
ASSERT_EQ(mount("tmpfs", self->root_mntpoint, "tmpfs", 0, NULL), 0);
ASSERT_EQ(chroot(self->root_mntpoint), 0);
ASSERT_EQ(chdir("/"), 0);
ASSERT_EQ(mkdir("a", 0700), 0);
ASSERT_EQ(mkdir("b", 0700), 0);
self->root_id = get_unique_mnt_id("/");
ASSERT_NE(self->root_id, 0);
for (i = 0; i < NUM_FAN_FDS; i++) {
int fan_fd = fanotify_init(FAN_REPORT_FID, 0);
// Verify that watching tmpfs mounted inside userns is allowed
ret = fanotify_mark(fan_fd, FAN_MARK_ADD | mark_types[i],
FAN_OPEN, AT_FDCWD, "/");
ASSERT_EQ(ret, 0);
// ...but watching entire orig root filesystem is not allowed
ret = fanotify_mark(fan_fd, FAN_MARK_ADD | FAN_MARK_FILESYSTEM,
FAN_OPEN, self->orig_root, ".");
ASSERT_NE(ret, 0);
close(fan_fd);
self->fan_fd[i] = fanotify_init(FAN_REPORT_MNT | FAN_NONBLOCK,
0);
ASSERT_GE(self->fan_fd[i], 0);
// Verify that watching mntns where group was created is allowed
ret = fanotify_mark(self->fan_fd[i], FAN_MARK_ADD |
FAN_MARK_MNTNS,
FAN_MNT_ATTACH | FAN_MNT_DETACH,
self->ns_fd, NULL);
ASSERT_EQ(ret, 0);
// ...but watching orig mntns is not allowed
ret = fanotify_mark(self->fan_fd[i], FAN_MARK_ADD |
FAN_MARK_MNTNS,
FAN_MNT_ATTACH | FAN_MNT_DETACH,
self->orig_ns_fd, NULL);
ASSERT_NE(ret, 0);
// On fd[0] we do an extra ADD that changes nothing.
// On fd[1]/fd[2] we REMOVE/FLUSH which removes the mark.
ret = fanotify_mark(self->fan_fd[i], mark_cmds[i] |
FAN_MARK_MNTNS,
FAN_MNT_ATTACH | FAN_MNT_DETACH,
self->ns_fd, NULL);
ASSERT_EQ(ret, 0);
}
self->rem = 0;
}
FIXTURE_TEARDOWN(fanotify)
{
int i;
ASSERT_EQ(self->rem, 0);
for (i = 0; i < NUM_FAN_FDS; i++)
close(self->fan_fd[i]);
ASSERT_EQ(fchdir(self->orig_root), 0);
ASSERT_EQ(chroot("."), 0);
EXPECT_EQ(umount2(self->root_mntpoint, MNT_DETACH), 0);
EXPECT_EQ(chdir(self->root_mntpoint), 0);
EXPECT_EQ(chdir("/"), 0);
EXPECT_EQ(rmdir(self->root_mntpoint), 0);
}
static uint64_t expect_notify(struct __test_metadata *const _metadata,
FIXTURE_DATA(fanotify) *self,
uint64_t *mask)
{
struct fanotify_event_metadata *meta;
struct fanotify_event_info_mnt *mnt;
unsigned int thislen;
if (!self->rem) {
ssize_t len;
int i;
for (i = NUM_FAN_FDS - 1; i >= 0; i--) {
len = read(self->fan_fd[i], self->buf,
sizeof(self->buf));
if (i > 0) {
// Groups 1,2 should get EAGAIN
ASSERT_EQ(len, -1);
ASSERT_EQ(errno, EAGAIN);
} else {
// Group 0 should get events
ASSERT_GT(len, 0);
}
}
self->rem = len;
self->next = (void *) self->buf;
}
meta = self->next;
ASSERT_TRUE(FAN_EVENT_OK(meta, self->rem));
thislen = meta->event_len;
self->rem -= thislen;
self->next += thislen;
*mask = meta->mask;
thislen -= sizeof(*meta);
mnt = ((void *) meta) + meta->event_len - thislen;
ASSERT_EQ(thislen, sizeof(*mnt));
return mnt->mnt_id;
}
static void expect_notify_n(struct __test_metadata *const _metadata,
FIXTURE_DATA(fanotify) *self,
unsigned int n, uint64_t mask[], uint64_t mnts[])
{
unsigned int i;
for (i = 0; i < n; i++)
mnts[i] = expect_notify(_metadata, self, &mask[i]);
}
static uint64_t expect_notify_mask(struct __test_metadata *const _metadata,
FIXTURE_DATA(fanotify) *self,
uint64_t expect_mask)
{
uint64_t mntid, mask;
mntid = expect_notify(_metadata, self, &mask);
ASSERT_EQ(expect_mask, mask);
return mntid;
}
static void expect_notify_mask_n(struct __test_metadata *const _metadata,
FIXTURE_DATA(fanotify) *self,
uint64_t mask, unsigned int n, uint64_t mnts[])
{
unsigned int i;
for (i = 0; i < n; i++)
mnts[i] = expect_notify_mask(_metadata, self, mask);
}
static void verify_mount_ids(struct __test_metadata *const _metadata,
const uint64_t list1[], const uint64_t list2[],
size_t num)
{
unsigned int i, j;
// Check that neither list has any duplicates
for (i = 0; i < num; i++) {
for (j = 0; j < num; j++) {
if (i != j) {
ASSERT_NE(list1[i], list1[j]);
ASSERT_NE(list2[i], list2[j]);
}
}
}
// Check that all list1 memebers can be found in list2. Together with
// the above it means that the list1 and list2 represent the same sets.
for (i = 0; i < num; i++) {
for (j = 0; j < num; j++) {
if (list1[i] == list2[j])
break;
}
ASSERT_NE(j, num);
}
}
static void check_mounted(struct __test_metadata *const _metadata,
const uint64_t mnts[], size_t num)
{
ssize_t ret;
uint64_t *list;
list = malloc((num + 1) * sizeof(list[0]));
ASSERT_NE(list, NULL);
ret = listmount(LSMT_ROOT, 0, 0, list, num + 1, 0);
ASSERT_EQ(ret, num);
verify_mount_ids(_metadata, mnts, list, num);
free(list);
}
static void setup_mount_tree(struct __test_metadata *const _metadata,
int log2_num)
{
int ret, i;
ret = mount("", "/", NULL, MS_SHARED, NULL);
ASSERT_EQ(ret, 0);
for (i = 0; i < log2_num; i++) {
ret = mount("/", "/", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
}
}
TEST_F(fanotify, bind)
{
int ret;
uint64_t mnts[2] = { self->root_id };
ret = mount("/", "/", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH);
ASSERT_NE(mnts[0], mnts[1]);
check_mounted(_metadata, mnts, 2);
// Cleanup
uint64_t detach_id;
ret = umount("/");
ASSERT_EQ(ret, 0);
detach_id = expect_notify_mask(_metadata, self, FAN_MNT_DETACH);
ASSERT_EQ(detach_id, mnts[1]);
check_mounted(_metadata, mnts, 1);
}
TEST_F(fanotify, move)
{
int ret;
uint64_t mnts[2] = { self->root_id };
uint64_t move_id;
ret = mount("/", "/a", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH);
ASSERT_NE(mnts[0], mnts[1]);
check_mounted(_metadata, mnts, 2);
ret = move_mount(AT_FDCWD, "/a", AT_FDCWD, "/b", 0);
ASSERT_EQ(ret, 0);
move_id = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH | FAN_MNT_DETACH);
ASSERT_EQ(move_id, mnts[1]);
// Cleanup
ret = umount("/b");
ASSERT_EQ(ret, 0);
check_mounted(_metadata, mnts, 1);
}
TEST_F(fanotify, propagate)
{
const unsigned int log2_num = 4;
const unsigned int num = (1 << log2_num);
uint64_t mnts[num];
setup_mount_tree(_metadata, log2_num);
expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, num - 1, mnts + 1);
mnts[0] = self->root_id;
check_mounted(_metadata, mnts, num);
// Cleanup
int ret;
uint64_t mnts2[num];
ret = umount2("/", MNT_DETACH);
ASSERT_EQ(ret, 0);
ret = mount("", "/", NULL, MS_PRIVATE, NULL);
ASSERT_EQ(ret, 0);
mnts2[0] = self->root_id;
expect_notify_mask_n(_metadata, self, FAN_MNT_DETACH, num - 1, mnts2 + 1);
verify_mount_ids(_metadata, mnts, mnts2, num);
check_mounted(_metadata, mnts, 1);
}
TEST_F(fanotify, fsmount)
{
int ret, fs, mnt;
uint64_t mnts[2] = { self->root_id };
fs = fsopen("tmpfs", 0);
ASSERT_GE(fs, 0);
ret = fsconfig(fs, FSCONFIG_CMD_CREATE, 0, 0, 0);
ASSERT_EQ(ret, 0);
mnt = fsmount(fs, 0, 0);
ASSERT_GE(mnt, 0);
close(fs);
ret = move_mount(mnt, "", AT_FDCWD, "/a", MOVE_MOUNT_F_EMPTY_PATH);
ASSERT_EQ(ret, 0);
close(mnt);
mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH);
ASSERT_NE(mnts[0], mnts[1]);
check_mounted(_metadata, mnts, 2);
// Cleanup
uint64_t detach_id;
ret = umount("/a");
ASSERT_EQ(ret, 0);
detach_id = expect_notify_mask(_metadata, self, FAN_MNT_DETACH);
ASSERT_EQ(detach_id, mnts[1]);
check_mounted(_metadata, mnts, 1);
}
TEST_F(fanotify, reparent)
{
uint64_t mnts[6] = { self->root_id };
uint64_t dmnts[3];
uint64_t masks[3];
unsigned int i;
int ret;
// Create setup with a[1] -> b[2] propagation
ret = mount("/", "/a", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
ret = mount("", "/a", NULL, MS_SHARED, NULL);
ASSERT_EQ(ret, 0);
ret = mount("/a", "/b", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
ret = mount("", "/b", NULL, MS_SLAVE, NULL);
ASSERT_EQ(ret, 0);
expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, 2, mnts + 1);
check_mounted(_metadata, mnts, 3);
// Mount on a[3], which is propagated to b[4]
ret = mount("/", "/a", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, 2, mnts + 3);
check_mounted(_metadata, mnts, 5);
// Mount on b[5], not propagated
ret = mount("/", "/b", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
mnts[5] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH);
check_mounted(_metadata, mnts, 6);
// Umount a[3], which is propagated to b[4], but not b[5]
// This will result in b[5] "falling" on b[2]
ret = umount("/a");
ASSERT_EQ(ret, 0);
expect_notify_n(_metadata, self, 3, masks, dmnts);
verify_mount_ids(_metadata, mnts + 3, dmnts, 3);
for (i = 0; i < 3; i++) {
if (dmnts[i] == mnts[5]) {
ASSERT_EQ(masks[i], FAN_MNT_ATTACH | FAN_MNT_DETACH);
} else {
ASSERT_EQ(masks[i], FAN_MNT_DETACH);
}
}
mnts[3] = mnts[5];
check_mounted(_metadata, mnts, 4);
// Cleanup
ret = umount("/b");
ASSERT_EQ(ret, 0);
ret = umount("/a");
ASSERT_EQ(ret, 0);
ret = umount("/b");
ASSERT_EQ(ret, 0);
expect_notify_mask_n(_metadata, self, FAN_MNT_DETACH, 3, dmnts);
verify_mount_ids(_metadata, mnts + 1, dmnts, 3);
check_mounted(_metadata, mnts, 1);
}
TEST_F(fanotify, rmdir)
{
uint64_t mnts[3] = { self->root_id };
int ret;
ret = mount("/", "/a", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
ret = mount("/", "/a/b", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH, 2, mnts + 1);
check_mounted(_metadata, mnts, 3);
ret = chdir("/a");
ASSERT_EQ(ret, 0);
ret = fork();
ASSERT_GE(ret, 0);
if (ret == 0) {
chdir("/");
unshare(CLONE_NEWNS);
mount("", "/", NULL, MS_REC|MS_PRIVATE, NULL);
umount2("/a", MNT_DETACH);
// This triggers a detach in the other namespace
rmdir("/a");
exit(0);
}
wait(NULL);
expect_notify_mask_n(_metadata, self, FAN_MNT_DETACH, 2, mnts + 1);
check_mounted(_metadata, mnts, 1);
// Cleanup
ret = chdir("/");
ASSERT_EQ(ret, 0);
}
TEST_F(fanotify, pivot_root)
{
uint64_t mnts[3] = { self->root_id };
uint64_t mnts2[3];
int ret;
ret = mount("tmpfs", "/a", "tmpfs", 0, NULL);
ASSERT_EQ(ret, 0);
mnts[2] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH);
ret = mkdir("/a/new", 0700);
ASSERT_EQ(ret, 0);
ret = mkdir("/a/old", 0700);
ASSERT_EQ(ret, 0);
ret = mount("/a", "/a/new", NULL, MS_BIND, NULL);
ASSERT_EQ(ret, 0);
mnts[1] = expect_notify_mask(_metadata, self, FAN_MNT_ATTACH);
check_mounted(_metadata, mnts, 3);
ret = syscall(SYS_pivot_root, "/a/new", "/a/new/old");
ASSERT_EQ(ret, 0);
expect_notify_mask_n(_metadata, self, FAN_MNT_ATTACH | FAN_MNT_DETACH, 2, mnts2);
verify_mount_ids(_metadata, mnts, mnts2, 2);
check_mounted(_metadata, mnts, 3);
// Cleanup
ret = syscall(SYS_pivot_root, "/old", "/old/a/new");
ASSERT_EQ(ret, 0);
ret = umount("/a/new");
ASSERT_EQ(ret, 0);
ret = umount("/a");
ASSERT_EQ(ret, 0);
check_mounted(_metadata, mnts, 1);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0-only
move_mount_test
@@ -0,0 +1,10 @@
# SPDX-License-Identifier: GPL-2.0-or-later
CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES)
LDLIBS += -lcap
TEST_GEN_PROGS := move_mount_test
include ../../lib.mk
$(OUTPUT)/move_mount_test: ../utils.c
@@ -0,0 +1,492 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2026 Christian Brauner <brauner@kernel.org>
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <unistd.h>
#include <sys/syscall.h>
#include "../wrappers.h"
#include "../utils.h"
#include "../statmount/statmount.h"
#include "../../kselftest_harness.h"
#include <linux/stat.h>
#ifndef MOVE_MOUNT_BENEATH
#define MOVE_MOUNT_BENEATH 0x00000200
#endif
static uint64_t get_unique_mnt_id_fd(int fd)
{
struct statx sx;
int ret;
ret = statx(fd, "", AT_EMPTY_PATH, STATX_MNT_ID_UNIQUE, &sx);
if (ret)
return 0;
if (!(sx.stx_mask & STATX_MNT_ID_UNIQUE))
return 0;
return sx.stx_mnt_id;
}
/*
* Create a locked overmount stack at /mnt_dir for testing MNT_LOCKED
* transfer on non-rootfs mounts.
*
* Mounts tmpfs A at /mnt_dir, overmounts with tmpfs B, then enters a
* new user+mount namespace where both become locked. Returns the exit
* code to use on failure, or 0 on success.
*/
static int setup_locked_overmount(void)
{
/* Isolate so mounts don't leak. */
if (unshare(CLONE_NEWNS))
return 1;
if (mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL))
return 2;
/*
* Create mounts while still in the initial user namespace so
* they become locked after the subsequent user namespace
* unshare.
*/
rmdir("/mnt_dir");
if (mkdir("/mnt_dir", 0755))
return 3;
/* Mount tmpfs A */
if (mount("tmpfs", "/mnt_dir", "tmpfs", 0, NULL))
return 4;
/* Overmount with tmpfs B */
if (mount("tmpfs", "/mnt_dir", "tmpfs", 0, NULL))
return 5;
/*
* Create user+mount namespace. Mounts A and B become locked
* because they might be covering something that is not supposed
* to be revealed.
*/
if (setup_userns())
return 6;
/* Sanity check: B must be locked */
if (!umount2("/mnt_dir", MNT_DETACH) || errno != EINVAL)
return 7;
return 0;
}
/*
* Create a detached tmpfs mount and return its fd, or -1 on failure.
*/
static int create_detached_tmpfs(void)
{
int fs_fd, mnt_fd;
fs_fd = sys_fsopen("tmpfs", FSOPEN_CLOEXEC);
if (fs_fd < 0)
return -1;
if (sys_fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) {
close(fs_fd);
return -1;
}
mnt_fd = sys_fsmount(fs_fd, FSMOUNT_CLOEXEC, 0);
close(fs_fd);
return mnt_fd;
}
FIXTURE(move_mount) {
uint64_t orig_root_id;
};
FIXTURE_SETUP(move_mount)
{
ASSERT_EQ(unshare(CLONE_NEWNS), 0);
ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
self->orig_root_id = get_unique_mnt_id("/");
ASSERT_NE(self->orig_root_id, 0);
}
FIXTURE_TEARDOWN(move_mount)
{
}
/*
* Test successful MOVE_MOUNT_BENEATH on the rootfs.
* Mount a clone beneath /, fchdir to the clone, chroot to switch root,
* then detach the old root.
*/
TEST_F(move_mount, beneath_rootfs_success)
{
int fd_tree, ret;
uint64_t clone_id, root_id;
fd_tree = sys_open_tree(AT_FDCWD, "/",
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
ASSERT_GE(fd_tree, 0);
clone_id = get_unique_mnt_id_fd(fd_tree);
ASSERT_NE(clone_id, 0);
ASSERT_NE(clone_id, self->orig_root_id);
ASSERT_EQ(fchdir(fd_tree), 0);
ret = sys_move_mount(fd_tree, "", AT_FDCWD, "/",
MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH);
ASSERT_EQ(ret, 0);
close(fd_tree);
/* Switch root to the clone */
ASSERT_EQ(chroot("."), 0);
/* Verify "/" is now the clone */
root_id = get_unique_mnt_id("/");
ASSERT_NE(root_id, 0);
ASSERT_EQ(root_id, clone_id);
/* Detach old root */
ASSERT_EQ(umount2(".", MNT_DETACH), 0);
}
/*
* Test that after MOVE_MOUNT_BENEATH on the rootfs the old root is
* stacked on top of the clone. Verify via statmount that the old
* root's parent is the clone.
*/
TEST_F(move_mount, beneath_rootfs_old_root_stacked)
{
int fd_tree, ret;
uint64_t clone_id;
struct statmount sm;
fd_tree = sys_open_tree(AT_FDCWD, "/",
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
ASSERT_GE(fd_tree, 0);
clone_id = get_unique_mnt_id_fd(fd_tree);
ASSERT_NE(clone_id, 0);
ASSERT_NE(clone_id, self->orig_root_id);
ASSERT_EQ(fchdir(fd_tree), 0);
ret = sys_move_mount(fd_tree, "", AT_FDCWD, "/",
MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH);
ASSERT_EQ(ret, 0);
close(fd_tree);
ASSERT_EQ(chroot("."), 0);
/* Old root's parent should now be the clone */
ASSERT_EQ(statmount(self->orig_root_id, 0, 0,
STATMOUNT_MNT_BASIC, &sm, sizeof(sm), 0), 0);
ASSERT_EQ(sm.mnt_parent_id, clone_id);
ASSERT_EQ(umount2(".", MNT_DETACH), 0);
}
/*
* Test that MOVE_MOUNT_BENEATH on rootfs fails when chroot'd into a
* subdirectory of the same mount. The caller's fs->root.dentry doesn't
* match mnt->mnt_root so the kernel rejects it.
*/
TEST_F(move_mount, beneath_rootfs_in_chroot_fail)
{
int fd_tree, ret;
uint64_t chroot_id, clone_id;
rmdir("/chroot_dir");
ASSERT_EQ(mkdir("/chroot_dir", 0755), 0);
chroot_id = get_unique_mnt_id("/chroot_dir");
ASSERT_NE(chroot_id, 0);
ASSERT_EQ(self->orig_root_id, chroot_id);
ASSERT_EQ(chdir("/chroot_dir"), 0);
ASSERT_EQ(chroot("."), 0);
fd_tree = sys_open_tree(AT_FDCWD, "/",
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
ASSERT_GE(fd_tree, 0);
clone_id = get_unique_mnt_id_fd(fd_tree);
ASSERT_NE(clone_id, 0);
ASSERT_NE(clone_id, chroot_id);
ASSERT_EQ(fchdir(fd_tree), 0);
/*
* Should fail: fs->root.dentry (/chroot_dir) doesn't match
* the mount's mnt_root (/).
*/
ret = sys_move_mount(fd_tree, "", AT_FDCWD, "/",
MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH);
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, EINVAL);
close(fd_tree);
}
/*
* Test that MOVE_MOUNT_BENEATH on rootfs succeeds when chroot'd into a
* separate tmpfs mount. The caller's root dentry matches the mount's
* mnt_root since it's a dedicated mount.
*/
TEST_F(move_mount, beneath_rootfs_in_chroot_success)
{
int fd_tree, ret;
uint64_t chroot_id, clone_id, root_id;
struct statmount sm;
rmdir("/chroot_dir");
ASSERT_EQ(mkdir("/chroot_dir", 0755), 0);
ASSERT_EQ(mount("tmpfs", "/chroot_dir", "tmpfs", 0, NULL), 0);
chroot_id = get_unique_mnt_id("/chroot_dir");
ASSERT_NE(chroot_id, 0);
ASSERT_EQ(chdir("/chroot_dir"), 0);
ASSERT_EQ(chroot("."), 0);
ASSERT_EQ(get_unique_mnt_id("/"), chroot_id);
fd_tree = sys_open_tree(AT_FDCWD, "/",
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
ASSERT_GE(fd_tree, 0);
clone_id = get_unique_mnt_id_fd(fd_tree);
ASSERT_NE(clone_id, 0);
ASSERT_NE(clone_id, chroot_id);
ASSERT_EQ(fchdir(fd_tree), 0);
ret = sys_move_mount(fd_tree, "", AT_FDCWD, "/",
MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH);
ASSERT_EQ(ret, 0);
close(fd_tree);
ASSERT_EQ(chroot("."), 0);
root_id = get_unique_mnt_id("/");
ASSERT_NE(root_id, 0);
ASSERT_EQ(root_id, clone_id);
ASSERT_EQ(statmount(chroot_id, 0, 0,
STATMOUNT_MNT_BASIC, &sm, sizeof(sm), 0), 0);
ASSERT_EQ(sm.mnt_parent_id, clone_id);
ASSERT_EQ(umount2(".", MNT_DETACH), 0);
}
/*
* Test MNT_LOCKED transfer when mounting beneath rootfs in a user+mount
* namespace. After mount-beneath the new root gets MNT_LOCKED and the
* old root has MNT_LOCKED cleared so it can be unmounted.
*/
TEST_F(move_mount, beneath_rootfs_locked_transfer)
{
int fd_tree, ret;
uint64_t clone_id, root_id;
ASSERT_EQ(setup_userns(), 0);
ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
fd_tree = sys_open_tree(AT_FDCWD, "/",
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC |
AT_RECURSIVE);
ASSERT_GE(fd_tree, 0);
clone_id = get_unique_mnt_id_fd(fd_tree);
ASSERT_NE(clone_id, 0);
ASSERT_EQ(fchdir(fd_tree), 0);
ret = sys_move_mount(fd_tree, "", AT_FDCWD, "/",
MOVE_MOUNT_F_EMPTY_PATH |
MOVE_MOUNT_BENEATH);
ASSERT_EQ(ret, 0);
close(fd_tree);
ASSERT_EQ(chroot("."), 0);
root_id = get_unique_mnt_id("/");
ASSERT_EQ(root_id, clone_id);
/*
* The old root should be unmountable (MNT_LOCKED was
* transferred to the clone). If MNT_LOCKED wasn't
* cleared, this would fail with EINVAL.
*/
ASSERT_EQ(umount2(".", MNT_DETACH), 0);
/* Verify "/" is still the clone after detaching old root */
root_id = get_unique_mnt_id("/");
ASSERT_EQ(root_id, clone_id);
}
/*
* Test containment invariant: after mount-beneath rootfs in a user+mount
* namespace, the new root must be MNT_LOCKED. The lock transfer from the
* old root preserves containment -- the process cannot unmount the new root
* to escape the namespace.
*/
TEST_F(move_mount, beneath_rootfs_locked_containment)
{
int fd_tree, ret;
uint64_t clone_id, root_id;
ASSERT_EQ(setup_userns(), 0);
ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
/* Sanity: rootfs must be locked in the new userns */
ASSERT_EQ(umount2("/", MNT_DETACH), -1);
ASSERT_EQ(errno, EINVAL);
fd_tree = sys_open_tree(AT_FDCWD, "/",
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC |
AT_RECURSIVE);
ASSERT_GE(fd_tree, 0);
clone_id = get_unique_mnt_id_fd(fd_tree);
ASSERT_NE(clone_id, 0);
ASSERT_EQ(fchdir(fd_tree), 0);
ret = sys_move_mount(fd_tree, "", AT_FDCWD, "/",
MOVE_MOUNT_F_EMPTY_PATH |
MOVE_MOUNT_BENEATH);
ASSERT_EQ(ret, 0);
close(fd_tree);
ASSERT_EQ(chroot("."), 0);
root_id = get_unique_mnt_id("/");
ASSERT_EQ(root_id, clone_id);
/* Detach old root (MNT_LOCKED was cleared from it) */
ASSERT_EQ(umount2(".", MNT_DETACH), 0);
/* Verify "/" is still the clone after detaching old root */
root_id = get_unique_mnt_id("/");
ASSERT_EQ(root_id, clone_id);
/*
* The new root must be locked (MNT_LOCKED was transferred
* from the old root). Attempting to unmount it must fail
* with EINVAL, preserving the containment invariant.
*/
ASSERT_EQ(umount2("/", MNT_DETACH), -1);
ASSERT_EQ(errno, EINVAL);
}
/*
* Test MNT_LOCKED transfer when mounting beneath a non-rootfs locked mount.
* Mounts created before unshare(CLONE_NEWUSER | CLONE_NEWNS) become locked
* in the new namespace. Mount-beneath transfers the lock from the displaced
* mount to the new mount, so the displaced mount can be unmounted.
*/
TEST_F(move_mount, beneath_non_rootfs_locked_transfer)
{
int mnt_fd, ret;
uint64_t mnt_new_id, mnt_visible_id;
ASSERT_EQ(setup_locked_overmount(), 0);
mnt_fd = create_detached_tmpfs();
ASSERT_GE(mnt_fd, 0);
mnt_new_id = get_unique_mnt_id_fd(mnt_fd);
ASSERT_NE(mnt_new_id, 0);
/* Move mount beneath B (which is locked) */
ret = sys_move_mount(mnt_fd, "", AT_FDCWD, "/mnt_dir",
MOVE_MOUNT_F_EMPTY_PATH |
MOVE_MOUNT_BENEATH);
ASSERT_EQ(ret, 0);
close(mnt_fd);
/*
* B should now be unmountable (MNT_LOCKED was transferred
* to the new mount beneath it). If MNT_LOCKED wasn't
* cleared from B, this would fail with EINVAL.
*/
ASSERT_EQ(umount2("/mnt_dir", MNT_DETACH), 0);
/* Verify the new mount is now visible */
mnt_visible_id = get_unique_mnt_id("/mnt_dir");
ASSERT_EQ(mnt_visible_id, mnt_new_id);
}
/*
* Test MNT_LOCKED containment when mounting beneath a non-rootfs mount
* that was locked during unshare(CLONE_NEWUSER | CLONE_NEWNS).
* Mounts created before unshare become locked in the new namespace.
* Mount-beneath transfers the lock, preserving containment: the new
* mount cannot be unmounted, but the displaced mount can.
*/
TEST_F(move_mount, beneath_non_rootfs_locked_containment)
{
int mnt_fd, ret;
uint64_t mnt_new_id, mnt_visible_id;
ASSERT_EQ(setup_locked_overmount(), 0);
mnt_fd = create_detached_tmpfs();
ASSERT_GE(mnt_fd, 0);
mnt_new_id = get_unique_mnt_id_fd(mnt_fd);
ASSERT_NE(mnt_new_id, 0);
/*
* Move new tmpfs beneath B at /mnt_dir.
* Stack becomes: A -> new -> B
* Lock transfers from B to new.
*/
ret = sys_move_mount(mnt_fd, "", AT_FDCWD, "/mnt_dir",
MOVE_MOUNT_F_EMPTY_PATH |
MOVE_MOUNT_BENEATH);
ASSERT_EQ(ret, 0);
close(mnt_fd);
/*
* B lost MNT_LOCKED -- unmounting it must succeed.
* This reveals the new mount at /mnt_dir.
*/
ASSERT_EQ(umount2("/mnt_dir", MNT_DETACH), 0);
/* Verify the new mount is now visible */
mnt_visible_id = get_unique_mnt_id("/mnt_dir");
ASSERT_EQ(mnt_visible_id, mnt_new_id);
/*
* The new mount gained MNT_LOCKED -- unmounting it must
* fail with EINVAL, preserving the containment invariant.
*/
ASSERT_EQ(umount2("/mnt_dir", MNT_DETACH), -1);
ASSERT_EQ(errno, EINVAL);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
owner
pidns
iterate_mntns
@@ -0,0 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-only
TEST_GEN_PROGS := owner pidns iterate_mntns
CFLAGS := -Wall -Werror
include ../../lib.mk
@@ -0,0 +1,3 @@
CONFIG_USER_NS=y
CONFIG_UTS_NS=y
CONFIG_PID_NS=y
@@ -0,0 +1,168 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2024 Christian Brauner <brauner@kernel.org>
#define _GNU_SOURCE
#include <fcntl.h>
#include <linux/auto_dev-ioctl.h>
#include <linux/errno.h>
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <unistd.h>
#include "kselftest_harness.h"
#define MNT_NS_COUNT 11
#define MNT_NS_LAST_INDEX 10
struct mnt_ns_info {
__u32 size;
__u32 nr_mounts;
__u64 mnt_ns_id;
};
#define MNT_NS_INFO_SIZE_VER0 16 /* size of first published struct */
/* Get information about namespace. */
#define NS_MNT_GET_INFO _IOR(0xb7, 10, struct mnt_ns_info)
/* Get next namespace. */
#define NS_MNT_GET_NEXT _IOR(0xb7, 11, struct mnt_ns_info)
/* Get previous namespace. */
#define NS_MNT_GET_PREV _IOR(0xb7, 12, struct mnt_ns_info)
FIXTURE(iterate_mount_namespaces) {
int fd_mnt_ns[MNT_NS_COUNT];
__u64 mnt_ns_id[MNT_NS_COUNT];
};
static inline bool mntns_in_list(__u64 *mnt_ns_id, struct mnt_ns_info *info)
{
for (int i = 0; i < MNT_NS_COUNT; i++) {
if (mnt_ns_id[i] == info->mnt_ns_id)
return true;
}
return false;
}
FIXTURE_SETUP(iterate_mount_namespaces)
{
for (int i = 0; i < MNT_NS_COUNT; i++)
self->fd_mnt_ns[i] = -EBADF;
for (int i = 0; i < MNT_NS_COUNT; i++) {
struct mnt_ns_info info = {};
ASSERT_EQ(unshare(CLONE_NEWNS), 0);
self->fd_mnt_ns[i] = open("/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC);
ASSERT_GE(self->fd_mnt_ns[i], 0);
ASSERT_EQ(ioctl(self->fd_mnt_ns[i], NS_MNT_GET_INFO, &info), 0);
self->mnt_ns_id[i] = info.mnt_ns_id;
}
}
FIXTURE_TEARDOWN(iterate_mount_namespaces)
{
for (int i = 0; i < MNT_NS_COUNT; i++) {
if (self->fd_mnt_ns[i] < 0)
continue;
ASSERT_EQ(close(self->fd_mnt_ns[i]), 0);
}
}
TEST_F(iterate_mount_namespaces, iterate_all_forward)
{
int fd_mnt_ns_cur, count = 0;
fd_mnt_ns_cur = fcntl(self->fd_mnt_ns[0], F_DUPFD_CLOEXEC);
ASSERT_GE(fd_mnt_ns_cur, 0);
for (;;) {
struct mnt_ns_info info = {};
int fd_mnt_ns_next;
fd_mnt_ns_next = ioctl(fd_mnt_ns_cur, NS_MNT_GET_NEXT, &info);
if (fd_mnt_ns_next < 0 && errno == ENOENT)
break;
if (mntns_in_list(self->mnt_ns_id, &info))
count++;
ASSERT_GE(fd_mnt_ns_next, 0);
ASSERT_EQ(close(fd_mnt_ns_cur), 0);
fd_mnt_ns_cur = fd_mnt_ns_next;
}
ASSERT_EQ(count, MNT_NS_LAST_INDEX);
}
TEST_F(iterate_mount_namespaces, iterate_all_backwards)
{
int fd_mnt_ns_cur, count = 0;
fd_mnt_ns_cur = fcntl(self->fd_mnt_ns[MNT_NS_LAST_INDEX], F_DUPFD_CLOEXEC);
ASSERT_GE(fd_mnt_ns_cur, 0);
for (;;) {
struct mnt_ns_info info = {};
int fd_mnt_ns_prev;
fd_mnt_ns_prev = ioctl(fd_mnt_ns_cur, NS_MNT_GET_PREV, &info);
if (fd_mnt_ns_prev < 0 && errno == ENOENT)
break;
if (mntns_in_list(self->mnt_ns_id, &info))
count++;
ASSERT_GE(fd_mnt_ns_prev, 0);
ASSERT_EQ(close(fd_mnt_ns_cur), 0);
fd_mnt_ns_cur = fd_mnt_ns_prev;
}
ASSERT_EQ(count, MNT_NS_LAST_INDEX);
}
TEST_F(iterate_mount_namespaces, iterate_forward)
{
int fd_mnt_ns_cur;
ASSERT_EQ(setns(self->fd_mnt_ns[0], CLONE_NEWNS), 0);
fd_mnt_ns_cur = self->fd_mnt_ns[0];
for (int i = 1; i < MNT_NS_COUNT; i++) {
struct mnt_ns_info info = {};
int fd_mnt_ns_next;
fd_mnt_ns_next = ioctl(fd_mnt_ns_cur, NS_MNT_GET_NEXT, &info);
ASSERT_GE(fd_mnt_ns_next, 0);
ASSERT_EQ(close(fd_mnt_ns_cur), 0);
fd_mnt_ns_cur = fd_mnt_ns_next;
}
}
TEST_F(iterate_mount_namespaces, iterate_backward)
{
int fd_mnt_ns_cur;
ASSERT_EQ(setns(self->fd_mnt_ns[MNT_NS_LAST_INDEX], CLONE_NEWNS), 0);
fd_mnt_ns_cur = self->fd_mnt_ns[MNT_NS_LAST_INDEX];
for (int i = MNT_NS_LAST_INDEX - 1; i >= 0; i--) {
struct mnt_ns_info info = {};
int fd_mnt_ns_prev;
fd_mnt_ns_prev = ioctl(fd_mnt_ns_cur, NS_MNT_GET_PREV, &info);
ASSERT_GE(fd_mnt_ns_prev, 0);
ASSERT_EQ(close(fd_mnt_ns_cur), 0);
fd_mnt_ns_cur = fd_mnt_ns_prev;
}
}
TEST_F(iterate_mount_namespaces, nfs_valid_ioctl)
{
ASSERT_NE(ioctl(self->fd_mnt_ns[0], AUTOFS_DEV_IOCTL_OPENMOUNT, NULL), 0);
ASSERT_EQ(errno, ENOTTY);
ASSERT_NE(ioctl(self->fd_mnt_ns[0], AUTOFS_DEV_IOCTL_CLOSEMOUNT, NULL), 0);
ASSERT_EQ(errno, ENOTTY);
ASSERT_NE(ioctl(self->fd_mnt_ns[0], AUTOFS_DEV_IOCTL_READY, NULL), 0);
ASSERT_EQ(errno, ENOTTY);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/wait.h>
#define NSIO 0xb7
#define NS_GET_USERNS _IO(NSIO, 0x1)
#define pr_err(fmt, ...) \
({ \
fprintf(stderr, "%s:%d:" fmt ": %m\n", \
__func__, __LINE__, ##__VA_ARGS__); \
1; \
})
int main(int argc, char *argvp[])
{
int pfd[2], ns, uns, init_uns;
struct stat st1, st2;
char path[128];
pid_t pid;
char c;
if (pipe(pfd))
return 1;
pid = fork();
if (pid < 0)
return pr_err("fork");
if (pid == 0) {
prctl(PR_SET_PDEATHSIG, SIGKILL);
if (unshare(CLONE_NEWUTS | CLONE_NEWUSER))
return pr_err("unshare");
close(pfd[0]);
close(pfd[1]);
while (1)
sleep(1);
return 0;
}
close(pfd[1]);
if (read(pfd[0], &c, 1) != 0)
return pr_err("Unable to read from pipe");
close(pfd[0]);
snprintf(path, sizeof(path), "/proc/%d/ns/uts", pid);
ns = open(path, O_RDONLY);
if (ns < 0)
return pr_err("Unable to open %s", path);
uns = ioctl(ns, NS_GET_USERNS);
if (uns < 0)
return pr_err("Unable to get an owning user namespace");
if (fstat(uns, &st1))
return pr_err("fstat");
snprintf(path, sizeof(path), "/proc/%d/ns/user", pid);
if (stat(path, &st2))
return pr_err("stat");
if (st1.st_ino != st2.st_ino)
return pr_err("NS_GET_USERNS returned a wrong namespace");
init_uns = ioctl(uns, NS_GET_USERNS);
if (uns < 0)
return pr_err("Unable to get an owning user namespace");
if (ioctl(init_uns, NS_GET_USERNS) >= 0 || errno != EPERM)
return pr_err("Don't get EPERM");
if (unshare(CLONE_NEWUSER))
return pr_err("unshare");
if (ioctl(ns, NS_GET_USERNS) >= 0 || errno != EPERM)
return pr_err("Don't get EPERM");
if (ioctl(init_uns, NS_GET_USERNS) >= 0 || errno != EPERM)
return pr_err("Don't get EPERM");
kill(pid, SIGKILL);
wait(NULL);
return 0;
}
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/wait.h>
#define pr_err(fmt, ...) \
({ \
fprintf(stderr, "%s:%d:" fmt ": %m\n", \
__func__, __LINE__, ##__VA_ARGS__); \
1; \
})
#define NSIO 0xb7
#define NS_GET_USERNS _IO(NSIO, 0x1)
#define NS_GET_PARENT _IO(NSIO, 0x2)
#define __stack_aligned__ __attribute__((aligned(16)))
struct cr_clone_arg {
char stack[128] __stack_aligned__;
char stack_ptr[];
};
static int child(void *args)
{
prctl(PR_SET_PDEATHSIG, SIGKILL);
while (1)
sleep(1);
exit(0);
}
int main(int argc, char *argv[])
{
char *ns_strs[] = {"pid", "user"};
char path[] = "/proc/0123456789/ns/pid";
struct cr_clone_arg ca;
struct stat st1, st2;
int ns, pns, i;
pid_t pid;
pid = clone(child, ca.stack_ptr, CLONE_NEWUSER | CLONE_NEWPID | SIGCHLD, NULL);
if (pid < 0)
return pr_err("clone");
for (i = 0; i < 2; i++) {
snprintf(path, sizeof(path), "/proc/%d/ns/%s", pid, ns_strs[i]);
ns = open(path, O_RDONLY);
if (ns < 0)
return pr_err("Unable to open %s", path);
pns = ioctl(ns, NS_GET_PARENT);
if (pns < 0)
return pr_err("Unable to get a parent pidns");
snprintf(path, sizeof(path), "/proc/self/ns/%s", ns_strs[i]);
if (stat(path, &st2))
return pr_err("Unable to stat %s", path);
if (fstat(pns, &st1))
return pr_err("Unable to stat the parent pidns");
if (st1.st_ino != st2.st_ino)
return pr_err("NS_GET_PARENT returned a wrong namespace");
if (ioctl(pns, NS_GET_PARENT) >= 0 || errno != EPERM)
return pr_err("Don't get EPERM");
}
kill(pid, SIGKILL);
wait(NULL);
return 0;
}
@@ -0,0 +1 @@
open_tree_ns_test
@@ -0,0 +1,10 @@
# SPDX-License-Identifier: GPL-2.0
TEST_GEN_PROGS := open_tree_ns_test
CFLAGS += -Wall -O0 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES)
LDLIBS := -lcap
include ../../lib.mk
$(OUTPUT)/open_tree_ns_test: open_tree_ns_test.c ../utils.c
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0-only
/*_test
@@ -0,0 +1,17 @@
# SPDX-License-Identifier: GPL-2.0-or-later
CFLAGS += $(KHDR_INCLUDES)
CFLAGS += -Wall -O2 -g -fsanitize=address -fsanitize=undefined $(TOOLS_INCLUDES)
TEST_GEN_PROGS := openat2_test resolve_test rename_attack_test emptypath_test
# gcc requires -static-libasan in order to ensure that Address Sanitizer's
# library is the first one loaded. However, clang already statically links the
# Address Sanitizer if -fsanitize is specified. Therefore, simply omit
# -static-libasan for clang builds.
ifeq ($(LLVM),)
CFLAGS += -static-libasan
endif
LOCAL_HDRS += helpers.h
include ../../lib.mk
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#define _GNU_SOURCE
#define __SANE_USERSPACE_TYPES__
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include "kselftest_harness.h"
#ifndef O_EMPTYPATH
#define O_EMPTYPATH (1 << 26)
#endif
#define EMPTYPATH_TEST_FILE "/tmp/emptypath_test"
FIXTURE(emptypath) {
int opath_fd;
};
FIXTURE_SETUP(emptypath)
{
int fd;
self->opath_fd = -1;
fd = open(EMPTYPATH_TEST_FILE, O_CREAT | O_WRONLY, S_IRWXU);
ASSERT_GE(fd, 0) {
TH_LOG("create %s: %s", EMPTYPATH_TEST_FILE, strerror(errno));
}
close(fd);
self->opath_fd = open(EMPTYPATH_TEST_FILE, O_PATH);
ASSERT_GE(self->opath_fd, 0) {
TH_LOG("open %s O_PATH: %s", EMPTYPATH_TEST_FILE, strerror(errno));
}
}
FIXTURE_TEARDOWN(emptypath)
{
if (self->opath_fd >= 0)
close(self->opath_fd);
unlink(EMPTYPATH_TEST_FILE);
}
/* An empty path is rejected with ENOENT unless O_EMPTYPATH is set. */
TEST_F(emptypath, without_flag_returns_enoent)
{
int fd = openat(self->opath_fd, "", O_RDONLY);
if (fd >= 0)
close(fd);
ASSERT_LT(fd, 0) {
TH_LOG("empty path without O_EMPTYPATH unexpectedly succeeded");
}
EXPECT_EQ(errno, ENOENT) {
TH_LOG("expected ENOENT, got %s", strerror(errno));
}
}
/* O_EMPTYPATH reopens the O_PATH fd through an empty path. */
TEST_F(emptypath, reopens_opath_fd)
{
int fd = openat(self->opath_fd, "", O_RDONLY | O_EMPTYPATH);
if (fd < 0 && errno == EINVAL)
SKIP(return, "O_EMPTYPATH not supported");
ASSERT_GE(fd, 0) {
TH_LOG("O_EMPTYPATH failed: %s", strerror(errno));
}
close(fd);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,135 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Author: Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2018-2019 SUSE LLC.
* Copyright (C) 2026 Amutable GmbH
*/
#ifndef __RESOLVEAT_H__
#define __RESOLVEAT_H__
#define _GNU_SOURCE
#include <stdint.h>
#include <stdbool.h>
#include <errno.h>
#include <limits.h>
#include <linux/types.h>
#include <linux/unistd.h>
#include <linux/openat2.h>
#include "kselftest_harness.h"
#define BUILD_BUG_ON(e) ((void)(sizeof(struct { int:(-!!(e)); })))
#define OPEN_HOW_SIZE_VER0 24 /* sizeof first published struct */
#define OPEN_HOW_SIZE_LATEST OPEN_HOW_SIZE_VER0
__maybe_unused
static bool needs_openat2(const struct open_how *how)
{
return how->resolve != 0;
}
__maybe_unused
static int raw_openat2(int dfd, const char *path, void *how, size_t size)
{
int ret = syscall(__NR_openat2, dfd, path, how, size);
return ret >= 0 ? ret : -errno;
}
__maybe_unused
static int sys_openat2(int dfd, const char *path, struct open_how *how)
{
return raw_openat2(dfd, path, how, sizeof(*how));
}
__maybe_unused
static int sys_openat(int dfd, const char *path, struct open_how *how)
{
int ret = openat(dfd, path, how->flags, how->mode);
return ret >= 0 ? ret : -errno;
}
__maybe_unused
static int sys_renameat2(int olddirfd, const char *oldpath,
int newdirfd, const char *newpath, unsigned int flags)
{
int ret = syscall(__NR_renameat2, olddirfd, oldpath,
newdirfd, newpath, flags);
return ret >= 0 ? ret : -errno;
}
__maybe_unused
static int touchat(int dfd, const char *path)
{
int fd = openat(dfd, path, O_CREAT, 0700);
if (fd >= 0)
close(fd);
return fd;
}
__maybe_unused
static char *fdreadlink(struct __test_metadata *_metadata, int fd)
{
char *target, *tmp;
ASSERT_GT(asprintf(&tmp, "/proc/self/fd/%d", fd), 0);
target = malloc(PATH_MAX);
ASSERT_NE(target, NULL);
memset(target, 0, PATH_MAX);
ASSERT_GT(readlink(tmp, target, PATH_MAX), 0);
free(tmp);
return target;
}
__maybe_unused
static bool fdequal(struct __test_metadata *_metadata, int fd,
int dfd, const char *path)
{
char *fdpath, *dfdpath, *other;
bool cmp;
fdpath = fdreadlink(_metadata, fd);
dfdpath = fdreadlink(_metadata, dfd);
if (!path) {
ASSERT_GT(asprintf(&other, "%s", dfdpath), 0);
} else if (*path == '/') {
ASSERT_GT(asprintf(&other, "%s", path), 0);
} else {
ASSERT_GT(asprintf(&other, "%s/%s", dfdpath, path), 0);
}
cmp = !strcmp(fdpath, other);
free(fdpath);
free(dfdpath);
free(other);
return cmp;
}
static bool openat2_supported = false;
__attribute__((constructor))
static void __detect_openat2_supported(void)
{
struct open_how how = {};
int fd;
BUILD_BUG_ON(sizeof(struct open_how) != OPEN_HOW_SIZE_VER0);
/* Check openat2(2) support. */
fd = sys_openat2(AT_FDCWD, ".", &how);
openat2_supported = (fd >= 0);
if (fd >= 0)
close(fd);
}
#endif /* __RESOLVEAT_H__ */
@@ -0,0 +1,362 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Author: Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2018-2019 SUSE LLC.
*/
#define _GNU_SOURCE
#define __SANE_USERSPACE_TYPES__ // Use ll64
#include <fcntl.h>
#include <sched.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mount.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include "helpers.h"
#include "kselftest_harness.h"
/*
* O_LARGEFILE is set to 0 by glibc.
* XXX: This is wrong on {mips, parisc, powerpc, sparc}.
*/
#undef O_LARGEFILE
#ifdef __aarch64__
#define O_LARGEFILE 0x20000
#else
#define O_LARGEFILE 0x8000
#endif
struct open_how_ext {
struct open_how inner;
uint32_t extra1;
char pad1[128];
uint32_t extra2;
char pad2[128];
uint32_t extra3;
};
struct struct_test {
const char *name;
struct open_how_ext arg;
size_t size;
int err;
};
struct flag_test {
const char *name;
struct open_how how;
int err;
};
FIXTURE(openat2) {};
FIXTURE_SETUP(openat2)
{
if (!openat2_supported)
SKIP(return, "openat2(2) not supported");
}
FIXTURE_TEARDOWN(openat2) {}
/*
* Verify that the struct size and misalignment handling for openat2(2) is
* correct, including that is_zeroed_user() works.
*/
TEST_F(openat2, struct_argument_sizes)
{
int misalignments[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 17, 87 };
struct struct_test tests[] = {
/* Normal struct. */
{ .name = "normal struct",
.arg.inner.flags = O_RDONLY,
.size = sizeof(struct open_how) },
/* Bigger struct, with zeroed out end. */
{ .name = "bigger struct (zeroed out)",
.arg.inner.flags = O_RDONLY,
.size = sizeof(struct open_how_ext) },
/* TODO: Once expanded, check zero-padding. */
/* Smaller than version-0 struct. */
{ .name = "zero-sized 'struct'",
.arg.inner.flags = O_RDONLY, .size = 0, .err = -EINVAL },
{ .name = "smaller-than-v0 struct",
.arg.inner.flags = O_RDONLY,
.size = OPEN_HOW_SIZE_VER0 - 1, .err = -EINVAL },
/* Bigger struct, with non-zero trailing bytes. */
{ .name = "bigger struct (non-zero data in first 'future field')",
.arg.inner.flags = O_RDONLY, .arg.extra1 = 0xdeadbeef,
.size = sizeof(struct open_how_ext), .err = -E2BIG },
{ .name = "bigger struct (non-zero data in middle of 'future fields')",
.arg.inner.flags = O_RDONLY, .arg.extra2 = 0xfeedcafe,
.size = sizeof(struct open_how_ext), .err = -E2BIG },
{ .name = "bigger struct (non-zero data at end of 'future fields')",
.arg.inner.flags = O_RDONLY, .arg.extra3 = 0xabad1dea,
.size = sizeof(struct open_how_ext), .err = -E2BIG },
};
for (int i = 0; i < ARRAY_SIZE(tests); i++) {
struct struct_test *test = &tests[i];
struct open_how_ext how_ext = test->arg;
for (int j = 0; j < ARRAY_SIZE(misalignments); j++) {
int fd, misalign = misalignments[j];
void *copy = NULL, *how_copy = &how_ext;
char *fdpath = NULL;
if (misalign) {
/*
* Explicitly misalign the structure copying it with the given
* (mis)alignment offset. The other data is set to be non-zero to
* make sure that non-zero bytes outside the struct aren't checked
*
* This is effectively to check that is_zeroed_user() works.
*/
copy = malloc(misalign + sizeof(how_ext));
how_copy = copy + misalign;
memset(copy, 0xff, misalign);
memcpy(how_copy, &how_ext, sizeof(how_ext));
}
fd = raw_openat2(AT_FDCWD, ".", how_copy, test->size);
if (fd >= 0) {
fdpath = fdreadlink(_metadata, fd);
close(fd);
}
if (test->err >= 0) {
EXPECT_GE(fd, 0) {
TH_LOG("openat2 with %s [misalign=%d] should succeed, got %d (%s)",
test->name, misalign,
fd, strerror(-fd));
}
} else {
EXPECT_EQ(test->err, fd) {
if (fdpath)
TH_LOG("openat2 with %s [misalign=%d] should fail with %d (%s), got %d['%s']",
test->name, misalign,
test->err,
strerror(-test->err),
fd, fdpath);
else
TH_LOG("openat2 with %s [misalign=%d] should fail with %d (%s), got %d (%s)",
test->name, misalign,
test->err,
strerror(-test->err),
fd, strerror(-fd));
}
}
free(copy);
free(fdpath);
}
}
}
/* Verify openat2(2) flag and mode validation. */
TEST_F(openat2, flag_validation)
{
struct flag_test tests[] = {
/* O_TMPFILE is incompatible with O_PATH and O_CREAT. */
{ .name = "incompatible flags (O_TMPFILE | O_PATH)",
.how.flags = O_TMPFILE | O_PATH | O_RDWR, .err = -EINVAL },
{ .name = "incompatible flags (O_TMPFILE | O_CREAT)",
.how.flags = O_TMPFILE | O_CREAT | O_RDWR, .err = -EINVAL },
/* O_PATH only permits certain other flags to be set ... */
{ .name = "compatible flags (O_PATH | O_CLOEXEC)",
.how.flags = O_PATH | O_CLOEXEC },
{ .name = "compatible flags (O_PATH | O_DIRECTORY)",
.how.flags = O_PATH | O_DIRECTORY },
{ .name = "compatible flags (O_PATH | O_NOFOLLOW)",
.how.flags = O_PATH | O_NOFOLLOW },
/* ... and others are absolutely not permitted. */
{ .name = "incompatible flags (O_PATH | O_RDWR)",
.how.flags = O_PATH | O_RDWR, .err = -EINVAL },
{ .name = "incompatible flags (O_PATH | O_CREAT)",
.how.flags = O_PATH | O_CREAT, .err = -EINVAL },
{ .name = "incompatible flags (O_PATH | O_EXCL)",
.how.flags = O_PATH | O_EXCL, .err = -EINVAL },
{ .name = "incompatible flags (O_PATH | O_NOCTTY)",
.how.flags = O_PATH | O_NOCTTY, .err = -EINVAL },
{ .name = "incompatible flags (O_PATH | O_DIRECT)",
.how.flags = O_PATH | O_DIRECT, .err = -EINVAL },
{ .name = "incompatible flags (O_PATH | O_LARGEFILE)",
.how.flags = O_PATH | O_LARGEFILE, .err = -EINVAL },
/* ->mode must only be set with O_{CREAT,TMPFILE}. */
{ .name = "non-zero how.mode and O_RDONLY",
.how.flags = O_RDONLY, .how.mode = 0600, .err = -EINVAL },
{ .name = "non-zero how.mode and O_PATH",
.how.flags = O_PATH, .how.mode = 0600, .err = -EINVAL },
{ .name = "valid how.mode and O_CREAT",
.how.flags = O_CREAT, .how.mode = 0600 },
{ .name = "valid how.mode and O_TMPFILE",
.how.flags = O_TMPFILE | O_RDWR, .how.mode = 0600 },
/* ->mode must only contain 0777 bits. */
{ .name = "invalid how.mode and O_CREAT",
.how.flags = O_CREAT,
.how.mode = 0xFFFF, .err = -EINVAL },
{ .name = "invalid (very large) how.mode and O_CREAT",
.how.flags = O_CREAT,
.how.mode = 0xC000000000000000ULL, .err = -EINVAL },
{ .name = "invalid how.mode and O_TMPFILE",
.how.flags = O_TMPFILE | O_RDWR,
.how.mode = 0x1337, .err = -EINVAL },
{ .name = "invalid (very large) how.mode and O_TMPFILE",
.how.flags = O_TMPFILE | O_RDWR,
.how.mode = 0x0000A00000000000ULL, .err = -EINVAL },
/* ->resolve flags must not conflict. */
{ .name = "incompatible resolve flags (BENEATH | IN_ROOT)",
.how.flags = O_RDONLY,
.how.resolve = RESOLVE_BENEATH | RESOLVE_IN_ROOT,
.err = -EINVAL },
/* ->resolve must only contain RESOLVE_* flags. */
{ .name = "invalid how.resolve and O_RDONLY",
.how.flags = O_RDONLY,
.how.resolve = 0x1337, .err = -EINVAL },
{ .name = "invalid how.resolve and O_CREAT",
.how.flags = O_CREAT,
.how.resolve = 0x1337, .err = -EINVAL },
{ .name = "invalid how.resolve and O_TMPFILE",
.how.flags = O_TMPFILE | O_RDWR,
.how.resolve = 0x1337, .err = -EINVAL },
{ .name = "invalid how.resolve and O_PATH",
.how.flags = O_PATH,
.how.resolve = 0x1337, .err = -EINVAL },
/* currently unknown upper 32 bit rejected. */
{ .name = "currently unknown bit (1 << 63)",
.how.flags = O_RDONLY | (1ULL << 63),
.how.resolve = 0, .err = -EINVAL },
};
for (int i = 0; i < ARRAY_SIZE(tests); i++) {
int fd, fdflags = -1;
char *path, *fdpath = NULL;
struct flag_test *test = &tests[i];
path = (test->how.flags & O_CREAT) ? "/tmp/ksft.openat2_tmpfile" : ".";
unlink(path);
fd = sys_openat2(AT_FDCWD, path, &test->how);
if (fd < 0 && fd == -EOPNOTSUPP) {
/*
* Skip the testcase if it failed because not supported
* by FS. (e.g. a valid O_TMPFILE combination on NFS)
*/
TH_LOG("openat2 with %s not supported by FS -- skipping",
test->name);
continue;
}
if (test->err >= 0) {
EXPECT_GE(fd, 0) {
TH_LOG("openat2 with %s should succeed, got %d (%s)",
test->name, fd, strerror(-fd));
}
if (fd >= 0) {
int otherflags;
fdpath = fdreadlink(_metadata, fd);
fdflags = fcntl(fd, F_GETFL);
otherflags = fcntl(fd, F_GETFD);
close(fd);
ASSERT_GE(fdflags, 0);
ASSERT_GE(otherflags, 0);
/* O_CLOEXEC isn't shown in F_GETFL. */
if (otherflags & FD_CLOEXEC)
fdflags |= O_CLOEXEC;
/* O_CREAT is hidden from F_GETFL. */
if (test->how.flags & O_CREAT)
fdflags |= O_CREAT;
if (!(test->how.flags & O_LARGEFILE))
fdflags &= ~O_LARGEFILE;
EXPECT_EQ(fdflags, (int)test->how.flags) {
TH_LOG("openat2 with %s: flags mismatch %X != %llX",
test->name, fdflags,
(unsigned long long)test->how.flags);
}
}
} else {
EXPECT_EQ(test->err, fd) {
if (fd >= 0) {
fdpath = fdreadlink(_metadata, fd);
TH_LOG("openat2 with %s should fail with %d (%s), got %d['%s']",
test->name, test->err,
strerror(-test->err),
fd, fdpath);
} else {
TH_LOG("openat2 with %s should fail with %d (%s), got %d (%s)",
test->name, test->err,
strerror(-test->err),
fd, strerror(-fd));
}
}
if (fd >= 0)
close(fd);
}
free(fdpath);
}
}
#ifndef OPENAT2_REGULAR
#define OPENAT2_REGULAR ((__u64)1 << 32)
#endif
#ifndef EFTYPE
#define EFTYPE 134
#endif
/* Kernel-internal carrier for OPENAT2_REGULAR (see __O_REGULAR in fcntl.h). */
#ifndef __O_REGULAR
#define __O_REGULAR (1 << 30)
#endif
/* Verify that OPENAT2_REGULAR rejects non-regular files with EFTYPE. */
TEST_F(openat2, regular_flag)
{
struct open_how how = {
.flags = OPENAT2_REGULAR | O_RDONLY,
};
int fd;
fd = sys_openat2(AT_FDCWD, "/dev/null", &how);
if (fd == -ENOENT)
SKIP(return, "/dev/null does not exist");
EXPECT_EQ(-EFTYPE, fd) {
TH_LOG("openat2 with OPENAT2_REGULAR should fail with %d (%s), got %d (%s)",
-EFTYPE, strerror(EFTYPE), fd, strerror(-fd));
}
if (fd >= 0)
close(fd);
}
/* open()/openat() must keep ignoring the internal __O_REGULAR bit. */
TEST(legacy_openat_ignores_o_regular)
{
int fd;
fd = openat(AT_FDCWD, "/dev/null", O_RDONLY | __O_REGULAR);
if (fd < 0 && errno == ENOENT)
SKIP(return, "/dev/null does not exist");
ASSERT_GE(fd, 0) {
TH_LOG("legacy openat() must ignore the __O_REGULAR carrier bit, got errno %d (%s)",
errno, strerror(errno));
}
close(fd);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,159 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Author: Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2018-2019 SUSE LLC.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mount.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <syscall.h>
#include <limits.h>
#include <unistd.h>
#include "helpers.h"
#include "kselftest_harness.h"
#define ROUNDS 400000
/* Swap @dirfd/@a and @dirfd/@b constantly. Parent must kill this process. */
pid_t spawn_attack(struct __test_metadata *_metadata,
int dirfd, char *a, char *b)
{
pid_t child = fork();
if (child != 0)
return child;
/* If the parent (the test process) dies, kill ourselves too. */
ASSERT_EQ(prctl(PR_SET_PDEATHSIG, SIGKILL), 0);
/* Swap @a and @b. */
for (;;)
renameat2(dirfd, a, dirfd, b, RENAME_EXCHANGE);
exit(1);
}
/*
* Construct a test directory with the following structure:
*
* root/
* |-- a/
* | `-- c/
* `-- b/
*/
FIXTURE(rename_attack) {
int dfd;
int afd;
pid_t child;
};
FIXTURE_SETUP(rename_attack)
{
char dirname[] = "/tmp/ksft-openat2-rename-attack.XXXXXX";
self->dfd = -1;
self->afd = -1;
self->child = 0;
/* Make the top-level directory. */
ASSERT_NE(mkdtemp(dirname), NULL);
self->dfd = open(dirname, O_PATH | O_DIRECTORY);
ASSERT_GE(self->dfd, 0);
ASSERT_EQ(mkdirat(self->dfd, "a", 0755), 0);
ASSERT_EQ(mkdirat(self->dfd, "b", 0755), 0);
ASSERT_EQ(mkdirat(self->dfd, "a/c", 0755), 0);
self->afd = openat(self->dfd, "a", O_PATH);
ASSERT_GE(self->afd, 0);
self->child = spawn_attack(_metadata, self->dfd, "a/c", "b");
ASSERT_GT(self->child, 0);
}
FIXTURE_TEARDOWN(rename_attack)
{
if (self->child > 0)
kill(self->child, SIGKILL);
if (self->afd >= 0)
close(self->afd);
if (self->dfd >= 0)
close(self->dfd);
}
FIXTURE_VARIANT(rename_attack) {
int resolve;
const char *name;
};
FIXTURE_VARIANT_ADD(rename_attack, resolve_beneath) {
.resolve = RESOLVE_BENEATH,
.name = "RESOLVE_BENEATH",
};
FIXTURE_VARIANT_ADD(rename_attack, resolve_in_root) {
.resolve = RESOLVE_IN_ROOT,
.name = "RESOLVE_IN_ROOT",
};
TEST_F_TIMEOUT(rename_attack, test, 120)
{
int escapes = 0, successes = 0, other_errs = 0, exdevs = 0, eagains = 0;
char *victim_path = "c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../..";
struct open_how how = {
.flags = O_PATH,
.resolve = variant->resolve,
};
if (!openat2_supported) {
how.resolve = 0;
TH_LOG("openat2(2) unsupported -- using openat(2) instead");
}
for (int i = 0; i < ROUNDS; i++) {
int fd;
if (openat2_supported)
fd = sys_openat2(self->afd, victim_path, &how);
else
fd = sys_openat(self->afd, victim_path, &how);
if (fd < 0) {
if (fd == -EAGAIN)
eagains++;
else if (fd == -EXDEV)
exdevs++;
else if (fd == -ENOENT)
escapes++; /* escaped outside and got ENOENT... */
else
other_errs++; /* unexpected error */
} else {
if (fdequal(_metadata, fd, self->afd, NULL))
successes++;
else
escapes++; /* we got an unexpected fd */
}
if (fd >= 0)
close(fd);
}
TH_LOG("non-escapes: EAGAIN=%d EXDEV=%d E<other>=%d success=%d",
eagains, exdevs, other_errs, successes);
ASSERT_EQ(escapes, 0) {
TH_LOG("rename attack with %s (%d runs, got %d escapes)",
variant->name, ROUNDS, escapes);
}
}
TEST_HARNESS_MAIN
@@ -0,0 +1,561 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Author: Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2018-2019 SUSE LLC.
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mount.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include "helpers.h"
#include "kselftest_harness.h"
struct resolve_test {
const char *name;
const char *dir;
const char *path;
struct open_how how;
bool pass;
union {
int err;
const char *path;
} out;
};
/*
* Verify a single resolve test case. This must be called from within a TEST_F
* function with _metadata in scope.
*/
static void verify_resolve_test(struct __test_metadata *_metadata,
int rootfd, int hardcoded_fd,
const struct resolve_test *test)
{
struct open_how how = test->how;
int dfd, fd;
char *fdpath = NULL;
/* Auto-set O_PATH. */
if (!(how.flags & O_CREAT))
how.flags |= O_PATH;
if (test->dir)
dfd = openat(rootfd, test->dir, O_PATH | O_DIRECTORY);
else
dfd = dup(rootfd);
ASSERT_GE(dfd, 0) TH_LOG("failed to open dir '%s': %m", test->dir ?: ".");
ASSERT_EQ(dup2(dfd, hardcoded_fd), hardcoded_fd);
fd = sys_openat2(dfd, test->path, &how);
if (test->pass) {
EXPECT_GE(fd, 0) {
TH_LOG("%s: expected success, got %d (%s)",
test->name, fd, strerror(-fd));
}
if (fd >= 0) {
EXPECT_TRUE(fdequal(_metadata, fd, rootfd, test->out.path)) {
fdpath = fdreadlink(_metadata, fd);
TH_LOG("%s: wrong path '%s', expected '%s'",
test->name, fdpath,
test->out.path ?: ".");
free(fdpath);
}
}
} else {
EXPECT_EQ(test->out.err, fd) {
if (fd >= 0) {
fdpath = fdreadlink(_metadata, fd);
TH_LOG("%s: expected %d (%s), got %d['%s']",
test->name, test->out.err,
strerror(-test->out.err), fd, fdpath);
free(fdpath);
} else {
TH_LOG("%s: expected %d (%s), got %d (%s)",
test->name, test->out.err,
strerror(-test->out.err),
fd, strerror(-fd));
}
}
}
if (fd >= 0)
close(fd);
close(dfd);
}
/*
* Construct a test directory with the following structure:
*
* root/
* |-- procexe -> /proc/self/exe
* |-- procroot -> /proc/self/root
* |-- root/
* |-- mnt/ [mountpoint]
* | |-- self -> ../mnt/
* | `-- absself -> /mnt/
* |-- etc/
* | `-- passwd
* |-- creatlink -> /newfile3
* |-- reletc -> etc/
* |-- relsym -> etc/passwd
* |-- absetc -> /etc/
* |-- abssym -> /etc/passwd
* |-- abscheeky -> /cheeky
* `-- cheeky/
* |-- absself -> /
* |-- self -> ../../root/
* |-- garbageself -> /../../root/
* |-- passwd -> ../cheeky/../etc/../etc/passwd
* |-- abspasswd -> /../cheeky/../etc/../etc/passwd
* |-- dotdotlink -> ../../../../../../../../../../../../../../etc/passwd
* `-- garbagelink -> /../../../../../../../../../../../../../../etc/passwd
*/
FIXTURE(openat2_resolve) {
int rootfd;
int hardcoded_fd;
char *hardcoded_fdpath;
char *procselfexe;
};
FIXTURE_SETUP(openat2_resolve)
{
char dirname[] = "/tmp/ksft-openat2-testdir.XXXXXX";
int dfd, tmpfd;
self->rootfd = -1;
self->hardcoded_fd = -1;
self->hardcoded_fdpath = NULL;
self->procselfexe = NULL;
/* NOTE: We should be checking for CAP_SYS_ADMIN here... */
if (geteuid() != 0)
SKIP(return, "all tests require euid == 0");
if (!openat2_supported)
SKIP(return, "openat2(2) not supported");
/* Unshare and make /tmp a new directory. */
ASSERT_EQ(unshare(CLONE_NEWNS), 0);
ASSERT_EQ(mount("", "/tmp", "", MS_PRIVATE, ""), 0);
/* Make the top-level directory. */
ASSERT_NE(mkdtemp(dirname), NULL);
dfd = open(dirname, O_PATH | O_DIRECTORY);
ASSERT_GE(dfd, 0);
/* A sub-directory which is actually used for tests. */
ASSERT_EQ(mkdirat(dfd, "root", 0755), 0);
tmpfd = openat(dfd, "root", O_PATH | O_DIRECTORY);
ASSERT_GE(tmpfd, 0);
close(dfd);
dfd = tmpfd;
ASSERT_EQ(symlinkat("/proc/self/exe", dfd, "procexe"), 0);
ASSERT_EQ(symlinkat("/proc/self/root", dfd, "procroot"), 0);
ASSERT_EQ(mkdirat(dfd, "root", 0755), 0);
/* There is no mountat(2), so use chdir. */
ASSERT_EQ(mkdirat(dfd, "mnt", 0755), 0);
ASSERT_EQ(fchdir(dfd), 0);
ASSERT_EQ(mount("tmpfs", "./mnt", "tmpfs", MS_NOSUID | MS_NODEV, ""), 0);
ASSERT_EQ(symlinkat("../mnt/", dfd, "mnt/self"), 0);
ASSERT_EQ(symlinkat("/mnt/", dfd, "mnt/absself"), 0);
ASSERT_EQ(mkdirat(dfd, "etc", 0755), 0);
ASSERT_GE(touchat(dfd, "etc/passwd"), 0);
ASSERT_EQ(symlinkat("/newfile3", dfd, "creatlink"), 0);
ASSERT_EQ(symlinkat("etc/", dfd, "reletc"), 0);
ASSERT_EQ(symlinkat("etc/passwd", dfd, "relsym"), 0);
ASSERT_EQ(symlinkat("/etc/", dfd, "absetc"), 0);
ASSERT_EQ(symlinkat("/etc/passwd", dfd, "abssym"), 0);
ASSERT_EQ(symlinkat("/cheeky", dfd, "abscheeky"), 0);
ASSERT_EQ(mkdirat(dfd, "cheeky", 0755), 0);
ASSERT_EQ(symlinkat("/", dfd, "cheeky/absself"), 0);
ASSERT_EQ(symlinkat("../../root/", dfd, "cheeky/self"), 0);
ASSERT_EQ(symlinkat("/../../root/", dfd, "cheeky/garbageself"), 0);
ASSERT_EQ(symlinkat("../cheeky/../etc/../etc/passwd",
dfd, "cheeky/passwd"), 0);
ASSERT_EQ(symlinkat("/../cheeky/../etc/../etc/passwd",
dfd, "cheeky/abspasswd"), 0);
ASSERT_EQ(symlinkat("../../../../../../../../../../../../../../etc/passwd",
dfd, "cheeky/dotdotlink"), 0);
ASSERT_EQ(symlinkat("/../../../../../../../../../../../../../../etc/passwd",
dfd, "cheeky/garbagelink"), 0);
self->rootfd = dfd;
self->hardcoded_fd = open("/dev/null", O_RDONLY);
ASSERT_GE(self->hardcoded_fd, 0);
ASSERT_GE(asprintf(&self->hardcoded_fdpath, "self/fd/%d",
self->hardcoded_fd), 0);
ASSERT_GE(asprintf(&self->procselfexe, "/proc/%d/exe", getpid()), 0);
}
FIXTURE_TEARDOWN(openat2_resolve)
{
free(self->procselfexe);
free(self->hardcoded_fdpath);
if (self->hardcoded_fd >= 0)
close(self->hardcoded_fd);
if (self->rootfd >= 0)
close(self->rootfd);
}
/* Attempts to cross the dirfd should be blocked with -EXDEV. */
TEST_F(openat2_resolve, resolve_beneath)
{
struct resolve_test tests[] = {
/* Attempts to cross dirfd should be blocked. */
{ .name = "[beneath] jump to /",
.path = "/", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] absolute link to $root",
.path = "cheeky/absself", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] chained absolute links to $root",
.path = "abscheeky/absself", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] jump outside $root",
.path = "..", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] temporary jump outside $root",
.path = "../root/", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] symlink temporary jump outside $root",
.path = "cheeky/self", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] chained symlink temporary jump outside $root",
.path = "abscheeky/self", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] garbage links to $root",
.path = "cheeky/garbageself", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] chained garbage links to $root",
.path = "abscheeky/garbageself", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
/* Only relative paths that stay inside dirfd should work. */
{ .name = "[beneath] ordinary path to 'root'",
.path = "root", .how.resolve = RESOLVE_BENEATH,
.out.path = "root", .pass = true },
{ .name = "[beneath] ordinary path to 'etc'",
.path = "etc", .how.resolve = RESOLVE_BENEATH,
.out.path = "etc", .pass = true },
{ .name = "[beneath] ordinary path to 'etc/passwd'",
.path = "etc/passwd", .how.resolve = RESOLVE_BENEATH,
.out.path = "etc/passwd", .pass = true },
{ .name = "[beneath] relative symlink inside $root",
.path = "relsym", .how.resolve = RESOLVE_BENEATH,
.out.path = "etc/passwd", .pass = true },
{ .name = "[beneath] chained-'..' relative symlink inside $root",
.path = "cheeky/passwd", .how.resolve = RESOLVE_BENEATH,
.out.path = "etc/passwd", .pass = true },
{ .name = "[beneath] absolute symlink component outside $root",
.path = "abscheeky/passwd", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] absolute symlink target outside $root",
.path = "abssym", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] absolute path outside $root",
.path = "/etc/passwd", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] cheeky absolute path outside $root",
.path = "cheeky/abspasswd", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] chained cheeky absolute path outside $root",
.path = "abscheeky/abspasswd", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
/* Tricky paths should fail. */
{ .name = "[beneath] tricky '..'-chained symlink outside $root",
.path = "cheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] tricky absolute + '..'-chained symlink outside $root",
.path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] tricky garbage link outside $root",
.path = "cheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
{ .name = "[beneath] tricky absolute + garbage link outside $root",
.path = "abscheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,
.out.err = -EXDEV, .pass = false },
};
for (int i = 0; i < ARRAY_SIZE(tests); i++)
verify_resolve_test(_metadata, self->rootfd,
self->hardcoded_fd, &tests[i]);
}
/* All attempts to cross the dirfd will be scoped-to-root. */
TEST_F(openat2_resolve, resolve_in_root)
{
struct resolve_test tests[] = {
{ .name = "[in_root] jump to /",
.path = "/", .how.resolve = RESOLVE_IN_ROOT,
.out.path = NULL, .pass = true },
{ .name = "[in_root] absolute symlink to /root",
.path = "cheeky/absself", .how.resolve = RESOLVE_IN_ROOT,
.out.path = NULL, .pass = true },
{ .name = "[in_root] chained absolute symlinks to /root",
.path = "abscheeky/absself", .how.resolve = RESOLVE_IN_ROOT,
.out.path = NULL, .pass = true },
{ .name = "[in_root] '..' at root",
.path = "..", .how.resolve = RESOLVE_IN_ROOT,
.out.path = NULL, .pass = true },
{ .name = "[in_root] '../root' at root",
.path = "../root/", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "root", .pass = true },
{ .name = "[in_root] relative symlink containing '..' above root",
.path = "cheeky/self", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "root", .pass = true },
{ .name = "[in_root] garbage link to /root",
.path = "cheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "root", .pass = true },
{ .name = "[in_root] chained garbage links to /root",
.path = "abscheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "root", .pass = true },
{ .name = "[in_root] relative path to 'root'",
.path = "root", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "root", .pass = true },
{ .name = "[in_root] relative path to 'etc'",
.path = "etc", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc", .pass = true },
{ .name = "[in_root] relative path to 'etc/passwd'",
.path = "etc/passwd", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] relative symlink to 'etc/passwd'",
.path = "relsym", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] chained-'..' relative symlink to 'etc/passwd'",
.path = "cheeky/passwd", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] chained-'..' absolute + relative symlink to 'etc/passwd'",
.path = "abscheeky/passwd", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] absolute symlink to 'etc/passwd'",
.path = "abssym", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] absolute path 'etc/passwd'",
.path = "/etc/passwd", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] cheeky absolute path 'etc/passwd'",
.path = "cheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] chained cheeky absolute path 'etc/passwd'",
.path = "abscheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] tricky '..'-chained symlink outside $root",
.path = "cheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] tricky absolute + '..'-chained symlink outside $root",
.path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] tricky absolute path + absolute + '..'-chained symlink outside $root",
.path = "/../../../../abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] tricky garbage link outside $root",
.path = "cheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] tricky absolute + garbage link outside $root",
.path = "abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
{ .name = "[in_root] tricky absolute path + absolute + garbage link outside $root",
.path = "/../../../../abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
.out.path = "etc/passwd", .pass = true },
/* O_CREAT should handle trailing symlinks correctly. */
{ .name = "[in_root] O_CREAT of relative path inside $root",
.path = "newfile1", .how.flags = O_CREAT,
.how.mode = 0700,
.how.resolve = RESOLVE_IN_ROOT,
.out.path = "newfile1", .pass = true },
{ .name = "[in_root] O_CREAT of absolute path",
.path = "/newfile2", .how.flags = O_CREAT,
.how.mode = 0700,
.how.resolve = RESOLVE_IN_ROOT,
.out.path = "newfile2", .pass = true },
{ .name = "[in_root] O_CREAT of tricky symlink outside root",
.path = "/creatlink", .how.flags = O_CREAT,
.how.mode = 0700,
.how.resolve = RESOLVE_IN_ROOT,
.out.path = "newfile3", .pass = true },
};
for (int i = 0; i < ARRAY_SIZE(tests); i++)
verify_resolve_test(_metadata, self->rootfd,
self->hardcoded_fd, &tests[i]);
}
/* Crossing mount boundaries should be blocked. */
TEST_F(openat2_resolve, resolve_no_xdev)
{
struct resolve_test tests[] = {
/* Crossing *down* into a mountpoint is disallowed. */
{ .name = "[no_xdev] cross into $mnt",
.path = "mnt", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
{ .name = "[no_xdev] cross into $mnt/",
.path = "mnt/", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
{ .name = "[no_xdev] cross into $mnt/.",
.path = "mnt/.", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
/* Crossing *up* out of a mountpoint is disallowed. */
{ .name = "[no_xdev] goto mountpoint root",
.dir = "mnt", .path = ".", .how.resolve = RESOLVE_NO_XDEV,
.out.path = "mnt", .pass = true },
{ .name = "[no_xdev] cross up through '..'",
.dir = "mnt", .path = "..", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
{ .name = "[no_xdev] temporary cross up through '..'",
.dir = "mnt", .path = "../mnt", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
{ .name = "[no_xdev] temporary relative symlink cross up",
.dir = "mnt", .path = "self", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
{ .name = "[no_xdev] temporary absolute symlink cross up",
.dir = "mnt", .path = "absself", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
/* Jumping to "/" is ok, but later components cannot cross. */
{ .name = "[no_xdev] jump to / directly",
.dir = "mnt", .path = "/", .how.resolve = RESOLVE_NO_XDEV,
.out.path = "/", .pass = true },
{ .name = "[no_xdev] jump to / (from /) directly",
.dir = "/", .path = "/", .how.resolve = RESOLVE_NO_XDEV,
.out.path = "/", .pass = true },
{ .name = "[no_xdev] jump to / then proc",
.path = "/proc/1", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
{ .name = "[no_xdev] jump to / then tmp",
.path = "/tmp", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
/* Magic-links are blocked since they can switch vfsmounts. */
{ .name = "[no_xdev] cross through magic-link to self/root",
.dir = "/proc", .path = "self/root", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
{ .name = "[no_xdev] cross through magic-link to self/cwd",
.dir = "/proc", .path = "self/cwd", .how.resolve = RESOLVE_NO_XDEV,
.out.err = -EXDEV, .pass = false },
/* Except magic-link jumps inside the same vfsmount. */
{ .name = "[no_xdev] jump through magic-link to same procfs",
.dir = "/proc", .path = self->hardcoded_fdpath, .how.resolve = RESOLVE_NO_XDEV,
.out.path = "/proc", .pass = true, },
};
for (int i = 0; i < ARRAY_SIZE(tests); i++)
verify_resolve_test(_metadata, self->rootfd,
self->hardcoded_fd, &tests[i]);
}
/* Procfs-style magic-link resolution should be blocked. */
TEST_F(openat2_resolve, resolve_no_magiclinks)
{
struct resolve_test tests[] = {
/* Regular symlinks should work. */
{ .name = "[no_magiclinks] ordinary relative symlink",
.path = "relsym", .how.resolve = RESOLVE_NO_MAGICLINKS,
.out.path = "etc/passwd", .pass = true },
/* Magic-links should not work. */
{ .name = "[no_magiclinks] symlink to magic-link",
.path = "procexe", .how.resolve = RESOLVE_NO_MAGICLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_magiclinks] normal path to magic-link",
.path = "/proc/self/exe", .how.resolve = RESOLVE_NO_MAGICLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_magiclinks] normal path to magic-link with O_NOFOLLOW",
.path = "/proc/self/exe", .how.flags = O_NOFOLLOW,
.how.resolve = RESOLVE_NO_MAGICLINKS,
.out.path = self->procselfexe, .pass = true },
{ .name = "[no_magiclinks] symlink to magic-link path component",
.path = "procroot/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_magiclinks] magic-link path component",
.path = "/proc/self/root/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_magiclinks] magic-link path component with O_NOFOLLOW",
.path = "/proc/self/root/etc", .how.flags = O_NOFOLLOW,
.how.resolve = RESOLVE_NO_MAGICLINKS,
.out.err = -ELOOP, .pass = false },
};
for (int i = 0; i < ARRAY_SIZE(tests); i++)
verify_resolve_test(_metadata, self->rootfd,
self->hardcoded_fd, &tests[i]);
}
/* All symlink resolution should be blocked. */
TEST_F(openat2_resolve, resolve_no_symlinks)
{
struct resolve_test tests[] = {
/* Normal paths should work. */
{ .name = "[no_symlinks] ordinary path to '.'",
.path = ".", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.path = NULL, .pass = true },
{ .name = "[no_symlinks] ordinary path to 'root'",
.path = "root", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.path = "root", .pass = true },
{ .name = "[no_symlinks] ordinary path to 'etc'",
.path = "etc", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.path = "etc", .pass = true },
{ .name = "[no_symlinks] ordinary path to 'etc/passwd'",
.path = "etc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.path = "etc/passwd", .pass = true },
/* Regular symlinks are blocked. */
{ .name = "[no_symlinks] relative symlink target",
.path = "relsym", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_symlinks] relative symlink component",
.path = "reletc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_symlinks] absolute symlink target",
.path = "abssym", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_symlinks] absolute symlink component",
.path = "absetc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_symlinks] cheeky garbage link",
.path = "cheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_symlinks] cheeky absolute + garbage link",
.path = "abscheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_symlinks] cheeky absolute + absolute symlink",
.path = "abscheeky/absself", .how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
/* Trailing symlinks with NO_FOLLOW. */
{ .name = "[no_symlinks] relative symlink with O_NOFOLLOW",
.path = "relsym", .how.flags = O_NOFOLLOW,
.how.resolve = RESOLVE_NO_SYMLINKS,
.out.path = "relsym", .pass = true },
{ .name = "[no_symlinks] absolute symlink with O_NOFOLLOW",
.path = "abssym", .how.flags = O_NOFOLLOW,
.how.resolve = RESOLVE_NO_SYMLINKS,
.out.path = "abssym", .pass = true },
{ .name = "[no_symlinks] trailing symlink with O_NOFOLLOW",
.path = "cheeky/garbagelink", .how.flags = O_NOFOLLOW,
.how.resolve = RESOLVE_NO_SYMLINKS,
.out.path = "cheeky/garbagelink", .pass = true },
{ .name = "[no_symlinks] multiple symlink components with O_NOFOLLOW",
.path = "abscheeky/absself", .how.flags = O_NOFOLLOW,
.how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
{ .name = "[no_symlinks] multiple symlink (and garbage link) components with O_NOFOLLOW",
.path = "abscheeky/garbagelink", .how.flags = O_NOFOLLOW,
.how.resolve = RESOLVE_NO_SYMLINKS,
.out.err = -ELOOP, .pass = false },
};
for (int i = 0; i < ARRAY_SIZE(tests); i++)
verify_resolve_test(_metadata, self->rootfd,
self->hardcoded_fd, &tests[i]);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,3 @@
# SPDX-License-Identifier: GPL-2.0-only
dev_in_maps
set_layers_via_fds
@@ -0,0 +1,14 @@
# SPDX-License-Identifier: GPL-2.0
CFLAGS += -Wall
CFLAGS += $(KHDR_INCLUDES)
LDLIBS += -lcap
LOCAL_HDRS += ../wrappers.h log.h
TEST_GEN_PROGS := dev_in_maps
TEST_GEN_PROGS += set_layers_via_fds
include ../../lib.mk
$(OUTPUT)/set_layers_via_fds: ../utils.c
@@ -0,0 +1,160 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#define __SANE_USERSPACE_TYPES__ // Use ll64
#include <inttypes.h>
#include <unistd.h>
#include <stdio.h>
#include <linux/unistd.h>
#include <linux/types.h>
#include <linux/mount.h>
#include <sys/syscall.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sched.h>
#include <fcntl.h>
#include "kselftest.h"
#include "log.h"
#include "../wrappers.h"
static long get_file_dev_and_inode(void *addr, struct statx *stx)
{
char buf[4096];
FILE *mapf;
mapf = fopen("/proc/self/maps", "r");
if (mapf == NULL)
return pr_perror("fopen(/proc/self/maps)");
while (fgets(buf, sizeof(buf), mapf)) {
unsigned long start, end;
uint32_t maj, min;
__u64 ino;
if (sscanf(buf, "%lx-%lx %*s %*s %x:%x %llu",
&start, &end, &maj, &min, &ino) != 5)
return pr_perror("unable to parse: %s", buf);
if (start == (unsigned long)addr) {
stx->stx_dev_major = maj;
stx->stx_dev_minor = min;
stx->stx_ino = ino;
return 0;
}
}
return pr_err("unable to find the mapping");
}
static int ovl_mount(void)
{
int tmpfs, fsfd, ovl;
fsfd = sys_fsopen("tmpfs", 0);
if (fsfd == -1)
return pr_perror("fsopen(tmpfs)");
if (sys_fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) == -1)
return pr_perror("FSCONFIG_CMD_CREATE");
tmpfs = sys_fsmount(fsfd, 0, 0);
if (tmpfs == -1)
return pr_perror("fsmount");
close(fsfd);
/* overlayfs can't be constructed on top of a detached mount. */
if (sys_move_mount(tmpfs, "", AT_FDCWD, "/tmp", MOVE_MOUNT_F_EMPTY_PATH))
return pr_perror("move_mount");
close(tmpfs);
if (mkdir("/tmp/w", 0755) == -1 ||
mkdir("/tmp/u", 0755) == -1 ||
mkdir("/tmp/l", 0755) == -1)
return pr_perror("mkdir");
fsfd = sys_fsopen("overlay", 0);
if (fsfd == -1)
return pr_perror("fsopen(overlay)");
if (sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "source", "test", 0) == -1 ||
sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "lowerdir", "/tmp/l", 0) == -1 ||
sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "upperdir", "/tmp/u", 0) == -1 ||
sys_fsconfig(fsfd, FSCONFIG_SET_STRING, "workdir", "/tmp/w", 0) == -1)
return pr_perror("fsconfig");
if (sys_fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) == -1)
return pr_perror("fsconfig");
ovl = sys_fsmount(fsfd, 0, 0);
if (ovl == -1)
return pr_perror("fsmount");
return ovl;
}
/*
* Check that the file device and inode shown in /proc/pid/maps match values
* returned by stat(2).
*/
static int test(void)
{
struct statx stx, mstx;
int ovl, fd;
void *addr;
ovl = ovl_mount();
if (ovl == -1)
return -1;
fd = openat(ovl, "test", O_RDWR | O_CREAT, 0644);
if (fd == -1)
return pr_perror("openat");
addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, 0);
if (addr == MAP_FAILED)
return pr_perror("mmap");
if (get_file_dev_and_inode(addr, &mstx))
return -1;
if (statx(fd, "", AT_EMPTY_PATH | AT_STATX_SYNC_AS_STAT, STATX_INO, &stx))
return pr_perror("statx");
if (stx.stx_dev_major != mstx.stx_dev_major ||
stx.stx_dev_minor != mstx.stx_dev_minor ||
stx.stx_ino != mstx.stx_ino)
return pr_fail("unmatched dev:ino %x:%x:%llx (expected %x:%x:%llx)\n",
mstx.stx_dev_major, mstx.stx_dev_minor, mstx.stx_ino,
stx.stx_dev_major, stx.stx_dev_minor, stx.stx_ino);
ksft_test_result_pass("devices are matched\n");
return 0;
}
int main(int argc, char **argv)
{
int fsfd;
fsfd = sys_fsopen("overlay", 0);
if (fsfd == -1) {
ksft_test_result_skip("unable to create overlay mount\n");
return 1;
}
close(fsfd);
/* Create a new mount namespace to not care about cleaning test mounts. */
if (unshare(CLONE_NEWNS) == -1) {
ksft_test_result_skip("unable to create a new mount namespace\n");
return 1;
}
if (sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL) == -1) {
pr_perror("mount");
return 1;
}
ksft_set_plan(1);
if (test())
return 1;
ksft_exit_pass();
return 0;
}
@@ -0,0 +1,26 @@
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef __SELFTEST_TIMENS_LOG_H__
#define __SELFTEST_TIMENS_LOG_H__
#define pr_msg(fmt, lvl, ...) \
ksft_print_msg("[%s] (%s:%d)\t" fmt "\n", \
lvl, __FILE__, __LINE__, ##__VA_ARGS__)
#define pr_p(func, fmt, ...) func(fmt ": %m", ##__VA_ARGS__)
#define pr_err(fmt, ...) \
({ \
ksft_test_result_error(fmt "\n", ##__VA_ARGS__); \
-1; \
})
#define pr_fail(fmt, ...) \
({ \
ksft_test_result_fail(fmt, ##__VA_ARGS__); \
-1; \
})
#define pr_perror(fmt, ...) pr_p(pr_err, fmt, ##__VA_ARGS__)
#endif
@@ -0,0 +1,720 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#define __SANE_USERSPACE_TYPES__ // Use ll64
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <sys/mount.h>
#include <unistd.h>
#include "kselftest_harness.h"
#include "../../pidfd/pidfd.h"
#include "log.h"
#include "../utils.h"
#include "../wrappers.h"
FIXTURE(set_layers_via_fds) {
int pidfd;
};
FIXTURE_SETUP(set_layers_via_fds)
{
self->pidfd = -EBADF;
EXPECT_EQ(mkdir("/set_layers_via_fds", 0755), 0);
EXPECT_EQ(mkdir("/set_layers_via_fds_tmpfs", 0755), 0);
}
FIXTURE_TEARDOWN(set_layers_via_fds)
{
if (self->pidfd >= 0) {
EXPECT_EQ(sys_pidfd_send_signal(self->pidfd, SIGKILL, NULL, 0), 0);
EXPECT_EQ(close(self->pidfd), 0);
}
umount2("/set_layers_via_fds", 0);
EXPECT_EQ(rmdir("/set_layers_via_fds"), 0);
umount2("/set_layers_via_fds_tmpfs", 0);
EXPECT_EQ(rmdir("/set_layers_via_fds_tmpfs"), 0);
}
TEST_F(set_layers_via_fds, set_layers_via_fds)
{
int fd_context, fd_tmpfs, fd_overlay;
int layer_fds[] = { [0 ... 8] = -EBADF };
bool layers_found[] = { [0 ... 8] = false };
size_t len = 0;
char *line = NULL;
FILE *f_mountinfo;
ASSERT_EQ(unshare(CLONE_NEWNS), 0);
ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0);
fd_context = sys_fsopen("tmpfs", 0);
ASSERT_GE(fd_context, 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
fd_tmpfs = sys_fsmount(fd_context, 0, 0);
ASSERT_GE(fd_tmpfs, 0);
ASSERT_EQ(close(fd_context), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "w", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "u", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l1", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l2", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l3", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l4", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "d1", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "d2", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "d3", 0755), 0);
layer_fds[0] = openat(fd_tmpfs, "w", O_DIRECTORY);
ASSERT_GE(layer_fds[0], 0);
layer_fds[1] = openat(fd_tmpfs, "u", O_DIRECTORY);
ASSERT_GE(layer_fds[1], 0);
layer_fds[2] = openat(fd_tmpfs, "l1", O_DIRECTORY);
ASSERT_GE(layer_fds[2], 0);
layer_fds[3] = openat(fd_tmpfs, "l2", O_DIRECTORY);
ASSERT_GE(layer_fds[3], 0);
layer_fds[4] = openat(fd_tmpfs, "l3", O_DIRECTORY);
ASSERT_GE(layer_fds[4], 0);
layer_fds[5] = openat(fd_tmpfs, "l4", O_DIRECTORY);
ASSERT_GE(layer_fds[5], 0);
layer_fds[6] = openat(fd_tmpfs, "d1", O_DIRECTORY);
ASSERT_GE(layer_fds[6], 0);
layer_fds[7] = openat(fd_tmpfs, "d2", O_DIRECTORY);
ASSERT_GE(layer_fds[7], 0);
layer_fds[8] = openat(fd_tmpfs, "d3", O_DIRECTORY);
ASSERT_GE(layer_fds[8], 0);
ASSERT_EQ(sys_move_mount(fd_tmpfs, "", -EBADF, "/tmp", MOVE_MOUNT_F_EMPTY_PATH), 0);
ASSERT_EQ(close(fd_tmpfs), 0);
fd_context = sys_fsopen("overlay", 0);
ASSERT_GE(fd_context, 0);
ASSERT_NE(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir", NULL, layer_fds[2]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "workdir", NULL, layer_fds[0]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "upperdir", NULL, layer_fds[1]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[2]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[3]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[4]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[5]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "datadir+", NULL, layer_fds[6]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "datadir+", NULL, layer_fds[7]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "datadir+", NULL, layer_fds[8]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_STRING, "metacopy", "on", 0), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
fd_overlay = sys_fsmount(fd_context, 0, 0);
ASSERT_GE(fd_overlay, 0);
ASSERT_EQ(sys_move_mount(fd_overlay, "", -EBADF, "/set_layers_via_fds", MOVE_MOUNT_F_EMPTY_PATH), 0);
f_mountinfo = fopen("/proc/self/mountinfo", "r");
ASSERT_NE(f_mountinfo, NULL);
while (getline(&line, &len, f_mountinfo) != -1) {
char *haystack = line;
if (strstr(haystack, "workdir=/tmp/w"))
layers_found[0] = true;
if (strstr(haystack, "upperdir=/tmp/u"))
layers_found[1] = true;
if (strstr(haystack, "lowerdir+=/tmp/l1"))
layers_found[2] = true;
if (strstr(haystack, "lowerdir+=/tmp/l2"))
layers_found[3] = true;
if (strstr(haystack, "lowerdir+=/tmp/l3"))
layers_found[4] = true;
if (strstr(haystack, "lowerdir+=/tmp/l4"))
layers_found[5] = true;
if (strstr(haystack, "datadir+=/tmp/d1"))
layers_found[6] = true;
if (strstr(haystack, "datadir+=/tmp/d2"))
layers_found[7] = true;
if (strstr(haystack, "datadir+=/tmp/d3"))
layers_found[8] = true;
}
free(line);
for (int i = 0; i < ARRAY_SIZE(layer_fds); i++) {
ASSERT_EQ(layers_found[i], true);
ASSERT_EQ(close(layer_fds[i]), 0);
}
ASSERT_EQ(close(fd_context), 0);
ASSERT_EQ(close(fd_overlay), 0);
ASSERT_EQ(fclose(f_mountinfo), 0);
}
TEST_F(set_layers_via_fds, set_500_layers_via_fds)
{
int fd_context, fd_tmpfs, fd_overlay, fd_work, fd_upper, fd_lower;
int layer_fds[500] = { [0 ... 499] = -EBADF };
ASSERT_EQ(unshare(CLONE_NEWNS), 0);
ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0);
fd_context = sys_fsopen("tmpfs", 0);
ASSERT_GE(fd_context, 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
fd_tmpfs = sys_fsmount(fd_context, 0, 0);
ASSERT_GE(fd_tmpfs, 0);
ASSERT_EQ(close(fd_context), 0);
for (int i = 0; i < ARRAY_SIZE(layer_fds); i++) {
char path[100];
sprintf(path, "l%d", i);
ASSERT_EQ(mkdirat(fd_tmpfs, path, 0755), 0);
layer_fds[i] = openat(fd_tmpfs, path, O_DIRECTORY);
ASSERT_GE(layer_fds[i], 0);
}
ASSERT_EQ(mkdirat(fd_tmpfs, "w", 0755), 0);
fd_work = openat(fd_tmpfs, "w", O_DIRECTORY);
ASSERT_GE(fd_work, 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "u", 0755), 0);
fd_upper = openat(fd_tmpfs, "u", O_DIRECTORY);
ASSERT_GE(fd_upper, 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l501", 0755), 0);
fd_lower = openat(fd_tmpfs, "l501", O_DIRECTORY);
ASSERT_GE(fd_lower, 0);
ASSERT_EQ(sys_move_mount(fd_tmpfs, "", -EBADF, "/tmp", MOVE_MOUNT_F_EMPTY_PATH), 0);
ASSERT_EQ(close(fd_tmpfs), 0);
fd_context = sys_fsopen("overlay", 0);
ASSERT_GE(fd_context, 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "workdir", NULL, fd_work), 0);
ASSERT_EQ(close(fd_work), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "upperdir", NULL, fd_upper), 0);
ASSERT_EQ(close(fd_upper), 0);
for (int i = 0; i < ARRAY_SIZE(layer_fds); i++) {
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[i]), 0);
ASSERT_EQ(close(layer_fds[i]), 0);
}
ASSERT_NE(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, fd_lower), 0);
ASSERT_EQ(close(fd_lower), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
fd_overlay = sys_fsmount(fd_context, 0, 0);
ASSERT_GE(fd_overlay, 0);
ASSERT_EQ(close(fd_context), 0);
ASSERT_EQ(close(fd_overlay), 0);
}
TEST_F(set_layers_via_fds, set_override_creds)
{
int fd_context, fd_tmpfs, fd_overlay;
int layer_fds[] = { [0 ... 3] = -EBADF };
pid_t pid;
int pidfd;
ASSERT_EQ(unshare(CLONE_NEWNS), 0);
ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0);
fd_context = sys_fsopen("tmpfs", 0);
ASSERT_GE(fd_context, 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
fd_tmpfs = sys_fsmount(fd_context, 0, 0);
ASSERT_GE(fd_tmpfs, 0);
ASSERT_EQ(close(fd_context), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "w", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "u", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l1", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l2", 0755), 0);
layer_fds[0] = openat(fd_tmpfs, "w", O_DIRECTORY);
ASSERT_GE(layer_fds[0], 0);
layer_fds[1] = openat(fd_tmpfs, "u", O_DIRECTORY);
ASSERT_GE(layer_fds[1], 0);
layer_fds[2] = openat(fd_tmpfs, "l1", O_DIRECTORY);
ASSERT_GE(layer_fds[2], 0);
layer_fds[3] = openat(fd_tmpfs, "l2", O_DIRECTORY);
ASSERT_GE(layer_fds[3], 0);
ASSERT_EQ(sys_move_mount(fd_tmpfs, "", -EBADF, "/tmp", MOVE_MOUNT_F_EMPTY_PATH), 0);
ASSERT_EQ(close(fd_tmpfs), 0);
fd_context = sys_fsopen("overlay", 0);
ASSERT_GE(fd_context, 0);
ASSERT_NE(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir", NULL, layer_fds[2]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "workdir", NULL, layer_fds[0]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "upperdir", NULL, layer_fds[1]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[2]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[3]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_STRING, "metacopy", "on", 0), 0);
pid = create_child(&pidfd, 0);
ASSERT_GE(pid, 0);
if (pid == 0) {
if (sys_fsconfig(fd_context, FSCONFIG_SET_FLAG, "override_creds", NULL, 0)) {
TH_LOG("sys_fsconfig should have succeeded");
_exit(EXIT_FAILURE);
}
_exit(EXIT_SUCCESS);
}
ASSERT_GE(sys_waitid(P_PID, pid, NULL, WEXITED), 0);
ASSERT_GE(close(pidfd), 0);
pid = create_child(&pidfd, 0);
ASSERT_GE(pid, 0);
if (pid == 0) {
if (sys_fsconfig(fd_context, FSCONFIG_SET_FLAG, "nooverride_creds", NULL, 0)) {
TH_LOG("sys_fsconfig should have succeeded");
_exit(EXIT_FAILURE);
}
_exit(EXIT_SUCCESS);
}
ASSERT_GE(sys_waitid(P_PID, pid, NULL, WEXITED), 0);
ASSERT_GE(close(pidfd), 0);
pid = create_child(&pidfd, 0);
ASSERT_GE(pid, 0);
if (pid == 0) {
if (sys_fsconfig(fd_context, FSCONFIG_SET_FLAG, "override_creds", NULL, 0)) {
TH_LOG("sys_fsconfig should have succeeded");
_exit(EXIT_FAILURE);
}
_exit(EXIT_SUCCESS);
}
ASSERT_GE(sys_waitid(P_PID, pid, NULL, WEXITED), 0);
ASSERT_GE(close(pidfd), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
fd_overlay = sys_fsmount(fd_context, 0, 0);
ASSERT_GE(fd_overlay, 0);
ASSERT_EQ(sys_move_mount(fd_overlay, "", -EBADF, "/set_layers_via_fds", MOVE_MOUNT_F_EMPTY_PATH), 0);
ASSERT_EQ(close(fd_context), 0);
ASSERT_EQ(close(fd_overlay), 0);
}
TEST_F(set_layers_via_fds, set_override_creds_invalid)
{
int fd_context, fd_tmpfs, fd_overlay, ret;
int layer_fds[] = { [0 ... 3] = -EBADF };
pid_t pid;
int fd_userns1, fd_userns2;
int ipc_sockets[2];
char c;
const unsigned int predictable_fd_context_nr = 123;
fd_userns1 = get_userns_fd(0, 0, 10000);
ASSERT_GE(fd_userns1, 0);
fd_userns2 = get_userns_fd(0, 1234, 10000);
ASSERT_GE(fd_userns2, 0);
ret = socketpair(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, ipc_sockets);
ASSERT_GE(ret, 0);
pid = create_child(&self->pidfd, 0);
ASSERT_GE(pid, 0);
if (pid == 0) {
if (close(ipc_sockets[0])) {
TH_LOG("close should have succeeded");
_exit(EXIT_FAILURE);
}
if (!switch_userns(fd_userns2, 0, 0, false)) {
TH_LOG("switch_userns should have succeeded");
_exit(EXIT_FAILURE);
}
if (read_nointr(ipc_sockets[1], &c, 1) != 1) {
TH_LOG("read_nointr should have succeeded");
_exit(EXIT_FAILURE);
}
if (close(ipc_sockets[1])) {
TH_LOG("close should have succeeded");
_exit(EXIT_FAILURE);
}
if (!sys_fsconfig(predictable_fd_context_nr, FSCONFIG_SET_FLAG, "override_creds", NULL, 0)) {
TH_LOG("sys_fsconfig should have failed");
_exit(EXIT_FAILURE);
}
_exit(EXIT_SUCCESS);
}
ASSERT_EQ(close(ipc_sockets[1]), 0);
ASSERT_EQ(switch_userns(fd_userns1, 0, 0, false), true);
ASSERT_EQ(unshare(CLONE_NEWNS), 0);
ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0);
fd_context = sys_fsopen("tmpfs", 0);
ASSERT_GE(fd_context, 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
fd_tmpfs = sys_fsmount(fd_context, 0, 0);
ASSERT_GE(fd_tmpfs, 0);
ASSERT_EQ(close(fd_context), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "w", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "u", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l1", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l2", 0755), 0);
layer_fds[0] = openat(fd_tmpfs, "w", O_DIRECTORY);
ASSERT_GE(layer_fds[0], 0);
layer_fds[1] = openat(fd_tmpfs, "u", O_DIRECTORY);
ASSERT_GE(layer_fds[1], 0);
layer_fds[2] = openat(fd_tmpfs, "l1", O_DIRECTORY);
ASSERT_GE(layer_fds[2], 0);
layer_fds[3] = openat(fd_tmpfs, "l2", O_DIRECTORY);
ASSERT_GE(layer_fds[3], 0);
ASSERT_EQ(sys_move_mount(fd_tmpfs, "", -EBADF, "/tmp", MOVE_MOUNT_F_EMPTY_PATH), 0);
ASSERT_EQ(close(fd_tmpfs), 0);
fd_context = sys_fsopen("overlay", 0);
ASSERT_GE(fd_context, 0);
ASSERT_EQ(dup3(fd_context, predictable_fd_context_nr, 0), predictable_fd_context_nr);
ASSERT_EQ(close(fd_context), 0);
fd_context = predictable_fd_context_nr;
ASSERT_EQ(write_nointr(ipc_sockets[0], "1", 1), 1);
ASSERT_EQ(close(ipc_sockets[0]), 0);
ASSERT_EQ(wait_for_pid(pid), 0);
ASSERT_EQ(close(self->pidfd), 0);
self->pidfd = -EBADF;
ASSERT_NE(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir", NULL, layer_fds[2]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "workdir", NULL, layer_fds[0]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "upperdir", NULL, layer_fds[1]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[2]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[3]), 0);
for (int i = 0; i < ARRAY_SIZE(layer_fds); i++)
ASSERT_EQ(close(layer_fds[i]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FLAG, "userxattr", NULL, 0), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
fd_overlay = sys_fsmount(fd_context, 0, 0);
ASSERT_GE(fd_overlay, 0);
ASSERT_EQ(sys_move_mount(fd_overlay, "", -EBADF, "/set_layers_via_fds", MOVE_MOUNT_F_EMPTY_PATH), 0);
ASSERT_EQ(close(fd_context), 0);
ASSERT_EQ(close(fd_overlay), 0);
ASSERT_EQ(close(fd_userns1), 0);
ASSERT_EQ(close(fd_userns2), 0);
}
TEST_F(set_layers_via_fds, set_override_creds_nomknod)
{
int fd_context, fd_tmpfs, fd_overlay;
int layer_fds[] = { [0 ... 3] = -EBADF };
pid_t pid;
int pidfd;
ASSERT_EQ(unshare(CLONE_NEWNS), 0);
ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0);
fd_context = sys_fsopen("tmpfs", 0);
ASSERT_GE(fd_context, 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
fd_tmpfs = sys_fsmount(fd_context, 0, 0);
ASSERT_GE(fd_tmpfs, 0);
ASSERT_EQ(close(fd_context), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "w", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "u", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l1", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l2", 0755), 0);
layer_fds[0] = openat(fd_tmpfs, "w", O_DIRECTORY);
ASSERT_GE(layer_fds[0], 0);
layer_fds[1] = openat(fd_tmpfs, "u", O_DIRECTORY);
ASSERT_GE(layer_fds[1], 0);
layer_fds[2] = openat(fd_tmpfs, "l1", O_DIRECTORY);
ASSERT_GE(layer_fds[2], 0);
layer_fds[3] = openat(fd_tmpfs, "l2", O_DIRECTORY);
ASSERT_GE(layer_fds[3], 0);
ASSERT_EQ(sys_move_mount(fd_tmpfs, "", -EBADF, "/tmp", MOVE_MOUNT_F_EMPTY_PATH), 0);
ASSERT_EQ(close(fd_tmpfs), 0);
fd_context = sys_fsopen("overlay", 0);
ASSERT_GE(fd_context, 0);
ASSERT_NE(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir", NULL, layer_fds[2]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "workdir", NULL, layer_fds[0]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "upperdir", NULL, layer_fds[1]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[2]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[3]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FLAG, "userxattr", NULL, 0), 0);
pid = create_child(&pidfd, 0);
ASSERT_GE(pid, 0);
if (pid == 0) {
if (!cap_down(CAP_MKNOD))
_exit(EXIT_FAILURE);
if (!cap_down(CAP_SYS_ADMIN))
_exit(EXIT_FAILURE);
if (sys_fsconfig(fd_context, FSCONFIG_SET_FLAG, "override_creds", NULL, 0))
_exit(EXIT_FAILURE);
_exit(EXIT_SUCCESS);
}
ASSERT_EQ(sys_waitid(P_PID, pid, NULL, WEXITED), 0);
ASSERT_GE(close(pidfd), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
fd_overlay = sys_fsmount(fd_context, 0, 0);
ASSERT_GE(fd_overlay, 0);
ASSERT_EQ(sys_move_mount(fd_overlay, "", -EBADF, "/set_layers_via_fds", MOVE_MOUNT_F_EMPTY_PATH), 0);
ASSERT_EQ(mknodat(fd_overlay, "dev-zero", S_IFCHR | 0644, makedev(1, 5)), -1);
ASSERT_EQ(errno, EPERM);
ASSERT_EQ(close(fd_context), 0);
ASSERT_EQ(close(fd_overlay), 0);
}
TEST_F(set_layers_via_fds, set_500_layers_via_opath_fds)
{
int fd_context, fd_tmpfs, fd_overlay, fd_work, fd_upper, fd_lower;
int layer_fds[500] = { [0 ... 499] = -EBADF };
ASSERT_EQ(unshare(CLONE_NEWNS), 0);
ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0);
fd_context = sys_fsopen("tmpfs", 0);
ASSERT_GE(fd_context, 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
fd_tmpfs = sys_fsmount(fd_context, 0, 0);
ASSERT_GE(fd_tmpfs, 0);
ASSERT_EQ(close(fd_context), 0);
for (int i = 0; i < ARRAY_SIZE(layer_fds); i++) {
char path[100];
sprintf(path, "l%d", i);
ASSERT_EQ(mkdirat(fd_tmpfs, path, 0755), 0);
layer_fds[i] = openat(fd_tmpfs, path, O_DIRECTORY | O_PATH);
ASSERT_GE(layer_fds[i], 0);
}
ASSERT_EQ(mkdirat(fd_tmpfs, "w", 0755), 0);
fd_work = openat(fd_tmpfs, "w", O_DIRECTORY | O_PATH);
ASSERT_GE(fd_work, 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "u", 0755), 0);
fd_upper = openat(fd_tmpfs, "u", O_DIRECTORY | O_PATH);
ASSERT_GE(fd_upper, 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l501", 0755), 0);
fd_lower = openat(fd_tmpfs, "l501", O_DIRECTORY | O_PATH);
ASSERT_GE(fd_lower, 0);
ASSERT_EQ(sys_move_mount(fd_tmpfs, "", -EBADF, "/tmp", MOVE_MOUNT_F_EMPTY_PATH), 0);
ASSERT_EQ(close(fd_tmpfs), 0);
fd_context = sys_fsopen("overlay", 0);
ASSERT_GE(fd_context, 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "workdir", NULL, fd_work), 0);
ASSERT_EQ(close(fd_work), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "upperdir", NULL, fd_upper), 0);
ASSERT_EQ(close(fd_upper), 0);
for (int i = 0; i < ARRAY_SIZE(layer_fds); i++) {
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[i]), 0);
ASSERT_EQ(close(layer_fds[i]), 0);
}
ASSERT_NE(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, fd_lower), 0);
ASSERT_EQ(close(fd_lower), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
fd_overlay = sys_fsmount(fd_context, 0, 0);
ASSERT_GE(fd_overlay, 0);
ASSERT_EQ(close(fd_context), 0);
ASSERT_EQ(close(fd_overlay), 0);
}
TEST_F(set_layers_via_fds, set_layers_via_detached_mount_fds)
{
int fd_context, fd_tmpfs, fd_overlay, fd_tmp;
int layer_fds[] = { [0 ... 8] = -EBADF };
bool layers_found[] = { [0 ... 8] = false };
size_t len = 0;
char *line = NULL;
FILE *f_mountinfo;
ASSERT_EQ(unshare(CLONE_NEWNS), 0);
ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0);
fd_context = sys_fsopen("tmpfs", 0);
ASSERT_GE(fd_context, 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
fd_tmpfs = sys_fsmount(fd_context, 0, 0);
ASSERT_GE(fd_tmpfs, 0);
ASSERT_EQ(close(fd_context), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "u", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "u/upper", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "u/work", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l1", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l2", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l3", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "l4", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "d1", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "d2", 0755), 0);
ASSERT_EQ(mkdirat(fd_tmpfs, "d3", 0755), 0);
ASSERT_EQ(sys_move_mount(fd_tmpfs, "", -EBADF, "/set_layers_via_fds_tmpfs", MOVE_MOUNT_F_EMPTY_PATH), 0);
fd_tmp = open_tree(fd_tmpfs, "u", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
ASSERT_GE(fd_tmp, 0);
layer_fds[0] = openat(fd_tmp, "upper", O_CLOEXEC | O_DIRECTORY | O_PATH);
ASSERT_GE(layer_fds[0], 0);
layer_fds[1] = openat(fd_tmp, "work", O_CLOEXEC | O_DIRECTORY | O_PATH);
ASSERT_GE(layer_fds[1], 0);
layer_fds[2] = open_tree(fd_tmpfs, "l1", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
ASSERT_GE(layer_fds[2], 0);
layer_fds[3] = open_tree(fd_tmpfs, "l2", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
ASSERT_GE(layer_fds[3], 0);
layer_fds[4] = open_tree(fd_tmpfs, "l3", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
ASSERT_GE(layer_fds[4], 0);
layer_fds[5] = open_tree(fd_tmpfs, "l4", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
ASSERT_GE(layer_fds[5], 0);
layer_fds[6] = open_tree(fd_tmpfs, "d1", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
ASSERT_GE(layer_fds[6], 0);
layer_fds[7] = open_tree(fd_tmpfs, "d2", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
ASSERT_GE(layer_fds[7], 0);
layer_fds[8] = open_tree(fd_tmpfs, "d3", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
ASSERT_GE(layer_fds[8], 0);
ASSERT_EQ(close(fd_tmpfs), 0);
fd_context = sys_fsopen("overlay", 0);
ASSERT_GE(fd_context, 0);
ASSERT_NE(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir", NULL, layer_fds[2]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "upperdir", NULL, layer_fds[0]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "workdir", NULL, layer_fds[1]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[2]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[3]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[4]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "lowerdir+", NULL, layer_fds[5]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "datadir+", NULL, layer_fds[6]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "datadir+", NULL, layer_fds[7]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_FD, "datadir+", NULL, layer_fds[8]), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_SET_STRING, "metacopy", "on", 0), 0);
ASSERT_EQ(sys_fsconfig(fd_context, FSCONFIG_CMD_CREATE, NULL, NULL, 0), 0);
fd_overlay = sys_fsmount(fd_context, 0, 0);
ASSERT_GE(fd_overlay, 0);
ASSERT_EQ(sys_move_mount(fd_overlay, "", -EBADF, "/set_layers_via_fds", MOVE_MOUNT_F_EMPTY_PATH), 0);
f_mountinfo = fopen("/proc/self/mountinfo", "r");
ASSERT_NE(f_mountinfo, NULL);
while (getline(&line, &len, f_mountinfo) != -1) {
char *haystack = line;
if (strstr(haystack, "workdir=/tmp/w"))
layers_found[0] = true;
if (strstr(haystack, "upperdir=/tmp/u"))
layers_found[1] = true;
if (strstr(haystack, "lowerdir+=/tmp/l1"))
layers_found[2] = true;
if (strstr(haystack, "lowerdir+=/tmp/l2"))
layers_found[3] = true;
if (strstr(haystack, "lowerdir+=/tmp/l3"))
layers_found[4] = true;
if (strstr(haystack, "lowerdir+=/tmp/l4"))
layers_found[5] = true;
if (strstr(haystack, "datadir+=/tmp/d1"))
layers_found[6] = true;
if (strstr(haystack, "datadir+=/tmp/d2"))
layers_found[7] = true;
if (strstr(haystack, "datadir+=/tmp/d3"))
layers_found[8] = true;
}
free(line);
for (int i = 0; i < ARRAY_SIZE(layer_fds); i++) {
ASSERT_EQ(layers_found[i], true);
ASSERT_EQ(close(layer_fds[i]), 0);
}
ASSERT_EQ(close(fd_context), 0);
ASSERT_EQ(close(fd_overlay), 0);
ASSERT_EQ(fclose(f_mountinfo), 0);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,3 @@
# SPDX-License-Identifier: GPL-2.0-only
statmount_test_ns
/*_test
@@ -0,0 +1,10 @@
# SPDX-License-Identifier: GPL-2.0-or-later
CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES)
LDLIBS += -lcap
TEST_GEN_PROGS := statmount_test statmount_test_ns listmount_test
include ../../lib.mk
$(OUTPUT)/statmount_test_ns: ../utils.c
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2024 Christian Brauner <brauner@kernel.org>
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <unistd.h>
#include "statmount.h"
#include "kselftest_harness.h"
#ifndef LISTMOUNT_REVERSE
#define LISTMOUNT_REVERSE (1 << 0) /* List later mounts first */
#endif
#define LISTMNT_BUFFER 10
/* Check that all mount ids are in increasing order. */
TEST(listmount_forward)
{
uint64_t list[LISTMNT_BUFFER], last_mnt_id = 0;
for (;;) {
ssize_t nr_mounts;
nr_mounts = listmount(LSMT_ROOT, 0, last_mnt_id,
list, LISTMNT_BUFFER, 0);
ASSERT_GE(nr_mounts, 0);
if (nr_mounts == 0)
break;
for (size_t cur = 0; cur < nr_mounts; cur++) {
if (cur < nr_mounts - 1)
ASSERT_LT(list[cur], list[cur + 1]);
last_mnt_id = list[cur];
}
}
}
/* Check that all mount ids are in decreasing order. */
TEST(listmount_backward)
{
uint64_t list[LISTMNT_BUFFER], last_mnt_id = 0;
for (;;) {
ssize_t nr_mounts;
nr_mounts = listmount(LSMT_ROOT, 0, last_mnt_id,
list, LISTMNT_BUFFER, LISTMOUNT_REVERSE);
ASSERT_GE(nr_mounts, 0);
if (nr_mounts == 0)
break;
for (size_t cur = 0; cur < nr_mounts; cur++) {
if (cur < nr_mounts - 1)
ASSERT_GT(list[cur], list[cur + 1]);
last_mnt_id = list[cur];
}
}
}
TEST_HARNESS_MAIN
@@ -0,0 +1,138 @@
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef __STATMOUNT_H
#define __STATMOUNT_H
#include <errno.h>
#include <stdint.h>
#include <stdlib.h>
#include <linux/mount.h>
#include <asm/unistd.h>
#define STATMOUNT_BUFSIZE (1 << 15)
#ifndef __NR_statmount
#if defined __alpha__
#define __NR_statmount 567
#elif defined _MIPS_SIM
#if _MIPS_SIM == _MIPS_SIM_ABI32 /* o32 */
#define __NR_statmount 4457
#endif
#if _MIPS_SIM == _MIPS_SIM_NABI32 /* n32 */
#define __NR_statmount 6457
#endif
#if _MIPS_SIM == _MIPS_SIM_ABI64 /* n64 */
#define __NR_statmount 5457
#endif
#else
#define __NR_statmount 457
#endif
#endif
#ifndef __NR_listmount
#if defined __alpha__
#define __NR_listmount 568
#elif defined _MIPS_SIM
#if _MIPS_SIM == _MIPS_SIM_ABI32 /* o32 */
#define __NR_listmount 4458
#endif
#if _MIPS_SIM == _MIPS_SIM_NABI32 /* n32 */
#define __NR_listmount 6458
#endif
#if _MIPS_SIM == _MIPS_SIM_ABI64 /* n64 */
#define __NR_listmount 5458
#endif
#else
#define __NR_listmount 458
#endif
#endif
static inline int statmount(uint64_t mnt_id, uint64_t mnt_ns_id, uint32_t fd,
uint64_t mask, struct statmount *buf, size_t bufsize,
unsigned int flags)
{
struct mnt_id_req req = {
.size = MNT_ID_REQ_SIZE_VER0,
.param = mask,
};
if (flags & STATMOUNT_BY_FD) {
req.size = MNT_ID_REQ_SIZE_VER1;
req.mnt_fd = fd;
} else {
req.mnt_id = mnt_id;
if (mnt_ns_id) {
req.size = MNT_ID_REQ_SIZE_VER1;
req.mnt_ns_id = mnt_ns_id;
}
}
return syscall(__NR_statmount, &req, buf, bufsize, flags);
}
static inline ssize_t listmount(uint64_t mnt_id, uint64_t mnt_ns_id,
uint64_t last_mnt_id, uint64_t list[], size_t num,
unsigned int flags)
{
struct mnt_id_req req = {
.size = MNT_ID_REQ_SIZE_VER0,
.mnt_id = mnt_id,
.param = last_mnt_id,
};
if (mnt_ns_id) {
req.size = MNT_ID_REQ_SIZE_VER1;
req.mnt_ns_id = mnt_ns_id;
}
return syscall(__NR_listmount, &req, list, num, flags);
}
static inline struct statmount *statmount_alloc(uint64_t mnt_id, uint64_t mnt_ns_id,
uint64_t mask, unsigned int flags)
{
struct statmount *buf;
size_t bufsize = STATMOUNT_BUFSIZE;
int ret;
for (;;) {
buf = malloc(bufsize);
if (!buf)
return NULL;
ret = statmount(mnt_id, mnt_ns_id, 0, mask, buf, bufsize, flags);
if (ret == 0)
return buf;
free(buf);
if (errno != EOVERFLOW)
return NULL;
bufsize <<= 1;
}
}
static inline struct statmount *statmount_alloc_by_fd(int fd, uint64_t mask)
{
struct statmount *buf;
size_t bufsize = STATMOUNT_BUFSIZE;
int ret;
for (;;) {
buf = malloc(bufsize);
if (!buf)
return NULL;
ret = statmount(0, 0, fd, mask, buf, bufsize, STATMOUNT_BY_FD);
if (ret == 0)
return buf;
free(buf);
if (errno != EOVERFLOW)
return NULL;
bufsize <<= 1;
}
}
#endif /* __STATMOUNT_H */
@@ -0,0 +1,894 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#define _GNU_SOURCE
#include <assert.h>
#include <stddef.h>
#include <sched.h>
#include <fcntl.h>
#include <sys/param.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/statfs.h>
#include <linux/stat.h>
#include "statmount.h"
#include "kselftest.h"
static const char *const known_fs[] = {
"9p", "adfs", "affs", "afs", "aio", "anon_inodefs", "apparmorfs",
"autofs", "bcachefs", "bdev", "befs", "bfs", "binder", "binfmt_misc",
"bpf", "btrfs", "btrfs_test_fs", "ceph", "cgroup", "cgroup2", "cifs",
"coda", "configfs", "cpuset", "cramfs", "cxl", "dax", "debugfs",
"devpts", "devtmpfs", "dmabuf", "drm", "ecryptfs", "efivarfs", "efs",
"erofs", "exfat", "ext2", "ext3", "ext4", "f2fs", "functionfs",
"fuse", "fuseblk", "fusectl", "gadgetfs", "gfs2", "gfs2meta", "hfs",
"hfsplus", "hostfs", "hpfs", "hugetlbfs", "ibmasmfs", "iomem",
"ipathfs", "iso9660", "jffs2", "jfs", "minix", "mqueue", "msdos",
"nfs", "nfs4", "nfsd", "nilfs2", "nsfs", "ntfs", "ntfs3", "ocfs2",
"ocfs2_dlmfs", "omfs", "openpromfs", "overlay", "pipefs", "proc",
"pstore", "pvfs2", "qnx4", "qnx6", "ramfs", "resctrl", "romfs",
"rootfs", "rpc_pipefs", "s390_hypfs", "secretmem", "securityfs",
"selinuxfs", "smackfs", "smb3", "sockfs", "spufs", "squashfs", "sysfs",
"sysv", "tmpfs", "tracefs", "ubifs", "udf", "ufs", "v7", "vboxsf",
"vfat", "virtiofs", "vxfs", "xenfs", "xfs", "zonefs", NULL };
static void write_file(const char *path, const char *val)
{
int fd = open(path, O_WRONLY);
size_t len = strlen(val);
int ret;
if (fd == -1)
ksft_exit_fail_msg("opening %s for write: %s\n", path, strerror(errno));
ret = write(fd, val, len);
if (ret == -1)
ksft_exit_fail_msg("writing to %s: %s\n", path, strerror(errno));
if (ret != len)
ksft_exit_fail_msg("short write to %s\n", path);
ret = close(fd);
if (ret == -1)
ksft_exit_fail_msg("closing %s\n", path);
}
static uint64_t get_mnt_id(const char *name, const char *path, uint64_t mask)
{
struct statx sx;
int ret;
ret = statx(AT_FDCWD, path, 0, mask, &sx);
if (ret == -1)
ksft_exit_fail_msg("retrieving %s mount ID for %s: %s\n",
mask & STATX_MNT_ID_UNIQUE ? "unique" : "old",
name, strerror(errno));
if (!(sx.stx_mask & mask))
ksft_exit_fail_msg("no %s mount ID available for %s\n",
mask & STATX_MNT_ID_UNIQUE ? "unique" : "old",
name);
return sx.stx_mnt_id;
}
static char root_mntpoint[] = "/tmp/statmount_test_root.XXXXXX";
static int orig_root;
static uint64_t root_id, parent_id;
static uint32_t old_root_id, old_parent_id;
static FILE *f_mountinfo;
static void cleanup_namespace(void)
{
int ret;
ret = fchdir(orig_root);
if (ret == -1)
ksft_perror("fchdir to original root");
ret = chroot(".");
if (ret == -1)
ksft_perror("chroot to original root");
umount2(root_mntpoint, MNT_DETACH);
rmdir(root_mntpoint);
}
static void setup_namespace(void)
{
int ret;
char buf[32];
uid_t uid = getuid();
gid_t gid = getgid();
ret = unshare(CLONE_NEWNS|CLONE_NEWUSER|CLONE_NEWPID);
if (ret == -1)
ksft_exit_fail_msg("unsharing mountns and userns: %s\n",
strerror(errno));
sprintf(buf, "0 %d 1", uid);
write_file("/proc/self/uid_map", buf);
write_file("/proc/self/setgroups", "deny");
sprintf(buf, "0 %d 1", gid);
write_file("/proc/self/gid_map", buf);
f_mountinfo = fopen("/proc/self/mountinfo", "re");
if (!f_mountinfo)
ksft_exit_fail_msg("failed to open mountinfo: %s\n",
strerror(errno));
ret = mount("", "/", NULL, MS_REC|MS_PRIVATE, NULL);
if (ret == -1)
ksft_exit_fail_msg("making mount tree private: %s\n",
strerror(errno));
if (!mkdtemp(root_mntpoint))
ksft_exit_fail_msg("creating temporary directory %s: %s\n",
root_mntpoint, strerror(errno));
old_parent_id = get_mnt_id("parent", root_mntpoint, STATX_MNT_ID);
parent_id = get_mnt_id("parent", root_mntpoint, STATX_MNT_ID_UNIQUE);
orig_root = open("/", O_PATH);
if (orig_root == -1)
ksft_exit_fail_msg("opening root directory: %s",
strerror(errno));
atexit(cleanup_namespace);
ret = mount(root_mntpoint, root_mntpoint, NULL, MS_BIND, NULL);
if (ret == -1)
ksft_exit_fail_msg("mounting temp root %s: %s\n",
root_mntpoint, strerror(errno));
ret = chroot(root_mntpoint);
if (ret == -1)
ksft_exit_fail_msg("chroot to temp root %s: %s\n",
root_mntpoint, strerror(errno));
ret = chdir("/");
if (ret == -1)
ksft_exit_fail_msg("chdir to root: %s\n", strerror(errno));
old_root_id = get_mnt_id("root", "/", STATX_MNT_ID);
root_id = get_mnt_id("root", "/", STATX_MNT_ID_UNIQUE);
}
static int setup_mount_tree(int log2_num)
{
int ret, i;
ret = mount("", "/", NULL, MS_REC|MS_SHARED, NULL);
if (ret == -1) {
ksft_test_result_fail("making mount tree shared: %s\n",
strerror(errno));
return -1;
}
for (i = 0; i < log2_num; i++) {
ret = mount("/", "/", NULL, MS_BIND, NULL);
if (ret == -1) {
ksft_test_result_fail("mounting submount %s: %s\n",
root_mntpoint, strerror(errno));
return -1;
}
}
return 0;
}
static void test_listmount_empty_root(void)
{
ssize_t res;
const unsigned int size = 32;
uint64_t list[size];
res = listmount(LSMT_ROOT, 0, 0, list, size, 0);
if (res == -1) {
ksft_test_result_fail("listmount: %s\n", strerror(errno));
return;
}
if (res != 1) {
ksft_test_result_fail("listmount result is %zi != 1\n", res);
return;
}
if (list[0] != root_id) {
ksft_test_result_fail("listmount ID doesn't match 0x%llx != 0x%llx\n",
(unsigned long long) list[0],
(unsigned long long) root_id);
return;
}
ksft_test_result_pass("listmount empty root\n");
}
static void test_statmount_zero_mask(void)
{
struct statmount sm;
int ret;
ret = statmount(root_id, 0, 0, 0, &sm, sizeof(sm), 0);
if (ret == -1) {
ksft_test_result_fail("statmount zero mask: %s\n",
strerror(errno));
return;
}
if (sm.size != sizeof(sm)) {
ksft_test_result_fail("unexpected size: %u != %u\n",
sm.size, (uint32_t) sizeof(sm));
return;
}
if (sm.mask != 0) {
ksft_test_result_fail("unexpected mask: 0x%llx != 0x0\n",
(unsigned long long) sm.mask);
return;
}
ksft_test_result_pass("statmount zero mask\n");
}
static void test_statmount_mnt_basic(void)
{
struct statmount sm;
int ret;
uint64_t mask = STATMOUNT_MNT_BASIC;
ret = statmount(root_id, 0, 0, mask, &sm, sizeof(sm), 0);
if (ret == -1) {
ksft_test_result_fail("statmount mnt basic: %s\n",
strerror(errno));
return;
}
if (sm.size != sizeof(sm)) {
ksft_test_result_fail("unexpected size: %u != %u\n",
sm.size, (uint32_t) sizeof(sm));
return;
}
if (sm.mask != mask) {
ksft_test_result_skip("statmount mnt basic unavailable\n");
return;
}
if (sm.mnt_id != root_id) {
ksft_test_result_fail("unexpected root ID: 0x%llx != 0x%llx\n",
(unsigned long long) sm.mnt_id,
(unsigned long long) root_id);
return;
}
if (sm.mnt_id_old != old_root_id) {
ksft_test_result_fail("unexpected old root ID: %u != %u\n",
sm.mnt_id_old, old_root_id);
return;
}
if (sm.mnt_parent_id != parent_id) {
ksft_test_result_fail("unexpected parent ID: 0x%llx != 0x%llx\n",
(unsigned long long) sm.mnt_parent_id,
(unsigned long long) parent_id);
return;
}
if (sm.mnt_parent_id_old != old_parent_id) {
ksft_test_result_fail("unexpected old parent ID: %u != %u\n",
sm.mnt_parent_id_old, old_parent_id);
return;
}
if (sm.mnt_propagation != MS_PRIVATE) {
ksft_test_result_fail("unexpected propagation: 0x%llx\n",
(unsigned long long) sm.mnt_propagation);
return;
}
ksft_test_result_pass("statmount mnt basic\n");
}
static void test_statmount_sb_basic(void)
{
struct statmount sm;
int ret;
uint64_t mask = STATMOUNT_SB_BASIC;
struct statx sx;
struct statfs sf;
ret = statmount(root_id, 0, 0, mask, &sm, sizeof(sm), 0);
if (ret == -1) {
ksft_test_result_fail("statmount sb basic: %s\n",
strerror(errno));
return;
}
if (sm.size != sizeof(sm)) {
ksft_test_result_fail("unexpected size: %u != %u\n",
sm.size, (uint32_t) sizeof(sm));
return;
}
if (sm.mask != mask) {
ksft_test_result_skip("statmount sb basic unavailable\n");
return;
}
ret = statx(AT_FDCWD, "/", 0, 0, &sx);
if (ret == -1) {
ksft_test_result_fail("stat root failed: %s\n",
strerror(errno));
return;
}
if (sm.sb_dev_major != sx.stx_dev_major ||
sm.sb_dev_minor != sx.stx_dev_minor) {
ksft_test_result_fail("unexpected sb dev %u:%u != %u:%u\n",
sm.sb_dev_major, sm.sb_dev_minor,
sx.stx_dev_major, sx.stx_dev_minor);
return;
}
ret = statfs("/", &sf);
if (ret == -1) {
ksft_test_result_fail("statfs root failed: %s\n",
strerror(errno));
return;
}
if (sm.sb_magic != sf.f_type) {
ksft_test_result_fail("unexpected sb magic: 0x%llx != 0x%lx\n",
(unsigned long long) sm.sb_magic,
sf.f_type);
return;
}
ksft_test_result_pass("statmount sb basic\n");
}
static void test_statmount_mnt_point(void)
{
struct statmount *sm;
sm = statmount_alloc(root_id, 0, STATMOUNT_MNT_POINT, 0);
if (!sm) {
ksft_test_result_fail("statmount mount point: %s\n",
strerror(errno));
return;
}
if (!(sm->mask & STATMOUNT_MNT_POINT)) {
ksft_test_result_fail("missing STATMOUNT_MNT_POINT in mask\n");
return;
}
if (strcmp(sm->str + sm->mnt_point, "/") != 0) {
ksft_test_result_fail("unexpected mount point: '%s' != '/'\n",
sm->str + sm->mnt_point);
goto out;
}
ksft_test_result_pass("statmount mount point\n");
out:
free(sm);
}
static void test_statmount_mnt_root(void)
{
struct statmount *sm;
const char *mnt_root, *last_dir, *last_root;
last_dir = strrchr(root_mntpoint, '/');
assert(last_dir);
last_dir++;
sm = statmount_alloc(root_id, 0, STATMOUNT_MNT_ROOT, 0);
if (!sm) {
ksft_test_result_fail("statmount mount root: %s\n",
strerror(errno));
return;
}
if (!(sm->mask & STATMOUNT_MNT_ROOT)) {
ksft_test_result_fail("missing STATMOUNT_MNT_ROOT in mask\n");
return;
}
mnt_root = sm->str + sm->mnt_root;
last_root = strrchr(mnt_root, '/');
if (last_root)
last_root++;
else
last_root = mnt_root;
if (strcmp(last_dir, last_root) != 0) {
ksft_test_result_fail("unexpected mount root last component: '%s' != '%s'\n",
last_root, last_dir);
goto out;
}
ksft_test_result_pass("statmount mount root\n");
out:
free(sm);
}
static void test_statmount_fs_type(void)
{
struct statmount *sm;
const char *fs_type;
const char *const *s;
sm = statmount_alloc(root_id, 0, STATMOUNT_FS_TYPE, 0);
if (!sm) {
ksft_test_result_fail("statmount fs type: %s\n",
strerror(errno));
return;
}
if (!(sm->mask & STATMOUNT_FS_TYPE)) {
ksft_test_result_fail("missing STATMOUNT_FS_TYPE in mask\n");
return;
}
fs_type = sm->str + sm->fs_type;
for (s = known_fs; s != NULL; s++) {
if (strcmp(fs_type, *s) == 0)
break;
}
if (!s)
ksft_print_msg("unknown filesystem type: %s\n", fs_type);
ksft_test_result_pass("statmount fs type\n");
free(sm);
}
static void test_statmount_mnt_opts(void)
{
struct statmount *sm;
const char *statmount_opts;
char *line = NULL;
size_t len = 0;
sm = statmount_alloc(root_id, 0, STATMOUNT_MNT_BASIC | STATMOUNT_MNT_OPTS,
0);
if (!sm) {
ksft_test_result_fail("statmount mnt opts: %s\n",
strerror(errno));
return;
}
if (!(sm->mask & STATMOUNT_MNT_BASIC)) {
ksft_test_result_fail("missing STATMOUNT_MNT_BASIC in mask\n");
return;
}
while (getline(&line, &len, f_mountinfo) != -1) {
int i;
char *p, *p2;
unsigned int old_mnt_id;
old_mnt_id = atoi(line);
if (old_mnt_id != sm->mnt_id_old)
continue;
for (p = line, i = 0; p && i < 5; i++)
p = strchr(p + 1, ' ');
if (!p)
continue;
p2 = strchr(p + 1, ' ');
if (!p2)
continue;
*p2 = '\0';
p = strchr(p2 + 1, '-');
if (!p)
continue;
for (p++, i = 0; p && i < 2; i++)
p = strchr(p + 1, ' ');
if (!p)
continue;
p++;
/* skip generic superblock options */
if (strncmp(p, "ro", 2) == 0)
p += 2;
else if (strncmp(p, "rw", 2) == 0)
p += 2;
if (*p == ',')
p++;
if (strncmp(p, "sync", 4) == 0)
p += 4;
if (*p == ',')
p++;
if (strncmp(p, "dirsync", 7) == 0)
p += 7;
if (*p == ',')
p++;
if (strncmp(p, "lazytime", 8) == 0)
p += 8;
if (*p == ',')
p++;
p2 = strrchr(p, '\n');
if (p2)
*p2 = '\0';
if (sm->mask & STATMOUNT_MNT_OPTS)
statmount_opts = sm->str + sm->mnt_opts;
else
statmount_opts = "";
if (strcmp(statmount_opts, p) != 0)
ksft_test_result_fail(
"unexpected mount options: '%s' != '%s'\n",
statmount_opts, p);
else
ksft_test_result_pass("statmount mount options\n");
free(sm);
free(line);
return;
}
ksft_test_result_fail("didnt't find mount entry\n");
free(sm);
free(line);
}
static void test_statmount_string(uint64_t mask, size_t off, const char *name)
{
struct statmount *sm;
size_t len, shortsize, exactsize;
uint32_t start, i;
int ret;
sm = statmount_alloc(root_id, 0, mask, 0);
if (!sm) {
ksft_test_result_fail("statmount %s: %s\n", name,
strerror(errno));
goto out;
}
if (sm->size < sizeof(*sm)) {
ksft_test_result_fail("unexpected size: %u < %u\n",
sm->size, (uint32_t) sizeof(*sm));
goto out;
}
if (sm->mask != mask) {
ksft_test_result_skip("statmount %s unavailable\n", name);
goto out;
}
len = sm->size - sizeof(*sm);
start = ((uint32_t *) sm)[off];
for (i = start;; i++) {
if (i >= len) {
ksft_test_result_fail("string out of bounds\n");
goto out;
}
if (!sm->str[i])
break;
}
exactsize = sm->size;
shortsize = sizeof(*sm) + i;
ret = statmount(root_id, 0, 0, mask, sm, exactsize, 0);
if (ret == -1) {
ksft_test_result_fail("statmount exact size: %s\n",
strerror(errno));
goto out;
}
errno = 0;
ret = statmount(root_id, 0, 0, mask, sm, shortsize, 0);
if (ret != -1 || errno != EOVERFLOW) {
ksft_test_result_fail("should have failed with EOVERFLOW: %s\n",
strerror(errno));
goto out;
}
ksft_test_result_pass("statmount string %s\n", name);
out:
free(sm);
}
static void test_listmount_tree(void)
{
ssize_t res;
const unsigned int log2_num = 4;
const unsigned int step = 3;
const unsigned int size = (1 << log2_num) + step + 1;
size_t num, expect = 1 << log2_num;
uint64_t list[size];
uint64_t list2[size];
size_t i;
res = setup_mount_tree(log2_num);
if (res == -1)
return;
num = res = listmount(LSMT_ROOT, 0, 0, list, size, 0);
if (res == -1) {
ksft_test_result_fail("listmount: %s\n", strerror(errno));
return;
}
if (num != expect) {
ksft_test_result_fail("listmount result is %zi != %zi\n",
res, expect);
return;
}
for (i = 0; i < size - step;) {
res = listmount(LSMT_ROOT, 0, i ? list2[i - 1] : 0, list2 + i, step, 0);
if (res == -1)
ksft_test_result_fail("short listmount: %s\n",
strerror(errno));
i += res;
if (res < step)
break;
}
if (i != num) {
ksft_test_result_fail("different number of entries: %zu != %zu\n",
i, num);
return;
}
for (i = 0; i < num; i++) {
if (list2[i] != list[i]) {
ksft_test_result_fail("different value for entry %zu: 0x%llx != 0x%llx\n",
i,
(unsigned long long) list2[i],
(unsigned long long) list[i]);
}
}
ksft_test_result_pass("listmount tree\n");
}
static void test_statmount_by_fd(void)
{
struct statmount *sm = NULL;
char tmpdir[] = "/statmount.fd.XXXXXX";
const char root[] = "/test";
char subdir[PATH_MAX], tmproot[PATH_MAX];
int fd;
if (!mkdtemp(tmpdir)) {
ksft_perror("mkdtemp");
return;
}
if (mount("statmount.test", tmpdir, "tmpfs", 0, NULL)) {
ksft_perror("mount");
rmdir(tmpdir);
return;
}
snprintf(subdir, PATH_MAX, "%s%s", tmpdir, root);
snprintf(tmproot, PATH_MAX, "%s/%s", tmpdir, "chroot");
if (mkdir(subdir, 0755)) {
ksft_perror("mkdir");
goto err_tmpdir;
}
if (mount(subdir, subdir, NULL, MS_BIND, 0)) {
ksft_perror("mount");
goto err_subdir;
}
if (mkdir(tmproot, 0755)) {
ksft_perror("mkdir");
goto err_subdir;
}
fd = open(subdir, O_PATH);
if (fd < 0) {
ksft_perror("open");
goto err_tmproot;
}
if (chroot(tmproot)) {
ksft_perror("chroot");
goto err_fd;
}
sm = statmount_alloc_by_fd(fd, STATMOUNT_MNT_ROOT | STATMOUNT_MNT_POINT);
if (!sm) {
ksft_test_result_fail("statmount by fd failed: %s\n", strerror(errno));
goto err_chroot;
}
if (sm->size < sizeof(*sm)) {
ksft_test_result_fail("unexpected size: %u < %u\n",
sm->size, (uint32_t) sizeof(*sm));
goto err_chroot;
}
if (sm->mask & STATMOUNT_MNT_POINT) {
ksft_test_result_fail("STATMOUNT_MNT_POINT unexpectedly set in statmount\n");
goto err_chroot;
}
if (!(sm->mask & STATMOUNT_MNT_ROOT)) {
ksft_test_result_fail("STATMOUNT_MNT_ROOT not set in statmount\n");
goto err_chroot;
}
if (strcmp(root, sm->str + sm->mnt_root) != 0) {
ksft_test_result_fail("statmount returned incorrect mnt_root,"
"statmount mnt_root: %s != %s\n",
sm->str + sm->mnt_root, root);
goto err_chroot;
}
if (chroot(".")) {
ksft_perror("chroot");
goto out;
}
free(sm);
sm = statmount_alloc_by_fd(fd, STATMOUNT_MNT_ROOT | STATMOUNT_MNT_POINT);
if (!sm) {
ksft_test_result_fail("statmount by fd failed: %s\n", strerror(errno));
goto err_fd;
}
if (sm->size < sizeof(*sm)) {
ksft_test_result_fail("unexpected size: %u < %u\n",
sm->size, (uint32_t) sizeof(*sm));
goto out;
}
if (!(sm->mask & STATMOUNT_MNT_POINT)) {
ksft_test_result_fail("STATMOUNT_MNT_POINT not set in statmount\n");
goto out;
}
if (!(sm->mask & STATMOUNT_MNT_ROOT)) {
ksft_test_result_fail("STATMOUNT_MNT_ROOT not set in statmount\n");
goto out;
}
if (strcmp(subdir, sm->str + sm->mnt_point) != 0) {
ksft_test_result_fail("statmount returned incorrect mnt_point,"
"statmount mnt_point: %s != %s\n", sm->str + sm->mnt_point, subdir);
goto out;
}
if (strcmp(root, sm->str + sm->mnt_root) != 0) {
ksft_test_result_fail("statmount returned incorrect mnt_root,"
"statmount mnt_root: %s != %s\n", sm->str + sm->mnt_root, root);
goto out;
}
ksft_test_result_pass("statmount by fd\n");
goto out;
err_chroot:
chroot(".");
out:
free(sm);
err_fd:
close(fd);
err_tmproot:
rmdir(tmproot);
err_subdir:
umount2(subdir, MNT_DETACH);
rmdir(subdir);
err_tmpdir:
umount2(tmpdir, MNT_DETACH);
rmdir(tmpdir);
}
static void test_statmount_by_fd_unmounted(void)
{
const char root[] = "/test.unmounted";
char tmpdir[] = "/statmount.fd.XXXXXX";
char subdir[PATH_MAX];
int fd;
struct statmount *sm = NULL;
if (!mkdtemp(tmpdir)) {
ksft_perror("mkdtemp");
return;
}
if (mount("statmount.test", tmpdir, "tmpfs", 0, NULL)) {
ksft_perror("mount");
rmdir(tmpdir);
return;
}
snprintf(subdir, PATH_MAX, "%s%s", tmpdir, root);
if (mkdir(subdir, 0755)) {
ksft_perror("mkdir");
goto err_tmpdir;
}
if (mount(subdir, subdir, 0, MS_BIND, NULL)) {
ksft_perror("mount");
goto err_subdir;
}
fd = open(subdir, O_PATH);
if (fd < 0) {
ksft_perror("open");
goto err_subdir;
}
if (umount2(tmpdir, MNT_DETACH)) {
ksft_perror("umount2");
goto err_fd;
}
sm = statmount_alloc_by_fd(fd, STATMOUNT_MNT_POINT | STATMOUNT_MNT_ROOT);
if (!sm) {
ksft_test_result_fail("statmount by fd unmounted: %s\n",
strerror(errno));
goto err_sm;
}
if (sm->size < sizeof(*sm)) {
ksft_test_result_fail("unexpected size: %u < %u\n",
sm->size, (uint32_t) sizeof(*sm));
goto err_sm;
}
if (sm->mask & STATMOUNT_MNT_POINT) {
ksft_test_result_fail("STATMOUNT_MNT_POINT unexpectedly set in mask\n");
goto err_sm;
}
if (!(sm->mask & STATMOUNT_MNT_ROOT)) {
ksft_test_result_fail("STATMOUNT_MNT_ROOT not set in mask\n");
goto err_sm;
}
if (strcmp(sm->str + sm->mnt_root, root) != 0) {
ksft_test_result_fail("statmount returned incorrect mnt_root,"
"statmount mnt_root: %s != %s\n",
sm->str + sm->mnt_root, root);
goto err_sm;
}
ksft_test_result_pass("statmount by fd on unmounted mount\n");
err_sm:
free(sm);
err_fd:
close(fd);
err_subdir:
umount2(subdir, MNT_DETACH);
rmdir(subdir);
err_tmpdir:
umount2(tmpdir, MNT_DETACH);
rmdir(tmpdir);
}
#define str_off(memb) (offsetof(struct statmount, memb) / sizeof(uint32_t))
int main(void)
{
int ret;
uint64_t all_mask = STATMOUNT_SB_BASIC | STATMOUNT_MNT_BASIC |
STATMOUNT_PROPAGATE_FROM | STATMOUNT_MNT_ROOT |
STATMOUNT_MNT_POINT | STATMOUNT_FS_TYPE | STATMOUNT_MNT_NS_ID;
ksft_print_header();
ret = statmount(0, 0, 0, 0, NULL, 0, 0);
assert(ret == -1);
if (errno == ENOSYS)
ksft_exit_skip("statmount() syscall not supported\n");
setup_namespace();
ksft_set_plan(17);
test_listmount_empty_root();
test_statmount_zero_mask();
test_statmount_mnt_basic();
test_statmount_sb_basic();
test_statmount_mnt_root();
test_statmount_mnt_point();
test_statmount_fs_type();
test_statmount_mnt_opts();
test_statmount_string(STATMOUNT_MNT_ROOT, str_off(mnt_root), "mount root");
test_statmount_string(STATMOUNT_MNT_POINT, str_off(mnt_point), "mount point");
test_statmount_string(STATMOUNT_FS_TYPE, str_off(fs_type), "fs type");
test_statmount_string(all_mask, str_off(mnt_root), "mount root & all");
test_statmount_string(all_mask, str_off(mnt_point), "mount point & all");
test_statmount_string(all_mask, str_off(fs_type), "fs type & all");
test_listmount_tree();
test_statmount_by_fd_unmounted();
test_statmount_by_fd();
if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
ksft_exit_fail();
else
ksft_exit_pass();
}
@@ -0,0 +1,361 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#define _GNU_SOURCE
#include <assert.h>
#include <fcntl.h>
#include <limits.h>
#include <sched.h>
#include <stdlib.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <linux/nsfs.h>
#include <linux/stat.h>
#include "statmount.h"
#include "../utils.h"
#include "kselftest.h"
#define NSID_PASS 0
#define NSID_FAIL 1
#define NSID_SKIP 2
#define NSID_ERROR 3
static void handle_result(int ret, const char *testname)
{
if (ret == NSID_PASS)
ksft_test_result_pass("%s\n", testname);
else if (ret == NSID_FAIL)
ksft_test_result_fail("%s\n", testname);
else if (ret == NSID_ERROR)
ksft_exit_fail_msg("%s\n", testname);
else
ksft_test_result_skip("%s\n", testname);
}
static int get_mnt_ns_id(const char *mnt_ns, uint64_t *mnt_ns_id)
{
int fd = open(mnt_ns, O_RDONLY);
if (fd < 0) {
ksft_print_msg("failed to open for ns %s: %s\n",
mnt_ns, strerror(errno));
sleep(60);
return NSID_ERROR;
}
if (ioctl(fd, NS_GET_MNTNS_ID, mnt_ns_id) < 0) {
ksft_print_msg("failed to get the nsid for ns %s: %s\n",
mnt_ns, strerror(errno));
return NSID_ERROR;
}
close(fd);
return NSID_PASS;
}
static int setup_namespace(void)
{
if (setup_userns() != 0)
return NSID_ERROR;
return NSID_PASS;
}
static int _test_statmount_mnt_ns_id(void)
{
struct statmount sm;
uint64_t mnt_ns_id;
uint64_t root_id;
int ret;
ret = get_mnt_ns_id("/proc/self/ns/mnt", &mnt_ns_id);
if (ret != NSID_PASS)
return ret;
root_id = get_unique_mnt_id("/");
if (!root_id)
return NSID_ERROR;
ret = statmount(root_id, 0, 0, STATMOUNT_MNT_NS_ID, &sm, sizeof(sm), 0);
if (ret == -1) {
ksft_print_msg("statmount mnt ns id: %s\n", strerror(errno));
return NSID_ERROR;
}
if (sm.size != sizeof(sm)) {
ksft_print_msg("unexpected size: %u != %u\n", sm.size,
(uint32_t)sizeof(sm));
return NSID_FAIL;
}
if (sm.mask != STATMOUNT_MNT_NS_ID) {
ksft_print_msg("statmount mnt ns id unavailable\n");
return NSID_SKIP;
}
if (sm.mnt_ns_id != mnt_ns_id) {
ksft_print_msg("unexpected mnt ns ID: 0x%llx != 0x%llx\n",
(unsigned long long)sm.mnt_ns_id,
(unsigned long long)mnt_ns_id);
return NSID_FAIL;
}
return NSID_PASS;
}
static int _test_statmount_mnt_ns_id_by_fd(void)
{
struct statmount sm;
uint64_t mnt_ns_id;
int ret, fd, mounted = 1, status = NSID_ERROR;
char mnt[] = "/statmount.fd.XXXXXX";
ret = get_mnt_ns_id("/proc/self/ns/mnt", &mnt_ns_id);
if (ret != NSID_PASS)
return ret;
if (!mkdtemp(mnt)) {
ksft_print_msg("statmount by fd mnt ns id mkdtemp: %s\n", strerror(errno));
return NSID_ERROR;
}
if (mount(mnt, mnt, NULL, MS_BIND, 0)) {
ksft_print_msg("statmount by fd mnt ns id mount: %s\n", strerror(errno));
status = NSID_ERROR;
goto err;
}
fd = open(mnt, O_PATH);
if (fd < 0) {
ksft_print_msg("statmount by fd mnt ns id open: %s\n", strerror(errno));
goto err;
}
ret = statmount(0, 0, fd, STATMOUNT_MNT_NS_ID, &sm, sizeof(sm), STATMOUNT_BY_FD);
if (ret == -1) {
ksft_print_msg("statmount mnt ns id statmount: %s\n", strerror(errno));
status = NSID_ERROR;
goto out;
}
if (sm.size != sizeof(sm)) {
ksft_print_msg("unexpected size: %u != %u\n", sm.size,
(uint32_t)sizeof(sm));
status = NSID_FAIL;
goto out;
}
if (sm.mask != STATMOUNT_MNT_NS_ID) {
ksft_print_msg("statmount mnt ns id unavailable\n");
status = NSID_SKIP;
goto out;
}
if (sm.mnt_ns_id != mnt_ns_id) {
ksft_print_msg("unexpected mnt ns ID: 0x%llx != 0x%llx\n",
(unsigned long long)sm.mnt_ns_id,
(unsigned long long)mnt_ns_id);
status = NSID_FAIL;
goto out;
}
mounted = 0;
if (umount2(mnt, MNT_DETACH)) {
ksft_print_msg("statmount by fd mnt ns id umount2: %s\n", strerror(errno));
goto out;
}
ret = statmount(0, 0, fd, STATMOUNT_MNT_NS_ID, &sm, sizeof(sm), STATMOUNT_BY_FD);
if (ret == -1) {
ksft_print_msg("statmount mnt ns id statmount: %s\n", strerror(errno));
status = NSID_ERROR;
goto out;
}
if (sm.size != sizeof(sm)) {
ksft_print_msg("unexpected size: %u != %u\n", sm.size,
(uint32_t)sizeof(sm));
status = NSID_FAIL;
goto out;
}
if (sm.mask == STATMOUNT_MNT_NS_ID) {
ksft_print_msg("unexpected STATMOUNT_MNT_NS_ID in mask\n");
status = NSID_FAIL;
goto out;
}
status = NSID_PASS;
out:
close(fd);
if (mounted)
umount2(mnt, MNT_DETACH);
err:
rmdir(mnt);
return status;
}
static void test_statmount_mnt_ns_id(void)
{
pid_t pid;
int ret;
pid = fork();
if (pid < 0)
ksft_exit_fail_msg("failed to fork: %s\n", strerror(errno));
/* We're the original pid, wait for the result. */
if (pid != 0) {
ret = wait_for_pid(pid);
handle_result(ret, "test statmount ns id");
return;
}
ret = setup_namespace();
if (ret != NSID_PASS)
exit(ret);
ret = _test_statmount_mnt_ns_id();
if (ret != NSID_PASS)
exit(ret);
ret = _test_statmount_mnt_ns_id_by_fd();
exit(ret);
}
static int validate_external_listmount(pid_t pid, uint64_t child_nr_mounts)
{
uint64_t list[256];
uint64_t mnt_ns_id;
uint64_t nr_mounts;
char buf[256];
int ret;
/* Get the mount ns id for our child. */
snprintf(buf, sizeof(buf), "/proc/%lu/ns/mnt", (unsigned long)pid);
ret = get_mnt_ns_id(buf, &mnt_ns_id);
nr_mounts = listmount(LSMT_ROOT, mnt_ns_id, 0, list, 256, 0);
if (nr_mounts == (uint64_t)-1) {
ksft_print_msg("listmount: %s\n", strerror(errno));
return NSID_ERROR;
}
if (nr_mounts != child_nr_mounts) {
ksft_print_msg("listmount results is %zi != %zi\n", nr_mounts,
child_nr_mounts);
return NSID_FAIL;
}
/* Validate that all of our entries match our mnt_ns_id. */
for (int i = 0; i < nr_mounts; i++) {
struct statmount sm;
ret = statmount(list[i], mnt_ns_id, 0, STATMOUNT_MNT_NS_ID, &sm,
sizeof(sm), 0);
if (ret < 0) {
ksft_print_msg("statmount mnt ns id: %s\n", strerror(errno));
return NSID_ERROR;
}
if (sm.mask != STATMOUNT_MNT_NS_ID) {
ksft_print_msg("statmount mnt ns id unavailable\n");
return NSID_SKIP;
}
if (sm.mnt_ns_id != mnt_ns_id) {
ksft_print_msg("listmount gave us the wrong ns id: 0x%llx != 0x%llx\n",
(unsigned long long)sm.mnt_ns_id,
(unsigned long long)mnt_ns_id);
return NSID_FAIL;
}
}
return NSID_PASS;
}
static void test_listmount_ns(void)
{
uint64_t nr_mounts;
char pval;
int child_ready_pipe[2];
int parent_ready_pipe[2];
pid_t pid;
int ret, child_ret;
if (pipe(child_ready_pipe) < 0)
ksft_exit_fail_msg("failed to create the child pipe: %s\n",
strerror(errno));
if (pipe(parent_ready_pipe) < 0)
ksft_exit_fail_msg("failed to create the parent pipe: %s\n",
strerror(errno));
pid = fork();
if (pid < 0)
ksft_exit_fail_msg("failed to fork: %s\n", strerror(errno));
if (pid == 0) {
char cval;
uint64_t list[256];
close(child_ready_pipe[0]);
close(parent_ready_pipe[1]);
ret = setup_namespace();
if (ret != NSID_PASS)
exit(ret);
nr_mounts = listmount(LSMT_ROOT, 0, 0, list, 256, 0);
if (nr_mounts == (uint64_t)-1) {
ksft_print_msg("listmount: %s\n", strerror(errno));
exit(NSID_FAIL);
}
/*
* Tell our parent how many mounts we have, and then wait for it
* to tell us we're done.
*/
if (write(child_ready_pipe[1], &nr_mounts, sizeof(nr_mounts)) !=
sizeof(nr_mounts))
ret = NSID_ERROR;
if (read(parent_ready_pipe[0], &cval, sizeof(cval)) != sizeof(cval))
ret = NSID_ERROR;
exit(NSID_PASS);
}
close(child_ready_pipe[1]);
close(parent_ready_pipe[0]);
/* Wait until the child has created everything. */
if (read(child_ready_pipe[0], &nr_mounts, sizeof(nr_mounts)) !=
sizeof(nr_mounts))
ret = NSID_ERROR;
ret = validate_external_listmount(pid, nr_mounts);
if (write(parent_ready_pipe[1], &pval, sizeof(pval)) != sizeof(pval))
ret = NSID_ERROR;
child_ret = wait_for_pid(pid);
if (child_ret != NSID_PASS)
ret = child_ret;
handle_result(ret, "test listmount ns id");
}
int main(void)
{
int ret;
ksft_print_header();
ret = statmount(0, 0, 0, 0, NULL, 0, 0);
assert(ret == -1);
if (errno == ENOSYS)
ksft_exit_skip("statmount() syscall not supported\n");
ksft_set_plan(2);
test_statmount_mnt_ns_id();
test_listmount_ns();
if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
ksft_exit_fail();
else
ksft_exit_pass();
}
@@ -0,0 +1,615 @@
// SPDX-License-Identifier: GPL-2.0
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <fcntl.h>
#include <sys/types.h>
#include <dirent.h>
#include <grp.h>
#include <linux/limits.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/eventfd.h>
#include <sys/fsuid.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/xattr.h>
#include <sys/mount.h>
#include "kselftest.h"
#include "wrappers.h"
#include "utils.h"
#define MAX_USERNS_LEVEL 32
#define syserror(format, ...) \
({ \
fprintf(stderr, "%m - " format "\n", ##__VA_ARGS__); \
(-errno); \
})
#define syserror_set(__ret__, format, ...) \
({ \
typeof(__ret__) __internal_ret__ = (__ret__); \
errno = labs(__ret__); \
fprintf(stderr, "%m - " format "\n", ##__VA_ARGS__); \
__internal_ret__; \
})
#define STRLITERALLEN(x) (sizeof(""x"") - 1)
#define INTTYPE_TO_STRLEN(type) \
(2 + (sizeof(type) <= 1 \
? 3 \
: sizeof(type) <= 2 \
? 5 \
: sizeof(type) <= 4 \
? 10 \
: sizeof(type) <= 8 ? 20 : sizeof(int[-2 * (sizeof(type) > 8)])))
#define list_for_each(__iterator, __list) \
for (__iterator = (__list)->next; __iterator != __list; __iterator = __iterator->next)
typedef enum idmap_type_t {
ID_TYPE_UID,
ID_TYPE_GID
} idmap_type_t;
struct id_map {
idmap_type_t map_type;
__u32 nsid;
__u32 hostid;
__u32 range;
};
struct list {
void *elem;
struct list *next;
struct list *prev;
};
struct userns_hierarchy {
int fd_userns;
int fd_event;
unsigned int level;
struct list id_map;
};
static inline void list_init(struct list *list)
{
list->elem = NULL;
list->next = list->prev = list;
}
static inline int list_empty(const struct list *list)
{
return list == list->next;
}
static inline void __list_add(struct list *new, struct list *prev, struct list *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
static inline void list_add_tail(struct list *head, struct list *list)
{
__list_add(list, head->prev, head);
}
static inline void list_del(struct list *list)
{
struct list *next, *prev;
next = list->next;
prev = list->prev;
next->prev = prev;
prev->next = next;
}
static ssize_t read_nointr(int fd, void *buf, size_t count)
{
ssize_t ret;
do {
ret = read(fd, buf, count);
} while (ret < 0 && errno == EINTR);
return ret;
}
static ssize_t write_nointr(int fd, const void *buf, size_t count)
{
ssize_t ret;
do {
ret = write(fd, buf, count);
} while (ret < 0 && errno == EINTR);
return ret;
}
#define __STACK_SIZE (8 * 1024 * 1024)
static pid_t do_clone(int (*fn)(void *), void *arg, int flags)
{
void *stack;
stack = malloc(__STACK_SIZE);
if (!stack)
return -ENOMEM;
#ifdef __ia64__
return __clone2(fn, stack, __STACK_SIZE, flags | SIGCHLD, arg, NULL);
#else
return clone(fn, stack + __STACK_SIZE, flags | SIGCHLD, arg, NULL);
#endif
}
static int get_userns_fd_cb(void *data)
{
for (;;)
pause();
_exit(0);
}
int wait_for_pid(pid_t pid)
{
int status, ret;
again:
ret = waitpid(pid, &status, 0);
if (ret == -1) {
if (errno == EINTR)
goto again;
return -1;
}
if (!WIFEXITED(status))
return -1;
return WEXITSTATUS(status);
}
static int write_id_mapping(idmap_type_t map_type, pid_t pid, const char *buf, size_t buf_size)
{
int fd = -EBADF, setgroups_fd = -EBADF;
int fret = -1;
int ret;
char path[STRLITERALLEN("/proc/") + INTTYPE_TO_STRLEN(pid_t) +
STRLITERALLEN("/setgroups") + 1];
if (geteuid() != 0 && map_type == ID_TYPE_GID) {
ret = snprintf(path, sizeof(path), "/proc/%d/setgroups", pid);
if (ret < 0 || ret >= sizeof(path))
goto out;
setgroups_fd = open(path, O_WRONLY | O_CLOEXEC);
if (setgroups_fd < 0 && errno != ENOENT) {
syserror("Failed to open \"%s\"", path);
goto out;
}
if (setgroups_fd >= 0) {
ret = write_nointr(setgroups_fd, "deny\n", STRLITERALLEN("deny\n"));
if (ret != STRLITERALLEN("deny\n")) {
syserror("Failed to write \"deny\" to \"/proc/%d/setgroups\"", pid);
goto out;
}
}
}
ret = snprintf(path, sizeof(path), "/proc/%d/%cid_map", pid, map_type == ID_TYPE_UID ? 'u' : 'g');
if (ret < 0 || ret >= sizeof(path))
goto out;
fd = open(path, O_WRONLY | O_CLOEXEC);
if (fd < 0) {
syserror("Failed to open \"%s\"", path);
goto out;
}
ret = write_nointr(fd, buf, buf_size);
if (ret != buf_size) {
syserror("Failed to write %cid mapping to \"%s\"",
map_type == ID_TYPE_UID ? 'u' : 'g', path);
goto out;
}
fret = 0;
out:
close(fd);
close(setgroups_fd);
return fret;
}
static int map_ids_from_idmap(struct list *idmap, pid_t pid)
{
int fill, left;
char mapbuf[4096] = {};
bool had_entry = false;
idmap_type_t map_type, u_or_g;
if (list_empty(idmap))
return 0;
for (map_type = ID_TYPE_UID, u_or_g = 'u';
map_type <= ID_TYPE_GID; map_type++, u_or_g = 'g') {
char *pos = mapbuf;
int ret;
struct list *iterator;
list_for_each(iterator, idmap) {
struct id_map *map = iterator->elem;
if (map->map_type != map_type)
continue;
had_entry = true;
left = 4096 - (pos - mapbuf);
fill = snprintf(pos, left, "%u %u %u\n", map->nsid, map->hostid, map->range);
/*
* The kernel only takes <= 4k for writes to
* /proc/<pid>/{g,u}id_map
*/
if (fill <= 0 || fill >= left)
return syserror_set(-E2BIG, "Too many %cid mappings defined", u_or_g);
pos += fill;
}
if (!had_entry)
continue;
ret = write_id_mapping(map_type, pid, mapbuf, pos - mapbuf);
if (ret < 0)
return syserror("Failed to write mapping: %s", mapbuf);
memset(mapbuf, 0, sizeof(mapbuf));
}
return 0;
}
static int get_userns_fd_from_idmap(struct list *idmap)
{
int ret;
pid_t pid;
char path_ns[STRLITERALLEN("/proc/") + INTTYPE_TO_STRLEN(pid_t) +
STRLITERALLEN("/ns/user") + 1];
pid = do_clone(get_userns_fd_cb, NULL, CLONE_NEWUSER | CLONE_NEWNS);
if (pid < 0)
return -errno;
ret = map_ids_from_idmap(idmap, pid);
if (ret < 0)
return ret;
ret = snprintf(path_ns, sizeof(path_ns), "/proc/%d/ns/user", pid);
if (ret < 0 || (size_t)ret >= sizeof(path_ns))
ret = -EIO;
else
ret = open(path_ns, O_RDONLY | O_CLOEXEC | O_NOCTTY);
(void)kill(pid, SIGKILL);
(void)wait_for_pid(pid);
return ret;
}
int get_userns_fd(unsigned long nsid, unsigned long hostid, unsigned long range)
{
struct list head, uid_mapl, gid_mapl;
struct id_map uid_map = {
.map_type = ID_TYPE_UID,
.nsid = nsid,
.hostid = hostid,
.range = range,
};
struct id_map gid_map = {
.map_type = ID_TYPE_GID,
.nsid = nsid,
.hostid = hostid,
.range = range,
};
list_init(&head);
uid_mapl.elem = &uid_map;
gid_mapl.elem = &gid_map;
list_add_tail(&head, &uid_mapl);
list_add_tail(&head, &gid_mapl);
return get_userns_fd_from_idmap(&head);
}
bool switch_ids(uid_t uid, gid_t gid)
{
if (setgroups(0, NULL))
return syserror("failure: setgroups");
if (setresgid(gid, gid, gid))
return syserror("failure: setresgid");
if (setresuid(uid, uid, uid))
return syserror("failure: setresuid");
/* Ensure we can access proc files from processes we can ptrace. */
if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0))
return syserror("failure: make dumpable");
return true;
}
static int create_userns_hierarchy(struct userns_hierarchy *h);
static int userns_fd_cb(void *data)
{
struct userns_hierarchy *h = data;
char c;
int ret;
ret = read_nointr(h->fd_event, &c, 1);
if (ret < 0)
return syserror("failure: read from socketpair");
/* Only switch ids if someone actually wrote a mapping for us. */
if (c == '1') {
if (!switch_ids(0, 0))
return syserror("failure: switch ids to 0");
}
ret = write_nointr(h->fd_event, "1", 1);
if (ret < 0)
return syserror("failure: write to socketpair");
ret = create_userns_hierarchy(++h);
if (ret < 0)
return syserror("failure: userns level %d", h->level);
return 0;
}
static int create_userns_hierarchy(struct userns_hierarchy *h)
{
int fret = -1;
char c;
int fd_socket[2];
int fd_userns = -EBADF, ret = -1;
ssize_t bytes;
pid_t pid;
char path[256];
if (h->level == MAX_USERNS_LEVEL)
return 0;
ret = socketpair(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, fd_socket);
if (ret < 0)
return syserror("failure: create socketpair");
/* Note the CLONE_FILES | CLONE_VM when mucking with fds and memory. */
h->fd_event = fd_socket[1];
pid = do_clone(userns_fd_cb, h, CLONE_NEWUSER | CLONE_FILES | CLONE_VM);
if (pid < 0) {
syserror("failure: userns level %d", h->level);
goto out_close;
}
ret = map_ids_from_idmap(&h->id_map, pid);
if (ret < 0) {
kill(pid, SIGKILL);
syserror("failure: writing id mapping for userns level %d for %d", h->level, pid);
goto out_wait;
}
if (!list_empty(&h->id_map))
bytes = write_nointr(fd_socket[0], "1", 1); /* Inform the child we wrote a mapping. */
else
bytes = write_nointr(fd_socket[0], "0", 1); /* Inform the child we didn't write a mapping. */
if (bytes < 0) {
kill(pid, SIGKILL);
syserror("failure: write to socketpair");
goto out_wait;
}
/* Wait for child to set*id() and become dumpable. */
bytes = read_nointr(fd_socket[0], &c, 1);
if (bytes < 0) {
kill(pid, SIGKILL);
syserror("failure: read from socketpair");
goto out_wait;
}
snprintf(path, sizeof(path), "/proc/%d/ns/user", pid);
fd_userns = open(path, O_RDONLY | O_CLOEXEC);
if (fd_userns < 0) {
kill(pid, SIGKILL);
syserror("failure: open userns level %d for %d", h->level, pid);
goto out_wait;
}
fret = 0;
out_wait:
if (!wait_for_pid(pid) && !fret) {
h->fd_userns = fd_userns;
fd_userns = -EBADF;
}
out_close:
if (fd_userns >= 0)
close(fd_userns);
close(fd_socket[0]);
close(fd_socket[1]);
return fret;
}
int write_file(const char *path, const char *val)
{
int fd = open(path, O_WRONLY);
size_t len = strlen(val);
int ret;
if (fd == -1) {
ksft_print_msg("opening %s for write: %s\n", path, strerror(errno));
return -1;
}
ret = write(fd, val, len);
if (ret == -1) {
ksft_print_msg("writing to %s: %s\n", path, strerror(errno));
return -1;
}
if (ret != len) {
ksft_print_msg("short write to %s\n", path);
return -1;
}
ret = close(fd);
if (ret == -1) {
ksft_print_msg("closing %s\n", path);
return -1;
}
return 0;
}
int setup_userns(void)
{
int ret;
char buf[32];
uid_t uid = getuid();
gid_t gid = getgid();
ret = unshare(CLONE_NEWNS|CLONE_NEWUSER);
if (ret) {
ksft_exit_fail_msg("unsharing mountns and userns: %s\n",
strerror(errno));
return ret;
}
sprintf(buf, "0 %d 1", uid);
ret = write_file("/proc/self/uid_map", buf);
if (ret)
return ret;
ret = write_file("/proc/self/setgroups", "deny");
if (ret)
return ret;
sprintf(buf, "0 %d 1", gid);
ret = write_file("/proc/self/gid_map", buf);
if (ret)
return ret;
ret = mount("", "/", NULL, MS_REC|MS_PRIVATE, NULL);
if (ret) {
ksft_print_msg("making mount tree private: %s\n", strerror(errno));
return ret;
}
return 0;
}
int enter_userns(void)
{
int ret;
char buf[32];
uid_t uid = getuid();
gid_t gid = getgid();
ret = unshare(CLONE_NEWUSER);
if (ret)
return ret;
sprintf(buf, "0 %d 1", uid);
ret = write_file("/proc/self/uid_map", buf);
if (ret)
return ret;
ret = write_file("/proc/self/setgroups", "deny");
if (ret)
return ret;
sprintf(buf, "0 %d 1", gid);
ret = write_file("/proc/self/gid_map", buf);
if (ret)
return ret;
return 0;
}
/* caps_down - lower all effective caps */
int caps_down(void)
{
bool fret = false;
cap_t caps = NULL;
int ret = -1;
caps = cap_get_proc();
if (!caps)
goto out;
ret = cap_clear_flag(caps, CAP_EFFECTIVE);
if (ret)
goto out;
ret = cap_set_proc(caps);
if (ret)
goto out;
fret = true;
out:
cap_free(caps);
return fret;
}
/* cap_down - lower an effective cap */
int cap_down(cap_value_t down)
{
bool fret = false;
cap_t caps = NULL;
cap_value_t cap = down;
int ret = -1;
caps = cap_get_proc();
if (!caps)
goto out;
ret = cap_set_flag(caps, CAP_EFFECTIVE, 1, &cap, 0);
if (ret)
goto out;
ret = cap_set_proc(caps);
if (ret)
goto out;
fret = true;
out:
cap_free(caps);
return fret;
}
uint64_t get_unique_mnt_id(const char *path)
{
struct statx sx;
int ret;
ret = statx(AT_FDCWD, path, 0, STATX_MNT_ID_UNIQUE, &sx);
if (ret == -1) {
ksft_print_msg("retrieving unique mount ID for %s: %s\n", path,
strerror(errno));
return 0;
}
if (!(sx.stx_mask & STATX_MNT_ID_UNIQUE)) {
ksft_print_msg("no unique mount ID available for %s\n", path);
return 0;
}
return sx.stx_mnt_id;
}
@@ -0,0 +1,51 @@
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef __IDMAP_UTILS_H
#define __IDMAP_UTILS_H
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <errno.h>
#include <linux/types.h>
#include <sched.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syscall.h>
#include <sys/capability.h>
#include <sys/fsuid.h>
#include <sys/types.h>
#include <unistd.h>
extern int get_userns_fd(unsigned long nsid, unsigned long hostid,
unsigned long range);
extern int caps_down(void);
extern int cap_down(cap_value_t down);
extern bool switch_ids(uid_t uid, gid_t gid);
extern int setup_userns(void);
extern int enter_userns(void);
static inline bool switch_userns(int fd, uid_t uid, gid_t gid, bool drop_caps)
{
if (setns(fd, CLONE_NEWUSER))
return false;
if (!switch_ids(uid, gid))
return false;
if (drop_caps && !caps_down())
return false;
return true;
}
extern int wait_for_pid(pid_t pid);
extern int write_file(const char *path, const char *val);
extern uint64_t get_unique_mnt_id(const char *path);
#endif /* __IDMAP_UTILS_H */
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: GPL-2.0
//
#ifndef __SELFTEST_OVERLAYFS_WRAPPERS_H__
#define __SELFTEST_OVERLAYFS_WRAPPERS_H__
#define _GNU_SOURCE
#include <linux/types.h>
#include <linux/mount.h>
#include <sys/syscall.h>
#ifndef STATX_MNT_ID_UNIQUE
#define STATX_MNT_ID_UNIQUE 0x00004000U /* Want/got extended stx_mount_id */
#endif
static inline int sys_fsopen(const char *fsname, unsigned int flags)
{
return syscall(__NR_fsopen, fsname, flags);
}
static inline int sys_fsconfig(int fd, unsigned int cmd, const char *key,
const char *value, int aux)
{
return syscall(__NR_fsconfig, fd, cmd, key, value, aux);
}
static inline int sys_fsmount(int fd, unsigned int flags,
unsigned int attr_flags)
{
return syscall(__NR_fsmount, fd, flags, attr_flags);
}
static inline int sys_mount(const char *src, const char *tgt, const char *fst,
unsigned long flags, const void *data)
{
return syscall(__NR_mount, src, tgt, fst, flags, data);
}
#ifndef MOVE_MOUNT_F_EMPTY_PATH
#define MOVE_MOUNT_F_EMPTY_PATH 0x00000004 /* Empty from path permitted */
#endif
#ifndef MOVE_MOUNT_T_EMPTY_PATH
#define MOVE_MOUNT_T_EMPTY_PATH 0x00000040 /* Empty to path permitted */
#endif
#ifndef __NR_move_mount
#if defined __alpha__
#define __NR_move_mount 539
#elif defined _MIPS_SIM
#if _MIPS_SIM == _MIPS_SIM_ABI32 /* o32 */
#define __NR_move_mount 4429
#endif
#if _MIPS_SIM == _MIPS_SIM_NABI32 /* n32 */
#define __NR_move_mount 6429
#endif
#if _MIPS_SIM == _MIPS_SIM_ABI64 /* n64 */
#define __NR_move_mount 5429
#endif
#else
#define __NR_move_mount 429
#endif
#endif
static inline int sys_move_mount(int from_dfd, const char *from_pathname,
int to_dfd, const char *to_pathname,
unsigned int flags)
{
return syscall(__NR_move_mount, from_dfd, from_pathname, to_dfd,
to_pathname, flags);
}
#ifndef OPEN_TREE_CLONE
#define OPEN_TREE_CLONE 1
#endif
#ifndef OPEN_TREE_CLOEXEC
#define OPEN_TREE_CLOEXEC O_CLOEXEC
#endif
#ifndef AT_RECURSIVE
#define AT_RECURSIVE 0x8000 /* Apply to the entire subtree */
#endif
#ifndef __NR_open_tree
#if defined __alpha__
#define __NR_open_tree 538
#elif defined _MIPS_SIM
#if _MIPS_SIM == _MIPS_SIM_ABI32 /* o32 */
#define __NR_open_tree 4428
#endif
#if _MIPS_SIM == _MIPS_SIM_NABI32 /* n32 */
#define __NR_open_tree 6428
#endif
#if _MIPS_SIM == _MIPS_SIM_ABI64 /* n64 */
#define __NR_open_tree 5428
#endif
#else
#define __NR_open_tree 428
#endif
#endif
static inline int sys_open_tree(int dfd, const char *filename, unsigned int flags)
{
return syscall(__NR_open_tree, dfd, filename, flags);
}
#endif
@@ -0,0 +1,3 @@
xattr_socket_test
xattr_sockfs_test
xattr_socket_types_test
@@ -0,0 +1,6 @@
# SPDX-License-Identifier: GPL-2.0
CFLAGS += $(KHDR_INCLUDES)
TEST_GEN_PROGS := xattr_socket_test xattr_sockfs_test xattr_socket_types_test
include ../../lib.mk
@@ -0,0 +1,470 @@
// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2026 Christian Brauner <brauner@kernel.org>
/*
* Test extended attributes on path-based Unix domain sockets.
*
* Path-based Unix domain sockets are bound to a filesystem path and their
* inodes live on the underlying filesystem (e.g. tmpfs). These tests verify
* that user.* and trusted.* xattr operations work correctly on them using
* path-based syscalls (setxattr, getxattr, etc.).
*
* Covers SOCK_STREAM, SOCK_DGRAM, and SOCK_SEQPACKET socket types.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/xattr.h>
#include <unistd.h>
#include "../../kselftest_harness.h"
#define TEST_XATTR_NAME "user.testattr"
#define TEST_XATTR_VALUE "testvalue"
#define TEST_XATTR_VALUE2 "newvalue"
/*
* Fixture for path-based Unix domain socket tests.
* Creates a SOCK_STREAM socket bound to a path in /tmp (typically tmpfs).
*/
FIXTURE(xattr_socket)
{
char socket_path[PATH_MAX];
int sockfd;
};
FIXTURE_VARIANT(xattr_socket)
{
int sock_type;
const char *name;
};
FIXTURE_VARIANT_ADD(xattr_socket, stream) {
.sock_type = SOCK_STREAM,
.name = "stream",
};
FIXTURE_VARIANT_ADD(xattr_socket, dgram) {
.sock_type = SOCK_DGRAM,
.name = "dgram",
};
FIXTURE_VARIANT_ADD(xattr_socket, seqpacket) {
.sock_type = SOCK_SEQPACKET,
.name = "seqpacket",
};
FIXTURE_SETUP(xattr_socket)
{
struct sockaddr_un addr;
int ret;
self->sockfd = -1;
snprintf(self->socket_path, sizeof(self->socket_path),
"/tmp/xattr_socket_test_%s.%d", variant->name, getpid());
unlink(self->socket_path);
self->sockfd = socket(AF_UNIX, variant->sock_type, 0);
ASSERT_GE(self->sockfd, 0) {
TH_LOG("Failed to create socket: %s", strerror(errno));
}
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, self->socket_path, sizeof(addr.sun_path) - 1);
ret = bind(self->sockfd, (struct sockaddr *)&addr, sizeof(addr));
ASSERT_EQ(ret, 0) {
TH_LOG("Failed to bind socket to %s: %s",
self->socket_path, strerror(errno));
}
}
FIXTURE_TEARDOWN(xattr_socket)
{
if (self->sockfd >= 0)
close(self->sockfd);
unlink(self->socket_path);
}
TEST_F(xattr_socket, set_user_xattr)
{
int ret;
ret = setxattr(self->socket_path, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0) {
TH_LOG("setxattr failed: %s (errno=%d)", strerror(errno), errno);
}
}
TEST_F(xattr_socket, get_user_xattr)
{
char buf[256];
ssize_t ret;
ret = setxattr(self->socket_path, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0) {
TH_LOG("setxattr failed: %s", strerror(errno));
}
memset(buf, 0, sizeof(buf));
ret = getxattr(self->socket_path, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE)) {
TH_LOG("getxattr returned %zd, expected %zu: %s",
ret, strlen(TEST_XATTR_VALUE), strerror(errno));
}
ASSERT_STREQ(buf, TEST_XATTR_VALUE);
}
TEST_F(xattr_socket, list_user_xattr)
{
char list[1024];
ssize_t ret;
bool found = false;
char *ptr;
ret = setxattr(self->socket_path, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0) {
TH_LOG("setxattr failed: %s", strerror(errno));
}
memset(list, 0, sizeof(list));
ret = listxattr(self->socket_path, list, sizeof(list));
ASSERT_GT(ret, 0) {
TH_LOG("listxattr failed: %s", strerror(errno));
}
for (ptr = list; ptr < list + ret; ptr += strlen(ptr) + 1) {
if (strcmp(ptr, TEST_XATTR_NAME) == 0) {
found = true;
break;
}
}
ASSERT_TRUE(found) {
TH_LOG("xattr %s not found in list", TEST_XATTR_NAME);
}
}
TEST_F(xattr_socket, remove_user_xattr)
{
char buf[256];
ssize_t ret;
ret = setxattr(self->socket_path, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0) {
TH_LOG("setxattr failed: %s", strerror(errno));
}
ret = removexattr(self->socket_path, TEST_XATTR_NAME);
ASSERT_EQ(ret, 0) {
TH_LOG("removexattr failed: %s", strerror(errno));
}
ret = getxattr(self->socket_path, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, ENODATA) {
TH_LOG("Expected ENODATA, got %s", strerror(errno));
}
}
/*
* Test that xattrs persist across socket close and reopen.
* The xattr is on the filesystem inode, not the socket fd.
*/
TEST_F(xattr_socket, xattr_persistence)
{
char buf[256];
ssize_t ret;
ret = setxattr(self->socket_path, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0) {
TH_LOG("setxattr failed: %s", strerror(errno));
}
close(self->sockfd);
self->sockfd = -1;
memset(buf, 0, sizeof(buf));
ret = getxattr(self->socket_path, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE)) {
TH_LOG("getxattr after close failed: %s", strerror(errno));
}
ASSERT_STREQ(buf, TEST_XATTR_VALUE);
}
TEST_F(xattr_socket, update_user_xattr)
{
char buf[256];
ssize_t ret;
ret = setxattr(self->socket_path, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0);
ret = setxattr(self->socket_path, TEST_XATTR_NAME,
TEST_XATTR_VALUE2, strlen(TEST_XATTR_VALUE2), 0);
ASSERT_EQ(ret, 0);
memset(buf, 0, sizeof(buf));
ret = getxattr(self->socket_path, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE2));
ASSERT_STREQ(buf, TEST_XATTR_VALUE2);
}
TEST_F(xattr_socket, xattr_create_flag)
{
int ret;
ret = setxattr(self->socket_path, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0);
ret = setxattr(self->socket_path, TEST_XATTR_NAME,
TEST_XATTR_VALUE2, strlen(TEST_XATTR_VALUE2), XATTR_CREATE);
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, EEXIST);
}
TEST_F(xattr_socket, xattr_replace_flag)
{
int ret;
ret = setxattr(self->socket_path, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), XATTR_REPLACE);
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, ENODATA);
}
TEST_F(xattr_socket, multiple_xattrs)
{
char buf[256];
ssize_t ret;
int i;
char name[64], value[64];
const int num_xattrs = 5;
for (i = 0; i < num_xattrs; i++) {
snprintf(name, sizeof(name), "user.test%d", i);
snprintf(value, sizeof(value), "value%d", i);
ret = setxattr(self->socket_path, name, value, strlen(value), 0);
ASSERT_EQ(ret, 0) {
TH_LOG("setxattr %s failed: %s", name, strerror(errno));
}
}
for (i = 0; i < num_xattrs; i++) {
snprintf(name, sizeof(name), "user.test%d", i);
snprintf(value, sizeof(value), "value%d", i);
memset(buf, 0, sizeof(buf));
ret = getxattr(self->socket_path, name, buf, sizeof(buf));
ASSERT_EQ(ret, (ssize_t)strlen(value));
ASSERT_STREQ(buf, value);
}
}
TEST_F(xattr_socket, xattr_empty_value)
{
char buf[256];
ssize_t ret;
ret = setxattr(self->socket_path, TEST_XATTR_NAME, "", 0, 0);
ASSERT_EQ(ret, 0);
ret = getxattr(self->socket_path, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, 0);
}
TEST_F(xattr_socket, xattr_get_size)
{
ssize_t ret;
ret = setxattr(self->socket_path, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0);
ret = getxattr(self->socket_path, TEST_XATTR_NAME, NULL, 0);
ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE));
}
TEST_F(xattr_socket, xattr_buffer_too_small)
{
char buf[2];
ssize_t ret;
ret = setxattr(self->socket_path, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0);
ret = getxattr(self->socket_path, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, ERANGE);
}
TEST_F(xattr_socket, xattr_nonexistent)
{
char buf[256];
ssize_t ret;
ret = getxattr(self->socket_path, "user.nonexistent", buf, sizeof(buf));
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, ENODATA);
}
TEST_F(xattr_socket, remove_nonexistent_xattr)
{
int ret;
ret = removexattr(self->socket_path, "user.nonexistent");
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, ENODATA);
}
TEST_F(xattr_socket, large_xattr_value)
{
char large_value[4096];
char read_buf[4096];
ssize_t ret;
memset(large_value, 'A', sizeof(large_value));
ret = setxattr(self->socket_path, TEST_XATTR_NAME,
large_value, sizeof(large_value), 0);
ASSERT_EQ(ret, 0) {
TH_LOG("setxattr with large value failed: %s", strerror(errno));
}
memset(read_buf, 0, sizeof(read_buf));
ret = getxattr(self->socket_path, TEST_XATTR_NAME,
read_buf, sizeof(read_buf));
ASSERT_EQ(ret, (ssize_t)sizeof(large_value));
ASSERT_EQ(memcmp(large_value, read_buf, sizeof(large_value)), 0);
}
/*
* Test lsetxattr/lgetxattr (don't follow symlinks).
* Socket files aren't symlinks, so this should work the same.
*/
TEST_F(xattr_socket, lsetxattr_lgetxattr)
{
char buf[256];
ssize_t ret;
ret = lsetxattr(self->socket_path, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0) {
TH_LOG("lsetxattr failed: %s", strerror(errno));
}
memset(buf, 0, sizeof(buf));
ret = lgetxattr(self->socket_path, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE));
ASSERT_STREQ(buf, TEST_XATTR_VALUE);
}
/*
* Fixture for trusted.* xattr tests.
* These require CAP_SYS_ADMIN.
*/
FIXTURE(xattr_socket_trusted)
{
char socket_path[PATH_MAX];
int sockfd;
};
FIXTURE_VARIANT(xattr_socket_trusted)
{
int sock_type;
const char *name;
};
FIXTURE_VARIANT_ADD(xattr_socket_trusted, stream) {
.sock_type = SOCK_STREAM,
.name = "stream",
};
FIXTURE_VARIANT_ADD(xattr_socket_trusted, dgram) {
.sock_type = SOCK_DGRAM,
.name = "dgram",
};
FIXTURE_VARIANT_ADD(xattr_socket_trusted, seqpacket) {
.sock_type = SOCK_SEQPACKET,
.name = "seqpacket",
};
FIXTURE_SETUP(xattr_socket_trusted)
{
struct sockaddr_un addr;
int ret;
self->sockfd = -1;
snprintf(self->socket_path, sizeof(self->socket_path),
"/tmp/xattr_socket_trusted_%s.%d", variant->name, getpid());
unlink(self->socket_path);
self->sockfd = socket(AF_UNIX, variant->sock_type, 0);
ASSERT_GE(self->sockfd, 0);
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, self->socket_path, sizeof(addr.sun_path) - 1);
ret = bind(self->sockfd, (struct sockaddr *)&addr, sizeof(addr));
ASSERT_EQ(ret, 0);
}
FIXTURE_TEARDOWN(xattr_socket_trusted)
{
if (self->sockfd >= 0)
close(self->sockfd);
unlink(self->socket_path);
}
TEST_F(xattr_socket_trusted, set_trusted_xattr)
{
char buf[256];
ssize_t len;
int ret;
ret = setxattr(self->socket_path, "trusted.testattr",
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
if (ret == -1 && errno == EPERM)
SKIP(return, "Need CAP_SYS_ADMIN for trusted.* xattrs");
ASSERT_EQ(ret, 0) {
TH_LOG("setxattr trusted.testattr failed: %s", strerror(errno));
}
memset(buf, 0, sizeof(buf));
len = getxattr(self->socket_path, "trusted.testattr",
buf, sizeof(buf));
ASSERT_EQ(len, (ssize_t)strlen(TEST_XATTR_VALUE));
ASSERT_STREQ(buf, TEST_XATTR_VALUE);
}
TEST_F(xattr_socket_trusted, get_trusted_xattr_unprivileged)
{
char buf[256];
ssize_t ret;
ret = getxattr(self->socket_path, "trusted.testattr", buf, sizeof(buf));
ASSERT_EQ(ret, -1);
ASSERT_TRUE(errno == ENODATA || errno == EPERM) {
TH_LOG("Expected ENODATA or EPERM, got %s", strerror(errno));
}
}
TEST_HARNESS_MAIN
@@ -0,0 +1,177 @@
// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2026 Christian Brauner <brauner@kernel.org>
/*
* Test user.* xattrs on various socket families.
*
* All socket types use sockfs for their inodes, so user.* xattrs should
* work on any socket regardless of address family. This tests AF_INET,
* AF_INET6, AF_NETLINK, AF_PACKET, and abstract namespace AF_UNIX sockets.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/xattr.h>
#include <linux/netlink.h>
#include <unistd.h>
#include "../../kselftest_harness.h"
#define TEST_XATTR_NAME "user.testattr"
#define TEST_XATTR_VALUE "testvalue"
FIXTURE(xattr_socket_types)
{
int sockfd;
};
FIXTURE_VARIANT(xattr_socket_types)
{
int family;
int type;
int protocol;
};
FIXTURE_VARIANT_ADD(xattr_socket_types, inet) {
.family = AF_INET,
.type = SOCK_STREAM,
.protocol = 0,
};
FIXTURE_VARIANT_ADD(xattr_socket_types, inet6) {
.family = AF_INET6,
.type = SOCK_STREAM,
.protocol = 0,
};
FIXTURE_VARIANT_ADD(xattr_socket_types, netlink) {
.family = AF_NETLINK,
.type = SOCK_RAW,
.protocol = NETLINK_USERSOCK,
};
FIXTURE_VARIANT_ADD(xattr_socket_types, packet) {
.family = AF_PACKET,
.type = SOCK_DGRAM,
.protocol = 0,
};
FIXTURE_SETUP(xattr_socket_types)
{
self->sockfd = socket(variant->family, variant->type,
variant->protocol);
if (self->sockfd < 0 &&
(errno == EAFNOSUPPORT || errno == EPERM || errno == EACCES))
SKIP(return, "socket(%d, %d, %d) not available: %s",
variant->family, variant->type, variant->protocol,
strerror(errno));
ASSERT_GE(self->sockfd, 0) {
TH_LOG("Failed to create socket(%d, %d, %d): %s",
variant->family, variant->type, variant->protocol,
strerror(errno));
}
}
FIXTURE_TEARDOWN(xattr_socket_types)
{
if (self->sockfd >= 0)
close(self->sockfd);
}
TEST_F(xattr_socket_types, set_get_list_remove)
{
char buf[256], list[4096], *ptr;
ssize_t ret;
bool found;
ret = fsetxattr(self->sockfd, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0) {
TH_LOG("fsetxattr failed: %s", strerror(errno));
}
memset(buf, 0, sizeof(buf));
ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE));
ASSERT_STREQ(buf, TEST_XATTR_VALUE);
memset(list, 0, sizeof(list));
ret = flistxattr(self->sockfd, list, sizeof(list));
ASSERT_GT(ret, 0);
found = false;
for (ptr = list; ptr < list + ret; ptr += strlen(ptr) + 1) {
if (strcmp(ptr, TEST_XATTR_NAME) == 0)
found = true;
}
ASSERT_TRUE(found);
ret = fremovexattr(self->sockfd, TEST_XATTR_NAME);
ASSERT_EQ(ret, 0);
ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, ENODATA);
}
/*
* Test abstract namespace AF_UNIX socket.
* Abstract sockets don't have a filesystem path; their inodes live in
* sockfs so user.* xattrs should work via fsetxattr/fgetxattr.
*/
FIXTURE(xattr_abstract)
{
int sockfd;
};
FIXTURE_SETUP(xattr_abstract)
{
struct sockaddr_un addr;
char name[64];
int ret, len;
self->sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_GE(self->sockfd, 0);
len = snprintf(name, sizeof(name), "xattr_test_abstract_%d", getpid());
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
addr.sun_path[0] = '\0';
memcpy(&addr.sun_path[1], name, len);
ret = bind(self->sockfd, (struct sockaddr *)&addr,
offsetof(struct sockaddr_un, sun_path) + 1 + len);
ASSERT_EQ(ret, 0);
}
FIXTURE_TEARDOWN(xattr_abstract)
{
if (self->sockfd >= 0)
close(self->sockfd);
}
TEST_F(xattr_abstract, set_get)
{
char buf[256];
ssize_t ret;
ret = fsetxattr(self->sockfd, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0) {
TH_LOG("fsetxattr on abstract socket failed: %s",
strerror(errno));
}
memset(buf, 0, sizeof(buf));
ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE));
ASSERT_STREQ(buf, TEST_XATTR_VALUE);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,363 @@
// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2026 Christian Brauner <brauner@kernel.org>
/*
* Test extended attributes on sockfs sockets.
*
* Sockets created via socket() have their inodes in sockfs, which supports
* user.* xattrs with per-inode limits: up to 128 xattrs and 128KB total
* value size. These tests verify xattr operations via fsetxattr/fgetxattr/
* flistxattr/fremovexattr on the socket fd, as well as limit enforcement.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <unistd.h>
#include "../../kselftest_harness.h"
#define TEST_XATTR_NAME "user.testattr"
#define TEST_XATTR_VALUE "testvalue"
#define TEST_XATTR_VALUE2 "newvalue"
/* Per-inode limits for user.* xattrs on sockfs (from include/linux/xattr.h) */
#define SIMPLE_XATTR_MAX_NR 128
#define SIMPLE_XATTR_MAX_SIZE (128 << 10) /* 128 KB */
#ifndef XATTR_SIZE_MAX
#define XATTR_SIZE_MAX 65536
#endif
/*
* Fixture for sockfs socket xattr tests.
* Creates an AF_UNIX socket (lives in sockfs, not bound to any path).
*/
FIXTURE(xattr_sockfs)
{
int sockfd;
};
FIXTURE_SETUP(xattr_sockfs)
{
self->sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_GE(self->sockfd, 0) {
TH_LOG("Failed to create socket: %s", strerror(errno));
}
}
FIXTURE_TEARDOWN(xattr_sockfs)
{
if (self->sockfd >= 0)
close(self->sockfd);
}
TEST_F(xattr_sockfs, set_get_user_xattr)
{
char buf[256];
ssize_t ret;
ret = fsetxattr(self->sockfd, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0) {
TH_LOG("fsetxattr failed: %s", strerror(errno));
}
memset(buf, 0, sizeof(buf));
ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE)) {
TH_LOG("fgetxattr returned %zd: %s", ret, strerror(errno));
}
ASSERT_STREQ(buf, TEST_XATTR_VALUE);
}
/*
* Test listing xattrs on a sockfs socket.
* Should include user.* xattrs and system.sockprotoname.
*/
TEST_F(xattr_sockfs, list_user_xattr)
{
char list[4096];
ssize_t ret;
char *ptr;
bool found_user = false;
bool found_proto = false;
ret = fsetxattr(self->sockfd, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0) {
TH_LOG("fsetxattr failed: %s", strerror(errno));
}
memset(list, 0, sizeof(list));
ret = flistxattr(self->sockfd, list, sizeof(list));
ASSERT_GT(ret, 0) {
TH_LOG("flistxattr failed: %s", strerror(errno));
}
for (ptr = list; ptr < list + ret; ptr += strlen(ptr) + 1) {
if (strcmp(ptr, TEST_XATTR_NAME) == 0)
found_user = true;
if (strcmp(ptr, "system.sockprotoname") == 0)
found_proto = true;
}
ASSERT_TRUE(found_user) {
TH_LOG("user xattr not found in list");
}
ASSERT_TRUE(found_proto) {
TH_LOG("system.sockprotoname not found in list");
}
}
TEST_F(xattr_sockfs, remove_user_xattr)
{
char buf[256];
ssize_t ret;
ret = fsetxattr(self->sockfd, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0);
ret = fremovexattr(self->sockfd, TEST_XATTR_NAME);
ASSERT_EQ(ret, 0) {
TH_LOG("fremovexattr failed: %s", strerror(errno));
}
ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, ENODATA);
}
TEST_F(xattr_sockfs, update_user_xattr)
{
char buf[256];
ssize_t ret;
ret = fsetxattr(self->sockfd, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0);
ret = fsetxattr(self->sockfd, TEST_XATTR_NAME,
TEST_XATTR_VALUE2, strlen(TEST_XATTR_VALUE2), 0);
ASSERT_EQ(ret, 0);
memset(buf, 0, sizeof(buf));
ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE2));
ASSERT_STREQ(buf, TEST_XATTR_VALUE2);
}
TEST_F(xattr_sockfs, xattr_create_flag)
{
int ret;
ret = fsetxattr(self->sockfd, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0);
ret = fsetxattr(self->sockfd, TEST_XATTR_NAME,
TEST_XATTR_VALUE2, strlen(TEST_XATTR_VALUE2),
XATTR_CREATE);
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, EEXIST);
}
TEST_F(xattr_sockfs, xattr_replace_flag)
{
int ret;
ret = fsetxattr(self->sockfd, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE),
XATTR_REPLACE);
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, ENODATA);
}
TEST_F(xattr_sockfs, get_nonexistent)
{
char buf[256];
ssize_t ret;
ret = fgetxattr(self->sockfd, "user.nonexistent", buf, sizeof(buf));
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, ENODATA);
}
TEST_F(xattr_sockfs, empty_value)
{
ssize_t ret;
ret = fsetxattr(self->sockfd, TEST_XATTR_NAME, "", 0, 0);
ASSERT_EQ(ret, 0);
ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, NULL, 0);
ASSERT_EQ(ret, 0);
}
TEST_F(xattr_sockfs, get_size)
{
ssize_t ret;
ret = fsetxattr(self->sockfd, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0);
ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, NULL, 0);
ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE));
}
TEST_F(xattr_sockfs, buffer_too_small)
{
char buf[2];
ssize_t ret;
ret = fsetxattr(self->sockfd, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0);
ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, ERANGE);
}
/*
* Test maximum number of user.* xattrs per socket.
* The kernel enforces SIMPLE_XATTR_MAX_NR (128), so the 129th should
* fail with ENOSPC.
*/
TEST_F(xattr_sockfs, max_nr_xattrs)
{
char name[32];
int i, ret;
for (i = 0; i < SIMPLE_XATTR_MAX_NR; i++) {
snprintf(name, sizeof(name), "user.test%03d", i);
ret = fsetxattr(self->sockfd, name, "v", 1, 0);
ASSERT_EQ(ret, 0) {
TH_LOG("fsetxattr %s failed at i=%d: %s",
name, i, strerror(errno));
}
}
ret = fsetxattr(self->sockfd, "user.overflow", "v", 1, 0);
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, ENOSPC) {
TH_LOG("Expected ENOSPC for xattr %d, got %s",
SIMPLE_XATTR_MAX_NR + 1, strerror(errno));
}
}
/*
* Test maximum total value size for user.* xattrs.
* The kernel enforces SIMPLE_XATTR_MAX_SIZE (128KB). Individual xattr
* values are limited to XATTR_SIZE_MAX (64KB) by the VFS, so we need
* at least two xattrs to hit the total limit.
*/
TEST_F(xattr_sockfs, max_xattr_size)
{
char *value;
int ret;
value = malloc(XATTR_SIZE_MAX);
ASSERT_NE(value, NULL);
memset(value, 'A', XATTR_SIZE_MAX);
/* First 64KB xattr - total = 64KB */
ret = fsetxattr(self->sockfd, "user.big1", value, XATTR_SIZE_MAX, 0);
ASSERT_EQ(ret, 0) {
TH_LOG("first large xattr failed: %s", strerror(errno));
}
/* Second 64KB xattr - total = 128KB (exactly at limit) */
ret = fsetxattr(self->sockfd, "user.big2", value, XATTR_SIZE_MAX, 0);
free(value);
ASSERT_EQ(ret, 0) {
TH_LOG("second large xattr failed: %s", strerror(errno));
}
/* Third xattr with 1 byte - total > 128KB, should fail */
ret = fsetxattr(self->sockfd, "user.big3", "v", 1, 0);
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, ENOSPC) {
TH_LOG("Expected ENOSPC when exceeding size limit, got %s",
strerror(errno));
}
}
/*
* Test that removing an xattr frees limit space, allowing re-addition.
*/
TEST_F(xattr_sockfs, limit_remove_readd)
{
char name[32];
int i, ret;
/* Fill up to the maximum count */
for (i = 0; i < SIMPLE_XATTR_MAX_NR; i++) {
snprintf(name, sizeof(name), "user.test%03d", i);
ret = fsetxattr(self->sockfd, name, "v", 1, 0);
ASSERT_EQ(ret, 0);
}
/* Verify we're at the limit */
ret = fsetxattr(self->sockfd, "user.overflow", "v", 1, 0);
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, ENOSPC);
/* Remove one xattr */
ret = fremovexattr(self->sockfd, "user.test000");
ASSERT_EQ(ret, 0);
/* Now we should be able to add one more */
ret = fsetxattr(self->sockfd, "user.newattr", "v", 1, 0);
ASSERT_EQ(ret, 0) {
TH_LOG("re-add after remove failed: %s", strerror(errno));
}
}
/*
* Test that two different sockets have independent xattr limits.
*/
TEST_F(xattr_sockfs, limits_per_inode)
{
char buf[256];
int sock2;
ssize_t ret;
sock2 = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_GE(sock2, 0);
/* Set xattr on first socket */
ret = fsetxattr(self->sockfd, TEST_XATTR_NAME,
TEST_XATTR_VALUE, strlen(TEST_XATTR_VALUE), 0);
ASSERT_EQ(ret, 0);
/* First socket's xattr should not be visible on second socket */
ret = fgetxattr(sock2, TEST_XATTR_NAME, NULL, 0);
ASSERT_EQ(ret, -1);
ASSERT_EQ(errno, ENODATA);
/* Second socket should independently accept xattrs */
ret = fsetxattr(sock2, TEST_XATTR_NAME,
TEST_XATTR_VALUE2, strlen(TEST_XATTR_VALUE2), 0);
ASSERT_EQ(ret, 0);
/* Verify each socket has its own value */
memset(buf, 0, sizeof(buf));
ret = fgetxattr(self->sockfd, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE));
ASSERT_STREQ(buf, TEST_XATTR_VALUE);
memset(buf, 0, sizeof(buf));
ret = fgetxattr(sock2, TEST_XATTR_NAME, buf, sizeof(buf));
ASSERT_EQ(ret, (ssize_t)strlen(TEST_XATTR_VALUE2));
ASSERT_STREQ(buf, TEST_XATTR_VALUE2);
close(sock2);
}
TEST_HARNESS_MAIN