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; +}