mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
User namespace entries under `/proc/[pid]/ns` currently render as fake namespace symlinks. They look like the other namespace files, but opening them does not produce an `nsfs` file that `setns(2)` can use. Rootless container tools such as `buildah` and `podman` rely on that file when they re-enter the pause process user namespace, so the second lifecycle command fails with `EINVAL`. Make `UserNamespace` implement `vfs.Namespace` and give each user namespace an `nsfs` inode when it is created. `/proc/[pid]/ns/user` now uses the regular namespace symlink path, so opening it returns a joinable namespace file instead of a fake link target. `Setns` now accepts `CLONE_NEWUSER` from both `nsfd`s and `pidfd`s. It follows the Linux restrictions for user namespace joins by rejecting the caller's current user namespace, requiring `CAP_SYS_ADMIN` in the target user namespace, rejecting multithreaded callers, and rejecting callers with `fs` state shared outside the thread group. The capability checks for any other namespaces in the same `setns` call use the credentials the caller would have after joining the user namespace. Add a syscall regression test that creates a child user namespace, opens `/proc/<pid>/ns/user`, and verifies that `setns(CLONE_NEWUSER)` succeeds. Fixes https://github.com/google/gvisor/issues/13314 COPYBARA_INTEGRATE_REVIEW=https://github.com/google/gvisor/pull/13323 from shayonj:issue-13314-userns-setns 8060b5f91ba1f2e9c7b6746da233d644728be2e9 PiperOrigin-RevId: 926855389
339 lines
11 KiB
C++
339 lines
11 KiB
C++
// Copyright 2023 The gVisor Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include <limits.h>
|
|
#include <linux/prctl.h>
|
|
#include <sched.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <sys/prctl.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include <cerrno>
|
|
#include <cstdint>
|
|
#include <utility>
|
|
|
|
#include "gmock/gmock.h"
|
|
#include "gtest/gtest.h"
|
|
#include "absl/time/clock.h"
|
|
#include "absl/time/time.h"
|
|
#include "test/util/cleanup.h"
|
|
#include "test/util/file_descriptor.h"
|
|
#include "test/util/linux_capability_util.h"
|
|
#include "test/util/logging.h"
|
|
#include "test/util/multiprocess_util.h"
|
|
#include "test/util/posix_error.h"
|
|
#include "test/util/test_util.h"
|
|
|
|
namespace gvisor {
|
|
namespace testing {
|
|
namespace {
|
|
|
|
struct UserNamespaceChild {
|
|
FileDescriptor nsfd;
|
|
Cleanup cleanup;
|
|
};
|
|
|
|
UserNamespaceChild CreateUserNamespaceChild() {
|
|
int pfd[2];
|
|
TEST_PCHECK(pipe(pfd) == 0);
|
|
FileDescriptor pipe_read(pfd[0]);
|
|
FileDescriptor pipe_write(pfd[1]);
|
|
|
|
pid_t child = fork();
|
|
TEST_PCHECK(child >= 0);
|
|
if (child == 0) {
|
|
pipe_read.reset();
|
|
TEST_CHECK_SUCCESS(unshare(CLONE_NEWUSER));
|
|
TEST_CHECK_SUCCESS(write(pipe_write.get(), "R", 1));
|
|
pipe_write.reset();
|
|
pause();
|
|
_exit(0);
|
|
}
|
|
Cleanup cleanup([child] {
|
|
kill(child, SIGKILL);
|
|
int status;
|
|
RetryEINTR(waitpid)(child, &status, 0);
|
|
});
|
|
pipe_write.reset();
|
|
|
|
char buf;
|
|
TEST_PCHECK(read(pipe_read.get(), &buf, 1) == 1);
|
|
|
|
char nspath[PATH_MAX];
|
|
snprintf(nspath, sizeof(nspath), "/proc/%d/ns/user", child);
|
|
FileDescriptor nsfd = TEST_CHECK_NO_ERRNO_AND_VALUE(Open(nspath, O_RDONLY));
|
|
|
|
return {std::move(nsfd), std::move(cleanup)};
|
|
}
|
|
|
|
TEST(SetnsTest, ChangeIPCNamespace) {
|
|
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
|
|
|
|
struct stat st;
|
|
uint64_t ipcns1, ipcns2, ipcns3;
|
|
const FileDescriptor nsfd =
|
|
ASSERT_NO_ERRNO_AND_VALUE(Open("/proc/thread-self/ns/ipc", O_RDONLY));
|
|
ASSERT_THAT(stat("/proc/thread-self/ns/ipc", &st), SyscallSucceeds());
|
|
ipcns1 = st.st_ino;
|
|
|
|
// Use unshare(CLONE_NEWIPC) to change into a new IPC namespace.
|
|
ASSERT_THAT(unshare(CLONE_NEWIPC), SyscallSucceedsWithValue(0));
|
|
ASSERT_THAT(stat("/proc/thread-self/ns/ipc", &st), SyscallSucceeds());
|
|
ipcns2 = st.st_ino;
|
|
ASSERT_NE(ipcns1, ipcns2);
|
|
|
|
ASSERT_THAT(setns(nsfd.get(), CLONE_NEWIPC), SyscallSucceedsWithValue(0));
|
|
ASSERT_THAT(stat("/proc/thread-self/ns/ipc", &st), SyscallSucceeds());
|
|
ipcns3 = st.st_ino;
|
|
EXPECT_EQ(ipcns1, ipcns3);
|
|
}
|
|
|
|
TEST(SetnsTest, ChangeUTSNamespace) {
|
|
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
|
|
|
|
struct stat st;
|
|
uint64_t utsns1, utsns2, utsns3;
|
|
const FileDescriptor nsfd =
|
|
ASSERT_NO_ERRNO_AND_VALUE(Open("/proc/thread-self/ns/uts", O_RDONLY));
|
|
ASSERT_THAT(stat("/proc/thread-self/ns/uts", &st), SyscallSucceeds());
|
|
utsns1 = st.st_ino;
|
|
|
|
// Use unshare(CLONE_NEWUTS) to change into a new UTS namespace.
|
|
ASSERT_THAT(unshare(CLONE_NEWUTS), SyscallSucceedsWithValue(0));
|
|
ASSERT_THAT(stat("/proc/thread-self/ns/uts", &st), SyscallSucceeds());
|
|
utsns2 = st.st_ino;
|
|
ASSERT_NE(utsns1, utsns2);
|
|
|
|
ASSERT_THAT(setns(nsfd.get(), CLONE_NEWUTS), SyscallSucceedsWithValue(0));
|
|
ASSERT_THAT(stat("/proc/thread-self/ns/uts", &st), SyscallSucceeds());
|
|
utsns3 = st.st_ino;
|
|
EXPECT_EQ(utsns1, utsns3);
|
|
}
|
|
|
|
TEST(SetnsTest, ChangePIDNamespace) {
|
|
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
|
|
|
|
auto sigh = [](int sig) { _exit(5); };
|
|
signal(SIGUSR1, sigh);
|
|
|
|
auto child_init_in_pidns = [](void* args) {
|
|
int32_t fd;
|
|
|
|
TEST_PCHECK((fd = open("/proc/self/ns/pid", O_RDONLY)) >= 0);
|
|
TEST_PCHECK(setns(fd, 0) == 0);
|
|
TEST_PCHECK(setns(fd, CLONE_NEWPID) == 0);
|
|
close(fd);
|
|
while (1) {
|
|
absl::SleepFor(absl::Seconds(1));
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
// Check that a subreaper doesn't affect how pidns is destroyed.
|
|
ASSERT_THAT(prctl(PR_SET_CHILD_SUBREAPER, 1), SyscallSucceeds());
|
|
|
|
// Fork a test process in a new PID namespace, because it needs to manipulate
|
|
// with reparented processes.
|
|
struct clone_arg {
|
|
// Reserve some space for clone() to locate arguments and retcode in this
|
|
// place.
|
|
char stack[128] __attribute__((aligned(16)));
|
|
char stack_ptr[0];
|
|
} ca;
|
|
pid_t pid;
|
|
ASSERT_THAT(pid = clone(child_init_in_pidns, ca.stack_ptr,
|
|
CLONE_NEWPID | SIGCHLD, &ca),
|
|
SyscallSucceeds());
|
|
pid_t setns_pid = fork();
|
|
EXPECT_THAT(pid, SyscallSucceeds());
|
|
if (setns_pid == 0) {
|
|
int32_t fd;
|
|
char nspath[PATH_MAX];
|
|
|
|
snprintf(nspath, sizeof(nspath), "/proc/%d/ns/pid", pid);
|
|
|
|
TEST_PCHECK((fd = open(nspath, O_RDONLY)) >= 0);
|
|
TEST_PCHECK(setns(fd, 0) == 0);
|
|
close(fd);
|
|
pid = fork();
|
|
TEST_PCHECK(pid >= 0);
|
|
if (pid == 0) {
|
|
TEST_PCHECK(kill(1, SIGUSR1) == 0);
|
|
while (1) {
|
|
absl::SleepFor(absl::Seconds(1));
|
|
}
|
|
}
|
|
int status;
|
|
TEST_PCHECK(waitpid(pid, &status, 0) == pid);
|
|
TEST_CHECK(WTERMSIG(status) == SIGKILL);
|
|
_exit(0);
|
|
}
|
|
int status;
|
|
ASSERT_THAT(RetryEINTR(waitpid)(setns_pid, &status, 0),
|
|
SyscallSucceedsWithValue(setns_pid));
|
|
EXPECT_EQ(status, 0);
|
|
ASSERT_THAT(RetryEINTR(waitpid)(pid, &status, 0),
|
|
SyscallSucceedsWithValue(pid));
|
|
EXPECT_EQ(WEXITSTATUS(status), 5);
|
|
|
|
ASSERT_THAT(prctl(PR_SET_CHILD_SUBREAPER, 0), SyscallSucceeds());
|
|
}
|
|
|
|
TEST(SetnsTest, ChangeMountNamespaceZeroFlags) {
|
|
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
|
|
|
|
const FileDescriptor nsfd =
|
|
ASSERT_NO_ERRNO_AND_VALUE(Open("/proc/thread-self/ns/mnt", O_RDONLY));
|
|
ASSERT_THAT(setns(nsfd.get(), 0), SyscallSucceedsWithValue(0));
|
|
}
|
|
|
|
TEST(SetnsTest, ChangeUserNamespace) {
|
|
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(CanCreateUserNamespace()));
|
|
|
|
UserNamespaceChild child = CreateUserNamespaceChild();
|
|
EXPECT_THAT(InForkedProcess([&child] {
|
|
TEST_CHECK_SUCCESS(setns(child.nsfd.get(), CLONE_NEWUSER));
|
|
}),
|
|
IsPosixErrorOkAndHolds(0));
|
|
}
|
|
|
|
TEST(SetnsTest, ChangeUserNamespaceRejectsCurrentUserNamespace) {
|
|
const FileDescriptor nsfd =
|
|
ASSERT_NO_ERRNO_AND_VALUE(Open("/proc/thread-self/ns/user", O_RDONLY));
|
|
EXPECT_THAT(setns(nsfd.get(), CLONE_NEWUSER), SyscallFailsWithErrno(EINVAL));
|
|
}
|
|
|
|
TEST(SetnsTest, ChangeUserNamespaceRejectsMultithreadedCaller) {
|
|
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(CanCreateUserNamespace()));
|
|
|
|
UserNamespaceChild child = CreateUserNamespaceChild();
|
|
|
|
int nsfd = child.nsfd.get();
|
|
EXPECT_THAT(InForkedProcess([nsfd] {
|
|
int ready_fds[2];
|
|
TEST_PCHECK(pipe(ready_fds) == 0);
|
|
int stop_fds[2];
|
|
TEST_PCHECK(pipe(stop_fds) == 0);
|
|
int done_fds[2];
|
|
TEST_PCHECK(pipe(done_fds) == 0);
|
|
|
|
struct ThreadArgs {
|
|
int ready_read_fd;
|
|
int ready_write_fd;
|
|
int stop_read_fd;
|
|
int stop_write_fd;
|
|
int done_read_fd;
|
|
int done_write_fd;
|
|
} args = {ready_fds[0], ready_fds[1], stop_fds[0],
|
|
stop_fds[1], done_fds[0], done_fds[1]};
|
|
struct clone_arg {
|
|
char stack[1024] __attribute__((aligned(16)));
|
|
char stack_ptr[0];
|
|
} ca;
|
|
pid_t tid = clone(
|
|
+[](void* arg) {
|
|
ThreadArgs* args = static_cast<ThreadArgs*>(arg);
|
|
TEST_PCHECK(close(args->ready_read_fd) == 0);
|
|
TEST_PCHECK(close(args->stop_write_fd) == 0);
|
|
TEST_PCHECK(close(args->done_read_fd) == 0);
|
|
TEST_PCHECK(write(args->ready_write_fd, "R", 1) == 1);
|
|
TEST_PCHECK(close(args->ready_write_fd) == 0);
|
|
char buf;
|
|
TEST_PCHECK(read(args->stop_read_fd, &buf, 1) == 1);
|
|
TEST_PCHECK(close(args->stop_read_fd) == 0);
|
|
TEST_PCHECK(write(args->done_write_fd, "D", 1) == 1);
|
|
TEST_PCHECK(close(args->done_write_fd) == 0);
|
|
return 0;
|
|
},
|
|
ca.stack_ptr, CLONE_SIGHAND | CLONE_THREAD | CLONE_VM,
|
|
&args);
|
|
TEST_PCHECK(tid >= 0);
|
|
TEST_PCHECK(close(ready_fds[1]) == 0);
|
|
TEST_PCHECK(close(stop_fds[0]) == 0);
|
|
TEST_PCHECK(close(done_fds[1]) == 0);
|
|
|
|
char buf;
|
|
TEST_PCHECK(read(ready_fds[0], &buf, 1) == 1);
|
|
TEST_PCHECK(close(ready_fds[0]) == 0);
|
|
TEST_CHECK_ERRNO(setns(nsfd, CLONE_NEWUSER), EINVAL);
|
|
TEST_PCHECK(write(stop_fds[1], "S", 1) == 1);
|
|
TEST_PCHECK(close(stop_fds[1]) == 0);
|
|
TEST_PCHECK(read(done_fds[0], &buf, 1) == 1);
|
|
TEST_PCHECK(close(done_fds[0]) == 0);
|
|
}),
|
|
IsPosixErrorOkAndHolds(0));
|
|
}
|
|
|
|
TEST(SetnsTest, ChangeUserNamespaceRejectsSharedFS) {
|
|
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(CanCreateUserNamespace()));
|
|
|
|
UserNamespaceChild userns_child = CreateUserNamespaceChild();
|
|
int nsfd = userns_child.nsfd.get();
|
|
|
|
EXPECT_THAT(
|
|
InForkedProcess([nsfd] {
|
|
int pfd[2];
|
|
TEST_PCHECK(pipe(pfd) == 0);
|
|
|
|
struct CloneFSArgs {
|
|
int read_fd;
|
|
int write_fd;
|
|
} args = {pfd[0], pfd[1]};
|
|
struct clone_arg {
|
|
char stack[128] __attribute__((aligned(16)));
|
|
char stack_ptr[0];
|
|
} ca;
|
|
pid_t fs_child = clone(
|
|
+[](void* arg) {
|
|
CloneFSArgs* args = static_cast<CloneFSArgs*>(arg);
|
|
TEST_PCHECK(close(args->write_fd) == 0);
|
|
char buf;
|
|
TEST_PCHECK(read(args->read_fd, &buf, 1) >= 0);
|
|
TEST_PCHECK(close(args->read_fd) == 0);
|
|
_exit(0);
|
|
return 0;
|
|
},
|
|
ca.stack_ptr, CLONE_FS | SIGCHLD, &args);
|
|
TEST_PCHECK(fs_child >= 0);
|
|
TEST_PCHECK(close(pfd[0]) == 0);
|
|
|
|
TEST_CHECK_ERRNO(setns(nsfd, CLONE_NEWUSER), EINVAL);
|
|
|
|
TEST_PCHECK(close(pfd[1]) == 0);
|
|
int status;
|
|
TEST_PCHECK(RetryEINTR(waitpid)(fs_child, &status, 0) == fs_child);
|
|
TEST_CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
|
}),
|
|
IsPosixErrorOkAndHolds(0));
|
|
}
|
|
|
|
TEST(SetnsTest, ChangeUserNamespaceRejectsMissingTargetCapability) {
|
|
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(CanCreateUserNamespace()));
|
|
|
|
const FileDescriptor parent_nsfd =
|
|
ASSERT_NO_ERRNO_AND_VALUE(Open("/proc/thread-self/ns/user", O_RDONLY));
|
|
EXPECT_THAT(InForkedProcess([&parent_nsfd] {
|
|
TEST_CHECK_SUCCESS(unshare(CLONE_NEWUSER));
|
|
TEST_CHECK_ERRNO(setns(parent_nsfd.get(), CLONE_NEWUSER),
|
|
EPERM);
|
|
}),
|
|
IsPosixErrorOkAndHolds(0));
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace testing
|
|
} // namespace gvisor
|