diff --git a/tests/regression/apparmor/Makefile b/tests/regression/apparmor/Makefile index 5653d8bb2..396ddb576 100644 --- a/tests/regression/apparmor/Makefile +++ b/tests/regression/apparmor/Makefile @@ -307,6 +307,9 @@ unix_socket_client: unix_socket_client.c unix_socket_common.o unix_socket: unix_socket.c unix_socket_common.o unix_socket_client ${CC} ${CFLAGS} ${LDFLAGS} $(filter-out unix_socket_client, $^) -o $@ ${LDLIBS} +mount: mount.c + ${CC} ${CFLAGS} -std=gnu99 ${LDFLAGS} $^ -o $@ ${LDLIBS} + tests: all @if [ `whoami` = "root" ] ;\ then \ diff --git a/tests/regression/apparmor/mkprofile.pl b/tests/regression/apparmor/mkprofile.pl index f6fcc7d7b..b4f68b63a 100755 --- a/tests/regression/apparmor/mkprofile.pl +++ b/tests/regression/apparmor/mkprofile.pl @@ -407,8 +407,8 @@ sub gen_from_args() { if ($rule =~ /^qual=([^:]*):(.*)/) { # Strip qualifiers from rule to pass as separate argument $qualifier = "$1 "; - $qualifier =~ s/,/ /g; $rule = $2; + $qualifier =~ s/,/ /g; } #($fn, @rules) = split (/:/, $rule); diff --git a/tests/regression/apparmor/mount.c b/tests/regression/apparmor/mount.c index a250aa8b2..73e771eec 100644 --- a/tests/regression/apparmor/mount.c +++ b/tests/regression/apparmor/mount.c @@ -14,27 +14,163 @@ #include #include #include +#include + +struct mnt_keyword_table { + const char *keyword; + unsigned long set; + unsigned long clear; +}; + +static struct mnt_keyword_table mnt_opts_table[] = { + { "rw", 0, MS_RDONLY }, /* read-write */ + { "ro", MS_RDONLY, 0 }, /* read-only */ + + { "exec", 0, MS_NOEXEC }, /* permit execution of binaries */ + { "noexec", MS_NOEXEC, 0 }, /* don't execute binaries */ + + { "suid", 0, MS_NOSUID }, /* honor suid executables */ + { "nosuid", MS_NOSUID, 0 }, /* don't honor suid executables */ + + { "dev", 0, MS_NODEV }, /* interpret device files */ + { "nodev", MS_NODEV, 0 }, /* don't interpret devices */ + + { "async", 0, MS_SYNCHRONOUS }, /* asynchronous I/O */ + { "sync", MS_SYNCHRONOUS, 0 }, /* synchronous I/O */ + + { "loud", 0, MS_SILENT }, /* print out messages. */ + { "silent", MS_SILENT, 0 }, /* be quiet */ + + { "nomand", 0, MS_MANDLOCK }, /* forbid mandatory locks on this FS */ + { "mand", MS_MANDLOCK, 0 }, /* allow mandatory locks on this FS */ + + { "atime", 0, MS_NOATIME }, /* update access time */ + { "noatime", MS_NOATIME, 0 }, /* do not update access time */ + + { "noiversion", 0, MS_I_VERSION }, /* don't update inode I_version time */ + { "iversion", MS_I_VERSION, 0 }, /* update inode I_version time */ + + { "diratime", 0, MS_NODIRATIME }, /* update dir access times */ + { "nodiratime", MS_NODIRATIME, 0 }, /* do not update dir access times */ + + { "nostrictatime", 0, MS_STRICTATIME }, /* kernel default atime */ + { "strictatime", MS_STRICTATIME, 0 }, /* strict atime semantics */ + +/* MS_LAZYTIME added in 4.0 kernel */ +#ifdef MS_LAZYTIME + { "nolazytime", 0, MS_LAZYTIME }, + { "lazytime", MS_LAZYTIME, 0 }, /* update {a,m,c}time on the in-memory inode only */ +#endif + + { "acl", MS_POSIXACL, 0 }, + { "noacl", 0, MS_POSIXACL }, + + { "norelatime", 0, MS_RELATIME }, + { "relatime", MS_RELATIME, 0 }, + + { "dirsync", MS_DIRSYNC, 0 }, /* synchronous directory modifications */ + { "nodirsync", 0, MS_DIRSYNC }, + +/* MS_NOSYMFOLLOW added in 5.10 kernel */ +#ifdef MS_NOSYMFOLLOW + { "nosymfollow", MS_NOSYMFOLLOW, 0 }, + { "symfollow", 0, MS_NOSYMFOLLOW }, +#endif + + { "bind", MS_BIND, 0 }, /* remount part of the tree elsewhere */ + { "rbind", MS_BIND | MS_REC, 0 }, /* idem, plus mounted subtrees */ + { "unbindable", MS_UNBINDABLE, 0 }, /* unbindable */ + { "runbindable", MS_UNBINDABLE | MS_REC, 0 }, + { "private", MS_PRIVATE, 0 }, /* private */ + { "rprivate", MS_PRIVATE | MS_REC, 0 }, + { "slave", MS_SLAVE, 0 }, /* slave */ + { "rslave", MS_SLAVE | MS_REC, 0 }, + { "shared", MS_SHARED, 0 }, /* shared */ + { "rshared", MS_SHARED | MS_REC, 0 }, + + { "move", MS_MOVE, 0 }, + + { "remount", MS_REMOUNT, 0 }, +}; + +const unsigned int mnt_opts_table_size = + sizeof(mnt_opts_table) / sizeof(struct mnt_keyword_table); + + +unsigned long get_mnt_opt_bit(char *key) +{ + for (unsigned int i = 0; i < mnt_opts_table_size; i++) { + if (strcmp(mnt_opts_table[i].keyword, key) == 0) { + return mnt_opts_table[i].set; + } + } + fprintf(stderr, "FAIL: invalid option\n"); + exit(1); +} + +static void usage(char *prog_name) +{ + fprintf(stderr, "Usage: %s mount|umount [options]\n", prog_name); + fprintf(stderr, "Options are:\n"); + fprintf(stderr, "-o flags sent to the mount syscall\n"); + fprintf(stderr, "-d data sent to the mount syscall\n"); + exit(1); +} int main(int argc, char *argv[]) { - if (argc != 4) { - fprintf(stderr, "usage: %s [mount|umount] loopdev mountpoint\n", - argv[0]); - return 1; + char *options = NULL; + char *data = NULL; + int index; + int c; + char *op, *source, *target, *token; + unsigned long flags = 0; + + while ((c = getopt (argc, argv, "o:d:h")) != -1) { + switch (c) + { + case 'o': + options = optarg; + break; + case 'd': + data = optarg; + break; + case 'h': + usage(argv[0]); + break; + default: + break; + } } - if (strcmp(argv[1], "mount") == 0) { - if (mount(argv[2], argv[3], "ext2", 0xc0ed0000 | MS_NODEV, NULL ) == -1) { + index = optind; + if (argc - optind < 3) { + fprintf(stderr, "FAIL: missing positional arguments\n"); + usage(argv[0]); + } + + op = argv[index++]; + source = argv[index++]; + target = argv[index++]; + + if (options) { + token = strtok(options, ","); + while (token) { + flags |= get_mnt_opt_bit(token); + token = strtok(NULL, ","); + } + } + + if (strcmp(op, "mount") == 0) { + if (mount(source, target, "ext2", flags, data) == -1) { fprintf(stderr, "FAIL: mount %s on %s failed - %s\n", - argv[2], argv[3], - strerror(errno)); + source, target, strerror(errno)); return errno; } - } else if (strcmp(argv[1], "umount") == 0) { - if (umount(argv[3]) == -1) { + } else if (strcmp(op, "umount") == 0) { + if (umount(target) == -1) { fprintf(stderr, "FAIL: umount %s failed - %s\n", - argv[3], - strerror(errno)); + target, strerror(errno)); return errno; } } else { diff --git a/tests/regression/apparmor/mount.sh b/tests/regression/apparmor/mount.sh index bfd2905b5..49f6e77ab 100755 --- a/tests/regression/apparmor/mount.sh +++ b/tests/regression/apparmor/mount.sh @@ -28,9 +28,11 @@ bin=$pwd mount_file=$tmpdir/mountfile mount_point=$tmpdir/mountpoint +mount_point2=$tmpdir/mountpoint2 mount_bad=$tmpdir/mountbad loop_device="unset" fstype="ext2" +root_was_shared="no" setup_mnt() { /bin/mount -n -t${fstype} ${loop_device} ${mount_point} @@ -41,6 +43,10 @@ remove_mnt() { if [ $? -eq 0 ] ; then /bin/umount -t${fstype} ${mount_point} fi + mountpoint -q "${mount_point2}" + if [ $? -eq 0 ] ; then + /bin/umount -t${fstype} ${mount_point2} + fi mountpoint -q "${mount_bad}" if [ $? -eq 0 ] ; then /bin/umount -t${fstype} ${mount_bad} @@ -53,12 +59,16 @@ mount_cleanup() { then /sbin/losetup -d ${loop_device} &> /dev/null fi + if [ "${root_was_shared}" = "yes" ] ; then + mount --make-shared / + fi } do_onexit="mount_cleanup" dd if=/dev/zero of=${mount_file} bs=1024 count=512 2> /dev/null /sbin/mkfs -t${fstype} -F ${mount_file} > /dev/null 2> /dev/null /bin/mkdir ${mount_point} +/bin/mkdir ${mount_point2} /bin/mkdir ${mount_bad} # in a modular udev world, the devices won't exist until the loopback @@ -71,6 +81,190 @@ fi loop_device=$(losetup -f) || fatalerror 'Unable to find a free loop device' /sbin/losetup "$loop_device" ${mount_file} > /dev/null 2> /dev/null +# systemd mounts / and everything under it MS_SHARED which does +# not work with "move", so attempt to detect it, and remount / +# MS_PRIVATE temporarily. snippet from pivot_root.sh +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 + mount --make-private / +fi + +options=( + # default and non-default options + "rw,ro" + "exec,noexec" + "suid,nosuid" + "dev,nodev" + "async,sync" + "loud,silent" + "nomand,mand" + "atime,noatime" + "noiversion,iversion" + "diratime,nodiratime" + "nostrictatime,strictatime" + "norelatime,relatime" + "nodirsync,dirsync" + "noacl,acl" +) + +# Options added in newer kernels +new_options=( + "nolazytime,lazytime" + "symfollow,nosymfollow" +) + +prop_options=( + "unbindable" + "runbindable" + "private" + "rprivate" + "slave" + "rslave" + "shared" + "rshared" +) + +combinations=() + +setup_all_combinations() { + n=${#options[@]} + for (( i = 1; i < (1 << n); i++ )); do + list=() + for (( j = 0; j < n; j++ )); do + if (( (1 << j) & i )); then + current_options="${options[j]}" + nondefault=${current_options#*,} + list+=("$nondefault") + fi + done + combination=$(IFS=,; printf "%s" "${list[*]}") + combinations+=($combination) + done +} + +run_all_combinations_test() { + for combination in "${combinations[@]}"; do + if [ "$(parser_supports "mount options=($combination),")" = "true" ] ; then + genprofile cap:sys_admin "mount:options=($combination)" + runchecktest "MOUNT (confined cap mount combination pass test $combination)" pass mount ${loop_device} ${mount_point} -o $combination + remove_mnt + + genprofile cap:sys_admin "mount:ALL" "qual=deny:mount:options=($combination)" + runchecktest "MOUNT (confined cap mount combination deny test $combination)" fail mount ${loop_device} ${mount_point} -o $combination + remove_mnt + fi + + genprofile cap:sys_admin "mount:options=(rw)" + runchecktest "MOUNT (confined cap mount combination fail test $combination)" fail mount ${loop_device} ${mount_point} -o $combination + remove_mnt + done +} + +test_nonfs_options() { + if [ "$(parser_supports "mount options=($1),")" != "true" ] ; then + return + fi + + genprofile cap:sys_admin "mount:options=($1)" + runchecktest "MOUNT (confined cap mount $1)" pass mount ${loop_device} ${mount_point} -o $1 + remove_mnt + + genprofile cap:sys_admin "mount:ALL" "qual=deny:mount:options=($1)" + runchecktest "MOUNT (confined cap mount deny $1)" fail mount ${loop_device} ${mount_point} -o $1 + remove_mnt + + genprofile cap:sys_admin "mount:options=($1)" + runchecktest "MOUNT (confined cap mount bad option $2)" fail mount ${loop_device} ${mount_point} -o $2 + remove_mnt +} + +test_dir_options() { + if [ "$(parser_supports "mount options=($1),")" != "true" ] ; then + return + fi + + genprofile cap:sys_admin "mount:ALL" + runchecktest "MOUNT (confined cap mount dir setup $1)" pass mount ${loop_device} ${mount_point} + genprofile cap:sys_admin "mount:options=($1)" + runchecktest "MOUNT (confined cap mount dir $1)" pass mount ${mount_point} ${mount_point2} -o $1 + remove_mnt + + genprofile cap:sys_admin "mount:ALL" "qual=deny:mount:options=($1)" + runchecktest "MOUNT (confined cap mount dir setup 2 $1)" pass mount ${loop_device} ${mount_point} + runchecktest "MOUNT (confined cap mount dir deny $1)" fail mount ${mount_point} ${mount_point2} -o $1 + remove_mnt +} + +test_propagation_options() { + if [ "$(parser_supports "mount options=($1),")" != "true" ] ; then + return + fi + + genprofile cap:sys_admin "mount:ALL" + runchecktest "MOUNT (confined cap mount propagation setup $1)" pass mount ${loop_device} ${mount_point} + genprofile cap:sys_admin "mount:options=($1)" + runchecktest "MOUNT (confined cap mount propagation $1)" pass mount none ${mount_point} -o $1 + remove_mnt + + genprofile cap:sys_admin "mount:ALL" "qual=deny:mount:options=($1)" + runchecktest "MOUNT (confined cap mount propagation deny setup 2 $1)" pass mount ${loop_device} ${mount_point} + runchecktest "MOUNT (confined cap mount propagation deny $1)" fail mount none ${mount_point} -o $1 + remove_mnt +} + +test_remount() { + # setup by mounting first + genprofile cap:sys_admin "mount:ALL" + runchecktest "MOUNT (confined cap mount remount setup)" pass mount ${loop_device} ${mount_point} + + genprofile cap:sys_admin "mount:options=(remount)" + runchecktest "MOUNT (confined cap mount remount option)" pass mount ${loop_device} ${mount_point} -o remount + + genprofile cap:sys_admin "remount:ALL" + runchecktest "MOUNT (confined cap mount remount)" pass mount ${loop_device} ${mount_point} -o remount + + genprofile cap:sys_admin "mount:ALL" "qual=deny:mount:options=(remount)" + runchecktest "MOUNT (confined cap mount remount deny option)" fail mount ${loop_device} ${mount_point} -o remount + + genprofile cap:sys_admin "qual=deny:remount:ALL" + runchecktest "MOUNT (confined cap mount remount deny)" fail mount ${loop_device} ${mount_point} -o remount + + # TODO: add test for remount options + remove_mnt +} + +test_options() { + for i in "${options[@]}"; do + default="${i%,*}" + nondefault="${i#*,}" + + test_nonfs_options $default $nondefault + test_nonfs_options $nondefault $default + done + + for i in "bind" "rbind" "move"; do + test_dir_options $i + done + + for i in "${prop_options[@]}"; do + test_propagation_options $i + done + + test_remount + + # the following combinations tests take a long time to complete + # setup_all_combinations + # run_all_combinations_test +} # TEST 1. Make sure can mount and umount unconfined runchecktest "MOUNT (unconfined)" pass mount ${loop_device} ${mount_point} @@ -80,6 +274,43 @@ setup_mnt runchecktest "UMOUNT (unconfined)" pass umount ${loop_device} ${mount_point} remove_mnt +# Check mount options that may not be available on this kernel +for i in "${new_options[@]}"; do + default="${i%,*}" + if "$bin/mount" mount ${loop_device} ${mount_point} -o $default > /dev/null 2>&1; then + remove_mnt + options+=($i) + else + echo " not supported by kernel - skipping mount options=($i)," + fi +done + +for i in "${options[@]}"; do + default="${i%,*}" + nondefault="${i#*,}" + + runchecktest "MOUNT (unconfined mount $default)" pass mount ${loop_device} ${mount_point} -o $default + remove_mnt + runchecktest "MOUNT (unconfined mount $nondefault)" pass mount ${loop_device} ${mount_point} -o $nondefault + remove_mnt +done + +for i in "bind" "rbind" "move"; do + runchecktest "MOUNT (unconfined mount setup $i)" pass mount ${loop_device} ${mount_point} + runchecktest "MOUNT (unconfined mount $i)" pass mount ${mount_point} ${mount_point2} -o $i + remove_mnt +done + +for i in "${prop_options[@]}"; do + runchecktest "MOUNT (unconfined mount dir setup $i)" pass mount ${loop_device} ${mount_point} + runchecktest "MOUNT (unconfined mount dir $i)" pass mount none ${mount_point} -o $i + remove_mnt +done + +runchecktest "MOUNT (unconfined mount remount setup)" pass mount ${loop_device} ${mount_point} +runchecktest "MOUNT (unconfined mount remount)" pass mount ${loop_device} ${mount_point} -o remount +remove_mnt + # TEST A2. confine MOUNT no perms genprofile runchecktest "MOUNT (confined no perm)" fail mount ${loop_device} ${mount_point} @@ -157,6 +388,7 @@ else runchecktest "UMOUNT (confined cap umount:ALL)" pass umount ${loop_device} ${mount_point} remove_mnt + test_options fi -#need tests for move mount, remount, bind mount, chroot +#need tests for chroot