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 skip securebits option.
TEST_F(CliTest,valid_skip_securebits)162 TEST_F(CliTest, valid_skip_securebits) {
163 // An empty string is the same as 0.
164 std::vector<std::string> argv = {"-B", "", "/bin/sh"};
165 ASSERT_TRUE(parse_args_(argv));
166
167 argv[1] = "0xAB";
168 ASSERT_TRUE(parse_args_(argv));
169
170 argv[1] = "1234";
171 ASSERT_TRUE(parse_args_(argv));
172 }
173
174 // Invalid calls to the skip securebits option.
TEST_F(CliTest,invalid_skip_securebits)175 TEST_F(CliTest, invalid_skip_securebits) {
176 std::vector<std::string> argv = {"-B", "", "/bin/sh"};
177
178 argv[1] = "xja";
179 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
180 }
181
182 // Valid calls to the caps option.
TEST_F(CliTest,valid_caps)183 TEST_F(CliTest, valid_caps) {
184 // An empty string is the same as 0.
185 std::vector<std::string> argv = {"-c", "", "/bin/sh"};
186 ASSERT_TRUE(parse_args_(argv));
187
188 argv[1] = "0xAB";
189 ASSERT_TRUE(parse_args_(argv));
190
191 argv[1] = "1234";
192 ASSERT_TRUE(parse_args_(argv));
193 }
194
195 // Invalid calls to the caps option.
TEST_F(CliTest,invalid_caps)196 TEST_F(CliTest, invalid_caps) {
197 std::vector<std::string> argv = {"-c", "", "/bin/sh"};
198
199 argv[1] = "xja";
200 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
201 }
202
203 // Valid calls to the logging option.
TEST_F(CliTest,valid_logging)204 TEST_F(CliTest, valid_logging) {
205 std::vector<std::string> argv = {"--logging", "", "/bin/sh"};
206
207 // This should list all valid logging targets.
208 const std::vector<std::string> profiles = {
209 "stderr",
210 "syslog",
211 };
212
213 for (const auto profile : profiles) {
214 argv[1] = profile;
215 ASSERT_TRUE(parse_args_(argv));
216 }
217 }
218
219 // Invalid calls to the logging option.
TEST_F(CliTest,invalid_logging)220 TEST_F(CliTest, invalid_logging) {
221 std::vector<std::string> argv = {"--logging", "", "/bin/sh"};
222 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
223
224 argv[1] = "stdout";
225 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
226 }
227
228 // Valid calls to the rlimit option.
TEST_F(CliTest,valid_rlimit)229 TEST_F(CliTest, valid_rlimit) {
230 std::vector<std::string> argv = {"-R", "", "/bin/sh"};
231
232 argv[1] = "0,1,2";
233 ASSERT_TRUE(parse_args_(argv));
234
235 argv[1] = "0,0x100,4";
236 ASSERT_TRUE(parse_args_(argv));
237
238 argv[1] = "1,1,unlimited";
239 ASSERT_TRUE(parse_args_(argv));
240
241 argv[1] = "2,unlimited,2";
242 ASSERT_TRUE(parse_args_(argv));
243
244 argv[1] = "RLIMIT_AS,unlimited,unlimited";
245 ASSERT_TRUE(parse_args_(argv));
246 }
247
248 // Invalid calls to the rlimit option.
TEST_F(CliTest,invalid_rlimit)249 TEST_F(CliTest, invalid_rlimit) {
250 std::vector<std::string> argv = {"-R", "", "/bin/sh"};
251 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
252
253 // Missing cur & max.
254 argv[1] = "0";
255 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
256
257 // Missing max.
258 argv[1] = "0,0";
259 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
260
261 // Too many options.
262 argv[1] = "0,0,0,0";
263 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
264
265 // Non-numeric limits
266 argv[1] = "0,0,invalid-limit";
267 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
268
269 // Invalid number.
270 argv[1] = "0,0,0j";
271 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
272
273 // Invalid hex number.
274 argv[1] = "0,0x1jf,0";
275 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
276
277 // Invalid rlimit constant.
278 argv[1] = "RLIMIT_GOGOOGOG,0,0";
279 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
280 }
281
282 // Valid calls to the profile option.
TEST_F(CliTest,valid_profile)283 TEST_F(CliTest, valid_profile) {
284 std::vector<std::string> argv = {"--profile", "", "/bin/sh"};
285
286 // This should list all valid profiles.
287 const std::vector<std::string> profiles = {
288 "minimalistic-mountns",
289 };
290
291 for (const auto profile : profiles) {
292 argv[1] = profile;
293 ASSERT_TRUE(parse_args_(argv));
294 }
295 }
296
297 // Invalid calls to the profile option.
TEST_F(CliTest,invalid_profile)298 TEST_F(CliTest, invalid_profile) {
299 std::vector<std::string> argv = {"--profile", "", "/bin/sh"};
300 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
301
302 argv[1] = "random-unknown-profile";
303 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
304 }
305
306 // Valid calls to the chroot option.
TEST_F(CliTest,valid_chroot)307 TEST_F(CliTest, valid_chroot) {
308 std::vector<std::string> argv = {"-C", "/", "/bin/sh"};
309 ASSERT_TRUE(parse_args_(argv));
310 }
311
312 // Valid calls to the pivot root option.
TEST_F(CliTest,valid_pivot_root)313 TEST_F(CliTest, valid_pivot_root) {
314 std::vector<std::string> argv = {"-P", "/", "/bin/sh"};
315 ASSERT_TRUE(parse_args_(argv));
316 }
317
318 // We cannot handle multiple options with chroot/profile/pivot root.
TEST_F(CliTest,conflicting_roots)319 TEST_F(CliTest, conflicting_roots) {
320 std::vector<std::string> argv;
321
322 // Chroot & pivot root.
323 argv = {"-C", "/", "-P", "/", "/bin/sh"};
324 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
325
326 // Chroot & minimalistic-mountns profile.
327 argv = {"-C", "/", "--profile", "minimalistic-mountns", "/bin/sh"};
328 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
329
330 // Pivot root & minimalistic-mountns profile.
331 argv = {"-P", "/", "--profile", "minimalistic-mountns", "/bin/sh"};
332 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
333 }
334
335 // Valid calls to the uidmap option.
TEST_F(CliTest,valid_uidmap)336 TEST_F(CliTest, valid_uidmap) {
337 std::vector<std::string> argv = {"-m", "/bin/sh"};
338 // Use a default map (no option from user).
339 ASSERT_TRUE(parse_args_(argv));
340
341 // Use a single map.
342 argv = {"-m0 0 1", "/bin/sh"};
343 ASSERT_TRUE(parse_args_(argv));
344
345 // Multiple maps.
346 argv = {"-m0 0 1,100 100 1", "/bin/sh"};
347 ASSERT_TRUE(parse_args_(argv));
348 }
349
350 // Valid calls to the gidmap option.
TEST_F(CliTest,valid_gidmap)351 TEST_F(CliTest, valid_gidmap) {
352 std::vector<std::string> argv = {"-M", "/bin/sh"};
353 // Use a default map (no option from user).
354 ASSERT_TRUE(parse_args_(argv));
355
356 // Use a single map.
357 argv = {"-M0 0 1", "/bin/sh"};
358 ASSERT_TRUE(parse_args_(argv));
359
360 // Multiple maps.
361 argv = {"-M0 0 1,100 100 1", "/bin/sh"};
362 ASSERT_TRUE(parse_args_(argv));
363 }
364
365 // Invalid calls to the uidmap/gidmap options.
366 // Note: Can't really test these as all validation is delayed/left to the
367 // runtime kernel. Minijail will simply write verbatim what the user gave
368 // it to the corresponding /proc/.../[ug]id_map.
369
370 // Valid calls to the binding option.
TEST_F(CliTest,valid_binding)371 TEST_F(CliTest, valid_binding) {
372 std::vector<std::string> argv = {"-v", "-b", "", "/bin/sh"};
373
374 // Dest & writable are optional.
375 argv[1] = "/";
376 ASSERT_TRUE(parse_args_(argv));
377
378 // Writable is optional.
379 argv[1] = "/,/";
380 ASSERT_TRUE(parse_args_(argv));
381
382 // Writable is an integer.
383 argv[1] = "/,/,0";
384 ASSERT_TRUE(parse_args_(argv));
385 argv[1] = "/,/,1";
386 ASSERT_TRUE(parse_args_(argv));
387
388 // Dest is optional.
389 argv[1] = "/,,0";
390 ASSERT_TRUE(parse_args_(argv));
391 }
392
393 // Invalid calls to the binding option.
TEST_F(CliTest,invalid_binding)394 TEST_F(CliTest, invalid_binding) {
395 std::vector<std::string> argv = {"-v", "-b", "", "/bin/sh"};
396
397 // Missing source.
398 argv[2] = "";
399 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
400
401 // Too many args.
402 argv[2] = "/,/,0,what";
403 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
404
405 // Missing mount namespace/etc...
406 argv = {"-b", "/", "/bin/sh"};
407 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
408 }
409
410 // Valid calls to the mount option.
TEST_F(CliTest,valid_mount)411 TEST_F(CliTest, valid_mount) {
412 std::vector<std::string> argv = {"-v", "-k", "", "/bin/sh"};
413
414 // Flags & data are optional.
415 argv[2] = "none,/,none";
416 ASSERT_TRUE(parse_args_(argv));
417
418 // Data is optional.
419 argv[2] = "none,/,none,0xe";
420 ASSERT_TRUE(parse_args_(argv));
421
422 // Flags are optional.
423 argv[2] = "none,/,none,,mode=755";
424 ASSERT_TRUE(parse_args_(argv));
425
426 // Multiple data options to the kernel.
427 argv[2] = "none,/,none,0xe,mode=755,uid=0,gid=10";
428 ASSERT_TRUE(parse_args_(argv));
429
430 // Single MS constant.
431 argv[2] = "none,/,none,MS_NODEV,mode=755";
432 ASSERT_TRUE(parse_args_(argv));
433
434 // Multiple MS constants.
435 argv[2] = "none,/,none,MS_NODEV|MS_NOEXEC,mode=755";
436 ASSERT_TRUE(parse_args_(argv));
437
438 // Mixed constant & number.
439 argv[2] = "none,/,none,MS_NODEV|0xf,mode=755";
440 ASSERT_TRUE(parse_args_(argv));
441 }
442
443 // Invalid calls to the mount option.
TEST_F(CliTest,invalid_mount)444 TEST_F(CliTest, invalid_mount) {
445 std::vector<std::string> argv = {"-v", "-k", "", "/bin/sh"};
446
447 // Missing source.
448 argv[2] = "";
449 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
450
451 // Missing dest.
452 argv[2] = "none";
453 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
454
455 // Missing type.
456 argv[2] = "none,/";
457 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
458
459 // Unknown MS constant.
460 argv[2] = "none,/,none,MS_WHOOPS";
461 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
462 }
463
464 // Valid calls to the remount mode option.
TEST_F(CliTest,valid_remount_mode)465 TEST_F(CliTest, valid_remount_mode) {
466 std::vector<std::string> argv = {"-v", "", "/bin/sh"};
467
468 // Mode is optional.
469 argv[1] = "-K";
470 ASSERT_TRUE(parse_args_(argv));
471
472 // This should list all valid modes.
473 const std::vector<std::string> modes = {
474 "shared",
475 "private",
476 "slave",
477 "unbindable",
478 };
479
480 for (const auto& mode : modes) {
481 argv[1] = "-K" + mode;
482 ASSERT_TRUE(parse_args_(argv));
483 }
484 }
485
486 // Invalid calls to the remount mode option.
TEST_F(CliTest,invalid_remount_mode)487 TEST_F(CliTest, invalid_remount_mode) {
488 std::vector<std::string> argv = {"-v", "", "/bin/sh"};
489
490 // Unknown mode.
491 argv[1] = "-Kfoo";
492 ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
493 }
494