From c6f1981cd82570ff8e21ec1a04e4cbfdac1b6805 Mon Sep 17 00:00:00 2001 From: Georgia Garcia Date: Fri, 4 Nov 2022 09:56:03 +0000 Subject: [PATCH] tests: expand userns tests to use setns Setns is used to associate to an existing user namespace, so the kernel security hook for user namespace creation is not called. The restriction for setns is that it should have the capability sys_admin. Signed-off-by: Georgia Garcia --- .gitignore | 1 + tests/regression/apparmor/Makefile | 7 ++ tests/regression/apparmor/userns.c | 108 ++++++++++++++++++++++- tests/regression/apparmor/userns.h | 67 ++++++++++++++ tests/regression/apparmor/userns.sh | 51 ++++++++--- tests/regression/apparmor/userns_setns.c | 53 +++++++++++ 6 files changed, 274 insertions(+), 13 deletions(-) create mode 100644 tests/regression/apparmor/userns.h create mode 100644 tests/regression/apparmor/userns_setns.c diff --git a/.gitignore b/.gitignore index f6c66862b..3ac06fb18 100644 --- a/.gitignore +++ b/.gitignore @@ -296,6 +296,7 @@ tests/regression/apparmor/unix_socket tests/regression/apparmor/unix_socket_client tests/regression/apparmor/unlink tests/regression/apparmor/userns +tests/regression/apparmor/userns_setns tests/regression/apparmor/uservars.inc tests/regression/apparmor/xattrs tests/regression/apparmor/xattrs_profile diff --git a/tests/regression/apparmor/Makefile b/tests/regression/apparmor/Makefile index d035cf14c..8fa433c48 100644 --- a/tests/regression/apparmor/Makefile +++ b/tests/regression/apparmor/Makefile @@ -143,6 +143,7 @@ SRC=access.c \ unix_socket_client.c \ unlink.c \ userns.c \ + userns_setns.c \ xattrs.c \ xattrs_profile.c @@ -332,6 +333,12 @@ unix_fd_client: unix_fd_client.c unix_fd_common.o attach_disconnected: attach_disconnected.c unix_fd_common.o ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} +userns: userns.c userns.h + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +userns_setns: userns_setns.c userns.h + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + build-dep: @if [ `whoami` = "root" ] ;\ then \ diff --git a/tests/regression/apparmor/userns.c b/tests/regression/apparmor/userns.c index e00235acc..bc2beb596 100644 --- a/tests/regression/apparmor/userns.c +++ b/tests/regression/apparmor/userns.c @@ -20,6 +20,10 @@ #include #include #include +#include +#include +#include +#include "userns.h" static void usage(char *pname) { @@ -27,6 +31,8 @@ static void usage(char *pname) fprintf(stderr, "Options can be:\n"); fprintf(stderr, " -c create user namespace using clone\n"); fprintf(stderr, " -u create user namespace using unshare\n"); + fprintf(stderr, " -s create user namespace using setns. requires the path of binary that will create the user namespace\n"); + fprintf(stderr, " -p named pipe path. used by setns\n"); exit(EXIT_FAILURE); } @@ -38,6 +44,91 @@ static int child(void *arg) return EXIT_SUCCESS; } +#ifndef __NR_pidfd_open +#define __NR_pidfd_open 434 /* System call # on most architectures */ +#endif + +static int +pidfd_open(pid_t pid, unsigned int flags) +{ + return syscall(__NR_pidfd_open, pid, flags); +} + +int userns_setns(char *client, char *pipename) +{ + int userns, exit_status, ret; + char *parentpipe = NULL, *childpipe = NULL; + + if (get_pipes(pipename, &parentpipe, &childpipe) == -1) { + fprintf(stderr, "FAIL - failed to allocate pipes\n"); + ret = EXIT_FAILURE; + goto out; + } + + if (mkfifo(parentpipe, 0666) == -1) + perror("FAIL - setns parent mkfifo"); + + /* exec the client */ + int pid = fork(); + if (pid == -1) { + perror("FAIL - could not fork"); + ret = EXIT_FAILURE; + goto out; + } else if (!pid) { + execl(client, client, pipename, NULL); + printf("FAIL %d - execlp %s %s- %m\n", getuid(), client, pipename); + ret = EXIT_FAILURE; + goto out; + } + + if (read_from_pipe(parentpipe) == -1) { // wait for child to unshare + fprintf(stderr, "FAIL - parent could not read from pipe\n"); + ret = EXIT_FAILURE; + goto out; + } + + userns = pidfd_open(pid, 0); + if (userns == -1) { + perror("FAIL - pidfd_open"); + ret = EXIT_FAILURE; + goto out; + } + + // enter child namespace + if (setns(userns, CLONE_NEWUSER) == -1) { + perror("FAIL - setns"); + ret = EXIT_FAILURE; + } + + if (write_to_pipe(childpipe) == -1) { // let child finish + fprintf(stderr, "FAIL - child could not write in pipe\n"); + ret = EXIT_FAILURE; + goto out; + } + + if (waitpid(pid, &exit_status, 0) == -1) { + perror("FAIL - setns waitpid"); + ret = EXIT_FAILURE; + goto out; + } + + if (WIFEXITED(exit_status)) { + if (WEXITSTATUS(exit_status) != 0) { + fprintf(stderr, "FAIL - setns child ended with failure %d\n", exit_status); + ret = EXIT_FAILURE; + goto out; + } + } + + ret = EXIT_SUCCESS; +out: + if (unlink(parentpipe) == -1) + perror("FAIL - could not remove parentpipe"); + free(parentpipe); + free(childpipe); + return ret; +} + int userns_unshare() { if (unshare(CLONE_NEWUSER) == -1) { @@ -60,7 +151,7 @@ int userns_clone() } if (waitpid(child_pid, &child_exit, 0) == -1) { - perror("FAIL - waitpid"); + perror("FAIL - clone waitpid"); return EXIT_FAILURE; } @@ -77,16 +168,26 @@ int userns_clone() enum op { CLONE, UNSHARE, + SETNS, }; int main(int argc, char *argv[]) { int opt, ret = 0, op; + char *client = "userns_setns"; + char *pipename = "/tmp/userns_pipe"; - while ((opt = getopt(argc, argv, "uc")) != -1) { + while ((opt = getopt(argc, argv, "us:cp:")) != -1) { switch (opt) { case 'c': op = CLONE; break; case 'u': op = UNSHARE; break; + case 's': + op = SETNS; + client = optarg; + break; + case 'p': + pipename = optarg; + break; default: usage(argv[0]); } } @@ -95,6 +196,9 @@ int main(int argc, char *argv[]) ret = userns_clone(); else if (op == UNSHARE) ret = userns_unshare(); + else if (op == SETNS) { + ret = userns_setns(client, pipename); + } else fprintf(stderr, "FAIL - user namespace method not defined\n"); diff --git a/tests/regression/apparmor/userns.h b/tests/regression/apparmor/userns.h new file mode 100644 index 000000000..d31cda51c --- /dev/null +++ b/tests/regression/apparmor/userns.h @@ -0,0 +1,67 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +int get_pipes(const char *pipename, char **parentpipe, char **childpipe) +{ + if (asprintf(parentpipe, "%s1", pipename) == -1) + return -1; + if (asprintf(childpipe, "%s2", pipename) == -1) + return -1; + return 0; +} + +int read_from_pipe(char *pipename) +{ + int fd, ret; + char buf; + fd_set set; + struct timeval timeout; + + fd = open(pipename, O_RDONLY | O_NONBLOCK); + if (fd == -1) { + perror("FAIL - open read pipe"); + return EXIT_FAILURE; + } + + FD_ZERO(&set); + FD_SET(fd, &set); + + timeout.tv_sec = 3; + timeout.tv_usec = 0; + + ret = select(fd + 1, &set, NULL, NULL, &timeout); + if (ret == -1) { + perror("FAIL - select"); + goto err; + } else if (ret == 0) { + fprintf(stderr, "FAIL - read timeout\n"); + goto err; + } else { + if (read(fd, &buf, 1) == -1) { // wait for client to unshare + perror("FAIL - read pipe"); + close(fd); + return EXIT_FAILURE; + } + } + return EXIT_SUCCESS; +err: + return EXIT_FAILURE; +} + +int write_to_pipe(char *pipename) +{ + int fd; + + fd = open(pipename, O_WRONLY | O_NONBLOCK); + if (fd == -1) { + perror("FAIL - open write pipe"); + return EXIT_FAILURE; + } + close(fd); + return EXIT_SUCCESS; +} diff --git a/tests/regression/apparmor/userns.sh b/tests/regression/apparmor/userns.sh index 417e0ba8d..327274a0c 100755 --- a/tests/regression/apparmor/userns.sh +++ b/tests/regression/apparmor/userns.sh @@ -21,6 +21,12 @@ bin=$pwd requires_kernel_features namespaces/mask/userns_create requires_parser_support "userns," +userns_bin=$bin/userns +userns_setns_bin=$bin/userns_setns +pipe=/tmp/pipe +parentpipe="$pipe"1 +childpipe="$pipe"2 + apparmor_restrict_unprivileged_userns_path=/proc/sys/kernel/apparmor_restrict_unprivileged_userns if [ ! -e $apparmor_restrict_unprivileged_userns_path ]; then echo "$apparmor_restrict_unprivileged_userns_path not available. Skipping tests ..." @@ -45,33 +51,48 @@ do_test() local desc="USERNS ($1)" expect_root=$2 expect_user=$3 - generate_profile=$4 + expect_setns_root=$4 + expect_setns_user=$5 + generate_profile=$6 + + if [ ! -z "$generate_profile" ]; then + # add profile for userns_setns_bin + # ptrace is needed because userns_bin needs to + # access userns_setns_bin's /proc/pid/ns/user + generate_setns_profile="$generate_profile $userns_setns_bin:px $parentpipe:rw $childpipe:rw cap:sys_ptrace ptrace:read -- image=$userns_setns_bin userns $parentpipe:rw $childpipe:wr ptrace:readby cap:sys_admin" + fi settest userns $generate_profile # settest removes the profile, so load it here runchecktest "$desc clone - root" $expect_root -c # clone runchecktest "$desc unshare - root" $expect_root -u # unshare + $generate_setns_profile + runchecktest "$desc setns - root" $expect_setns_root -s $userns_setns_bin -p $pipe # setns + settest -u "foo" userns # run tests as user foo $generate_profile # settest removes the profile, so load it here runchecktest "$desc clone - user" $expect_user -c # clone runchecktest "$desc unshare - user" $expect_user -u # unshare + + $generate_setns_profile + runchecktest "$desc setns - user" $expect_setns_user -s $userns_setns_bin -p $pipe # setns } if [ $unprivileged_userns_clone -eq 0 ]; then echo "WARN: unprivileged_userns_clone is enabled. Both confined and unconfined unprivileged user namespaces are not allowed" detail="unprivileged_userns_clone disabled" - do_test "unconfined - $detail" pass fail + do_test "unconfined - $detail" pass fail pass fail generate_profile="genprofile userns cap:sys_admin" - do_test "confined all perms $detail" pass fail "$generate_profile" + do_test "confined all perms $detail" pass fail pass fail "$generate_profile" generate_profile="genprofile cap:sys_admin" - do_test "confined no perms $detail" fail fail "$generate_profile" + do_test "confined no perms $detail" fail fail pass fail "$generate_profile" generate_profile="genprofile userns:create cap:sys_admin" - do_test "confined specific perms $detail" pass fail "$generate_profile" + do_test "confined specific perms $detail" pass fail pass fail "$generate_profile" exit 0 fi @@ -81,13 +102,21 @@ fi run_confined_tests() { generate_profile="genprofile userns" - do_test "confined all perms $1" pass pass "$generate_profile" + do_test "confined all perms $1" pass pass fail fail "$generate_profile" generate_profile="genprofile" - do_test "confined no perms $1" fail fail "$generate_profile" + do_test "confined no perms $1" fail fail fail fail "$generate_profile" generate_profile="genprofile userns:create" - do_test "confined specific perms $1" pass pass "$generate_profile" + do_test "confined specific perms $1" pass pass fail fail "$generate_profile" + + # setns tests only pass is cap_sys_admin regardless of apparmor permissions + # it only associates to the already created user namespace + generate_profile="genprofile userns cap:sys_admin" + do_test "confined specific perms $1" pass pass pass pass "$generate_profile" + + generate_profile="genprofile cap:sys_admin" + do_test "confined specific perms $1" fail fail pass pass "$generate_profile" } # ---------------------------------------------------- @@ -95,7 +124,7 @@ run_confined_tests() echo 0 > $apparmor_restrict_unprivileged_userns_path detail="apparmor_restrict_unprivileged_userns disabled" -do_test "unconfined - $detail" pass pass +do_test "unconfined - $detail" pass pass pass pass run_confined_tests "$detail" @@ -105,11 +134,11 @@ echo 1 > $apparmor_restrict_unprivileged_userns_path detail="apparmor_restrict_unprivileged_userns enabled" # user cannot create user namespace unless cap_sys_admin -do_test "unconfined $detail" pass fail +do_test "unconfined $detail" pass fail pass pass # it should work when running as user with cap_sys_admin setcap cap_sys_admin+pie $bin/userns -do_test "unconfined cap_sys_admin $detail" pass pass +do_test "unconfined cap_sys_admin $detail" pass pass pass pass # remove cap_sys_admin from binary setcap cap_sys_admin= $bin/userns diff --git a/tests/regression/apparmor/userns_setns.c b/tests/regression/apparmor/userns_setns.c new file mode 100644 index 000000000..64815e9b2 --- /dev/null +++ b/tests/regression/apparmor/userns_setns.c @@ -0,0 +1,53 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +#include "userns.h" + +int main(int argc, char *argv[]) +{ + int ret; + char *pipename = "/tmp/userns_pipe"; + char *parentpipe = NULL, *childpipe = NULL; + + if (argc > 1) + pipename = argv[1]; + + if (get_pipes(pipename, &parentpipe, &childpipe) == -1) { + fprintf(stderr, "FAIL - failed to allocate pipes\n"); + ret = EXIT_FAILURE; + goto out; + } + + if (mkfifo(childpipe, 0666) == -1) + perror("FAIL - setns child mkfifo"); + + if (unshare(CLONE_NEWUSER) == -1) { + perror("FAIL - unshare"); + ret = EXIT_FAILURE; + goto out; + } + + if (write_to_pipe(parentpipe) == -1) { // let parent know user namespace is created + fprintf(stderr, "FAIL - child could not write in pipe\n"); + ret = EXIT_FAILURE; + goto out; + } + if (read_from_pipe(childpipe) == -1) { // wait for parent tell child can finish + fprintf(stderr, "FAIL - child could not read from pipe\n"); + ret = EXIT_FAILURE; + goto out; + } + + ret = EXIT_SUCCESS; +out: + if (unlink(childpipe) == -1) + perror("FAIL - could not remove childpipe"); + free(parentpipe); + free(childpipe); + return ret; +}