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 <georgia.garcia@canonical.com>
This commit is contained in:
Georgia Garcia 2022-11-04 09:56:03 +00:00
parent 592a0743f0
commit c6f1981cd8
6 changed files with 274 additions and 13 deletions

1
.gitignore vendored
View file

@ -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

View file

@ -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 \

View file

@ -20,6 +20,10 @@
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/limits.h>
#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");

View file

@ -0,0 +1,67 @@
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
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;
}

View file

@ -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

View file

@ -0,0 +1,53 @@
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#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;
}