Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/freebsd-src
Path: blob/main/contrib/kyua/utils/process/isolation_test.cpp
48178 views
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
31
extern "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
58
namespace fs = utils::fs;
59
namespace passwd = utils::passwd;
60
namespace process = utils::process;
61
62
using utils::none;
63
using utils::optional;
64
65
66
namespace {
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.
77
template< typename Hook >
78
static process::status
79
fork_and_run(Hook hook)
80
{
81
std::unique_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.
95
static void
96
check_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.
140
class check_drop_privileges {
141
/// The user to drop the privileges to.
142
const passwd::user _unprivileged_user;
143
144
public:
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.
200
static void
201
check_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.
209
class check_enter_work_directory {
210
/// Directory to enter. May be releative.
211
const fs::path _directory;
212
213
public:
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.
241
static void
242
check_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.
252
static void
253
check_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.
273
static void
274
check_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.
286
static void
287
check_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
297
ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__clean_environment);
298
ATF_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
319
ATF_TEST_CASE(isolate_child__other_user_when_unprivileged);
320
ATF_TEST_CASE_HEAD(isolate_child__other_user_when_unprivileged)
321
{
322
set_md_var("require.user", "unprivileged");
323
}
324
ATF_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
338
ATF_TEST_CASE(isolate_child__drop_privileges);
339
ATF_TEST_CASE_HEAD(isolate_child__drop_privileges)
340
{
341
set_md_var("require.config", "unprivileged-user");
342
set_md_var("require.user", "root");
343
}
344
ATF_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
356
ATF_TEST_CASE(isolate_child__drop_privileges_fail_uid);
357
ATF_TEST_CASE_HEAD(isolate_child__drop_privileges_fail_uid)
358
{
359
set_md_var("require.user", "unprivileged");
360
}
361
ATF_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
382
ATF_TEST_CASE(isolate_child__drop_privileges_fail_gid);
383
ATF_TEST_CASE_HEAD(isolate_child__drop_privileges_fail_gid)
384
{
385
set_md_var("require.user", "unprivileged");
386
}
387
ATF_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
408
ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enable_core_dumps);
409
ATF_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
428
ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enter_work_directory);
429
ATF_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
440
ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enter_work_directory_failure);
441
ATF_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
453
ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__new_session);
454
ATF_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
462
ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__no_terminal);
463
ATF_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
471
ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__process_group);
472
ATF_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
480
ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__reset_umask);
481
ATF_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.
494
static void
495
do_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
521
ATF_TEST_CASE_WITHOUT_HEAD(isolate_path__no_user);
522
ATF_TEST_CASE_BODY(isolate_path__no_user)
523
{
524
do_isolate_path_test(none, none, none);
525
}
526
527
528
ATF_TEST_CASE_WITHOUT_HEAD(isolate_path__same_user);
529
ATF_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
536
ATF_TEST_CASE(isolate_path__other_user_when_unprivileged);
537
ATF_TEST_CASE_HEAD(isolate_path__other_user_when_unprivileged)
538
{
539
set_md_var("require.user", "unprivileged");
540
}
541
ATF_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
551
ATF_TEST_CASE(isolate_path__drop_privileges);
552
ATF_TEST_CASE_HEAD(isolate_path__drop_privileges)
553
{
554
set_md_var("require.config", "unprivileged-user");
555
set_md_var("require.user", "root");
556
}
557
ATF_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
567
ATF_TEST_CASE(isolate_path__drop_privileges_only_uid);
568
ATF_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
}
573
ATF_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
584
ATF_TEST_CASE(isolate_path__drop_privileges_only_gid);
585
ATF_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
}
590
ATF_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
601
ATF_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
624