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}