376 lines
6.8 KiB
C
376 lines
6.8 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
#define _GNU_SOURCE
|
|
#include <sched.h>
|
|
#include <stdio.h>
|
|
#include <errno.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/mount.h>
|
|
#include <sys/wait.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <stdbool.h>
|
|
#include <stdarg.h>
|
|
#include <sys/syscall.h>
|
|
|
|
#include "../kselftest_harness.h"
|
|
|
|
#ifndef CLONE_NEWNS
|
|
#define CLONE_NEWNS 0x00020000
|
|
#endif
|
|
|
|
#ifndef CLONE_NEWUSER
|
|
#define CLONE_NEWUSER 0x10000000
|
|
#endif
|
|
|
|
#ifndef MS_SHARED
|
|
#define MS_SHARED (1 << 20)
|
|
#endif
|
|
|
|
#ifndef MS_PRIVATE
|
|
#define MS_PRIVATE (1<<18)
|
|
#endif
|
|
|
|
#ifndef MOVE_MOUNT_SET_GROUP
|
|
#define MOVE_MOUNT_SET_GROUP 0x00000100
|
|
#endif
|
|
|
|
#ifndef MOVE_MOUNT_F_EMPTY_PATH
|
|
#define MOVE_MOUNT_F_EMPTY_PATH 0x00000004
|
|
#endif
|
|
|
|
#ifndef MOVE_MOUNT_T_EMPTY_PATH
|
|
#define MOVE_MOUNT_T_EMPTY_PATH 0x00000040
|
|
#endif
|
|
|
|
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;
|
|
}
|
|
|
|
static int write_file(const char *path, const void *buf, size_t count)
|
|
{
|
|
int fd;
|
|
ssize_t ret;
|
|
|
|
fd = open(path, O_WRONLY | O_CLOEXEC | O_NOCTTY | O_NOFOLLOW);
|
|
if (fd < 0)
|
|
return -1;
|
|
|
|
ret = write_nointr(fd, buf, count);
|
|
close(fd);
|
|
if (ret < 0 || (size_t)ret != count)
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int create_and_enter_userns(void)
|
|
{
|
|
uid_t uid;
|
|
gid_t gid;
|
|
char map[100];
|
|
|
|
uid = getuid();
|
|
gid = getgid();
|
|
|
|
if (unshare(CLONE_NEWUSER))
|
|
return -1;
|
|
|
|
if (write_file("/proc/self/setgroups", "deny", sizeof("deny") - 1) &&
|
|
errno != ENOENT)
|
|
return -1;
|
|
|
|
snprintf(map, sizeof(map), "0 %d 1", uid);
|
|
if (write_file("/proc/self/uid_map", map, strlen(map)))
|
|
return -1;
|
|
|
|
|
|
snprintf(map, sizeof(map), "0 %d 1", gid);
|
|
if (write_file("/proc/self/gid_map", map, strlen(map)))
|
|
return -1;
|
|
|
|
if (setgid(0))
|
|
return -1;
|
|
|
|
if (setuid(0))
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int prepare_unpriv_mountns(void)
|
|
{
|
|
if (create_and_enter_userns())
|
|
return -1;
|
|
|
|
if (unshare(CLONE_NEWNS))
|
|
return -1;
|
|
|
|
if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, 0))
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static char *get_field(char *src, int nfields)
|
|
{
|
|
int i;
|
|
char *p = src;
|
|
|
|
for (i = 0; i < nfields; i++) {
|
|
while (*p && *p != ' ' && *p != '\t')
|
|
p++;
|
|
|
|
if (!*p)
|
|
break;
|
|
|
|
p++;
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
static void null_endofword(char *word)
|
|
{
|
|
while (*word && *word != ' ' && *word != '\t')
|
|
word++;
|
|
*word = '\0';
|
|
}
|
|
|
|
static bool is_shared_mount(const char *path)
|
|
{
|
|
size_t len = 0;
|
|
char *line = NULL;
|
|
FILE *f = NULL;
|
|
|
|
f = fopen("/proc/self/mountinfo", "re");
|
|
if (!f)
|
|
return false;
|
|
|
|
while (getline(&line, &len, f) != -1) {
|
|
char *opts, *target;
|
|
|
|
target = get_field(line, 4);
|
|
if (!target)
|
|
continue;
|
|
|
|
opts = get_field(target, 2);
|
|
if (!opts)
|
|
continue;
|
|
|
|
null_endofword(target);
|
|
|
|
if (strcmp(target, path) != 0)
|
|
continue;
|
|
|
|
null_endofword(opts);
|
|
if (strstr(opts, "shared:"))
|
|
return true;
|
|
}
|
|
|
|
free(line);
|
|
fclose(f);
|
|
|
|
return false;
|
|
}
|
|
|
|
/* Attempt to de-conflict with the selftests tree. */
|
|
#ifndef SKIP
|
|
#define SKIP(s, ...) XFAIL(s, ##__VA_ARGS__)
|
|
#endif
|
|
|
|
#define SET_GROUP_FROM "/tmp/move_mount_set_group_supported_from"
|
|
#define SET_GROUP_TO "/tmp/move_mount_set_group_supported_to"
|
|
|
|
static bool move_mount_set_group_supported(void)
|
|
{
|
|
int ret;
|
|
|
|
if (mount("testing", "/tmp", "tmpfs", MS_NOATIME | MS_NODEV,
|
|
"size=100000,mode=700"))
|
|
return -1;
|
|
|
|
if (mount(NULL, "/tmp", NULL, MS_PRIVATE, 0))
|
|
return -1;
|
|
|
|
if (mkdir(SET_GROUP_FROM, 0777))
|
|
return -1;
|
|
|
|
if (mkdir(SET_GROUP_TO, 0777))
|
|
return -1;
|
|
|
|
if (mount("testing", SET_GROUP_FROM, "tmpfs", MS_NOATIME | MS_NODEV,
|
|
"size=100000,mode=700"))
|
|
return -1;
|
|
|
|
if (mount(SET_GROUP_FROM, SET_GROUP_TO, NULL, MS_BIND, NULL))
|
|
return -1;
|
|
|
|
if (mount(NULL, SET_GROUP_FROM, NULL, MS_SHARED, 0))
|
|
return -1;
|
|
|
|
ret = syscall(__NR_move_mount, AT_FDCWD, SET_GROUP_FROM,
|
|
AT_FDCWD, SET_GROUP_TO, MOVE_MOUNT_SET_GROUP);
|
|
umount2("/tmp", MNT_DETACH);
|
|
|
|
return ret >= 0;
|
|
}
|
|
|
|
FIXTURE(move_mount_set_group) {
|
|
};
|
|
|
|
#define SET_GROUP_A "/tmp/A"
|
|
|
|
FIXTURE_SETUP(move_mount_set_group)
|
|
{
|
|
bool ret;
|
|
|
|
ASSERT_EQ(prepare_unpriv_mountns(), 0);
|
|
|
|
ret = move_mount_set_group_supported();
|
|
ASSERT_GE(ret, 0);
|
|
if (!ret)
|
|
SKIP(return, "move_mount(MOVE_MOUNT_SET_GROUP) is not supported");
|
|
|
|
umount2("/tmp", MNT_DETACH);
|
|
|
|
ASSERT_EQ(mount("testing", "/tmp", "tmpfs", MS_NOATIME | MS_NODEV,
|
|
"size=100000,mode=700"), 0);
|
|
|
|
ASSERT_EQ(mkdir(SET_GROUP_A, 0777), 0);
|
|
|
|
ASSERT_EQ(mount("testing", SET_GROUP_A, "tmpfs", MS_NOATIME | MS_NODEV,
|
|
"size=100000,mode=700"), 0);
|
|
}
|
|
|
|
FIXTURE_TEARDOWN(move_mount_set_group)
|
|
{
|
|
bool ret;
|
|
|
|
ret = move_mount_set_group_supported();
|
|
ASSERT_GE(ret, 0);
|
|
if (!ret)
|
|
SKIP(return, "move_mount(MOVE_MOUNT_SET_GROUP) is not supported");
|
|
|
|
umount2("/tmp", MNT_DETACH);
|
|
}
|
|
|
|
#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 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);
|
|
}
|
|
|
|
struct child_args {
|
|
int unsfd;
|
|
int mntnsfd;
|
|
bool shared;
|
|
int mntfd;
|
|
};
|
|
|
|
static int get_nestedns_mount_cb(void *data)
|
|
{
|
|
struct child_args *ca = (struct child_args *)data;
|
|
int ret;
|
|
|
|
ret = prepare_unpriv_mountns();
|
|
if (ret)
|
|
return 1;
|
|
|
|
if (ca->shared) {
|
|
ret = mount(NULL, SET_GROUP_A, NULL, MS_SHARED, 0);
|
|
if (ret)
|
|
return 1;
|
|
}
|
|
|
|
ret = open("/proc/self/ns/user", O_RDONLY);
|
|
if (ret < 0)
|
|
return 1;
|
|
ca->unsfd = ret;
|
|
|
|
ret = open("/proc/self/ns/mnt", O_RDONLY);
|
|
if (ret < 0)
|
|
return 1;
|
|
ca->mntnsfd = ret;
|
|
|
|
ret = open(SET_GROUP_A, O_RDONLY);
|
|
if (ret < 0)
|
|
return 1;
|
|
ca->mntfd = ret;
|
|
|
|
return 0;
|
|
}
|
|
|
|
TEST_F(move_mount_set_group, complex_sharing_copying)
|
|
{
|
|
struct child_args ca_from = {
|
|
.shared = true,
|
|
};
|
|
struct child_args ca_to = {
|
|
.shared = false,
|
|
};
|
|
pid_t pid;
|
|
bool ret;
|
|
|
|
ret = move_mount_set_group_supported();
|
|
ASSERT_GE(ret, 0);
|
|
if (!ret)
|
|
SKIP(return, "move_mount(MOVE_MOUNT_SET_GROUP) is not supported");
|
|
|
|
pid = do_clone(get_nestedns_mount_cb, (void *)&ca_from, CLONE_VFORK |
|
|
CLONE_VM | CLONE_FILES); ASSERT_GT(pid, 0);
|
|
ASSERT_EQ(wait_for_pid(pid), 0);
|
|
|
|
pid = do_clone(get_nestedns_mount_cb, (void *)&ca_to, CLONE_VFORK |
|
|
CLONE_VM | CLONE_FILES); ASSERT_GT(pid, 0);
|
|
ASSERT_EQ(wait_for_pid(pid), 0);
|
|
|
|
ASSERT_EQ(syscall(__NR_move_mount, ca_from.mntfd, "",
|
|
ca_to.mntfd, "", MOVE_MOUNT_SET_GROUP
|
|
| MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH),
|
|
0);
|
|
|
|
ASSERT_EQ(setns(ca_to.mntnsfd, CLONE_NEWNS), 0);
|
|
ASSERT_EQ(is_shared_mount(SET_GROUP_A), 1);
|
|
}
|
|
|
|
TEST_HARNESS_MAIN
|