Path: blob/main/contrib/kyua/utils/process/isolation_test.cpp
48178 views
// Copyright 2014 The Kyua Authors.1// All rights reserved.2//3// Redistribution and use in source and binary forms, with or without4// modification, are permitted provided that the following conditions are5// met:6//7// * Redistributions of source code must retain the above copyright8// notice, this list of conditions and the following disclaimer.9// * Redistributions in binary form must reproduce the above copyright10// notice, this list of conditions and the following disclaimer in the11// documentation and/or other materials provided with the distribution.12// * Neither the name of Google Inc. nor the names of its contributors13// may be used to endorse or promote products derived from this software14// without specific prior written permission.15//16// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS17// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT18// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR19// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT20// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,21// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT22// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,23// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY24// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT25// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE26// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.2728#include "utils/process/isolation.hpp"2930extern "C" {31#include <sys/types.h>32#include <sys/resource.h>33#include <sys/stat.h>3435#include <unistd.h>36}3738#include <cerrno>39#include <cstdlib>40#include <fstream>41#include <iostream>4243#include <atf-c++.hpp>4445#include "utils/defs.hpp"46#include "utils/env.hpp"47#include "utils/format/macros.hpp"48#include "utils/fs/operations.hpp"49#include "utils/fs/path.hpp"50#include "utils/optional.ipp"51#include "utils/passwd.hpp"52#include "utils/process/child.ipp"53#include "utils/process/status.hpp"54#include "utils/sanity.hpp"55#include "utils/test_utils.ipp"5657namespace fs = utils::fs;58namespace passwd = utils::passwd;59namespace process = utils::process;6061using utils::none;62using utils::optional;636465namespace {666768/// Runs the given hook in a subprocess.69///70/// \param hook The code to run in the subprocess.71///72/// \return The status of the subprocess for further validation.73///74/// \post The subprocess.stdout and subprocess.stderr files, created in the75/// current directory, contain the output of the subprocess.76template< typename Hook >77static process::status78fork_and_run(Hook hook)79{80std::unique_ptr< process::child > child = process::child::fork_files(81hook, fs::path("subprocess.stdout"), fs::path("subprocess.stderr"));82const process::status status = child->wait();8384atf::utils::cat_file("subprocess.stdout", "isolated child stdout: ");85atf::utils::cat_file("subprocess.stderr", "isolated child stderr: ");8687return status;88}899091/// Subprocess that validates the cleanliness of the environment.92///93/// \post Exits with success if the environment is clean; failure otherwise.94static void95check_clean_environment(void)96{97fs::mkdir(fs::path("some-directory"), 0755);98process::isolate_child(none, fs::path("some-directory"));99100bool failed = false;101102const char* empty[] = { "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",103"LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC",104"LC_TIME", NULL };105const char** iter;106for (iter = empty; *iter != NULL; ++iter) {107if (utils::getenv(*iter)) {108failed = true;109std::cout << F("%s was not unset\n") % *iter;110}111}112113if (utils::getenv_with_default("HOME", "") != "some-directory") {114failed = true;115std::cout << "HOME was not set to the work directory\n";116}117118if (utils::getenv_with_default("TMPDIR", "") != "some-directory") {119failed = true;120std::cout << "TMPDIR was not set to the work directory\n";121}122123if (utils::getenv_with_default("TZ", "") != "UTC") {124failed = true;125std::cout << "TZ was not set to UTC\n";126}127128if (utils::getenv_with_default("LEAVE_ME_ALONE", "") != "kill-some-day") {129failed = true;130std::cout << "LEAVE_ME_ALONE was modified while it should not have "131"been\n";132}133134std::exit(failed ? EXIT_FAILURE : EXIT_SUCCESS);135}136137138/// Subprocess that checks if user privileges are dropped.139class check_drop_privileges {140/// The user to drop the privileges to.141const passwd::user _unprivileged_user;142143public:144/// Constructor.145///146/// \param unprivileged_user The user to drop the privileges to.147check_drop_privileges(const passwd::user& unprivileged_user) :148_unprivileged_user(unprivileged_user)149{150}151152/// Body of the subprocess.153///154/// \post Exits with success if the process has dropped privileges as155/// expected.156void157operator()(void) const158{159fs::mkdir(fs::path("subdir"), 0755);160process::isolate_child(utils::make_optional(_unprivileged_user),161fs::path("subdir"));162163if (::getuid() == 0) {164std::cout << "UID is still 0\n";165std::exit(EXIT_FAILURE);166}167168if (::getgid() == 0) {169std::cout << "GID is still 0\n";170std::exit(EXIT_FAILURE);171}172173::gid_t groups[1];174if (::getgroups(1, groups) == -1) {175// Should only fail if we get more than one group notifying about176// not enough space in the groups variable to store the whole177// result.178INV(errno == EINVAL);179std::exit(EXIT_FAILURE);180}181if (groups[0] == 0) {182std::cout << "Primary group is still 0\n";183std::exit(EXIT_FAILURE);184}185186std::ofstream output("file.txt");187if (!output) {188std::cout << "Cannot write to isolated directory; owner not "189"changed?\n";190std::exit(EXIT_FAILURE);191}192193std::exit(EXIT_SUCCESS);194}195};196197198/// Subprocess that dumps core to validate core dumping abilities.199static void200check_enable_core_dumps(void)201{202process::isolate_child(none, fs::path("."));203std::abort();204}205206207/// Subprocess that checks if the work directory is entered.208class check_enter_work_directory {209/// Directory to enter. May be releative.210const fs::path _directory;211212public:213/// Constructor.214///215/// \param directory Directory to enter.216check_enter_work_directory(const fs::path& directory) :217_directory(directory)218{219}220221/// Body of the subprocess.222///223/// \post Exits with success if the process has entered the given work224/// directory; false otherwise.225void226operator()(void) const227{228const fs::path exp_subdir = fs::current_path() / _directory;229process::isolate_child(none, _directory);230std::exit(fs::current_path() == exp_subdir ?231EXIT_SUCCESS : EXIT_FAILURE);232}233};234235236/// Subprocess that validates that it owns a session.237///238/// \post Exits with success if the process lives in its own session;239/// failure otherwise.240static void241check_new_session(void)242{243process::isolate_child(none, fs::path("."));244std::exit(::getsid(::getpid()) == ::getpid() ? EXIT_SUCCESS : EXIT_FAILURE);245}246247248/// Subprocess that validates the disconnection from any terminal.249///250/// \post Exits with success if the environment is clean; failure otherwise.251static void252check_no_terminal(void)253{254process::isolate_child(none, fs::path("."));255256const char* const args[] = {257"/bin/sh",258"-i",259"-c",260"echo success",261NULL262};263::execv("/bin/sh", UTILS_UNCONST(char*, args));264std::abort();265}266267268/// Subprocess that validates that it has become the leader of a process group.269///270/// \post Exits with success if the process lives in its own process group;271/// failure otherwise.272static void273check_process_group(void)274{275process::isolate_child(none, fs::path("."));276std::exit(::getpgid(::getpid()) == ::getpid() ?277EXIT_SUCCESS : EXIT_FAILURE);278}279280281/// Subprocess that validates that the umask has been reset.282///283/// \post Exits with success if the umask matches the expected value; failure284/// otherwise.285static void286check_umask(void)287{288process::isolate_child(none, fs::path("."));289std::exit(::umask(0) == 0022 ? EXIT_SUCCESS : EXIT_FAILURE);290}291292293} // anonymous namespace294295296ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__clean_environment);297ATF_TEST_CASE_BODY(isolate_child__clean_environment)298{299utils::setenv("HOME", "/non-existent/directory");300utils::setenv("TMPDIR", "/non-existent/directory");301utils::setenv("LANG", "C");302utils::setenv("LC_ALL", "C");303utils::setenv("LC_COLLATE", "C");304utils::setenv("LC_CTYPE", "C");305utils::setenv("LC_MESSAGES", "C");306utils::setenv("LC_MONETARY", "C");307utils::setenv("LC_NUMERIC", "C");308utils::setenv("LC_TIME", "C");309utils::setenv("LEAVE_ME_ALONE", "kill-some-day");310utils::setenv("TZ", "EST+5");311312const process::status status = fork_and_run(check_clean_environment);313ATF_REQUIRE(status.exited());314ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());315}316317318ATF_TEST_CASE(isolate_child__other_user_when_unprivileged);319ATF_TEST_CASE_HEAD(isolate_child__other_user_when_unprivileged)320{321set_md_var("require.user", "unprivileged");322}323ATF_TEST_CASE_BODY(isolate_child__other_user_when_unprivileged)324{325const passwd::user user = passwd::current_user();326327passwd::user other_user = user;328other_user.uid += 1;329other_user.gid += 1;330process::isolate_child(utils::make_optional(other_user), fs::path("."));331332ATF_REQUIRE_EQ(user.uid, ::getuid());333ATF_REQUIRE_EQ(user.gid, ::getgid());334}335336337ATF_TEST_CASE(isolate_child__drop_privileges);338ATF_TEST_CASE_HEAD(isolate_child__drop_privileges)339{340set_md_var("require.config", "unprivileged-user");341set_md_var("require.user", "root");342}343ATF_TEST_CASE_BODY(isolate_child__drop_privileges)344{345const passwd::user unprivileged_user = passwd::find_user_by_name(346get_config_var("unprivileged-user"));347348const process::status status = fork_and_run(check_drop_privileges(349unprivileged_user));350ATF_REQUIRE(status.exited());351ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());352}353354355ATF_TEST_CASE(isolate_child__drop_privileges_fail_uid);356ATF_TEST_CASE_HEAD(isolate_child__drop_privileges_fail_uid)357{358set_md_var("require.user", "unprivileged");359}360ATF_TEST_CASE_BODY(isolate_child__drop_privileges_fail_uid)361{362// Fake the current user as root so that we bypass the protections in363// isolate_child that prevent us from attempting a user switch when we are364// not root. We do this so we can trigger the setuid failure.365passwd::user root = passwd::user("root", 0, 0);366ATF_REQUIRE(root.is_root());367passwd::set_current_user_for_testing(root);368369passwd::user unprivileged_user = passwd::current_user();370unprivileged_user.uid += 1;371372const process::status status = fork_and_run(check_drop_privileges(373unprivileged_user));374ATF_REQUIRE(status.exited());375ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus());376ATF_REQUIRE(atf::utils::grep_file("(chown|setuid).*failed",377"subprocess.stderr"));378}379380381ATF_TEST_CASE(isolate_child__drop_privileges_fail_gid);382ATF_TEST_CASE_HEAD(isolate_child__drop_privileges_fail_gid)383{384set_md_var("require.user", "unprivileged");385}386ATF_TEST_CASE_BODY(isolate_child__drop_privileges_fail_gid)387{388// Fake the current user as root so that we bypass the protections in389// isolate_child that prevent us from attempting a user switch when we are390// not root. We do this so we can trigger the setgid failure.391passwd::user root = passwd::user("root", 0, 0);392ATF_REQUIRE(root.is_root());393passwd::set_current_user_for_testing(root);394395passwd::user unprivileged_user = passwd::current_user();396unprivileged_user.gid += 1;397398const process::status status = fork_and_run(check_drop_privileges(399unprivileged_user));400ATF_REQUIRE(status.exited());401ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus());402ATF_REQUIRE(atf::utils::grep_file("(chown|setgid).*failed",403"subprocess.stderr"));404}405406407ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enable_core_dumps);408ATF_TEST_CASE_BODY(isolate_child__enable_core_dumps)409{410utils::require_run_coredump_tests(this);411412struct ::rlimit rl;413if (::getrlimit(RLIMIT_CORE, &rl) == -1)414fail("Failed to query the core size limit");415if (rl.rlim_cur == 0 || rl.rlim_max == 0)416skip("Maximum core size is zero; cannot run test");417rl.rlim_cur = 0;418if (::setrlimit(RLIMIT_CORE, &rl) == -1)419fail("Failed to lower the core size limit");420421const process::status status = fork_and_run(check_enable_core_dumps);422ATF_REQUIRE(status.signaled());423ATF_REQUIRE(status.coredump());424}425426427ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enter_work_directory);428ATF_TEST_CASE_BODY(isolate_child__enter_work_directory)429{430const fs::path directory("some/sub/directory");431fs::mkdir_p(directory, 0755);432const process::status status = fork_and_run(433check_enter_work_directory(directory));434ATF_REQUIRE(status.exited());435ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());436}437438439ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enter_work_directory_failure);440ATF_TEST_CASE_BODY(isolate_child__enter_work_directory_failure)441{442const fs::path directory("some/sub/directory");443const process::status status = fork_and_run(444check_enter_work_directory(directory));445ATF_REQUIRE(status.exited());446ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus());447ATF_REQUIRE(atf::utils::grep_file("chdir\\(some/sub/directory\\) failed",448"subprocess.stderr"));449}450451452ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__new_session);453ATF_TEST_CASE_BODY(isolate_child__new_session)454{455const process::status status = fork_and_run(check_new_session);456ATF_REQUIRE(status.exited());457ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());458}459460461ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__no_terminal);462ATF_TEST_CASE_BODY(isolate_child__no_terminal)463{464const process::status status = fork_and_run(check_no_terminal);465ATF_REQUIRE(status.exited());466ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());467}468469470ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__process_group);471ATF_TEST_CASE_BODY(isolate_child__process_group)472{473const process::status status = fork_and_run(check_process_group);474ATF_REQUIRE(status.exited());475ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());476}477478479ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__reset_umask);480ATF_TEST_CASE_BODY(isolate_child__reset_umask)481{482const process::status status = fork_and_run(check_umask);483ATF_REQUIRE(status.exited());484ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());485}486487488/// Executes isolate_path() and compares the on-disk changes to expected values.489///490/// \param unprivileged_user The user to pass to isolate_path; may be none.491/// \param exp_uid Expected UID or none to expect the old value.492/// \param exp_gid Expected GID or none to expect the old value.493static void494do_isolate_path_test(const optional< passwd::user >& unprivileged_user,495const optional< uid_t >& exp_uid,496const optional< gid_t >& exp_gid)497{498const fs::path dir("dir");499fs::mkdir(dir, 0755);500struct ::stat old_sb;501ATF_REQUIRE(::stat(dir.c_str(), &old_sb) != -1);502503process::isolate_path(unprivileged_user, dir);504505struct ::stat new_sb;506ATF_REQUIRE(::stat(dir.c_str(), &new_sb) != -1);507508if (exp_uid)509ATF_REQUIRE_EQ(exp_uid.get(), new_sb.st_uid);510else511ATF_REQUIRE_EQ(old_sb.st_uid, new_sb.st_uid);512513if (exp_gid)514ATF_REQUIRE_EQ(exp_gid.get(), new_sb.st_gid);515else516ATF_REQUIRE_EQ(old_sb.st_gid, new_sb.st_gid);517}518519520ATF_TEST_CASE_WITHOUT_HEAD(isolate_path__no_user);521ATF_TEST_CASE_BODY(isolate_path__no_user)522{523do_isolate_path_test(none, none, none);524}525526527ATF_TEST_CASE_WITHOUT_HEAD(isolate_path__same_user);528ATF_TEST_CASE_BODY(isolate_path__same_user)529{530do_isolate_path_test(utils::make_optional(passwd::current_user()),531none, none);532}533534535ATF_TEST_CASE(isolate_path__other_user_when_unprivileged);536ATF_TEST_CASE_HEAD(isolate_path__other_user_when_unprivileged)537{538set_md_var("require.user", "unprivileged");539}540ATF_TEST_CASE_BODY(isolate_path__other_user_when_unprivileged)541{542passwd::user user = passwd::current_user();543user.uid += 1;544user.gid += 1;545546do_isolate_path_test(utils::make_optional(user), none, none);547}548549550ATF_TEST_CASE(isolate_path__drop_privileges);551ATF_TEST_CASE_HEAD(isolate_path__drop_privileges)552{553set_md_var("require.config", "unprivileged-user");554set_md_var("require.user", "root");555}556ATF_TEST_CASE_BODY(isolate_path__drop_privileges)557{558const passwd::user unprivileged_user = passwd::find_user_by_name(559get_config_var("unprivileged-user"));560do_isolate_path_test(utils::make_optional(unprivileged_user),561utils::make_optional(unprivileged_user.uid),562utils::make_optional(unprivileged_user.gid));563}564565566ATF_TEST_CASE(isolate_path__drop_privileges_only_uid);567ATF_TEST_CASE_HEAD(isolate_path__drop_privileges_only_uid)568{569set_md_var("require.config", "unprivileged-user");570set_md_var("require.user", "root");571}572ATF_TEST_CASE_BODY(isolate_path__drop_privileges_only_uid)573{574passwd::user unprivileged_user = passwd::find_user_by_name(575get_config_var("unprivileged-user"));576unprivileged_user.gid = ::getgid();577do_isolate_path_test(utils::make_optional(unprivileged_user),578utils::make_optional(unprivileged_user.uid),579none);580}581582583ATF_TEST_CASE(isolate_path__drop_privileges_only_gid);584ATF_TEST_CASE_HEAD(isolate_path__drop_privileges_only_gid)585{586set_md_var("require.config", "unprivileged-user");587set_md_var("require.user", "root");588}589ATF_TEST_CASE_BODY(isolate_path__drop_privileges_only_gid)590{591passwd::user unprivileged_user = passwd::find_user_by_name(592get_config_var("unprivileged-user"));593unprivileged_user.uid = ::getuid();594do_isolate_path_test(utils::make_optional(unprivileged_user),595none,596utils::make_optional(unprivileged_user.gid));597}598599600ATF_INIT_TEST_CASES(tcs)601{602ATF_ADD_TEST_CASE(tcs, isolate_child__clean_environment);603ATF_ADD_TEST_CASE(tcs, isolate_child__other_user_when_unprivileged);604ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges);605ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges_fail_uid);606ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges_fail_gid);607ATF_ADD_TEST_CASE(tcs, isolate_child__enable_core_dumps);608ATF_ADD_TEST_CASE(tcs, isolate_child__enter_work_directory);609ATF_ADD_TEST_CASE(tcs, isolate_child__enter_work_directory_failure);610ATF_ADD_TEST_CASE(tcs, isolate_child__new_session);611ATF_ADD_TEST_CASE(tcs, isolate_child__no_terminal);612ATF_ADD_TEST_CASE(tcs, isolate_child__process_group);613ATF_ADD_TEST_CASE(tcs, isolate_child__reset_umask);614615ATF_ADD_TEST_CASE(tcs, isolate_path__no_user);616ATF_ADD_TEST_CASE(tcs, isolate_path__same_user);617ATF_ADD_TEST_CASE(tcs, isolate_path__other_user_when_unprivileged);618ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges);619ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges_only_uid);620ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges_only_gid);621}622623624