From cb4a397b1e2ddc8cc41e37c2c4182ef144136e1f Mon Sep 17 00:00:00 2001 From: Georgia Garcia Date: Wed, 20 Oct 2021 19:45:25 +0000 Subject: [PATCH] tests: add attach_disconnected tests This test uses unix_fd_server to open a file and pass its file descriptor to the attach_disconnected tests, which then mounts, pivots root and then tries to open the file. Since the server execs the client, this commit also inverts the order of the parameters to allow the server to forward the client's args along with the unix_socket path. Signed-off-by: Georgia Garcia MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/810 Acked-by: John Johansen --- .gitignore | 1 + tests/regression/apparmor/Makefile | 5 + .../regression/apparmor/attach_disconnected.c | 180 ++++++++++++++++++ .../apparmor/attach_disconnected.sh | 118 ++++++++++++ tests/regression/apparmor/deleted.sh | 6 +- tests/regression/apparmor/unix_fd_server.c | 15 +- tests/regression/apparmor/unix_fd_server.sh | 28 +-- 7 files changed, 329 insertions(+), 24 deletions(-) create mode 100644 tests/regression/apparmor/attach_disconnected.c create mode 100644 tests/regression/apparmor/attach_disconnected.sh diff --git a/.gitignore b/.gitignore index 0af376bec..b294eef29 100644 --- a/.gitignore +++ b/.gitignore @@ -220,6 +220,7 @@ tests/regression/apparmor/*.o tests/regression/apparmor/aa_policy_cache tests/regression/apparmor/access tests/regression/apparmor/at_secure +tests/regression/apparmor/attach_disconnected tests/regression/apparmor/changehat tests/regression/apparmor/changehat_fail tests/regression/apparmor/changehat_fork diff --git a/tests/regression/apparmor/Makefile b/tests/regression/apparmor/Makefile index bf17431a3..5795989dd 100644 --- a/tests/regression/apparmor/Makefile +++ b/tests/regression/apparmor/Makefile @@ -74,6 +74,7 @@ CFLAGS += -g -O0 $(EXTRA_WARNINGS) SRC=access.c \ at_secure.c \ + attach_disconnected.c \ introspect.c \ changeprofile.c \ changehat.c \ @@ -200,6 +201,7 @@ EXEC=$(SRC:%.c=%) TESTS=aa_exec \ access \ + attach_disconnected \ at_secure \ introspect \ capabilities \ @@ -323,6 +325,9 @@ unix_fd_common.o: unix_fd_common.c unix_fd_common.h unix_fd_client: unix_fd_client.c unix_fd_common.o ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} +attach_disconnected: attach_disconnected.c unix_fd_common.o + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + build-dep: @if [ `whoami` = "root" ] ;\ then \ diff --git a/tests/regression/apparmor/attach_disconnected.c b/tests/regression/apparmor/attach_disconnected.c new file mode 100644 index 000000000..afefa547b --- /dev/null +++ b/tests/regression/apparmor/attach_disconnected.c @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2021 Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU General Public + * License published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Canonical Ltd. + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "unix_fd_common.h" + +struct clone_arg { + const char *socket; + const char *disk_img; + const char *new_root; + const char *put_old; +}; + +static int _pivot_root(const char *new_root, const char *put_old) +{ +#ifdef __NR_pivot_root + return syscall(__NR_pivot_root, new_root, put_old); +#else + errno = ENOSYS; + return -1; +#endif +} + +static int pivot_and_get_unix_clientfd(void *arg) +{ + int rc; + const char *socket = ((struct clone_arg *)arg)->socket; + const char *disk_img = ((struct clone_arg *)arg)->disk_img; + const char *new_root = ((struct clone_arg *)arg)->new_root; + const char *put_old = ((struct clone_arg *)arg)->put_old; + + char *tmp = strdup(put_old); + char *put_old_bname = basename(tmp); // don't free + + char *socket_put_old; + rc = asprintf(&socket_put_old, "/%s/%s", put_old_bname, socket); + if (rc < 0) { + perror("FAIL - asprintf socket_put_old"); + rc = errno; + socket_put_old = NULL; + goto out; + } + + rc = mkdir(new_root, 0777); + if (rc < 0 && errno != EEXIST) { + perror("FAIL - mkdir new_root"); + rc = 100; + goto out; + } + + rc = mount(disk_img, new_root, "ext2", 0, NULL); + if (rc < 0) { + perror("FAIL - mount disk_img"); + rc = 101; + goto out; + } + + rc = chdir(new_root); + if (rc < 0) { + perror("FAIL - chdir"); + rc = 102; + goto out; + } + + rc = mkdir(put_old, 0777); + if (rc < 0 && errno != EEXIST) { + perror("FAIL - mkdir put_old"); + rc = 103; + goto out; + } + + rc = _pivot_root(new_root, put_old); + if (rc < 0) { + perror("FAIL - pivot_root"); + rc = 104; + goto out; + } + + /* Actual test - it tries to open the socket which is detached. + * Only allowed when there's the flag attach_disconnected and/or + * attach_disconnected.path is defined. + */ + rc = get_unix_clientfd(socket_put_old); + +out: + free(tmp); + free(socket_put_old); + + exit(rc); +} + +static pid_t _clone(int (*fn)(void *), void *arg) +{ + size_t stack_size = sysconf(_SC_PAGESIZE); + void *stack = alloca(stack_size); + +#ifdef __ia64__ + return __clone2(fn, stack, stack_size, + CLONE_NEWNS | SIGCHLD, arg); +#else + return clone(fn, stack + stack_size, + CLONE_NEWNS | SIGCHLD, arg); +#endif +} + +int main(int argc, char **argv) +{ + struct clone_arg arg; + pid_t child; + int child_status, rc; + + if (argc != 5) { + fprintf(stderr, + "FAIL - usage: %s \n\n" + " \tThe path of the unix socket the server will connect to\n" + " \t\tThe loop device pointing to the disk image\n" + " \t\tThe new_root param of pivot_root()\n" + " \t\tThe put_old param of pivot_root()\n\n" + "This program clones itself in a new mount namespace, \n" + "does a pivot and then connects to the .\n" + "The test fails if the program does not have attach_disconnected\n" + "permission to access the unix_socket which is disconnected.\n", argv[0]); + exit(1); + } + + arg.socket = argv[1]; + arg.disk_img = argv[2]; + arg.new_root = argv[3]; + arg.put_old = argv[4]; + + child = _clone(pivot_and_get_unix_clientfd, &arg); + if (child < 0) { + perror("FAIL - clone"); + exit(2); + } + + rc = waitpid(child, &child_status, 0); + if (rc < 0) { + perror("FAIL - waitpid"); + exit(3); + } else if (!WIFEXITED(child_status)) { + fprintf(stderr, "FAIL - child didn't exit\n"); + exit(4); + } else if (WEXITSTATUS(child_status)) { + /* The child has already printed a FAIL message */ + exit(WEXITSTATUS(child_status)); + } + + printf("PASS\n"); + exit(0); +} diff --git a/tests/regression/apparmor/attach_disconnected.sh b/tests/regression/apparmor/attach_disconnected.sh new file mode 100644 index 000000000..1a176c5e5 --- /dev/null +++ b/tests/regression/apparmor/attach_disconnected.sh @@ -0,0 +1,118 @@ +#! /bin/bash +# Copyright (C) 2021 Canonical, Ltd. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, version 2 of the +# License. + +#=NAME attach_disconnected +#=DESCRIPTION +# This test verifies that the attached_disconnected flag is indeed restricting +# access to disconnected paths. +#=END + +pwd=`dirname $0` +pwd=`cd $pwd ; /bin/pwd` + +bin=$pwd + +. $bin/prologue.inc + +settest unix_fd_server +disk_img=$tmpdir/disk_img +new_root=$tmpdir/new_root/ +put_old=${new_root}put_old/ +root_was_shared="no" +fstype="ext2" +file=$tmpdir/file +socket=$tmpdir/unix_fd_test +att_dis_client=$pwd/attach_disconnected + +attach_disconnected_cleanup() { + if [ ! -z "$loop_device" ]; then + losetup -d $loop_device + fi + + mountpoint -q "$new_root" + if [ $? -eq 0 ] ; then + umount "$new_root" + fi + + if [ "$root_was_shared" = "yes" ] ; then + [ -n "$VERBOSE" ] && echo 'notice: re-mounting / as shared' + mount --make-shared / + fi +} +do_onexit="attach_disconnected_cleanup" + +if [ ! -b /dev/loop0 ] ; then + modprobe loop +fi + +# systemd mounts / and everything under it MS_SHARED. This breaks +# pivot_root entirely, so attempt to detect it, and remount / +# MS_PRIVATE temporarily. +FINDMNT=/bin/findmnt +if [ -x "${FINDMNT}" ] && ${FINDMNT} -no PROPAGATION / > /dev/null 2>&1 ; then + if [ "$(${FINDMNT} -no PROPAGATION /)" == "shared" ] ; then + root_was_shared="yes" + fi +elif [ "$(ps hp1 -ocomm)" = "systemd" ] ; then + # no findmnt or findmnt doesn't know the PROPAGATION column, + # but init is systemd so assume rootfs is shared + root_was_shared="yes" +fi +if [ "${root_was_shared}" = "yes" ] ; then + [ -n "$VERBOSE" ] && echo 'notice: re-mounting / as private' + mount --make-private / +fi + +dd if=/dev/zero of="$disk_img" bs=1024 count=512 2> /dev/null +/sbin/mkfs -t "$fstype" -F "$disk_img" > /dev/null 2> /dev/null +# mounting will be done by the test binary +loop_device=$(losetup -fP --show "${disk_img}") + +# content generated with: +# dd if=/dev/urandom bs=32 count=4 2> /dev/null | od -x | head -8 | sed -e 's/^[[:xdigit:]]\{7\}//g' -e 's/ //g' +# required by unix_fd_server which this test is based on +cat > ${file} << EOM +4bcd0f741e97195c57f1ff72dbdf2dd9 +8284a4cd56699628c185f6f647805a1c +8bdee094b7e73f9834ada004c570ad49 +a9a92856edb4a206f271b537fe73081f +ac62547499fffd79021898cc8653e36b +c943fd5f8f4cfa4690a08505e44b0906 +7532527375fb6dc0ddadfcb2f1bcdd82 +150223d965fefe996f8a6c602cc1b514 +EOM + +do_test() +{ + local desc="ATTACH_DISCONNECTED ($1)" + shift + runchecktest "$desc" "$@" +} + +# Needed for clone(CLONE_NEWNS) and pivot_root() +cap=capability:sys_admin +file_perm="$file:rw /put_old/$file:rw" +create_dir="$new_root:w $put_old:w" + +# Ensure everything works as expected when unconfined +do_test "attach_disconnected" pass $file $att_dis_client $socket $loop_device $new_root $put_old + +genprofile $file_perm unix:create $socket:rw $att_dis_client:px -- image=$att_dis_client $file_perm unix:create $socket:rw $create_dir $cap "pivot_root:ALL" "mount:ALL" flag:attach_disconnected + +do_test "attach_disconnected" pass $file $att_dis_client $socket $loop_device $new_root $put_old + +genprofile $file_perm unix:create $socket:rw $att_dis_client:px -- image=$att_dis_client $file_perm unix:create $socket:rw $create_dir $cap "pivot_root:ALL" "mount:ALL" flag:no_attach_disconnected + +do_test "no_attach_disconnected" fail $file $att_dis_client $socket $loop_device $new_root $put_old + +# Ensure default is no_attach_disconnected - no flags set +genprofile $file_perm unix:create $socket:rw $att_dis_client:px -- image=$att_dis_client $file_perm unix:create $socket:rw $create_dir $cap "pivot_root:ALL" "mount:ALL" + +do_test "no_attach_disconnected" fail $file $att_dis_client $socket $loop_device $new_root $put_old + +# TODO: Add .path to the attach_disconnected flag diff --git a/tests/regression/apparmor/deleted.sh b/tests/regression/apparmor/deleted.sh index 9ca937f6d..94b505da0 100755 --- a/tests/regression/apparmor/deleted.sh +++ b/tests/regression/apparmor/deleted.sh @@ -89,7 +89,7 @@ rm -f ${socket} genprofile $af_unix $file:$okperm $socket:rw $fd_client:ux -runchecktest "fd passing; unconfined client" pass $file $socket $fd_client "delete_file" +runchecktest "fd passing; unconfined client" pass $file $fd_client $socket "delete_file" sleep 1 cat > ${file} << EOM @@ -106,7 +106,7 @@ rm -f ${socket} # PASS - confined client, rw access to the file genprofile $af_unix $file:$okperm $socket:rw $fd_client:px -- image=$fd_client $af_unix $file:$okperm $socket:rw -runchecktest "fd passing; confined client w/ rw" pass $file $socket $fd_client "delete_file" +runchecktest "fd passing; confined client w/ rw" pass $file $fd_client $socket "delete_file" sleep 1 cat > ${file} << EOM @@ -123,7 +123,7 @@ rm -f ${socket} # FAIL - confined client, w access to the file genprofile $af_unix $file:$okperm $socket:rw $fd_client:px -- image=$fd_client $af_unix $file:$badperm $socket:rw -runchecktest "fd passing; confined client w/ w only" fail $file $socket $fd_client "delete_file" +runchecktest "fd passing; confined client w/ w only" fail $file $fd_client $socket "delete_file" sleep 1 rm -f ${socket} diff --git a/tests/regression/apparmor/unix_fd_server.c b/tests/regression/apparmor/unix_fd_server.c index 290e555e4..a3fe0ee4b 100644 --- a/tests/regression/apparmor/unix_fd_server.c +++ b/tests/regression/apparmor/unix_fd_server.c @@ -40,8 +40,9 @@ int main (int argc, char * argv[]) { struct cmsghdr *ctrl_mesg; struct pollfd pfd; - if (argc < 4 || argc > 5 || (argc == 5 && (strcmp(argv[4], "delete_file") != 0))) { - fprintf(stderr, "Usage: %s \n", argv[0]); + /* The server forwards the client's arguments */ + if (argc < 4) { + fprintf(stderr, "Usage: %s [client args ...]\n", argv[0]); return(1); } @@ -57,7 +58,7 @@ int main (int argc, char * argv[]) { return(1); } - if (argc == 5) { + if (argc == 5 && strcmp(argv[4], "delete_file") == 0) { if (unlink(argv[1]) == -1){ fprintf(stderr, "FAIL: unlink before passing fd - %s\n", strerror(errno)); @@ -73,7 +74,7 @@ int main (int argc, char * argv[]) { } local.sun_family = AF_UNIX; - strcpy(local.sun_path, argv[2]); + strcpy(local.sun_path, argv[3]); unlink(local.sun_path); len = strlen(local.sun_path) + sizeof(local.sun_family); @@ -92,7 +93,7 @@ int main (int argc, char * argv[]) { /* exec the client */ int pid = fork(); if (!pid) { - execlp(argv[3], argv[3], argv[2], NULL); + execvp(argv[2], &(argv[2])); exit(0); } @@ -108,8 +109,8 @@ int main (int argc, char * argv[]) { exit(1); } - vect.iov_base = argv[2]; - vect.iov_len = strlen(argv[2]) + 1; + vect.iov_base = argv[3]; + vect.iov_len = strlen(argv[3]) + 1; mesg.msg_name = NULL; mesg.msg_namelen = 0; diff --git a/tests/regression/apparmor/unix_fd_server.sh b/tests/regression/apparmor/unix_fd_server.sh index 0538feec6..3ce4e9790 100755 --- a/tests/regression/apparmor/unix_fd_server.sh +++ b/tests/regression/apparmor/unix_fd_server.sh @@ -49,7 +49,7 @@ rm -f ${socket} # PASS - unconfined -> unconfined -runchecktest "fd passing; unconfined -> unconfined" pass $file $socket $fd_client +runchecktest "fd passing; unconfined -> unconfined" pass $file $fd_client $socket sleep 1 rm -f ${socket} @@ -58,7 +58,7 @@ rm -f ${socket} genprofile $file:$okperm $af_unix $socket:rw $fd_client:ux -runchecktest "fd passing; confined -> unconfined" pass $file $socket $fd_client +runchecktest "fd passing; confined -> unconfined" pass $file $fd_client $socket sleep 1 rm -f ${socket} @@ -67,7 +67,7 @@ rm -f ${socket} genprofile $file:$badperm $af_unix $socket:rw $fd_client:ux -runchecktest "fd passing; confined (bad perm) -> unconfined" fail $file $socket $fd_client +runchecktest "fd passing; confined (bad perm) -> unconfined" fail $file $fd_client $socket sleep 1 rm -f ${socket} @@ -76,7 +76,7 @@ rm -f ${socket} genprofile $af_unix $socket:rw $fd_client:ux -runchecktest "fd passing; confined (no perm) -> unconfined" fail $file $socket $fd_client +runchecktest "fd passing; confined (no perm) -> unconfined" fail $file $fd_client $socket sleep 1 rm -f ${socket} @@ -84,7 +84,7 @@ rm -f ${socket} # PASS (due to delegation) - unconfined -> confined genprofile image=$fd_client $file:$okperm $af_unix $socket:rw -runchecktest "fd passing; unconfined -> confined" pass $file $socket $fd_client +runchecktest "fd passing; unconfined -> confined" pass $file $fd_client $socket sleep 1 rm -f ${socket} @@ -92,23 +92,23 @@ rm -f ${socket} # PASS (due to delegation) - unconfined -> confined (no perm) genprofile image=$fd_client $af_unix $socket:rw -runchecktest "fd passing; unconfined -> confined (no perm)" pass $file $socket $fd_client +runchecktest "fd passing; unconfined -> confined (no perm)" pass $file $fd_client $socket sleep 1 rm -f ${socket} # PASS - confined -> confined - +echo "PASS-----------------------------------------" genprofile $file:$okperm $af_unix $socket:rw $fd_client:px -- image=$fd_client $file:$okperm $af_unix $socket:rw -runchecktest "fd passing; confined -> confined" pass $file $socket $fd_client +runchecktest "fd passing; confined -> confined" pass $file $fd_client $socket sleep 1 rm -f ${socket} - +exit # FAIL - confined (bad perm) -> confined genprofile $file:$badperm $af_unix $socket:rw $fd_client:px -- image=$fd_client $file:$okperm $af_unix $socket:rw -runchecktest "fd passing; confined (bad perm) -> confined" fail $file $socket $fd_client +runchecktest "fd passing; confined (bad perm) -> confined" fail $file $fd_client $socket sleep 1 rm -f ${socket} @@ -116,7 +116,7 @@ rm -f ${socket} # FAIL - confined (no perm) -> confined genprofile $af_unix $socket:rw $fd_client:px -- image=$fd_client $file:$okperm $af_unix $socket:rw -runchecktest "fd passing; confined (no perm) -> confined" fail $file $socket $fd_client +runchecktest "fd passing; confined (no perm) -> confined" fail $file $fd_client $socket sleep 1 rm -f ${socket} @@ -124,7 +124,7 @@ rm -f ${socket} # FAIL - confined -> confined (bad perm) genprofile $file:$okperm $af_unix $socket:rw $fd_client:px -- image=$fd_client $file:$badperm $af_unix $socket:rw -runchecktest "fd passing; confined -> confined (bad perm)" fail $file $socket $fd_client +runchecktest "fd passing; confined -> confined (bad perm)" fail $file $fd_client $socket sleep 1 rm -f ${socket} @@ -132,7 +132,7 @@ rm -f ${socket} # FAIL - confined -> confined (no perm) genprofile $file:$okperm $af_unix $socket:rw $fd_client:px -- image=$fd_client $af_unix $socket:rw -runchecktest "fd passing; confined -> confined (no perm)" fail $file $socket $fd_client +runchecktest "fd passing; confined -> confined (no perm)" fail $file $fd_client $socket sleep 1 rm -f ${socket} @@ -141,7 +141,7 @@ if [ "$(kernel_features policy/network/af_unix)" == "true" -a "$(parser_supports # FAIL - confined client, no access to the socket file genprofile $file:$okperm $af_unix $socket:rw $fd_client:px -- image=$fd_client $file:$okperm $af_unix - runchecktest "fd passing; confined client w/o socket access" fail $file $socket $fd_client + runchecktest "fd passing; confined client w/o socket access" fail $file $fd_client $socket sleep 1 rm -f ${socket}