/* Copyright 2018 The ChromiumOS Authors * 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 #include #include #include "config_parser.h" #include "libminijail.h" #include "minijail0_cli.h" #include "test_util.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& 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 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; char **envp = NULL; int ret = parse_args(j, pargv.size(), const_cast(pargv.data()), NULL, exit_immediately, elftype, &preload_path, &envp); testing::internal::GetCapturedStdout(); minijail_destroy(j); return ret; } int parse_args_(const std::vector& 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 argv = {}; ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), ""); } // Should exit zero when we asked for help. TEST_F(CliTest, help) { std::vector 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 argv = {"/bin/sh"}; ASSERT_TRUE(parse_args_(argv)); } // Valid calls to the change user option. TEST_F(CliTest, valid_set_user) { std::vector 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 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 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 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 add-suppl-group option. TEST_F(CliTest, valid_add_supp_group) { std::vector argv = {"--add-suppl-group", "", "/bin/sh"}; argv[1] = kValidGroup; ASSERT_TRUE(parse_args_(argv)); argv[1] = kValidGid; ASSERT_TRUE(parse_args_(argv)); std::vector argv2 = {"--add-suppl-group", "", "--add-suppl-group", "", "/bin/sh"}; argv[1] = kValidGroup; argv[2] = kValidGid; ASSERT_TRUE(parse_args_(argv)); } // Invalid calls to the add-suppl-group option. TEST_F(CliTest, invalid_add_supp_group) { std::vector argv = {"--add-suppl-group", "", "/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), ""); } // Valid calls to the skip securebits option. TEST_F(CliTest, valid_skip_securebits) { // An empty string is the same as 0. std::vector 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 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 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 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 argv = {"--logging", "", "/bin/sh"}; // This should list all valid logging targets. const std::vector 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 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 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 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 argv = {"--profile", "", "/bin/sh"}; // This should list all valid profiles. const std::vector profiles = { "minimalistic-mountns", "minimalistic-mountns-nodev", }; 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 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 argv = {"-C", "/", "/bin/sh"}; ASSERT_TRUE(parse_args_(argv)); } // Valid calls to the pivot root option. TEST_F(CliTest, valid_pivot_root) { std::vector 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 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 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 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 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 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 . 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 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 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 argv = {"-v", "", "/bin/sh"}; // Mode is optional. argv[1] = "-K"; ASSERT_TRUE(parse_args_(argv)); // This should list all valid modes. const std::vector 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 argv = {"-v", "", "/bin/sh"}; // Unknown mode. argv[1] = "-Kfoo"; ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), ""); } TEST_F(CliTest, invalid_L_combo) { std::vector argv = {"", "", "", "/bin/sh"}; // Cannot call minijail0 with -L and a pre-compiled seccomp policy. argv[0] = "-L"; argv[1] = "--seccomp-bpf-binary"; argv[2] = "source"; ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), ""); argv[0] = "--seccomp-bpf-binary"; argv[1] = "source"; argv[2] = "-L"; ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), ""); } // Valid calls to the clear env option. TEST_F(CliTest, valid_clear_env) { std::vector argv = {"--env-reset", "/bin/sh"}; ASSERT_TRUE(parse_args_(argv)); } // Valid calls to the set env option. TEST_F(CliTest, valid_set_env) { std::vector argv1 = {"--env-add", "NAME=value", "/bin/sh"}; ASSERT_TRUE(parse_args_(argv1)); // multiple occurences are allowed. std::vector argv2 = {"--env-add", "A=b", "--env-add", "b=C=D", "/bin/sh"}; ASSERT_TRUE(parse_args_(argv2)); // --env-reset before any --env-add to not pass our own env. std::vector argv3 = {"--env-reset", "--env-add", "A=b", "/bin/sh"}; ASSERT_TRUE(parse_args_(argv3)); // --env-add before an --env-reset doesn't have any effect, but is allowed. std::vector argv4 = {"--env-add", "A=b", "--env-reset", "/bin/sh"}; ASSERT_TRUE(parse_args_(argv4)); } // Invalid calls to the set env options. TEST_F(CliTest, invalid_set_env) { // invalid env=value arguments. std::vector argv2 = {"--env-add", "", "/bin/sh"}; argv2[1] = "INVALID"; ASSERT_EXIT(parse_args_(argv2), testing::ExitedWithCode(1), ""); argv2[1] = "="; ASSERT_EXIT(parse_args_(argv2), testing::ExitedWithCode(1), ""); argv2[1] = "=foo"; ASSERT_EXIT(parse_args_(argv2), testing::ExitedWithCode(1), ""); } // Android unit tests do not support data file yet. #if !defined(__ANDROID__) TEST_F(CliTest, conf_parsing_invalid_key) { std::vector argv = {"--config", source_path("test/invalid.conf"), "/bin/sh"}; ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), ""); } TEST_F(CliTest, conf_parsing) { std::vector argv = {"--config", source_path("test/valid.conf"), "/bin/sh"}; ASSERT_TRUE(parse_args_(argv)); } TEST_F(CliTest, conf_parsing_with_dac_override) { std::vector argv = {"-c 2", "--config", source_path("test/valid.conf"), "/bin/sh"}; ASSERT_TRUE(parse_args_(argv)); } TEST_F(CliTest, conf_fs_path) { std::vector argv = {"-c 2", "--config", source_path("test/landlock.conf"), "/bin/sh"}; ASSERT_TRUE(parse_args_(argv)); } #endif // !__ANDROID__