1 /* Copyright 2018 The Chromium OS Authors. All rights reserved.
2 * Use of this source code is governed by a BSD-style license that can be
3 * found in the LICENSE file.
4 *
5 * Test the minijail0 CLI using gtest.
6 *
7 * Note: We don't verify that the minijail struct was set correctly from these
8 * flags as only libminijail.c knows that definition. If we wanted to improve
9 * this test, we'd have to pull that struct into a common (internal) header.
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14
15 #include <gtest/gtest.h>
16
17 #include "libminijail.h"
18 #include "minijail0_cli.h"
19
20 namespace {
21
22 constexpr char kValidUser[] = "nobody";
23 constexpr char kValidUid[] = "100";
24 constexpr char kValidGroup[] = "users";
25 constexpr char kValidGid[] = "100";
26
27 class CliTest : public ::testing::Test {
28 protected:
SetUp()29 virtual void SetUp() {
30 // Most tests do not care about this logic. For the few that do, make
31 // them opt into it so they can validate specifically.
32 elftype_ = ELFDYNAMIC;
33 }
TearDown()34 virtual void TearDown() {}
35
36 // We use a vector of strings rather than const char * pointers because we
37 // need the backing memory to be writable. The CLI might mutate the strings
38 // as it parses things (which is normally permissible with argv).
parse_args_(const std::vector<std::string> & argv,int * exit_immediately,ElfType * elftype)39 int parse_args_(const std::vector<std::string>& argv,
40 int* exit_immediately,
41 ElfType* elftype) {
42 // Make sure we reset the getopts state when scanning a new argv. Setting
43 // this to 0 is a GNU extension, but AOSP/BSD also checks this (as an alias
44 // to their "optreset").
45 optind = 0;
46
47 // We create & destroy this for every parse_args call because some API
48 // calls can dupe memory which confuses LSAN. https://crbug.com/844615
49 struct minijail *j = minijail_new();
50
51 std::vector<const char *> pargv;
52 pargv.push_back("minijail0");
53 for (const std::string& arg : argv)
54 pargv.push_back(arg.c_str());
55
56 // We grab stdout from parse_args itself as it might dump things we don't
57 // usually care about like help output.
58 testing::internal::CaptureStdout();
59
60 const char* preload_path = PRELOADPATH;
61 int ret =
62 parse_args(j, pargv.size(), const_cast<char* const*>(pargv.data()),
63 exit_immediately, elftype, &preload_path);
64 testing::internal::GetCapturedStdout();
65
66 minijail_destroy(j);
67
68 return ret;
69 }
70
parse_args_(const std::vector<std::string> & argv)71 int parse_args_(const std::vector<std::string>& argv) {
72 return parse_args_(argv, &exit_immediately_, &elftype_);
73 }
74
75 ElfType elftype_;
76 int exit_immediately_;
77 };
78
79 } // namespace
80
81 // Should exit non-zero when there's no arguments.
TEST_F(CliTest,no_args)82 TEST_F(CliTest, no_args) {
83 std::vector<std::string> argv = {};
84 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
85 }
86
87 // Should exit zero when we asked for help.
TEST_F(CliTest,help)88 TEST_F(CliTest, help) {
89 std::vector<std::string> argv = {"-h"};
90 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(0), "");
91
92 argv = {"--help"};
93 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(0), "");
94
95 argv = {"-H"};
96 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(0), "");
97 }
98
99 // Just a simple program to run.
TEST_F(CliTest,valid_program)100 TEST_F(CliTest, valid_program) {
101 std::vector<std::string> argv = {"/bin/sh"};
102 ASSERT_TRUE(parse_args_(argv));
103 }
104
105 // Valid calls to the change user option.
TEST_F(CliTest,valid_set_user)106 TEST_F(CliTest, valid_set_user) {
107 std::vector<std::string> argv = {"-u", "", "/bin/sh"};
108
109 argv[1] = kValidUser;
110 ASSERT_TRUE(parse_args_(argv));
111
112 argv[1] = kValidUid;
113 ASSERT_TRUE(parse_args_(argv));
114 }
115
116 // Invalid calls to the change user option.
TEST_F(CliTest,invalid_set_user)117 TEST_F(CliTest, invalid_set_user) {
118 std::vector<std::string> argv = {"-u", "", "/bin/sh"};
119 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
120
121 argv[1] = "j;lX:J*Pj;oijfs;jdlkjC;j";
122 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
123
124 argv[1] = "1000x";
125 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
126
127 // Supplying -u more than once is bad.
128 argv = {"-u", kValidUser, "-u", kValidUid, "/bin/sh"};
129 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1),
130 "-u provided multiple times");
131 }
132
133 // Valid calls to the change group option.
TEST_F(CliTest,valid_set_group)134 TEST_F(CliTest, valid_set_group) {
135 std::vector<std::string> argv = {"-g", "", "/bin/sh"};
136
137 argv[1] = kValidGroup;
138 ASSERT_TRUE(parse_args_(argv));
139
140 argv[1] = kValidGid;
141 ASSERT_TRUE(parse_args_(argv));
142 }
143
144 // Invalid calls to the change group option.
TEST_F(CliTest,invalid_set_group)145 TEST_F(CliTest, invalid_set_group) {
146 std::vector<std::string> argv = {"-g", "", "/bin/sh"};
147 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
148
149 argv[1] = "j;lX:J*Pj;oijfs;jdlkjC;j";
150 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
151
152 argv[1] = "1000x";
153 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
154
155 // Supplying -g more than once is bad.
156 argv = {"-g", kValidGroup, "-g", kValidGid, "/bin/sh"};
157 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1),
158 "-g provided multiple times");
159 }
160
161 // Valid calls to the add-suppl-group option.
TEST_F(CliTest,valid_add_supp_group)162 TEST_F(CliTest, valid_add_supp_group) {
163 std::vector<std::string> argv = {"--add-suppl-group", "", "/bin/sh"};
164
165 argv[1] = kValidGroup;
166 ASSERT_TRUE(parse_args_(argv));
167
168 argv[1] = kValidGid;
169 ASSERT_TRUE(parse_args_(argv));
170
171 std::vector<std::string> argv2 = {"--add-suppl-group", "",
172 "--add-suppl-group", "", "/bin/sh"};
173 argv[1] = kValidGroup;
174 argv[2] = kValidGid;
175 ASSERT_TRUE(parse_args_(argv));
176 }
177
178 // Invalid calls to the add-suppl-group option.
TEST_F(CliTest,invalid_add_supp_group)179 TEST_F(CliTest, invalid_add_supp_group) {
180 std::vector<std::string> argv = {"--add-suppl-group", "", "/bin/sh"};
181
182 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
183
184 argv[1] = "j;lX:J*Pj;oijfs;jdlkjC;j";
185 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
186
187 argv[1] = "1000x";
188 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
189 }
190
191 // Valid calls to the skip securebits option.
TEST_F(CliTest,valid_skip_securebits)192 TEST_F(CliTest, valid_skip_securebits) {
193 // An empty string is the same as 0.
194 std::vector<std::string> argv = {"-B", "", "/bin/sh"};
195 ASSERT_TRUE(parse_args_(argv));
196
197 argv[1] = "0xAB";
198 ASSERT_TRUE(parse_args_(argv));
199
200 argv[1] = "1234";
201 ASSERT_TRUE(parse_args_(argv));
202 }
203
204 // Invalid calls to the skip securebits option.
TEST_F(CliTest,invalid_skip_securebits)205 TEST_F(CliTest, invalid_skip_securebits) {
206 std::vector<std::string> argv = {"-B", "", "/bin/sh"};
207
208 argv[1] = "xja";
209 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
210 }
211
212 // Valid calls to the caps option.
TEST_F(CliTest,valid_caps)213 TEST_F(CliTest, valid_caps) {
214 // An empty string is the same as 0.
215 std::vector<std::string> argv = {"-c", "", "/bin/sh"};
216 ASSERT_TRUE(parse_args_(argv));
217
218 argv[1] = "0xAB";
219 ASSERT_TRUE(parse_args_(argv));
220
221 argv[1] = "1234";
222 ASSERT_TRUE(parse_args_(argv));
223 }
224
225 // Invalid calls to the caps option.
TEST_F(CliTest,invalid_caps)226 TEST_F(CliTest, invalid_caps) {
227 std::vector<std::string> argv = {"-c", "", "/bin/sh"};
228
229 argv[1] = "xja";
230 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
231 }
232
233 // Valid calls to the logging option.
TEST_F(CliTest,valid_logging)234 TEST_F(CliTest, valid_logging) {
235 std::vector<std::string> argv = {"--logging", "", "/bin/sh"};
236
237 // This should list all valid logging targets.
238 const std::vector<std::string> profiles = {
239 "stderr",
240 "syslog",
241 };
242
243 for (const auto& profile : profiles) {
244 argv[1] = profile;
245 ASSERT_TRUE(parse_args_(argv));
246 }
247 }
248
249 // Invalid calls to the logging option.
TEST_F(CliTest,invalid_logging)250 TEST_F(CliTest, invalid_logging) {
251 std::vector<std::string> argv = {"--logging", "", "/bin/sh"};
252 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
253
254 argv[1] = "stdout";
255 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
256 }
257
258 // Valid calls to the rlimit option.
TEST_F(CliTest,valid_rlimit)259 TEST_F(CliTest, valid_rlimit) {
260 std::vector<std::string> argv = {"-R", "", "/bin/sh"};
261
262 argv[1] = "0,1,2";
263 ASSERT_TRUE(parse_args_(argv));
264
265 argv[1] = "0,0x100,4";
266 ASSERT_TRUE(parse_args_(argv));
267
268 argv[1] = "1,1,unlimited";
269 ASSERT_TRUE(parse_args_(argv));
270
271 argv[1] = "2,unlimited,2";
272 ASSERT_TRUE(parse_args_(argv));
273
274 argv[1] = "RLIMIT_AS,unlimited,unlimited";
275 ASSERT_TRUE(parse_args_(argv));
276 }
277
278 // Invalid calls to the rlimit option.
TEST_F(CliTest,invalid_rlimit)279 TEST_F(CliTest, invalid_rlimit) {
280 std::vector<std::string> argv = {"-R", "", "/bin/sh"};
281 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
282
283 // Missing cur & max.
284 argv[1] = "0";
285 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
286
287 // Missing max.
288 argv[1] = "0,0";
289 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
290
291 // Too many options.
292 argv[1] = "0,0,0,0";
293 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
294
295 // Non-numeric limits
296 argv[1] = "0,0,invalid-limit";
297 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
298
299 // Invalid number.
300 argv[1] = "0,0,0j";
301 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
302
303 // Invalid hex number.
304 argv[1] = "0,0x1jf,0";
305 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
306
307 // Invalid rlimit constant.
308 argv[1] = "RLIMIT_GOGOOGOG,0,0";
309 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
310 }
311
312 // Valid calls to the profile option.
TEST_F(CliTest,valid_profile)313 TEST_F(CliTest, valid_profile) {
314 std::vector<std::string> argv = {"--profile", "", "/bin/sh"};
315
316 // This should list all valid profiles.
317 const std::vector<std::string> profiles = {
318 "minimalistic-mountns",
319 "minimalistic-mountns-nodev",
320 };
321
322 for (const auto& profile : profiles) {
323 argv[1] = profile;
324 ASSERT_TRUE(parse_args_(argv));
325 }
326 }
327
328 // Invalid calls to the profile option.
TEST_F(CliTest,invalid_profile)329 TEST_F(CliTest, invalid_profile) {
330 std::vector<std::string> argv = {"--profile", "", "/bin/sh"};
331 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
332
333 argv[1] = "random-unknown-profile";
334 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
335 }
336
337 // Valid calls to the chroot option.
TEST_F(CliTest,valid_chroot)338 TEST_F(CliTest, valid_chroot) {
339 std::vector<std::string> argv = {"-C", "/", "/bin/sh"};
340 ASSERT_TRUE(parse_args_(argv));
341 }
342
343 // Valid calls to the pivot root option.
TEST_F(CliTest,valid_pivot_root)344 TEST_F(CliTest, valid_pivot_root) {
345 std::vector<std::string> argv = {"-P", "/", "/bin/sh"};
346 ASSERT_TRUE(parse_args_(argv));
347 }
348
349 // We cannot handle multiple options with chroot/profile/pivot root.
TEST_F(CliTest,conflicting_roots)350 TEST_F(CliTest, conflicting_roots) {
351 std::vector<std::string> argv;
352
353 // Chroot & pivot root.
354 argv = {"-C", "/", "-P", "/", "/bin/sh"};
355 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
356
357 // Chroot & minimalistic-mountns profile.
358 argv = {"-C", "/", "--profile", "minimalistic-mountns", "/bin/sh"};
359 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
360
361 // Pivot root & minimalistic-mountns profile.
362 argv = {"-P", "/", "--profile", "minimalistic-mountns", "/bin/sh"};
363 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
364 }
365
366 // Valid calls to the uidmap option.
TEST_F(CliTest,valid_uidmap)367 TEST_F(CliTest, valid_uidmap) {
368 std::vector<std::string> argv = {"-m", "/bin/sh"};
369 // Use a default map (no option from user).
370 ASSERT_TRUE(parse_args_(argv));
371
372 // Use a single map.
373 argv = {"-m0 0 1", "/bin/sh"};
374 ASSERT_TRUE(parse_args_(argv));
375
376 // Multiple maps.
377 argv = {"-m0 0 1,100 100 1", "/bin/sh"};
378 ASSERT_TRUE(parse_args_(argv));
379 }
380
381 // Valid calls to the gidmap option.
TEST_F(CliTest,valid_gidmap)382 TEST_F(CliTest, valid_gidmap) {
383 std::vector<std::string> argv = {"-M", "/bin/sh"};
384 // Use a default map (no option from user).
385 ASSERT_TRUE(parse_args_(argv));
386
387 // Use a single map.
388 argv = {"-M0 0 1", "/bin/sh"};
389 ASSERT_TRUE(parse_args_(argv));
390
391 // Multiple maps.
392 argv = {"-M0 0 1,100 100 1", "/bin/sh"};
393 ASSERT_TRUE(parse_args_(argv));
394 }
395
396 // Invalid calls to the uidmap/gidmap options.
397 // Note: Can't really test these as all validation is delayed/left to the
398 // runtime kernel. Minijail will simply write verbatim what the user gave
399 // it to the corresponding /proc/.../[ug]id_map.
400
401 // Valid calls to the binding option.
TEST_F(CliTest,valid_binding)402 TEST_F(CliTest, valid_binding) {
403 std::vector<std::string> argv = {"-v", "-b", "", "/bin/sh"};
404
405 // Dest & writable are optional.
406 argv[1] = "/";
407 ASSERT_TRUE(parse_args_(argv));
408
409 // Writable is optional.
410 argv[1] = "/,/";
411 ASSERT_TRUE(parse_args_(argv));
412
413 // Writable is an integer.
414 argv[1] = "/,/,0";
415 ASSERT_TRUE(parse_args_(argv));
416 argv[1] = "/,/,1";
417 ASSERT_TRUE(parse_args_(argv));
418
419 // Dest is optional.
420 argv[1] = "/,,0";
421 ASSERT_TRUE(parse_args_(argv));
422 }
423
424 // Invalid calls to the binding option.
TEST_F(CliTest,invalid_binding)425 TEST_F(CliTest, invalid_binding) {
426 std::vector<std::string> argv = {"-v", "-b", "", "/bin/sh"};
427
428 // Missing source.
429 argv[2] = "";
430 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
431
432 // Too many args.
433 argv[2] = "/,/,0,what";
434 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
435
436 // Missing mount namespace/etc...
437 argv = {"-b", "/", "/bin/sh"};
438 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
439
440 // Bad value for <writable>.
441 argv = {"-b", "/,,writable", "/bin/sh"};
442 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
443 }
444
445 // Valid calls to the mount option.
TEST_F(CliTest,valid_mount)446 TEST_F(CliTest, valid_mount) {
447 std::vector<std::string> argv = {"-v", "-k", "", "/bin/sh"};
448
449 // Flags & data are optional.
450 argv[2] = "none,/,none";
451 ASSERT_TRUE(parse_args_(argv));
452
453 // Data is optional.
454 argv[2] = "none,/,none,0xe";
455 ASSERT_TRUE(parse_args_(argv));
456
457 // Flags are optional.
458 argv[2] = "none,/,none,,mode=755";
459 ASSERT_TRUE(parse_args_(argv));
460
461 // Multiple data options to the kernel.
462 argv[2] = "none,/,none,0xe,mode=755,uid=0,gid=10";
463 ASSERT_TRUE(parse_args_(argv));
464
465 // Single MS constant.
466 argv[2] = "none,/,none,MS_NODEV,mode=755";
467 ASSERT_TRUE(parse_args_(argv));
468
469 // Multiple MS constants.
470 argv[2] = "none,/,none,MS_NODEV|MS_NOEXEC,mode=755";
471 ASSERT_TRUE(parse_args_(argv));
472
473 // Mixed constant & number.
474 argv[2] = "none,/,none,MS_NODEV|0xf,mode=755";
475 ASSERT_TRUE(parse_args_(argv));
476 }
477
478 // Invalid calls to the mount option.
TEST_F(CliTest,invalid_mount)479 TEST_F(CliTest, invalid_mount) {
480 std::vector<std::string> argv = {"-v", "-k", "", "/bin/sh"};
481
482 // Missing source.
483 argv[2] = "";
484 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
485
486 // Missing dest.
487 argv[2] = "none";
488 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
489
490 // Missing type.
491 argv[2] = "none,/";
492 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
493
494 // Unknown MS constant.
495 argv[2] = "none,/,none,MS_WHOOPS";
496 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
497 }
498
499 // Valid calls to the remount mode option.
TEST_F(CliTest,valid_remount_mode)500 TEST_F(CliTest, valid_remount_mode) {
501 std::vector<std::string> argv = {"-v", "", "/bin/sh"};
502
503 // Mode is optional.
504 argv[1] = "-K";
505 ASSERT_TRUE(parse_args_(argv));
506
507 // This should list all valid modes.
508 const std::vector<std::string> modes = {
509 "shared",
510 "private",
511 "slave",
512 "unbindable",
513 };
514
515 for (const auto& mode : modes) {
516 argv[1] = "-K" + mode;
517 ASSERT_TRUE(parse_args_(argv));
518 }
519 }
520
521 // Invalid calls to the remount mode option.
TEST_F(CliTest,invalid_remount_mode)522 TEST_F(CliTest, invalid_remount_mode) {
523 std::vector<std::string> argv = {"-v", "", "/bin/sh"};
524
525 // Unknown mode.
526 argv[1] = "-Kfoo";
527 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
528 }
529