1 | // SPDX-License-Identifier: GPL-2.0 |
2 | #define _GNU_SOURCE |
3 | #include <stdio.h> |
4 | #include <errno.h> |
5 | #include <pwd.h> |
6 | #include <grp.h> |
7 | #include <string.h> |
8 | #include <syscall.h> |
9 | #include <sys/capability.h> |
10 | #include <sys/types.h> |
11 | #include <sys/mount.h> |
12 | #include <sys/prctl.h> |
13 | #include <sys/wait.h> |
14 | #include <stdlib.h> |
15 | #include <unistd.h> |
16 | #include <fcntl.h> |
17 | #include <stdbool.h> |
18 | #include <stdarg.h> |
19 | |
20 | /* |
21 | * NOTES about this test: |
22 | * - requries libcap-dev to be installed on test system |
23 | * - requires securityfs to me mounted at /sys/kernel/security, e.g.: |
24 | * mount -n -t securityfs -o nodev,noexec,nosuid securityfs /sys/kernel/security |
25 | * - needs CONFIG_SECURITYFS and CONFIG_SAFESETID to be enabled |
26 | */ |
27 | |
28 | #ifndef CLONE_NEWUSER |
29 | # define CLONE_NEWUSER 0x10000000 |
30 | #endif |
31 | |
32 | #define ROOT_UGID 0 |
33 | #define RESTRICTED_PARENT_UGID 1 |
34 | #define ALLOWED_CHILD1_UGID 2 |
35 | #define ALLOWED_CHILD2_UGID 3 |
36 | #define NO_POLICY_UGID 4 |
37 | |
38 | #define UGID_POLICY_STRING "1:2\n1:3\n2:2\n3:3\n" |
39 | |
40 | char* add_uid_whitelist_policy_file = "/sys/kernel/security/safesetid/uid_allowlist_policy" ; |
41 | char* add_gid_whitelist_policy_file = "/sys/kernel/security/safesetid/gid_allowlist_policy" ; |
42 | |
43 | static void die(char *fmt, ...) |
44 | { |
45 | va_list ap; |
46 | va_start(ap, fmt); |
47 | vfprintf(stderr, fmt, ap); |
48 | va_end(ap); |
49 | exit(EXIT_FAILURE); |
50 | } |
51 | |
52 | static bool vmaybe_write_file(bool enoent_ok, char *filename, char *fmt, va_list ap) |
53 | { |
54 | char buf[4096]; |
55 | int fd; |
56 | ssize_t written; |
57 | int buf_len; |
58 | |
59 | buf_len = vsnprintf(buf, sizeof(buf), fmt, ap); |
60 | if (buf_len < 0) { |
61 | printf("vsnprintf failed: %s\n" , |
62 | strerror(errno)); |
63 | return false; |
64 | } |
65 | if (buf_len >= sizeof(buf)) { |
66 | printf("vsnprintf output truncated\n" ); |
67 | return false; |
68 | } |
69 | |
70 | fd = open(filename, O_WRONLY); |
71 | if (fd < 0) { |
72 | if ((errno == ENOENT) && enoent_ok) |
73 | return true; |
74 | return false; |
75 | } |
76 | written = write(fd, buf, buf_len); |
77 | if (written != buf_len) { |
78 | if (written >= 0) { |
79 | printf("short write to %s\n" , filename); |
80 | return false; |
81 | } else { |
82 | printf("write to %s failed: %s\n" , |
83 | filename, strerror(errno)); |
84 | return false; |
85 | } |
86 | } |
87 | if (close(fd) != 0) { |
88 | printf("close of %s failed: %s\n" , |
89 | filename, strerror(errno)); |
90 | return false; |
91 | } |
92 | return true; |
93 | } |
94 | |
95 | static bool write_file(char *filename, char *fmt, ...) |
96 | { |
97 | va_list ap; |
98 | bool ret; |
99 | |
100 | va_start(ap, fmt); |
101 | ret = vmaybe_write_file(false, filename, fmt, ap); |
102 | va_end(ap); |
103 | |
104 | return ret; |
105 | } |
106 | |
107 | static void ensure_user_exists(uid_t uid) |
108 | { |
109 | struct passwd p; |
110 | |
111 | FILE *fd; |
112 | char name_str[10]; |
113 | |
114 | if (getpwuid(uid) == NULL) { |
115 | memset(&p,0x00,sizeof(p)); |
116 | fd=fopen("/etc/passwd" ,"a" ); |
117 | if (fd == NULL) |
118 | die(fmt: "couldn't open file\n" ); |
119 | if (fseek(fd, 0, SEEK_END)) |
120 | die(fmt: "couldn't fseek\n" ); |
121 | snprintf(name_str, 10, "user %d" , uid); |
122 | p.pw_name=name_str; |
123 | p.pw_uid=uid; |
124 | p.pw_gid=uid; |
125 | p.pw_gecos="Test account" ; |
126 | p.pw_dir="/dev/null" ; |
127 | p.pw_shell="/bin/false" ; |
128 | int value = putpwent(&p,fd); |
129 | if (value != 0) |
130 | die(fmt: "putpwent failed\n" ); |
131 | if (fclose(fd)) |
132 | die(fmt: "fclose failed\n" ); |
133 | } |
134 | } |
135 | |
136 | static void ensure_group_exists(gid_t gid) |
137 | { |
138 | struct group g; |
139 | |
140 | FILE *fd; |
141 | char name_str[10]; |
142 | |
143 | if (getgrgid(gid) == NULL) { |
144 | memset(&g,0x00,sizeof(g)); |
145 | fd=fopen("/etc/group" ,"a" ); |
146 | if (fd == NULL) |
147 | die(fmt: "couldn't open group file\n" ); |
148 | if (fseek(fd, 0, SEEK_END)) |
149 | die(fmt: "couldn't fseek group file\n" ); |
150 | snprintf(name_str, 10, "group %d" , gid); |
151 | g.gr_name=name_str; |
152 | g.gr_gid=gid; |
153 | g.gr_passwd=NULL; |
154 | g.gr_mem=NULL; |
155 | int value = putgrent(&g,fd); |
156 | if (value != 0) |
157 | die(fmt: "putgrent failed\n" ); |
158 | if (fclose(fd)) |
159 | die(fmt: "fclose failed\n" ); |
160 | } |
161 | } |
162 | |
163 | static void ensure_securityfs_mounted(void) |
164 | { |
165 | int fd = open(add_uid_whitelist_policy_file, O_WRONLY); |
166 | if (fd < 0) { |
167 | if (errno == ENOENT) { |
168 | // Need to mount securityfs |
169 | if (mount("securityfs" , "/sys/kernel/security" , |
170 | "securityfs" , 0, NULL) < 0) |
171 | die(fmt: "mounting securityfs failed\n" ); |
172 | } else { |
173 | die(fmt: "couldn't find securityfs for unknown reason\n" ); |
174 | } |
175 | } else { |
176 | if (close(fd) != 0) { |
177 | die("close of %s failed: %s\n" , |
178 | add_uid_whitelist_policy_file, strerror(errno)); |
179 | } |
180 | } |
181 | } |
182 | |
183 | static void write_uid_policies() |
184 | { |
185 | static char *policy_str = UGID_POLICY_STRING; |
186 | ssize_t written; |
187 | int fd; |
188 | |
189 | fd = open(add_uid_whitelist_policy_file, O_WRONLY); |
190 | if (fd < 0) |
191 | die(fmt: "can't open add_uid_whitelist_policy file\n" ); |
192 | written = write(fd, policy_str, strlen(policy_str)); |
193 | if (written != strlen(policy_str)) { |
194 | if (written >= 0) { |
195 | die(fmt: "short write to %s\n" , add_uid_whitelist_policy_file); |
196 | } else { |
197 | die("write to %s failed: %s\n" , |
198 | add_uid_whitelist_policy_file, strerror(errno)); |
199 | } |
200 | } |
201 | if (close(fd) != 0) { |
202 | die("close of %s failed: %s\n" , |
203 | add_uid_whitelist_policy_file, strerror(errno)); |
204 | } |
205 | } |
206 | |
207 | static void write_gid_policies() |
208 | { |
209 | static char *policy_str = UGID_POLICY_STRING; |
210 | ssize_t written; |
211 | int fd; |
212 | |
213 | fd = open(add_gid_whitelist_policy_file, O_WRONLY); |
214 | if (fd < 0) |
215 | die(fmt: "can't open add_gid_whitelist_policy file\n" ); |
216 | written = write(fd, policy_str, strlen(policy_str)); |
217 | if (written != strlen(policy_str)) { |
218 | if (written >= 0) { |
219 | die(fmt: "short write to %s\n" , add_gid_whitelist_policy_file); |
220 | } else { |
221 | die("write to %s failed: %s\n" , |
222 | add_gid_whitelist_policy_file, strerror(errno)); |
223 | } |
224 | } |
225 | if (close(fd) != 0) { |
226 | die("close of %s failed: %s\n" , |
227 | add_gid_whitelist_policy_file, strerror(errno)); |
228 | } |
229 | } |
230 | |
231 | |
232 | static bool test_userns(bool expect_success) |
233 | { |
234 | uid_t uid; |
235 | char map_file_name[32]; |
236 | size_t sz = sizeof(map_file_name); |
237 | pid_t cpid; |
238 | bool success; |
239 | |
240 | uid = getuid(); |
241 | |
242 | int clone_flags = CLONE_NEWUSER; |
243 | cpid = syscall(SYS_clone, clone_flags, NULL); |
244 | if (cpid == -1) { |
245 | printf("clone failed" ); |
246 | return false; |
247 | } |
248 | |
249 | if (cpid == 0) { /* Code executed by child */ |
250 | // Give parent 1 second to write map file |
251 | sleep(1); |
252 | exit(EXIT_SUCCESS); |
253 | } else { /* Code executed by parent */ |
254 | if(snprintf(map_file_name, sz, "/proc/%d/uid_map" , cpid) < 0) { |
255 | printf("preparing file name string failed" ); |
256 | return false; |
257 | } |
258 | success = write_file(map_file_name, "0 %d 1" , uid); |
259 | return success == expect_success; |
260 | } |
261 | |
262 | printf("should not reach here" ); |
263 | return false; |
264 | } |
265 | |
266 | static void test_setuid(uid_t child_uid, bool expect_success) |
267 | { |
268 | pid_t cpid, w; |
269 | int wstatus; |
270 | |
271 | cpid = fork(); |
272 | if (cpid == -1) { |
273 | die(fmt: "fork\n" ); |
274 | } |
275 | |
276 | if (cpid == 0) { /* Code executed by child */ |
277 | if (setuid(child_uid) < 0) |
278 | exit(EXIT_FAILURE); |
279 | if (getuid() == child_uid) |
280 | exit(EXIT_SUCCESS); |
281 | else |
282 | exit(EXIT_FAILURE); |
283 | } else { /* Code executed by parent */ |
284 | do { |
285 | w = waitpid(cpid, &wstatus, WUNTRACED | WCONTINUED); |
286 | if (w == -1) { |
287 | die(fmt: "waitpid\n" ); |
288 | } |
289 | |
290 | if (WIFEXITED(wstatus)) { |
291 | if (WEXITSTATUS(wstatus) == EXIT_SUCCESS) { |
292 | if (expect_success) { |
293 | return; |
294 | } else { |
295 | die(fmt: "unexpected success\n" ); |
296 | } |
297 | } else { |
298 | if (expect_success) { |
299 | die(fmt: "unexpected failure\n" ); |
300 | } else { |
301 | return; |
302 | } |
303 | } |
304 | } else if (WIFSIGNALED(wstatus)) { |
305 | if (WTERMSIG(wstatus) == 9) { |
306 | if (expect_success) |
307 | die(fmt: "killed unexpectedly\n" ); |
308 | else |
309 | return; |
310 | } else { |
311 | die(fmt: "unexpected signal: %d\n" , wstatus); |
312 | } |
313 | } else { |
314 | die(fmt: "unexpected status: %d\n" , wstatus); |
315 | } |
316 | } while (!WIFEXITED(wstatus) && !WIFSIGNALED(wstatus)); |
317 | } |
318 | |
319 | die(fmt: "should not reach here\n" ); |
320 | } |
321 | |
322 | static void test_setgid(gid_t child_gid, bool expect_success) |
323 | { |
324 | pid_t cpid, w; |
325 | int wstatus; |
326 | |
327 | cpid = fork(); |
328 | if (cpid == -1) { |
329 | die(fmt: "fork\n" ); |
330 | } |
331 | |
332 | if (cpid == 0) { /* Code executed by child */ |
333 | if (setgid(child_gid) < 0) |
334 | exit(EXIT_FAILURE); |
335 | if (getgid() == child_gid) |
336 | exit(EXIT_SUCCESS); |
337 | else |
338 | exit(EXIT_FAILURE); |
339 | } else { /* Code executed by parent */ |
340 | do { |
341 | w = waitpid(cpid, &wstatus, WUNTRACED | WCONTINUED); |
342 | if (w == -1) { |
343 | die(fmt: "waitpid\n" ); |
344 | } |
345 | |
346 | if (WIFEXITED(wstatus)) { |
347 | if (WEXITSTATUS(wstatus) == EXIT_SUCCESS) { |
348 | if (expect_success) { |
349 | return; |
350 | } else { |
351 | die(fmt: "unexpected success\n" ); |
352 | } |
353 | } else { |
354 | if (expect_success) { |
355 | die(fmt: "unexpected failure\n" ); |
356 | } else { |
357 | return; |
358 | } |
359 | } |
360 | } else if (WIFSIGNALED(wstatus)) { |
361 | if (WTERMSIG(wstatus) == 9) { |
362 | if (expect_success) |
363 | die(fmt: "killed unexpectedly\n" ); |
364 | else |
365 | return; |
366 | } else { |
367 | die(fmt: "unexpected signal: %d\n" , wstatus); |
368 | } |
369 | } else { |
370 | die(fmt: "unexpected status: %d\n" , wstatus); |
371 | } |
372 | } while (!WIFEXITED(wstatus) && !WIFSIGNALED(wstatus)); |
373 | } |
374 | |
375 | die(fmt: "should not reach here\n" ); |
376 | } |
377 | |
378 | static void test_setgroups(gid_t* child_groups, size_t len, bool expect_success) |
379 | { |
380 | pid_t cpid, w; |
381 | int wstatus; |
382 | gid_t groupset[len]; |
383 | int i, j; |
384 | |
385 | cpid = fork(); |
386 | if (cpid == -1) { |
387 | die(fmt: "fork\n" ); |
388 | } |
389 | |
390 | if (cpid == 0) { /* Code executed by child */ |
391 | if (setgroups(len, child_groups) != 0) |
392 | exit(EXIT_FAILURE); |
393 | if (getgroups(len, groupset) != len) |
394 | exit(EXIT_FAILURE); |
395 | for (i = 0; i < len; i++) { |
396 | for (j = 0; j < len; j++) { |
397 | if (child_groups[i] == groupset[j]) |
398 | break; |
399 | if (j == len - 1) |
400 | exit(EXIT_FAILURE); |
401 | } |
402 | } |
403 | exit(EXIT_SUCCESS); |
404 | } else { /* Code executed by parent */ |
405 | do { |
406 | w = waitpid(cpid, &wstatus, WUNTRACED | WCONTINUED); |
407 | if (w == -1) { |
408 | die(fmt: "waitpid\n" ); |
409 | } |
410 | |
411 | if (WIFEXITED(wstatus)) { |
412 | if (WEXITSTATUS(wstatus) == EXIT_SUCCESS) { |
413 | if (expect_success) { |
414 | return; |
415 | } else { |
416 | die(fmt: "unexpected success\n" ); |
417 | } |
418 | } else { |
419 | if (expect_success) { |
420 | die(fmt: "unexpected failure\n" ); |
421 | } else { |
422 | return; |
423 | } |
424 | } |
425 | } else if (WIFSIGNALED(wstatus)) { |
426 | if (WTERMSIG(wstatus) == 9) { |
427 | if (expect_success) |
428 | die(fmt: "killed unexpectedly\n" ); |
429 | else |
430 | return; |
431 | } else { |
432 | die(fmt: "unexpected signal: %d\n" , wstatus); |
433 | } |
434 | } else { |
435 | die(fmt: "unexpected status: %d\n" , wstatus); |
436 | } |
437 | } while (!WIFEXITED(wstatus) && !WIFSIGNALED(wstatus)); |
438 | } |
439 | |
440 | die(fmt: "should not reach here\n" ); |
441 | } |
442 | |
443 | |
444 | static void ensure_users_exist(void) |
445 | { |
446 | ensure_user_exists(ROOT_UGID); |
447 | ensure_user_exists(RESTRICTED_PARENT_UGID); |
448 | ensure_user_exists(ALLOWED_CHILD1_UGID); |
449 | ensure_user_exists(ALLOWED_CHILD2_UGID); |
450 | ensure_user_exists(NO_POLICY_UGID); |
451 | } |
452 | |
453 | static void ensure_groups_exist(void) |
454 | { |
455 | ensure_group_exists(ROOT_UGID); |
456 | ensure_group_exists(RESTRICTED_PARENT_UGID); |
457 | ensure_group_exists(ALLOWED_CHILD1_UGID); |
458 | ensure_group_exists(ALLOWED_CHILD2_UGID); |
459 | ensure_group_exists(NO_POLICY_UGID); |
460 | } |
461 | |
462 | static void drop_caps(bool setid_retained) |
463 | { |
464 | cap_value_t cap_values[] = {CAP_SETUID, CAP_SETGID}; |
465 | cap_t caps; |
466 | |
467 | caps = cap_get_proc(); |
468 | if (setid_retained) |
469 | cap_set_flag(caps, CAP_EFFECTIVE, 2, cap_values, CAP_SET); |
470 | else |
471 | cap_clear(caps); |
472 | cap_set_proc(caps); |
473 | cap_free(caps); |
474 | } |
475 | |
476 | int main(int argc, char **argv) |
477 | { |
478 | ensure_groups_exist(); |
479 | ensure_users_exist(); |
480 | ensure_securityfs_mounted(); |
481 | write_uid_policies(); |
482 | write_gid_policies(); |
483 | |
484 | if (prctl(PR_SET_KEEPCAPS, 1L)) |
485 | die(fmt: "Error with set keepcaps\n" ); |
486 | |
487 | // First test to make sure we can write userns mappings from a non-root |
488 | // user that doesn't have any restrictions (as long as it has |
489 | // CAP_SETUID); |
490 | if (setgid(NO_POLICY_UGID) < 0) |
491 | die(fmt: "Error with set gid(%d)\n" , NO_POLICY_UGID); |
492 | if (setuid(NO_POLICY_UGID) < 0) |
493 | die(fmt: "Error with set uid(%d)\n" , NO_POLICY_UGID); |
494 | // Take away all but setid caps |
495 | drop_caps(true); |
496 | // Need PR_SET_DUMPABLE flag set so we can write /proc/[pid]/uid_map |
497 | // from non-root parent process. |
498 | if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)) |
499 | die(fmt: "Error with set dumpable\n" ); |
500 | if (!test_userns(true)) { |
501 | die(fmt: "test_userns failed when it should work\n" ); |
502 | } |
503 | |
504 | // Now switch to a user/group with restrictions |
505 | if (setgid(RESTRICTED_PARENT_UGID) < 0) |
506 | die(fmt: "Error with set gid(%d)\n" , RESTRICTED_PARENT_UGID); |
507 | if (setuid(RESTRICTED_PARENT_UGID) < 0) |
508 | die(fmt: "Error with set uid(%d)\n" , RESTRICTED_PARENT_UGID); |
509 | |
510 | test_setuid(ROOT_UGID, false); |
511 | test_setuid(ALLOWED_CHILD1_UGID, true); |
512 | test_setuid(ALLOWED_CHILD2_UGID, true); |
513 | test_setuid(NO_POLICY_UGID, false); |
514 | |
515 | test_setgid(ROOT_UGID, false); |
516 | test_setgid(ALLOWED_CHILD1_UGID, true); |
517 | test_setgid(ALLOWED_CHILD2_UGID, true); |
518 | test_setgid(NO_POLICY_UGID, false); |
519 | |
520 | gid_t allowed_supp_groups[2] = {ALLOWED_CHILD1_UGID, ALLOWED_CHILD2_UGID}; |
521 | gid_t disallowed_supp_groups[2] = {ROOT_UGID, NO_POLICY_UGID}; |
522 | test_setgroups(allowed_supp_groups, 2, true); |
523 | test_setgroups(disallowed_supp_groups, 2, false); |
524 | |
525 | if (!test_userns(false)) { |
526 | die(fmt: "test_userns worked when it should fail\n" ); |
527 | } |
528 | |
529 | // Now take away all caps |
530 | drop_caps(false); |
531 | test_setuid(2, false); |
532 | test_setuid(3, false); |
533 | test_setuid(4, false); |
534 | test_setgid(2, false); |
535 | test_setgid(3, false); |
536 | test_setgid(4, false); |
537 | |
538 | // NOTE: this test doesn't clean up users that were created in |
539 | // /etc/passwd or flush policies that were added to the LSM. |
540 | printf("test successful!\n" ); |
541 | return EXIT_SUCCESS; |
542 | } |
543 | |