1// Copyright 2014 The Kyua Authors.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8// * Redistributions of source code must retain the above copyright
9//   notice, this list of conditions and the following disclaimer.
10// * Redistributions in binary form must reproduce the above copyright
11//   notice, this list of conditions and the following disclaimer in the
12//   documentation and/or other materials provided with the distribution.
13// * Neither the name of Google Inc. nor the names of its contributors
14//   may be used to endorse or promote products derived from this software
15//   without specific prior written permission.
16//
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29#include "utils/process/isolation.hpp"
30
31extern "C" {
32#include <sys/types.h>
33#include <sys/resource.h>
34#include <sys/stat.h>
35
36#include <unistd.h>
37}
38
39#include <cerrno>
40#include <cstdlib>
41#include <fstream>
42#include <iostream>
43
44#include <atf-c++.hpp>
45
46#include "utils/defs.hpp"
47#include "utils/env.hpp"
48#include "utils/format/macros.hpp"
49#include "utils/fs/operations.hpp"
50#include "utils/fs/path.hpp"
51#include "utils/optional.ipp"
52#include "utils/passwd.hpp"
53#include "utils/process/child.ipp"
54#include "utils/process/status.hpp"
55#include "utils/sanity.hpp"
56#include "utils/test_utils.ipp"
57
58namespace fs = utils::fs;
59namespace passwd = utils::passwd;
60namespace process = utils::process;
61
62using utils::none;
63using utils::optional;
64
65
66namespace {
67
68
69/// Runs the given hook in a subprocess.
70///
71/// \param hook The code to run in the subprocess.
72///
73/// \return The status of the subprocess for further validation.
74///
75/// \post The subprocess.stdout and subprocess.stderr files, created in the
76/// current directory, contain the output of the subprocess.
77template< typename Hook >
78static process::status
79fork_and_run(Hook hook)
80{
81    std::auto_ptr< process::child > child = process::child::fork_files(
82        hook, fs::path("subprocess.stdout"), fs::path("subprocess.stderr"));
83    const process::status status = child->wait();
84
85    atf::utils::cat_file("subprocess.stdout", "isolated child stdout: ");
86    atf::utils::cat_file("subprocess.stderr", "isolated child stderr: ");
87
88    return status;
89}
90
91
92/// Subprocess that validates the cleanliness of the environment.
93///
94/// \post Exits with success if the environment is clean; failure otherwise.
95static void
96check_clean_environment(void)
97{
98    fs::mkdir(fs::path("some-directory"), 0755);
99    process::isolate_child(none, fs::path("some-directory"));
100
101    bool failed = false;
102
103    const char* empty[] = { "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
104                            "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC",
105                            "LC_TIME", NULL };
106    const char** iter;
107    for (iter = empty; *iter != NULL; ++iter) {
108        if (utils::getenv(*iter)) {
109            failed = true;
110            std::cout << F("%s was not unset\n") % *iter;
111        }
112    }
113
114    if (utils::getenv_with_default("HOME", "") != "some-directory") {
115        failed = true;
116        std::cout << "HOME was not set to the work directory\n";
117    }
118
119    if (utils::getenv_with_default("TMPDIR", "") != "some-directory") {
120        failed = true;
121        std::cout << "TMPDIR was not set to the work directory\n";
122    }
123
124    if (utils::getenv_with_default("TZ", "") != "UTC") {
125        failed = true;
126        std::cout << "TZ was not set to UTC\n";
127    }
128
129    if (utils::getenv_with_default("LEAVE_ME_ALONE", "") != "kill-some-day") {
130        failed = true;
131        std::cout << "LEAVE_ME_ALONE was modified while it should not have "
132            "been\n";
133    }
134
135    std::exit(failed ? EXIT_FAILURE : EXIT_SUCCESS);
136}
137
138
139/// Subprocess that checks if user privileges are dropped.
140class check_drop_privileges {
141    /// The user to drop the privileges to.
142    const passwd::user _unprivileged_user;
143
144public:
145    /// Constructor.
146    ///
147    /// \param unprivileged_user The user to drop the privileges to.
148    check_drop_privileges(const passwd::user& unprivileged_user) :
149        _unprivileged_user(unprivileged_user)
150    {
151    }
152
153    /// Body of the subprocess.
154    ///
155    /// \post Exits with success if the process has dropped privileges as
156    /// expected.
157    void
158    operator()(void) const
159    {
160        fs::mkdir(fs::path("subdir"), 0755);
161        process::isolate_child(utils::make_optional(_unprivileged_user),
162                               fs::path("subdir"));
163
164        if (::getuid() == 0) {
165            std::cout << "UID is still 0\n";
166            std::exit(EXIT_FAILURE);
167        }
168
169        if (::getgid() == 0) {
170            std::cout << "GID is still 0\n";
171            std::exit(EXIT_FAILURE);
172        }
173
174        ::gid_t groups[1];
175        if (::getgroups(1, groups) == -1) {
176            // Should only fail if we get more than one group notifying about
177            // not enough space in the groups variable to store the whole
178            // result.
179            INV(errno == EINVAL);
180            std::exit(EXIT_FAILURE);
181        }
182        if (groups[0] == 0) {
183            std::cout << "Primary group is still 0\n";
184            std::exit(EXIT_FAILURE);
185        }
186
187        std::ofstream output("file.txt");
188        if (!output) {
189            std::cout << "Cannot write to isolated directory; owner not "
190                "changed?\n";
191            std::exit(EXIT_FAILURE);
192        }
193
194        std::exit(EXIT_SUCCESS);
195    }
196};
197
198
199/// Subprocess that dumps core to validate core dumping abilities.
200static void
201check_enable_core_dumps(void)
202{
203    process::isolate_child(none, fs::path("."));
204    std::abort();
205}
206
207
208/// Subprocess that checks if the work directory is entered.
209class check_enter_work_directory {
210    /// Directory to enter.  May be releative.
211    const fs::path _directory;
212
213public:
214    /// Constructor.
215    ///
216    /// \param directory Directory to enter.
217    check_enter_work_directory(const fs::path& directory) :
218        _directory(directory)
219    {
220    }
221
222    /// Body of the subprocess.
223    ///
224    /// \post Exits with success if the process has entered the given work
225    /// directory; false otherwise.
226    void
227    operator()(void) const
228    {
229        const fs::path exp_subdir = fs::current_path() / _directory;
230        process::isolate_child(none, _directory);
231        std::exit(fs::current_path() == exp_subdir ?
232                  EXIT_SUCCESS : EXIT_FAILURE);
233    }
234};
235
236
237/// Subprocess that validates that it owns a session.
238///
239/// \post Exits with success if the process lives in its own session;
240/// failure otherwise.
241static void
242check_new_session(void)
243{
244    process::isolate_child(none, fs::path("."));
245    std::exit(::getsid(::getpid()) == ::getpid() ? EXIT_SUCCESS : EXIT_FAILURE);
246}
247
248
249/// Subprocess that validates the disconnection from any terminal.
250///
251/// \post Exits with success if the environment is clean; failure otherwise.
252static void
253check_no_terminal(void)
254{
255    process::isolate_child(none, fs::path("."));
256
257    const char* const args[] = {
258        "/bin/sh",
259        "-i",
260        "-c",
261        "echo success",
262        NULL
263    };
264    ::execv("/bin/sh", UTILS_UNCONST(char*, args));
265    std::abort();
266}
267
268
269/// Subprocess that validates that it has become the leader of a process group.
270///
271/// \post Exits with success if the process lives in its own process group;
272/// failure otherwise.
273static void
274check_process_group(void)
275{
276    process::isolate_child(none, fs::path("."));
277    std::exit(::getpgid(::getpid()) == ::getpid() ?
278              EXIT_SUCCESS : EXIT_FAILURE);
279}
280
281
282/// Subprocess that validates that the umask has been reset.
283///
284/// \post Exits with success if the umask matches the expected value; failure
285/// otherwise.
286static void
287check_umask(void)
288{
289    process::isolate_child(none, fs::path("."));
290    std::exit(::umask(0) == 0022 ? EXIT_SUCCESS : EXIT_FAILURE);
291}
292
293
294}  // anonymous namespace
295
296
297ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__clean_environment);
298ATF_TEST_CASE_BODY(isolate_child__clean_environment)
299{
300    utils::setenv("HOME", "/non-existent/directory");
301    utils::setenv("TMPDIR", "/non-existent/directory");
302    utils::setenv("LANG", "C");
303    utils::setenv("LC_ALL", "C");
304    utils::setenv("LC_COLLATE", "C");
305    utils::setenv("LC_CTYPE", "C");
306    utils::setenv("LC_MESSAGES", "C");
307    utils::setenv("LC_MONETARY", "C");
308    utils::setenv("LC_NUMERIC", "C");
309    utils::setenv("LC_TIME", "C");
310    utils::setenv("LEAVE_ME_ALONE", "kill-some-day");
311    utils::setenv("TZ", "EST+5");
312
313    const process::status status = fork_and_run(check_clean_environment);
314    ATF_REQUIRE(status.exited());
315    ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
316}
317
318
319ATF_TEST_CASE(isolate_child__other_user_when_unprivileged);
320ATF_TEST_CASE_HEAD(isolate_child__other_user_when_unprivileged)
321{
322    set_md_var("require.user", "unprivileged");
323}
324ATF_TEST_CASE_BODY(isolate_child__other_user_when_unprivileged)
325{
326    const passwd::user user = passwd::current_user();
327
328    passwd::user other_user = user;
329    other_user.uid += 1;
330    other_user.gid += 1;
331    process::isolate_child(utils::make_optional(other_user), fs::path("."));
332
333    ATF_REQUIRE_EQ(user.uid, ::getuid());
334    ATF_REQUIRE_EQ(user.gid, ::getgid());
335}
336
337
338ATF_TEST_CASE(isolate_child__drop_privileges);
339ATF_TEST_CASE_HEAD(isolate_child__drop_privileges)
340{
341    set_md_var("require.config", "unprivileged-user");
342    set_md_var("require.user", "root");
343}
344ATF_TEST_CASE_BODY(isolate_child__drop_privileges)
345{
346    const passwd::user unprivileged_user = passwd::find_user_by_name(
347        get_config_var("unprivileged-user"));
348
349    const process::status status = fork_and_run(check_drop_privileges(
350        unprivileged_user));
351    ATF_REQUIRE(status.exited());
352    ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
353}
354
355
356ATF_TEST_CASE(isolate_child__drop_privileges_fail_uid);
357ATF_TEST_CASE_HEAD(isolate_child__drop_privileges_fail_uid)
358{
359    set_md_var("require.user", "unprivileged");
360}
361ATF_TEST_CASE_BODY(isolate_child__drop_privileges_fail_uid)
362{
363    // Fake the current user as root so that we bypass the protections in
364    // isolate_child that prevent us from attempting a user switch when we are
365    // not root.  We do this so we can trigger the setuid failure.
366    passwd::user root = passwd::user("root", 0, 0);
367    ATF_REQUIRE(root.is_root());
368    passwd::set_current_user_for_testing(root);
369
370    passwd::user unprivileged_user = passwd::current_user();
371    unprivileged_user.uid += 1;
372
373    const process::status status = fork_and_run(check_drop_privileges(
374        unprivileged_user));
375    ATF_REQUIRE(status.exited());
376    ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus());
377    ATF_REQUIRE(atf::utils::grep_file("(chown|setuid).*failed",
378                                      "subprocess.stderr"));
379}
380
381
382ATF_TEST_CASE(isolate_child__drop_privileges_fail_gid);
383ATF_TEST_CASE_HEAD(isolate_child__drop_privileges_fail_gid)
384{
385    set_md_var("require.user", "unprivileged");
386}
387ATF_TEST_CASE_BODY(isolate_child__drop_privileges_fail_gid)
388{
389    // Fake the current user as root so that we bypass the protections in
390    // isolate_child that prevent us from attempting a user switch when we are
391    // not root.  We do this so we can trigger the setgid failure.
392    passwd::user root = passwd::user("root", 0, 0);
393    ATF_REQUIRE(root.is_root());
394    passwd::set_current_user_for_testing(root);
395
396    passwd::user unprivileged_user = passwd::current_user();
397    unprivileged_user.gid += 1;
398
399    const process::status status = fork_and_run(check_drop_privileges(
400        unprivileged_user));
401    ATF_REQUIRE(status.exited());
402    ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus());
403    ATF_REQUIRE(atf::utils::grep_file("(chown|setgid).*failed",
404                                      "subprocess.stderr"));
405}
406
407
408ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enable_core_dumps);
409ATF_TEST_CASE_BODY(isolate_child__enable_core_dumps)
410{
411    utils::require_run_coredump_tests(this);
412
413    struct ::rlimit rl;
414    if (::getrlimit(RLIMIT_CORE, &rl) == -1)
415        fail("Failed to query the core size limit");
416    if (rl.rlim_cur == 0 || rl.rlim_max == 0)
417        skip("Maximum core size is zero; cannot run test");
418    rl.rlim_cur = 0;
419    if (::setrlimit(RLIMIT_CORE, &rl) == -1)
420        fail("Failed to lower the core size limit");
421
422    const process::status status = fork_and_run(check_enable_core_dumps);
423    ATF_REQUIRE(status.signaled());
424    ATF_REQUIRE(status.coredump());
425}
426
427
428ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enter_work_directory);
429ATF_TEST_CASE_BODY(isolate_child__enter_work_directory)
430{
431    const fs::path directory("some/sub/directory");
432    fs::mkdir_p(directory, 0755);
433    const process::status status = fork_and_run(
434        check_enter_work_directory(directory));
435    ATF_REQUIRE(status.exited());
436    ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
437}
438
439
440ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enter_work_directory_failure);
441ATF_TEST_CASE_BODY(isolate_child__enter_work_directory_failure)
442{
443    const fs::path directory("some/sub/directory");
444    const process::status status = fork_and_run(
445        check_enter_work_directory(directory));
446    ATF_REQUIRE(status.exited());
447    ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus());
448    ATF_REQUIRE(atf::utils::grep_file("chdir\\(some/sub/directory\\) failed",
449                                      "subprocess.stderr"));
450}
451
452
453ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__new_session);
454ATF_TEST_CASE_BODY(isolate_child__new_session)
455{
456    const process::status status = fork_and_run(check_new_session);
457    ATF_REQUIRE(status.exited());
458    ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
459}
460
461
462ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__no_terminal);
463ATF_TEST_CASE_BODY(isolate_child__no_terminal)
464{
465    const process::status status = fork_and_run(check_no_terminal);
466    ATF_REQUIRE(status.exited());
467    ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
468}
469
470
471ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__process_group);
472ATF_TEST_CASE_BODY(isolate_child__process_group)
473{
474    const process::status status = fork_and_run(check_process_group);
475    ATF_REQUIRE(status.exited());
476    ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
477}
478
479
480ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__reset_umask);
481ATF_TEST_CASE_BODY(isolate_child__reset_umask)
482{
483    const process::status status = fork_and_run(check_umask);
484    ATF_REQUIRE(status.exited());
485    ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
486}
487
488
489/// Executes isolate_path() and compares the on-disk changes to expected values.
490///
491/// \param unprivileged_user The user to pass to isolate_path; may be none.
492/// \param exp_uid Expected UID or none to expect the old value.
493/// \param exp_gid Expected GID or none to expect the old value.
494static void
495do_isolate_path_test(const optional< passwd::user >& unprivileged_user,
496                     const optional< uid_t >& exp_uid,
497                     const optional< gid_t >& exp_gid)
498{
499    const fs::path dir("dir");
500    fs::mkdir(dir, 0755);
501    struct ::stat old_sb;
502    ATF_REQUIRE(::stat(dir.c_str(), &old_sb) != -1);
503
504    process::isolate_path(unprivileged_user, dir);
505
506    struct ::stat new_sb;
507    ATF_REQUIRE(::stat(dir.c_str(), &new_sb) != -1);
508
509    if (exp_uid)
510        ATF_REQUIRE_EQ(exp_uid.get(), new_sb.st_uid);
511    else
512        ATF_REQUIRE_EQ(old_sb.st_uid, new_sb.st_uid);
513
514    if (exp_gid)
515        ATF_REQUIRE_EQ(exp_gid.get(), new_sb.st_gid);
516    else
517        ATF_REQUIRE_EQ(old_sb.st_gid, new_sb.st_gid);
518}
519
520
521ATF_TEST_CASE_WITHOUT_HEAD(isolate_path__no_user);
522ATF_TEST_CASE_BODY(isolate_path__no_user)
523{
524    do_isolate_path_test(none, none, none);
525}
526
527
528ATF_TEST_CASE_WITHOUT_HEAD(isolate_path__same_user);
529ATF_TEST_CASE_BODY(isolate_path__same_user)
530{
531    do_isolate_path_test(utils::make_optional(passwd::current_user()),
532                         none, none);
533}
534
535
536ATF_TEST_CASE(isolate_path__other_user_when_unprivileged);
537ATF_TEST_CASE_HEAD(isolate_path__other_user_when_unprivileged)
538{
539    set_md_var("require.user", "unprivileged");
540}
541ATF_TEST_CASE_BODY(isolate_path__other_user_when_unprivileged)
542{
543    passwd::user user = passwd::current_user();
544    user.uid += 1;
545    user.gid += 1;
546
547    do_isolate_path_test(utils::make_optional(user), none, none);
548}
549
550
551ATF_TEST_CASE(isolate_path__drop_privileges);
552ATF_TEST_CASE_HEAD(isolate_path__drop_privileges)
553{
554    set_md_var("require.config", "unprivileged-user");
555    set_md_var("require.user", "root");
556}
557ATF_TEST_CASE_BODY(isolate_path__drop_privileges)
558{
559    const passwd::user unprivileged_user = passwd::find_user_by_name(
560        get_config_var("unprivileged-user"));
561    do_isolate_path_test(utils::make_optional(unprivileged_user),
562                         utils::make_optional(unprivileged_user.uid),
563                         utils::make_optional(unprivileged_user.gid));
564}
565
566
567ATF_TEST_CASE(isolate_path__drop_privileges_only_uid);
568ATF_TEST_CASE_HEAD(isolate_path__drop_privileges_only_uid)
569{
570    set_md_var("require.config", "unprivileged-user");
571    set_md_var("require.user", "root");
572}
573ATF_TEST_CASE_BODY(isolate_path__drop_privileges_only_uid)
574{
575    passwd::user unprivileged_user = passwd::find_user_by_name(
576        get_config_var("unprivileged-user"));
577    unprivileged_user.gid = ::getgid();
578    do_isolate_path_test(utils::make_optional(unprivileged_user),
579                         utils::make_optional(unprivileged_user.uid),
580                         none);
581}
582
583
584ATF_TEST_CASE(isolate_path__drop_privileges_only_gid);
585ATF_TEST_CASE_HEAD(isolate_path__drop_privileges_only_gid)
586{
587    set_md_var("require.config", "unprivileged-user");
588    set_md_var("require.user", "root");
589}
590ATF_TEST_CASE_BODY(isolate_path__drop_privileges_only_gid)
591{
592    passwd::user unprivileged_user = passwd::find_user_by_name(
593        get_config_var("unprivileged-user"));
594    unprivileged_user.uid = ::getuid();
595    do_isolate_path_test(utils::make_optional(unprivileged_user),
596                         none,
597                         utils::make_optional(unprivileged_user.gid));
598}
599
600
601ATF_INIT_TEST_CASES(tcs)
602{
603    ATF_ADD_TEST_CASE(tcs, isolate_child__clean_environment);
604    ATF_ADD_TEST_CASE(tcs, isolate_child__other_user_when_unprivileged);
605    ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges);
606    ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges_fail_uid);
607    ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges_fail_gid);
608    ATF_ADD_TEST_CASE(tcs, isolate_child__enable_core_dumps);
609    ATF_ADD_TEST_CASE(tcs, isolate_child__enter_work_directory);
610    ATF_ADD_TEST_CASE(tcs, isolate_child__enter_work_directory_failure);
611    ATF_ADD_TEST_CASE(tcs, isolate_child__new_session);
612    ATF_ADD_TEST_CASE(tcs, isolate_child__no_terminal);
613    ATF_ADD_TEST_CASE(tcs, isolate_child__process_group);
614    ATF_ADD_TEST_CASE(tcs, isolate_child__reset_umask);
615
616    ATF_ADD_TEST_CASE(tcs, isolate_path__no_user);
617    ATF_ADD_TEST_CASE(tcs, isolate_path__same_user);
618    ATF_ADD_TEST_CASE(tcs, isolate_path__other_user_when_unprivileged);
619    ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges);
620    ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges_only_uid);
621    ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges_only_gid);
622}
623