blob: 0d6a07d4f8bc5f9dce14fbdb5310e0e1071d6c9c [file] [log] [blame]
/* Copyright 2018 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.
*
* Test the minijail0 CLI using gtest.
*
* Note: We don't verify that the minijail struct was set correctly from these
* flags as only libminijail.c knows that definition. If we wanted to improve
* this test, we'd have to pull that struct into a common (internal) header.
*/
#include <stdio.h>
#include <stdlib.h>
#include <gtest/gtest.h>
#include "libminijail.h"
#include "minijail0_cli.h"
namespace {
constexpr char kValidUser[] = "nobody";
constexpr char kValidUid[] = "100";
constexpr char kValidGroup[] = "users";
constexpr char kValidGid[] = "100";
class CliTest : public ::testing::Test {
protected:
virtual void SetUp() {
// Most tests do not care about this logic. For the few that do, make
// them opt into it so they can validate specifically.
elftype_ = ELFDYNAMIC;
}
virtual void TearDown() {}
// We use a vector of strings rather than const char * pointers because we
// need the backing memory to be writable. The CLI might mutate the strings
// as it parses things (which is normally permissible with argv).
int parse_args_(const std::vector<std::string>& argv,
int* exit_immediately,
ElfType* elftype) {
// Make sure we reset the getopts state when scanning a new argv. Setting
// this to 0 is a GNU extension, but AOSP/BSD also checks this (as an alias
// to their "optreset").
optind = 0;
// We create & destroy this for every parse_args call because some API
// calls can dupe memory which confuses LSAN. https://crbug.com/844615
struct minijail *j = minijail_new();
std::vector<const char *> pargv;
pargv.push_back("minijail0");
for (const std::string& arg : argv)
pargv.push_back(arg.c_str());
// We grab stdout from parse_args itself as it might dump things we don't
// usually care about like help output.
testing::internal::CaptureStdout();
const char* preload_path = PRELOADPATH;
int ret =
parse_args(j, pargv.size(), const_cast<char* const*>(pargv.data()),
exit_immediately, elftype, &preload_path);
testing::internal::GetCapturedStdout();
minijail_destroy(j);
return ret;
}
int parse_args_(const std::vector<std::string>& argv) {
return parse_args_(argv, &exit_immediately_, &elftype_);
}
ElfType elftype_;
int exit_immediately_;
};
} // namespace
// Should exit non-zero when there's no arguments.
TEST_F(CliTest, no_args) {
std::vector<std::string> argv = {};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}
// Should exit zero when we asked for help.
TEST_F(CliTest, help) {
std::vector<std::string> argv = {"-h"};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(0), "");
argv = {"--help"};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(0), "");
argv = {"-H"};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(0), "");
}
// Just a simple program to run.
TEST_F(CliTest, valid_program) {
std::vector<std::string> argv = {"/bin/sh"};
ASSERT_TRUE(parse_args_(argv));
}
// Valid calls to the change user option.
TEST_F(CliTest, valid_set_user) {
std::vector<std::string> argv = {"-u", "", "/bin/sh"};
argv[1] = kValidUser;
ASSERT_TRUE(parse_args_(argv));
argv[1] = kValidUid;
ASSERT_TRUE(parse_args_(argv));
}
// Invalid calls to the change user option.
TEST_F(CliTest, invalid_set_user) {
std::vector<std::string> argv = {"-u", "", "/bin/sh"};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
argv[1] = "j;lX:J*Pj;oijfs;jdlkjC;j";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
argv[1] = "1000x";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Supplying -u more than once is bad.
argv = {"-u", kValidUser, "-u", kValidUid, "/bin/sh"};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1),
"-u provided multiple times");
}
// Valid calls to the change group option.
TEST_F(CliTest, valid_set_group) {
std::vector<std::string> argv = {"-g", "", "/bin/sh"};
argv[1] = kValidGroup;
ASSERT_TRUE(parse_args_(argv));
argv[1] = kValidGid;
ASSERT_TRUE(parse_args_(argv));
}
// Invalid calls to the change group option.
TEST_F(CliTest, invalid_set_group) {
std::vector<std::string> argv = {"-g", "", "/bin/sh"};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
argv[1] = "j;lX:J*Pj;oijfs;jdlkjC;j";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
argv[1] = "1000x";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Supplying -g more than once is bad.
argv = {"-g", kValidGroup, "-g", kValidGid, "/bin/sh"};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1),
"-g provided multiple times");
}
// Valid calls to the skip securebits option.
TEST_F(CliTest, valid_skip_securebits) {
// An empty string is the same as 0.
std::vector<std::string> argv = {"-B", "", "/bin/sh"};
ASSERT_TRUE(parse_args_(argv));
argv[1] = "0xAB";
ASSERT_TRUE(parse_args_(argv));
argv[1] = "1234";
ASSERT_TRUE(parse_args_(argv));
}
// Invalid calls to the skip securebits option.
TEST_F(CliTest, invalid_skip_securebits) {
std::vector<std::string> argv = {"-B", "", "/bin/sh"};
argv[1] = "xja";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}
// Valid calls to the caps option.
TEST_F(CliTest, valid_caps) {
// An empty string is the same as 0.
std::vector<std::string> argv = {"-c", "", "/bin/sh"};
ASSERT_TRUE(parse_args_(argv));
argv[1] = "0xAB";
ASSERT_TRUE(parse_args_(argv));
argv[1] = "1234";
ASSERT_TRUE(parse_args_(argv));
}
// Invalid calls to the caps option.
TEST_F(CliTest, invalid_caps) {
std::vector<std::string> argv = {"-c", "", "/bin/sh"};
argv[1] = "xja";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}
// Valid calls to the logging option.
TEST_F(CliTest, valid_logging) {
std::vector<std::string> argv = {"--logging", "", "/bin/sh"};
// This should list all valid logging targets.
const std::vector<std::string> profiles = {
"stderr",
"syslog",
};
for (const auto profile : profiles) {
argv[1] = profile;
ASSERT_TRUE(parse_args_(argv));
}
}
// Invalid calls to the logging option.
TEST_F(CliTest, invalid_logging) {
std::vector<std::string> argv = {"--logging", "", "/bin/sh"};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
argv[1] = "stdout";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}
// Valid calls to the rlimit option.
TEST_F(CliTest, valid_rlimit) {
std::vector<std::string> argv = {"-R", "", "/bin/sh"};
argv[1] = "0,1,2";
ASSERT_TRUE(parse_args_(argv));
argv[1] = "0,0x100,4";
ASSERT_TRUE(parse_args_(argv));
argv[1] = "1,1,unlimited";
ASSERT_TRUE(parse_args_(argv));
argv[1] = "2,unlimited,2";
ASSERT_TRUE(parse_args_(argv));
argv[1] = "RLIMIT_AS,unlimited,unlimited";
ASSERT_TRUE(parse_args_(argv));
}
// Invalid calls to the rlimit option.
TEST_F(CliTest, invalid_rlimit) {
std::vector<std::string> argv = {"-R", "", "/bin/sh"};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Missing cur & max.
argv[1] = "0";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Missing max.
argv[1] = "0,0";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Too many options.
argv[1] = "0,0,0,0";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Non-numeric limits
argv[1] = "0,0,invalid-limit";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Invalid number.
argv[1] = "0,0,0j";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Invalid hex number.
argv[1] = "0,0x1jf,0";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Invalid rlimit constant.
argv[1] = "RLIMIT_GOGOOGOG,0,0";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}
// Valid calls to the profile option.
TEST_F(CliTest, valid_profile) {
std::vector<std::string> argv = {"--profile", "", "/bin/sh"};
// This should list all valid profiles.
const std::vector<std::string> profiles = {
"minimalistic-mountns",
};
for (const auto profile : profiles) {
argv[1] = profile;
ASSERT_TRUE(parse_args_(argv));
}
}
// Invalid calls to the profile option.
TEST_F(CliTest, invalid_profile) {
std::vector<std::string> argv = {"--profile", "", "/bin/sh"};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
argv[1] = "random-unknown-profile";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}
// Valid calls to the chroot option.
TEST_F(CliTest, valid_chroot) {
std::vector<std::string> argv = {"-C", "/", "/bin/sh"};
ASSERT_TRUE(parse_args_(argv));
}
// Valid calls to the pivot root option.
TEST_F(CliTest, valid_pivot_root) {
std::vector<std::string> argv = {"-P", "/", "/bin/sh"};
ASSERT_TRUE(parse_args_(argv));
}
// We cannot handle multiple options with chroot/profile/pivot root.
TEST_F(CliTest, conflicting_roots) {
std::vector<std::string> argv;
// Chroot & pivot root.
argv = {"-C", "/", "-P", "/", "/bin/sh"};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Chroot & minimalistic-mountns profile.
argv = {"-C", "/", "--profile", "minimalistic-mountns", "/bin/sh"};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Pivot root & minimalistic-mountns profile.
argv = {"-P", "/", "--profile", "minimalistic-mountns", "/bin/sh"};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}
// Valid calls to the uidmap option.
TEST_F(CliTest, valid_uidmap) {
std::vector<std::string> argv = {"-m", "/bin/sh"};
// Use a default map (no option from user).
ASSERT_TRUE(parse_args_(argv));
// Use a single map.
argv = {"-m0 0 1", "/bin/sh"};
ASSERT_TRUE(parse_args_(argv));
// Multiple maps.
argv = {"-m0 0 1,100 100 1", "/bin/sh"};
ASSERT_TRUE(parse_args_(argv));
}
// Valid calls to the gidmap option.
TEST_F(CliTest, valid_gidmap) {
std::vector<std::string> argv = {"-M", "/bin/sh"};
// Use a default map (no option from user).
ASSERT_TRUE(parse_args_(argv));
// Use a single map.
argv = {"-M0 0 1", "/bin/sh"};
ASSERT_TRUE(parse_args_(argv));
// Multiple maps.
argv = {"-M0 0 1,100 100 1", "/bin/sh"};
ASSERT_TRUE(parse_args_(argv));
}
// Invalid calls to the uidmap/gidmap options.
// Note: Can't really test these as all validation is delayed/left to the
// runtime kernel. Minijail will simply write verbatim what the user gave
// it to the corresponding /proc/.../[ug]id_map.
// Valid calls to the binding option.
TEST_F(CliTest, valid_binding) {
std::vector<std::string> argv = {"-v", "-b", "", "/bin/sh"};
// Dest & writable are optional.
argv[1] = "/";
ASSERT_TRUE(parse_args_(argv));
// Writable is optional.
argv[1] = "/,/";
ASSERT_TRUE(parse_args_(argv));
// Writable is an integer.
argv[1] = "/,/,0";
ASSERT_TRUE(parse_args_(argv));
argv[1] = "/,/,1";
ASSERT_TRUE(parse_args_(argv));
// Dest is optional.
argv[1] = "/,,0";
ASSERT_TRUE(parse_args_(argv));
}
// Invalid calls to the binding option.
TEST_F(CliTest, invalid_binding) {
std::vector<std::string> argv = {"-v", "-b", "", "/bin/sh"};
// Missing source.
argv[2] = "";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Too many args.
argv[2] = "/,/,0,what";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Missing mount namespace/etc...
argv = {"-b", "/", "/bin/sh"};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Bad value for <writable>.
argv = {"-b", "/,,writable", "/bin/sh"};
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}
// Valid calls to the mount option.
TEST_F(CliTest, valid_mount) {
std::vector<std::string> argv = {"-v", "-k", "", "/bin/sh"};
// Flags & data are optional.
argv[2] = "none,/,none";
ASSERT_TRUE(parse_args_(argv));
// Data is optional.
argv[2] = "none,/,none,0xe";
ASSERT_TRUE(parse_args_(argv));
// Flags are optional.
argv[2] = "none,/,none,,mode=755";
ASSERT_TRUE(parse_args_(argv));
// Multiple data options to the kernel.
argv[2] = "none,/,none,0xe,mode=755,uid=0,gid=10";
ASSERT_TRUE(parse_args_(argv));
// Single MS constant.
argv[2] = "none,/,none,MS_NODEV,mode=755";
ASSERT_TRUE(parse_args_(argv));
// Multiple MS constants.
argv[2] = "none,/,none,MS_NODEV|MS_NOEXEC,mode=755";
ASSERT_TRUE(parse_args_(argv));
// Mixed constant & number.
argv[2] = "none,/,none,MS_NODEV|0xf,mode=755";
ASSERT_TRUE(parse_args_(argv));
}
// Invalid calls to the mount option.
TEST_F(CliTest, invalid_mount) {
std::vector<std::string> argv = {"-v", "-k", "", "/bin/sh"};
// Missing source.
argv[2] = "";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Missing dest.
argv[2] = "none";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Missing type.
argv[2] = "none,/";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
// Unknown MS constant.
argv[2] = "none,/,none,MS_WHOOPS";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}
// Valid calls to the remount mode option.
TEST_F(CliTest, valid_remount_mode) {
std::vector<std::string> argv = {"-v", "", "/bin/sh"};
// Mode is optional.
argv[1] = "-K";
ASSERT_TRUE(parse_args_(argv));
// This should list all valid modes.
const std::vector<std::string> modes = {
"shared",
"private",
"slave",
"unbindable",
};
for (const auto& mode : modes) {
argv[1] = "-K" + mode;
ASSERT_TRUE(parse_args_(argv));
}
}
// Invalid calls to the remount mode option.
TEST_F(CliTest, invalid_remount_mode) {
std::vector<std::string> argv = {"-v", "", "/bin/sh"};
// Unknown mode.
argv[1] = "-Kfoo";
ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}