blob: 6e3c11fce7705072af839a2248c1dfc5a983ee3d [file] [log] [blame]
// Copyright 2016 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <sys/types.h>
#include <unistd.h>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <base/bind.h>
#include <base/bind_helpers.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/files/scoped_temp_dir.h>
#include <base/posix/eintr_wrapper.h>
#include <base/stl_util.h>
#include <base/strings/string_split.h>
#include <gtest/gtest.h>
#include "libcontainer/cgroup.h"
#include "libcontainer/config.h"
#include "libcontainer/container.h"
#include "libcontainer/libcontainer.h"
#include "libcontainer/libcontainer_util.h"
namespace libcontainer {
namespace {
using MinijailHookCallback = base::Callback<int()>;
constexpr int kTestCpuShares = 200;
constexpr int kTestCpuQuota = 20000;
constexpr int kTestCpuPeriod = 50000;
struct MockPosixState {
struct MountArgs {
std::string source;
base::FilePath target;
std::string filesystemtype;
unsigned long mountflags;
const void* data;
bool outside_mount;
};
std::vector<MountArgs> mount_args;
dev_t stat_rdev_ret = makedev(2, 3);
std::vector<int> kill_sigs;
base::FilePath mkdtemp_root;
};
MockPosixState* g_mock_posix_state = nullptr;
struct MockCgroupState {
struct AddedDevice {
bool allow;
int major;
int minor;
bool read;
bool write;
bool modify;
char type;
};
bool freeze_ret = true;
bool thaw_ret = true;
bool deny_all_devs_ret = true;
bool add_device_ret = true;
bool set_cpu_ret = true;
int deny_all_devs_called_count;
std::vector<AddedDevice> added_devices;
int set_cpu_shares_count;
int set_cpu_quota_count;
int set_cpu_period_count;
int set_cpu_rt_runtime_count;
int set_cpu_rt_period_count;
};
MockCgroupState* g_mock_cgroup_state = nullptr;
class MockCgroup : public libcontainer::Cgroup {
public:
explicit MockCgroup(MockCgroupState* state) : state_(state) {}
MockCgroup(const MockCgroup&) = delete;
MockCgroup& operator=(const MockCgroup&) = delete;
~MockCgroup() = default;
static std::unique_ptr<libcontainer::Cgroup> Create(
base::StringPiece name,
const base::FilePath& cgroup_root,
const base::FilePath& cgroup_parent,
uid_t cgroup_owner,
gid_t cgroup_group) {
return std::make_unique<MockCgroup>(g_mock_cgroup_state);
}
bool Freeze() override { return state_->freeze_ret; }
bool Thaw() override { return state_->thaw_ret; }
bool DenyAllDevices() override {
++state_->deny_all_devs_called_count;
return state_->deny_all_devs_ret;
}
bool AddDevice(bool allow,
int major,
int minor,
bool read,
bool write,
bool modify,
char type) override {
state_->added_devices.emplace_back(MockCgroupState::AddedDevice{
allow, major, minor, read, write, modify, type});
return state_->add_device_ret;
}
bool SetCpuShares(int shares) override {
state_->set_cpu_shares_count++;
return state_->set_cpu_ret;
}
bool SetCpuQuota(int quota) override {
state_->set_cpu_quota_count++;
return state_->set_cpu_ret;
}
bool SetCpuPeriod(int period) override {
state_->set_cpu_period_count++;
return state_->set_cpu_ret;
}
bool SetCpuRtRuntime(int rt_runtime) override {
state_->set_cpu_rt_runtime_count++;
return state_->set_cpu_ret;
}
bool SetCpuRtPeriod(int rt_period) override {
state_->set_cpu_rt_period_count++;
return state_->set_cpu_ret;
}
private:
MockCgroupState* const state_;
};
struct MockMinijailState {
std::string alt_syscall_table;
int ipc_called_count;
int vfs_called_count;
int net_called_count;
int pids_called_count;
int run_as_init_called_count;
int user_called_count;
int cgroups_called_count;
int wait_called_count;
int reset_signal_mask_called_count;
int reset_signal_handlers_called_count;
int set_supplementary_gids_called_count;
int pid;
std::map<minijail_hook_event_t, std::vector<MinijailHookCallback>> hooks;
};
MockMinijailState* g_mock_minijail_state = nullptr;
} // namespace
TEST(LibcontainerTest, PremountedRunfs) {
char premounted_runfs[] = "/tmp/cgtest_run/root";
struct container_config* config = container_config_create();
ASSERT_NE(nullptr, config);
container_config_premounted_runfs(config, premounted_runfs);
const char* result = container_config_get_premounted_runfs(config);
ASSERT_EQ(0, strcmp(result, premounted_runfs));
container_config_destroy(config);
}
TEST(LibcontainerTest, PidFilePath) {
char pid_file_path[] = "/tmp/cgtest_run/root/container.pid";
struct container_config* config = container_config_create();
ASSERT_NE(nullptr, config);
container_config_pid_file(config, pid_file_path);
const char* result = container_config_get_pid_file(config);
ASSERT_EQ(0, strcmp(result, pid_file_path));
container_config_destroy(config);
}
TEST(LibcontainerTest, DumpConfig) {
struct container_config* config = container_config_create();
ASSERT_NE(nullptr, config);
// Confirm that container_config_dump() returns a non-empty string.
std::unique_ptr<char, decltype(&free)> config_str(
container_config_dump(config, false /* sort_vector */), free);
ASSERT_NE(nullptr, config_str.get());
EXPECT_NE(0U, strlen(config_str.get()));
// Also confirm that the string has multiple lines.
EXPECT_LT(1U, base::SplitString(config_str.get(), "\n", base::KEEP_WHITESPACE,
base::SPLIT_WANT_NONEMPTY)
.size());
container_config_destroy(config);
}
class ContainerTest : public ::testing::Test {
public:
ContainerTest() = default;
ContainerTest(const ContainerTest&) = delete;
ContainerTest& operator=(const ContainerTest&) = delete;
~ContainerTest() override = default;
void SetUp() override {
g_mock_posix_state = new MockPosixState();
g_mock_cgroup_state = new MockCgroupState();
g_mock_minijail_state = new MockMinijailState();
libcontainer::Cgroup::SetCgroupFactoryForTesting(&MockCgroup::Create);
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
ASSERT_TRUE(base::CreateTemporaryDirInDir(temp_dir_.GetPath(),
"container_test", &rootfs_));
mount_flags_ = MS_NOSUID | MS_NODEV | MS_NOEXEC;
config_.reset(new Config());
container_config_uid_map(config_->get(), "0 0 4294967295");
container_config_gid_map(config_->get(), "0 0 4294967295");
container_config_rootfs(config_->get(), rootfs_.value().c_str());
static const char* kArgv[] = {
"/sbin/init",
};
container_config_program_argv(config_->get(), kArgv, 1);
container_config_alt_syscall_table(config_->get(), "testsyscalltable");
container_config_add_mount(config_->get(), "testtmpfs", "tmpfs", "/tmp",
"tmpfs", nullptr, nullptr, mount_flags_, 0, 1000,
1000, 0x666, 0, 0);
container_config_add_device(config_->get(), 'c', "/dev/foo",
S_IRWXU | S_IRWXG, 245, 2, 0, 0, 1000, 1001, 1,
1, 0);
// test dynamic minor on /dev/null
container_config_add_device(config_->get(), 'c', "/dev/null",
S_IRWXU | S_IRWXG, 1, -1, 0, 1, 1000, 1001, 1,
1, 0);
static const char* kNamespaces[] = {
"cgroup", "ipc", "mount", "network", "pid", "user",
};
container_config_namespaces(config_->get(), kNamespaces,
base::size(kNamespaces));
container_config_set_cpu_shares(config_->get(), kTestCpuShares);
container_config_set_cpu_cfs_params(config_->get(), kTestCpuQuota,
kTestCpuPeriod);
/* Invalid params, so this won't be applied. */
container_config_set_cpu_rt_params(config_->get(), 20000, 20000);
base::FilePath rundir;
ASSERT_TRUE(base::CreateTemporaryDirInDir(temp_dir_.GetPath(),
"container_test_run", &rundir));
container_.reset(new Container("containerUT", rundir));
ASSERT_NE(nullptr, container_.get());
}
void TearDown() override {
container_.reset();
config_.reset();
ASSERT_TRUE(temp_dir_.Delete());
delete g_mock_posix_state;
g_mock_posix_state = nullptr;
libcontainer::Cgroup::SetCgroupFactoryForTesting(nullptr);
delete g_mock_cgroup_state;
delete g_mock_minijail_state;
g_mock_minijail_state = nullptr;
}
protected:
const Config* config() const { return config_.get(); }
std::unique_ptr<Config> config_;
std::unique_ptr<Container> container_;
int mount_flags_;
base::FilePath rootfs_;
private:
base::ScopedTempDir temp_dir_;
};
TEST_F(ContainerTest, TestMountTmpStart) {
ASSERT_EQ(0, container_start(container_->get(), config_->get()));
ASSERT_EQ(2, g_mock_posix_state->mount_args.size());
EXPECT_EQ(false, g_mock_posix_state->mount_args[1].outside_mount);
EXPECT_STREQ("tmpfs", g_mock_posix_state->mount_args[1].source.c_str());
EXPECT_STREQ("/tmp",
g_mock_posix_state->mount_args[1].target.value().c_str());
EXPECT_STREQ("tmpfs",
g_mock_posix_state->mount_args[1].filesystemtype.c_str());
EXPECT_EQ(g_mock_posix_state->mount_args[1].mountflags,
static_cast<unsigned long>(mount_flags_));
EXPECT_EQ(nullptr, g_mock_posix_state->mount_args[1].data);
EXPECT_EQ(1, g_mock_minijail_state->ipc_called_count);
EXPECT_EQ(1, g_mock_minijail_state->vfs_called_count);
EXPECT_EQ(1, g_mock_minijail_state->net_called_count);
EXPECT_EQ(1, g_mock_minijail_state->pids_called_count);
EXPECT_EQ(1, g_mock_minijail_state->user_called_count);
EXPECT_EQ(1, g_mock_minijail_state->cgroups_called_count);
EXPECT_EQ(1, g_mock_minijail_state->run_as_init_called_count);
EXPECT_EQ(1, g_mock_cgroup_state->deny_all_devs_called_count);
ASSERT_EQ(2, g_mock_cgroup_state->added_devices.size());
EXPECT_EQ(1, g_mock_cgroup_state->added_devices[0].allow);
EXPECT_EQ(245, g_mock_cgroup_state->added_devices[0].major);
EXPECT_EQ(2, g_mock_cgroup_state->added_devices[0].minor);
EXPECT_EQ(1, g_mock_cgroup_state->added_devices[0].read);
EXPECT_EQ(1, g_mock_cgroup_state->added_devices[0].write);
EXPECT_EQ(0, g_mock_cgroup_state->added_devices[0].modify);
EXPECT_EQ('c', g_mock_cgroup_state->added_devices[0].type);
EXPECT_EQ(1, g_mock_cgroup_state->added_devices[1].allow);
EXPECT_EQ(1, g_mock_cgroup_state->added_devices[1].major);
EXPECT_EQ(-1, g_mock_cgroup_state->added_devices[1].minor);
EXPECT_EQ(1, g_mock_cgroup_state->added_devices[1].read);
EXPECT_EQ(1, g_mock_cgroup_state->added_devices[1].write);
EXPECT_EQ(0, g_mock_cgroup_state->added_devices[1].modify);
EXPECT_EQ('c', g_mock_cgroup_state->added_devices[1].type);
EXPECT_EQ(1, g_mock_cgroup_state->set_cpu_shares_count);
EXPECT_EQ(kTestCpuShares, container_config_get_cpu_shares(config_->get()));
EXPECT_EQ(1, g_mock_cgroup_state->set_cpu_quota_count);
EXPECT_EQ(kTestCpuQuota, container_config_get_cpu_quota(config_->get()));
EXPECT_EQ(1, g_mock_cgroup_state->set_cpu_period_count);
EXPECT_EQ(kTestCpuPeriod, container_config_get_cpu_period(config_->get()));
EXPECT_EQ(0, g_mock_cgroup_state->set_cpu_rt_runtime_count);
EXPECT_EQ(0, container_config_get_cpu_rt_runtime(config_->get()));
EXPECT_EQ(0, g_mock_cgroup_state->set_cpu_rt_period_count);
EXPECT_EQ(0, container_config_get_cpu_rt_period(config_->get()));
ASSERT_NE(std::string(), g_mock_minijail_state->alt_syscall_table);
EXPECT_EQ("testsyscalltable", g_mock_minijail_state->alt_syscall_table);
EXPECT_EQ(0, container_wait(container_->get()));
EXPECT_EQ(1, g_mock_minijail_state->wait_called_count);
EXPECT_EQ(1, g_mock_minijail_state->reset_signal_mask_called_count);
EXPECT_EQ(1, g_mock_minijail_state->reset_signal_handlers_called_count);
EXPECT_EQ(0, g_mock_minijail_state->set_supplementary_gids_called_count);
}
TEST_F(ContainerTest, TestKillContainer) {
ASSERT_EQ(0, container_start(container_->get(), config_->get()));
EXPECT_EQ(0, container_kill(container_->get()));
EXPECT_EQ(std::vector<int>{SIGKILL}, g_mock_posix_state->kill_sigs);
EXPECT_EQ(1, g_mock_minijail_state->wait_called_count);
}
// Does the same as LibcontainerTest.DumpConfig but with more configuration
// parameters similar to the production.
TEST_F(ContainerTest, DumpConfig) {
struct container_config* config = this->config()->get();
ASSERT_NE(nullptr, config);
std::unique_ptr<char, decltype(&free)> config_str(
container_config_dump(config, true /* sort_vector */), free);
ASSERT_NE(nullptr, config_str.get());
EXPECT_NE(0U, strlen(config_str.get()));
EXPECT_LT(1U, base::SplitString(config_str.get(), "\n", base::KEEP_WHITESPACE,
base::SPLIT_WANT_NONEMPTY)
.size());
}
} // namespace libcontainer
// libc stubs so the UT doesn't need root to call mount, etc.
extern "C" {
extern decltype(chmod) __real_chmod;
int __wrap_chmod(const char* path, mode_t mode) {
if (!libcontainer::g_mock_posix_state)
return __real_chmod(path, mode);
return 0;
}
extern decltype(chown) __real_chown;
int __wrap_chown(const char* path, uid_t owner, gid_t group) {
if (!libcontainer::g_mock_posix_state)
return __real_chown(path, owner, group);
return 0;
}
extern decltype(getuid) __real_getuid;
uid_t __wrap_getuid(void) {
if (!libcontainer::g_mock_posix_state)
return __real_getuid();
return 0;
}
extern decltype(kill) __real_kill;
int __wrap_kill(pid_t pid, int sig) {
if (!libcontainer::g_mock_posix_state)
return __real_kill(pid, sig);
libcontainer::g_mock_posix_state->kill_sigs.push_back(sig);
return 0;
}
extern decltype(mkdir) __real_mkdir;
int __wrap_mkdir(const char* pathname, mode_t mode) {
if (!libcontainer::g_mock_posix_state)
return __real_mkdir(pathname, mode);
return 0;
}
extern decltype(mkdtemp) __real_mkdtemp;
char* __wrap_mkdtemp(char* template_string) {
if (!libcontainer::g_mock_posix_state)
return __real_mkdtemp(template_string);
libcontainer::g_mock_posix_state->mkdtemp_root =
base::FilePath(template_string);
return template_string;
}
extern decltype(mount) __real_mount;
int __wrap_mount(const char* source,
const char* target,
const char* filesystemtype,
unsigned long mountflags,
const void* data) {
if (!libcontainer::g_mock_posix_state)
return __real_mount(source, target, filesystemtype, mountflags, data);
libcontainer::g_mock_posix_state->mount_args.emplace_back(
libcontainer::MockPosixState::MountArgs{source, base::FilePath(target),
filesystemtype, mountflags, data,
true});
return 0;
}
extern decltype(rmdir) __real_rmdir;
int __wrap_rmdir(const char* pathname) {
if (!libcontainer::g_mock_posix_state)
return __real_rmdir(pathname);
return 0;
}
extern decltype(umount) __real_umount;
int __wrap_umount(const char* target) {
if (!libcontainer::g_mock_posix_state)
return __real_umount(target);
return 0;
}
extern decltype(umount2) __real_umount2;
int __wrap_umount2(const char* target, int flags) {
if (!libcontainer::g_mock_posix_state)
return __real_umount2(target, flags);
return 0;
}
extern decltype(unlink) __real_unlink;
int __wrap_unlink(const char* pathname) {
if (!libcontainer::g_mock_posix_state)
return __real_unlink(pathname);
return 0;
}
extern decltype(__xmknod) __real___xmknod;
int __wrap___xmknod(int ver, const char* pathname, mode_t mode, dev_t* dev) {
if (!libcontainer::g_mock_posix_state)
return __real___xmknod(ver, pathname, mode, dev);
return 0;
}
extern decltype(__xstat) __real___xstat;
int __wrap___xstat(int ver, const char* path, struct stat* buf) {
if (!libcontainer::g_mock_posix_state)
return __real___xstat(ver, path, buf);
buf->st_rdev = libcontainer::g_mock_posix_state->stat_rdev_ret;
return 0;
}
extern decltype(setns) __real_setns;
int __wrap_setns(int fd, int nstype) {
if (!libcontainer::g_mock_posix_state)
return __real_setns(fd, nstype);
return 0;
}
/* Minijail stubs */
struct minijail* minijail_new(void) {
return (struct minijail*)0x55;
}
void minijail_destroy(struct minijail* j) {}
int minijail_mount_with_data(struct minijail* j,
const char* source,
const char* target,
const char* filesystemtype,
unsigned long mountflags,
const char* data) {
libcontainer::g_mock_posix_state->mount_args.emplace_back(
libcontainer::MockPosixState::MountArgs{source, base::FilePath(target),
filesystemtype, mountflags, data,
false});
return 0;
}
void minijail_namespace_user_disable_setgroups(struct minijail* j) {}
void minijail_namespace_vfs(struct minijail* j) {
++libcontainer::g_mock_minijail_state->vfs_called_count;
}
void minijail_namespace_ipc(struct minijail* j) {
++libcontainer::g_mock_minijail_state->ipc_called_count;
}
void minijail_namespace_net(struct minijail* j) {
++libcontainer::g_mock_minijail_state->net_called_count;
}
void minijail_namespace_pids(struct minijail* j) {
++libcontainer::g_mock_minijail_state->pids_called_count;
}
void minijail_namespace_user(struct minijail* j) {
++libcontainer::g_mock_minijail_state->user_called_count;
}
void minijail_namespace_cgroups(struct minijail* j) {
++libcontainer::g_mock_minijail_state->cgroups_called_count;
}
int minijail_uidmap(struct minijail* j, const char* uidmap) {
return 0;
}
int minijail_gidmap(struct minijail* j, const char* gidmap) {
return 0;
}
int minijail_enter_pivot_root(struct minijail* j, const char* dir) {
return 0;
}
void minijail_run_as_init(struct minijail* j) {
++libcontainer::g_mock_minijail_state->run_as_init_called_count;
}
int minijail_run_pid_pipes_no_preload(struct minijail* j,
const char* filename,
char* const argv[],
pid_t* pchild_pid,
int* pstdin_fd,
int* pstdout_fd,
int* pstderr_fd) {
libcontainer::g_mock_minijail_state->pid = fork();
if (libcontainer::g_mock_minijail_state->pid == -1)
return libcontainer::g_mock_minijail_state->pid;
if (libcontainer::g_mock_minijail_state->pid == 0) {
for (minijail_hook_event_t event :
{MINIJAIL_HOOK_EVENT_PRE_CHROOT, MINIJAIL_HOOK_EVENT_PRE_DROP_CAPS,
MINIJAIL_HOOK_EVENT_PRE_EXECVE}) {
auto it = libcontainer::g_mock_minijail_state->hooks.find(event);
if (it == libcontainer::g_mock_minijail_state->hooks.end())
continue;
for (auto& hook : it->second) {
int rc = hook.Run();
if (rc)
_exit(rc);
}
}
_exit(0);
}
*pchild_pid = libcontainer::g_mock_minijail_state->pid;
return 0;
}
int minijail_write_pid_file(struct minijail* j, const char* path) {
return 0;
}
int minijail_wait(struct minijail* j) {
++libcontainer::g_mock_minijail_state->wait_called_count;
int status;
if (HANDLE_EINTR(
waitpid(libcontainer::g_mock_minijail_state->pid, &status, 0)) < 0) {
PLOG(ERROR) << "Failed to wait for minijail";
return -errno;
}
if (!WIFEXITED(status)) {
LOG(ERROR) << "minijail terminated abnormally: " << std::hex << status;
return -ECANCELED;
}
// Exit status gets truncated to 8 bits. This should sign-extend it so that
// any negative values we passed are preserved.
return static_cast<int8_t>(WEXITSTATUS(status));
}
int minijail_use_alt_syscall(struct minijail* j, const char* table) {
libcontainer::g_mock_minijail_state->alt_syscall_table = table;
return 0;
}
int minijail_add_to_cgroup(struct minijail* j, const char* cg_path) {
return 0;
}
int minijail_forward_signals(struct minijail* j) {
return 0;
}
void minijail_reset_signal_mask(struct minijail* j) {
++libcontainer::g_mock_minijail_state->reset_signal_mask_called_count;
}
void minijail_reset_signal_handlers(struct minijail* j) {
++libcontainer::g_mock_minijail_state->reset_signal_handlers_called_count;
}
void minijail_skip_remount_private(struct minijail* j) {}
int minijail_preserve_fd(struct minijail* j, int parent_fd, int child_fd) {
return 0;
}
int minijail_add_hook(struct minijail* j,
minijail_hook_t hook,
void* payload,
minijail_hook_event_t event) {
auto it = libcontainer::g_mock_minijail_state->hooks.insert(
std::make_pair(event, std::vector<libcontainer::MinijailHookCallback>()));
it.first->second.emplace_back(base::Bind(hook, base::Unretained(payload)));
return 0;
}
void minijail_close_open_fds(struct minijail* j) {}
void minijail_set_supplementary_gids(struct minijail* j,
size_t size,
const gid_t* list) {
++libcontainer::g_mock_minijail_state->set_supplementary_gids_called_count;
}
} // extern "C"