2024-02-29 17:59:50 +00:00
#!/usr/bin/python3
# ----------------------------------------------------------------------
# Copyright (C) 2024 Canonical, Ltd.
2025-06-15 16:38:37 +02:00
# Copyright (C) 2025 Christian Boltz <apparmor@cboltz.de>
2024-02-29 17:59:50 +00:00
#
# 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 as 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.
#
# ----------------------------------------------------------------------
import unittest
from collections import namedtuple
from common_test import AATest , setup_all_loops
from apparmor . common import AppArmorException , AppArmorBug
from apparmor . translations import init_translation
2025-05-16 10:17:14 +02:00
from apparmor . rule . mount import MountRule , MountConditional
2024-02-29 17:59:50 +00:00
_ = init_translation ( )
class MountTestParse ( AATest ) :
tests = (
2024-03-04 09:24:58 -03:00
# Rule Operation Filesystem Options Source Destination Audit Deny Allow Comment
2024-05-17 13:53:42 +02:00
( ' mount -> **, ' , MountRule ( ' mount ' , MountRule . ALL , MountRule . ALL , MountRule . ALL , ' ** ' , False , False , False , ' ' ) ) ,
( ' mount options=(rw, shared) -> **, ' , MountRule ( ' mount ' , MountRule . ALL , ( ' = ' , ( ' rw ' , ' shared ' ) ) , MountRule . ALL , ' ** ' , False , False , False , ' ' ) ) ,
2025-06-04 22:42:34 +02:00
( ' mount options=rw, ' , MountRule ( ' mount ' , MountRule . ALL , ( ' = ' , ( ' rw ' ) ) , MountRule . ALL , MountRule . ALL , False , False , False , ' ' ) ) ,
2024-05-17 13:53:42 +02:00
( ' mount fstype=bpf options=rw bpf -> /sys/fs/bpf/, ' , MountRule ( ' mount ' , ( ' = ' , [ ' bpf ' ] ) , ( ' = ' , ( ' rw ' ) ) , ' bpf ' , ' /sys/fs/bpf/ ' , False , False , False , ' ' ) ) ,
( ' mount fstype=fuse.obex* options=rw bpf -> /sys/fs/bpf/, ' , MountRule ( ' mount ' , ( ' = ' , [ ' fuse.obex* ' ] ) , ( ' = ' , ( ' rw ' ) ) , ' bpf ' , ' /sys/fs/bpf/ ' , False , False , False , ' ' ) ) ,
( ' mount fstype=fuse.* options=rw bpf -> /sys/fs/bpf/, ' , MountRule ( ' mount ' , ( ' = ' , [ ' fuse.* ' ] ) , ( ' = ' , ( ' rw ' ) ) , ' bpf ' , ' /sys/fs/bpf/ ' , False , False , False , ' ' ) ) ,
( ' mount fstype=bpf options=(rw) random_label -> /sys/fs/bpf/, ' , MountRule ( ' mount ' , ( ' = ' , [ ' bpf ' ] ) , ( ' = ' , ( ' rw ' ) ) , ' random_label ' , ' /sys/fs/bpf/ ' , False , False , False , ' ' ) ) ,
( ' mount, ' , MountRule ( ' mount ' , MountRule . ALL , MountRule . ALL , MountRule . ALL , MountRule . ALL , False , False , False , ' ' ) ) ,
( ' mount fstype=(ext3, ext4), ' , MountRule ( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , MountRule . ALL , MountRule . ALL , MountRule . ALL , False , False , False , ' ' ) ) ,
( ' mount bpf, ' , MountRule ( ' mount ' , MountRule . ALL , MountRule . ALL , ' bpf ' , MountRule . ALL , False , False , False , ' ' ) ) ,
( ' mount none, ' , MountRule ( ' mount ' , MountRule . ALL , MountRule . ALL , ' none ' , MountRule . ALL , False , False , False , ' ' ) ) ,
2025-02-28 23:26:52 +01:00
( ' mount fstype=(procfs) none -> /foo, ' , MountRule ( ' mount ' , ( ' = ' , [ ' procfs ' ] ) , MountRule . ALL , ' none ' , ' /foo ' , False , False , False , ' ' ) ) ,
2024-05-17 13:53:42 +02:00
( ' mount fstype=(ext3, ext4) options=(ro), ' , MountRule ( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' ) ) , MountRule . ALL , MountRule . ALL , False , False , False , ' ' ) ) ,
( ' mount @ {mntpnt} , ' , MountRule ( ' mount ' , MountRule . ALL , MountRule . ALL , ' @ {mntpnt} ' , MountRule . ALL , False , False , False , ' ' ) ) ,
( ' mount /a, ' , MountRule ( ' mount ' , MountRule . ALL , MountRule . ALL , ' /a ' , MountRule . ALL , False , False , False , ' ' ) ) ,
2024-06-09 21:51:01 +02:00
( ' mount " /a space " , ' , MountRule ( ' mount ' , MountRule . ALL , MountRule . ALL , ' /a space ' , MountRule . ALL , False , False , False , ' ' ) ) ,
2024-05-17 13:53:42 +02:00
( ' mount fstype=(ext3, ext4) /a -> /b, ' , MountRule ( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , MountRule . ALL , ' /a ' , ' /b ' , False , False , False , ' ' ) ) ,
2024-06-09 21:51:01 +02:00
( ' mount fstype=(ext3, ext4) /a -> " /bar space " , ' , MountRule ( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , MountRule . ALL , ' /a ' , ' /bar space ' , False , False , False , ' ' ) ) ,
MountRule: Aligning behavior with apparmor_parser
Mount Rules with options in { remount, [make-] { [r]unbindable, [r]shared, [r]private, and [r]slave }} do not support specifying a source. This commit aligns utils implementation to apparmor_parser's, which prohibits having a both source and a destination simultaneously, instad of just prohibiting source.
Therefore, both `mount options=(unbindable) /a,` and `mount options=(unbindable) -> /a,` are now supported (and equivalent for apparmor_parser). However, `mount options=(unbindable) /a -> /b,` is invalid.
For the same reason, specifying a fstype in these cases is also prohibited.
Similarly, we prohibit to specify a fstype for bind mount rules.
Fixes: https://bugs.launchpad.net/ubuntu/+source/apparmor/+bug/2065685
Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2024-05-20 11:09:04 +02:00
( ' mount fstype=(ext3, ext4) options=(ro, sync) /a -> /b, ' , MountRule ( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' , ' sync ' ) ) , ' /a ' , ' /b ' , False , False , False , ' ' ) ) ,
( ' mount fstype=(ext3, ext4) options=(ro, sync) /a -> /b, #cmt ' , MountRule ( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' , ' sync ' ) ) , ' /a ' , ' /b ' , False , False , False , ' #cmt ' ) ) ,
( ' mount fstype=( { ext3,ext4}) options in (ro, sync) /a -> /b, ' , MountRule ( ' mount ' , ( ' = ' , [ ' { ext3,ext4} ' ] ) , ( ' in ' , ( ' ro ' , ' sync ' ) ) , ' /a ' , ' /b ' , False , False , False , ' ' ) ) ,
( ' mount fstype in (ext3, ext4) options=(ro, sync) /a -> /b, #cmt ' , MountRule ( ' mount ' , ( ' in ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' , ' sync ' ) ) , ' /a ' , ' /b ' , False , False , False , ' #cmt ' ) ) ,
( ' mount fstype in (ext3, ext4) option in (ro, sync) /a, #cmt ' , MountRule ( ' mount ' , ( ' in ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' in ' , ( ' ro ' , ' sync ' ) ) , ' /a ' , MountRule . ALL , False , False , False , ' #cmt ' ) ) ,
( ' mount fstype=(ext3, ext4) option=(ro, sync) /a -> /b, #cmt ' , MountRule ( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' , ' sync ' ) ) , ' /a ' , ' /b ' , False , False , False , ' #cmt ' ) ) ,
2025-02-28 23:26:52 +01:00
( ' mount fstype=fuse.revokefs-fuse options=(nosuid,nodev,rw) revokefs-fuse -> /foo-*/**/, ' ,
MountRule ( ' mount ' , ( ' = ' , [ ' fuse.revokefs-fuse ' ] ) , ( ' = ' , ( ' nosuid ' , ' nodev ' , ' rw ' ) ) , # noqa: E127
' revokefs-fuse ' , ' /foo-*/**/ ' , False , False , False , ' ' ) ) , # noqa: E127
2024-03-07 19:27:03 +00:00
( ' mount options=(rw, rbind) { ,/usr}/lib { ,32,64,x32}/modules/ -> /tmp/snap.rootfs_* { ,/usr}/lib/modules/, ' ,
2024-05-17 13:53:42 +02:00
MountRule ( ' mount ' , MountRule . ALL , ( ' = ' , ( ' rw ' , ' rbind ' ) ) , ' { ,/usr}/lib { ,32,64,x32}/modules/ ' , # noqa: E127
' /tmp/snap.rootfs_* { ,/usr}/lib/modules/ ' , # noqa: E127
False , False , False , ' ' ) ) , # noqa: E127
2024-06-09 23:03:13 +02:00
( ' mount options=(runbindable, rw) -> /, ' , MountRule ( ' mount ' , MountRule . ALL , ( ' = ' , [ ' runbindable ' , ' rw ' ] ) , MountRule . ALL , ' / ' , False , False , False , ' ' ) ) ,
( ' mount " " -> /, ' , MountRule ( ' mount ' , MountRule . ALL , MountRule . ALL , ' ' , ' / ' , False , False , False , ' ' ) ) ,
2025-05-16 10:17:14 +02:00
2025-05-22 15:51:45 -03:00
( ' mount options=(ro) options=(rw) fstype=ext4 -> /dest, ' , MountRule ( ' mount ' , ( ' = ' , [ ' ext4 ' ] ) , [ ( ' = ' , ( ' ro ' ) ) , ( ' = ' , ( ' rw ' ) ) ] , # noqa: E127
2025-05-16 10:17:14 +02:00
MountRule . ALL , ' /dest ' , False , False , False , ' ' ) ) , # noqa: E127
2025-05-22 15:51:45 -03:00
( ' mount options=(ro) fstype=ext4 options=(rw) /src -> /dest, ' , MountRule ( ' mount ' , ( ' = ' , [ ' ext4 ' ] ) , [ ( ' = ' , ( ' ro ' ) ) , ( ' = ' , ( ' rw ' ) ) ] , # noqa: E127
2025-05-16 10:17:14 +02:00
' /src ' , ' /dest ' , False , False , False , ' ' ) ) , # noqa: E127
2025-05-22 15:51:45 -03:00
( ' mount options in (ro) options in (rw) fstype=ext4 -> /dest, ' , MountRule ( ' mount ' , ( ' = ' , [ ' ext4 ' ] ) , [ ( ' in ' , ( ' ro ' ) ) , ( ' in ' , ( ' rw ' ) ) ] , # noqa: E127
2025-05-16 10:17:14 +02:00
MountRule . ALL , ' /dest ' , False , False , False , ' ' ) ) , # noqa: E127
2025-05-22 15:51:45 -03:00
( ' mount options in (ro) fstype=ext4 options in (rw) -> /dest, ' , MountRule ( ' mount ' , ( ' = ' , [ ' ext4 ' ] ) , [ ( ' in ' , ( ' ro ' ) ) , ( ' in ' , ( ' rw ' ) ) ] , # noqa: E127
2025-05-16 10:17:14 +02:00
MountRule . ALL , ' /dest ' , False , False , False , ' ' ) ) , # noqa: E127
2025-05-22 15:51:45 -03:00
( ' mount options = (ro) options in (rw) fstype=ext4 -> /dest, ' , MountRule ( ' mount ' , ( ' = ' , [ ' ext4 ' ] ) , [ ( ' = ' , ( ' ro ' ) ) , ( ' in ' , ( ' rw ' ) ) ] , # noqa: E127
2025-05-16 10:17:14 +02:00
MountRule . ALL , ' /dest ' , False , False , False , ' ' ) ) , # noqa: E127
2025-05-22 15:51:45 -03:00
( ' mount options = (ro) fstype=ext4 options in (rw) -> /dest, ' , MountRule ( ' mount ' , ( ' = ' , [ ' ext4 ' ] ) , [ ( ' = ' , ( ' ro ' ) ) , ( ' in ' , ( ' rw ' ) ) ] , # noqa: E127
2025-05-16 10:17:14 +02:00
MountRule . ALL , ' /dest ' , False , False , False , ' ' ) ) , # noqa: E127
2025-05-22 15:51:45 -03:00
( ' mount options=(ro) fstype=ext3 fstype=ext4 -> /dest, ' , MountRule ( ' mount ' , [ ( ' = ' , [ ' ext3 ' ] ) , ( ' = ' , [ ' ext4 ' ] ) ] ,
( ' = ' , ( ' ro ' ) ) , MountRule . ALL , ' /dest ' , False , False , False , ' ' ) ) , # noqa: E127
( ' mount fstype=ext3 options=(ro) fstype=ext4 src -> /dest, ' , MountRule ( ' mount ' , [ ( ' = ' , [ ' ext3 ' ] ) , ( ' = ' , [ ' ext4 ' ] ) ] ,
( ' = ' , ( ' ro ' ) ) , ' src ' , ' /dest ' , False , False , False , ' ' ) ) , # noqa: E127
( ' mount options=(ro) fstype in (ext3) fstype in (ext4) -> /dest, ' , MountRule ( ' mount ' , [ ( ' in ' , [ ' ext3 ' ] ) , ( ' in ' , [ ' ext4 ' ] ) ] ,
( ' = ' , ( ' ro ' ) ) , MountRule . ALL , ' /dest ' , False , False , False , ' ' ) ) , # noqa: E127
( ' mount fstype in (ext3) options=(ro) fstype in (ext4) -> /dest, ' , MountRule ( ' mount ' , [ ( ' in ' , [ ' ext3 ' ] ) , ( ' in ' , [ ' ext4 ' ] ) ] ,
( ' = ' , ( ' ro ' ) ) , MountRule . ALL , ' /dest ' , False , False , False , ' ' ) ) , # noqa: E127
( ' mount options=(ro) fstype in (ext3) fstype=(ext4) -> /dest, ' , MountRule ( ' mount ' , [ ( ' in ' , [ ' ext3 ' ] ) , ( ' = ' , [ ' ext4 ' ] ) ] ,
( ' = ' , ( ' ro ' ) ) , MountRule . ALL , ' /dest ' , False , False , False , ' ' ) ) , # noqa: E127
( ' mount fstype = (ext3) options=(ro) fstype in ext4 -> /dest, ' , MountRule ( ' mount ' , [ ( ' = ' , [ ' ext3 ' ] ) , ( ' in ' , [ ' ext4 ' ] ) ] ,
( ' = ' , ( ' ro ' ) ) , MountRule . ALL , ' /dest ' , False , False , False , ' ' ) ) , # noqa: E127
2024-05-17 13:53:42 +02:00
( ' umount, ' , MountRule ( ' umount ' , MountRule . ALL , MountRule . ALL , MountRule . ALL , MountRule . ALL , False , False , False , ' ' ) ) ,
( ' umount fstype=ext3, ' , MountRule ( ' umount ' , ( ' = ' , [ ' ext3 ' ] ) , MountRule . ALL , MountRule . ALL , MountRule . ALL , False , False , False , ' ' ) ) ,
( ' umount /a, ' , MountRule ( ' umount ' , MountRule . ALL , MountRule . ALL , MountRule . ALL , ' /a ' , False , False , False , ' ' ) ) ,
( ' remount, ' , MountRule ( ' remount ' , MountRule . ALL , MountRule . ALL , MountRule . ALL , MountRule . ALL , False , False , False , ' ' ) ) ,
( ' remount fstype=ext4, ' , MountRule ( ' remount ' , ( ' = ' , [ ' ext4 ' ] ) , MountRule . ALL , MountRule . ALL , MountRule . ALL , False , False , False , ' ' ) ) ,
( ' remount /b, ' , MountRule ( ' remount ' , MountRule . ALL , MountRule . ALL , MountRule . ALL , ' /b ' , False , False , False , ' ' ) ) ,
2024-02-29 17:59:50 +00:00
)
def _run_test ( self , rawrule , expected ) :
self . assertTrue ( MountRule . match ( rawrule ) )
obj = MountRule . create_instance ( rawrule )
expected . raw_rule = rawrule . strip ( )
2024-06-09 21:51:01 +02:00
self . assertTrue ( obj . is_equal ( expected , True ) , f ' \n { rawrule } expected, \n { obj . get_clean ( ) } returned by obj.get_clean() \n { expected . get_clean ( ) } returned by expected.get_clean() ' )
2024-02-29 17:59:50 +00:00
MountRule: Aligning behavior with apparmor_parser
Mount Rules with options in { remount, [make-] { [r]unbindable, [r]shared, [r]private, and [r]slave }} do not support specifying a source. This commit aligns utils implementation to apparmor_parser's, which prohibits having a both source and a destination simultaneously, instad of just prohibiting source.
Therefore, both `mount options=(unbindable) /a,` and `mount options=(unbindable) -> /a,` are now supported (and equivalent for apparmor_parser). However, `mount options=(unbindable) /a -> /b,` is invalid.
For the same reason, specifying a fstype in these cases is also prohibited.
Similarly, we prohibit to specify a fstype for bind mount rules.
Fixes: https://bugs.launchpad.net/ubuntu/+source/apparmor/+bug/2065685
Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2024-05-20 11:09:04 +02:00
def test_valid_mount_changing_propagation ( self ) :
# Rules changing propagation type can either specify a source or a dest (these are equivalent for apparmor_parser in this specific case) but not both.
MountRule ( ' mount ' , MountRule . ALL , ( ' = ' , ( ' runbindable ' ) ) , ' /foo ' , MountRule . ALL )
MountRule ( ' mount ' , MountRule . ALL , ( ' = ' , ( ' runbindable ' ) ) , MountRule . ALL , ' /foo ' )
def test_valid_bind_mount ( self ) :
# Fstype must remain empty in bind rules
MountRule ( ' mount ' , MountRule . ALL , ( ' = ' , ( ' bind ' ) ) , ' /foo ' , MountRule . ALL )
MountRule ( ' mount ' , MountRule . ALL , ( ' = ' , ( ' bind ' ) ) , MountRule . ALL , ' /bar ' )
MountRule ( ' mount ' , MountRule . ALL , ( ' = ' , ( ' bind ' ) ) , ' /foo ' , ' /bar ' )
2024-02-29 17:59:50 +00:00
class MountTestParseInvalid ( AATest ) :
tests = (
2025-06-29 17:31:03 +02:00
# exception matches regex
( ' mount fstype=, ' , ( AppArmorException , True ) ) ,
( ' mount fstype=(), ' , ( AppArmorException , True ) ) ,
( ' mount options=(), ' , ( AppArmorException , True ) ) ,
( ' mount option=(invalid), ' , ( AppArmorException , True ) ) ,
( ' mount option=(ext3ext4), ' , ( AppArmorException , True ) ) ,
( ' priority=-1042 umount, ' , ( AppArmorException , True ) ) ,
( ' mount fstype=( { unclosed_regex), ' , ( AppArmorException , True ) ) , # invalid AARE
( ' mount fstype=( {closed} twice}), ' , ( AppArmorException , True ) ) , # invalid AARE
2024-02-29 17:59:50 +00:00
)
def _run_test ( self , rawrule , expected ) :
2025-06-29 17:31:03 +02:00
self . parseInvalidRule ( MountRule , rawrule , expected )
2024-02-29 17:59:50 +00:00
def test_parse_fail ( self ) :
with self . assertRaises ( AppArmorException ) :
MountRule . create_instance ( ' foo, ' )
2025-06-15 18:34:54 +02:00
def test_invalid_priority ( self ) :
for keyword in [ ' mount ' , ' umount ' , ' unmount ' , ' remount ' ] :
with self . assertRaises ( AppArmorException ) :
MountRule . create_instance ( ' priority=a %s , ' % keyword )
def test_invalid_priority_1 ( self ) :
with self . assertRaises ( TypeError ) :
MountRule ( ' mount ' , MountRule . ALL , MountRule . ALL , MountRule . ALL , MountRule . ALL , priority = MountRule . ALL )
def test_invalid_priority_2 ( self ) :
with self . assertRaises ( AppArmorException ) :
MountRule ( ' mount ' , MountRule . ALL , MountRule . ALL , MountRule . ALL , MountRule . ALL , priority = ' invalid ' )
2024-02-29 17:59:50 +00:00
def test_diff_non_mountrule ( self ) :
2025-02-09 04:35:52 -08:00
exp = namedtuple ( ' exp ' , ( ' audit ' , ' deny ' , ' priority ' ) )
2024-03-28 10:42:12 +01:00
obj = MountRule ( ' mount ' , ( ' = ' , [ ' ext4 ' ] ) , MountRule . ALL , MountRule . ALL , MountRule . ALL )
2024-02-29 17:59:50 +00:00
with self . assertRaises ( AppArmorBug ) :
2025-02-09 04:35:52 -08:00
obj . is_equal ( exp ( False , False , None ) , False )
2024-02-29 17:59:50 +00:00
def test_diff_invalid_fstype_equals_or_in ( self ) :
with self . assertRaises ( AppArmorBug ) :
2024-03-03 15:52:14 +01:00
MountRule ( ' mount ' , ( ' ext3 ' , ' ext4 ' ) , MountRule . ALL , MountRule . ALL , MountRule . ALL ) # fstype[0] should be '=' or 'in'
2024-02-29 17:59:50 +00:00
2024-03-28 10:42:12 +01:00
def test_diff_invalid_fstype_aare_2 ( self ) :
fslists = [
2024-05-17 13:53:42 +02:00
[ ' invalid_ { _regex ' ] ,
[ ' ext4 ' , ' invalid_}_regex ' ] ,
[ ' ext4 ' , ' {invalid} {regex} ' ]
2024-03-28 10:42:12 +01:00
]
for fslist in fslists :
with self . assertRaises ( AppArmorException ) :
MountRule ( ' mount ' , ( ' = ' , fslist ) , MountRule . ALL , MountRule . ALL , MountRule . ALL )
2024-02-29 17:59:50 +00:00
def test_diff_invalid_options_equals_or_in ( self ) :
with self . assertRaises ( AppArmorBug ) :
2024-03-04 09:24:58 -03:00
MountRule ( ' mount ' , MountRule . ALL , ( ' rbind ' , ' rw ' ) , MountRule . ALL , MountRule . ALL ) # fstype[0] should be '=' or 'in'
2024-02-29 17:59:50 +00:00
2024-03-03 16:01:40 +01:00
def test_diff_invalid_options_keyword ( self ) :
with self . assertRaises ( AppArmorException ) :
2024-03-04 09:24:58 -03:00
MountRule ( ' mount ' , MountRule . ALL , ( ' = ' , ' invalid ' ) , MountRule . ALL , MountRule . ALL ) # fstype[0] should be '=' or 'in'
2024-02-29 17:59:50 +00:00
def test_diff_fstype ( self ) :
2024-03-28 10:42:12 +01:00
obj1 = MountRule ( ' mount ' , ( ' = ' , [ ' ext4 ' ] ) , MountRule . ALL , MountRule . ALL , MountRule . ALL )
2024-03-04 09:24:58 -03:00
obj2 = MountRule ( ' mount ' , MountRule . ALL , MountRule . ALL , MountRule . ALL , MountRule . ALL )
2024-02-29 17:59:50 +00:00
self . assertFalse ( obj1 . is_equal ( obj2 , False ) )
def test_diff_source ( self ) :
2024-03-04 09:24:58 -03:00
obj1 = MountRule ( ' mount ' , MountRule . ALL , MountRule . ALL , ' /foo ' , MountRule . ALL )
obj2 = MountRule ( ' mount ' , MountRule . ALL , MountRule . ALL , ' /bar ' , MountRule . ALL )
2024-02-29 17:59:50 +00:00
self . assertFalse ( obj1 . is_equal ( obj2 , False ) )
def test_invalid_umount_with_source ( self ) :
with self . assertRaises ( AppArmorException ) :
2024-03-04 09:24:58 -03:00
MountRule ( ' umount ' , MountRule . ALL , MountRule . ALL , ' /foo ' , MountRule . ALL ) # Umount and remount shall not have a source
2024-02-29 17:59:50 +00:00
def test_invalid_remount_with_source ( self ) :
with self . assertRaises ( AppArmorException ) :
2024-03-04 09:24:58 -03:00
MountRule ( ' remount ' , MountRule . ALL , MountRule . ALL , ' /foo ' , MountRule . ALL )
2024-02-29 17:59:50 +00:00
MountRule: Aligning behavior with apparmor_parser
Mount Rules with options in { remount, [make-] { [r]unbindable, [r]shared, [r]private, and [r]slave }} do not support specifying a source. This commit aligns utils implementation to apparmor_parser's, which prohibits having a both source and a destination simultaneously, instad of just prohibiting source.
Therefore, both `mount options=(unbindable) /a,` and `mount options=(unbindable) -> /a,` are now supported (and equivalent for apparmor_parser). However, `mount options=(unbindable) /a -> /b,` is invalid.
For the same reason, specifying a fstype in these cases is also prohibited.
Similarly, we prohibit to specify a fstype for bind mount rules.
Fixes: https://bugs.launchpad.net/ubuntu/+source/apparmor/+bug/2065685
Signed-off-by: Maxime Bélair <maxime.belair@canonical.com>
2024-05-20 11:09:04 +02:00
def test_invalid_mount_changing_propagation ( self ) :
# Rules changing propagation type can either specify a source or a dest (these are equivalent for apparmor_parser in this specific case) but not both.
with self . assertRaises ( AppArmorException ) :
MountRule ( ' mount ' , MountRule . ALL , ( ' = ' , ( ' runbindable ' ) ) , ' /foo ' , ' /bar ' )
# Rules changing propagation type cannot specify a fstype.
with self . assertRaises ( AppArmorException ) :
MountRule ( ' mount ' , ( ' = ' , ( ' ext4 ' ) ) , ( ' = ' , ( ' runbindable ' ) ) , MountRule . ALL , ' /foo ' )
def test_invalid_bind_mount ( self ) :
# Bind mount rules cannot specify a fstype.
with self . assertRaises ( AppArmorException ) :
MountRule ( ' mount ' , ( ' = ' , ( ' ext4 ' ) ) , ( ' = ' , ( ' bind ' ) ) , MountRule . ALL , ' /foo ' )
2025-05-22 15:51:45 -03:00
def test_invalid_cond_type ( self ) :
# cond_type cannot be None if all_values is False
with self . assertRaises ( AppArmorBug ) :
obj = MountConditional ( ' name ' , [ ] , False , ' = ' , cond_type = None )
obj . is_covered ( obj )
2024-02-29 17:59:50 +00:00
class MountTestGlob ( AATest ) :
def test_glob ( self ) :
globList = [ (
2024-03-04 09:24:58 -03:00
' mount options=(bind, rw) /home/user/Downloads/ -> /mnt/a/, ' ,
' mount options=(bind, rw) /home/user/Downloads/, ' ,
' mount options=(bind, rw) /home/user/*/, ' ,
' mount options=(bind, rw) /home/**/, ' ,
' mount options=(bind, rw), ' ,
' mount, ' ,
' mount, ' ,
2024-02-29 17:59:50 +00:00
) ]
for globs in globList :
2024-03-04 09:24:58 -03:00
for i in range ( len ( globs ) - 1 ) :
2024-02-29 17:59:50 +00:00
rule = MountRule . create_instance ( globs [ i ] )
rule . glob ( )
2024-03-04 09:24:58 -03:00
self . assertEqual ( rule . get_clean ( ) , globs [ i + 1 ] )
2024-02-29 17:59:50 +00:00
class MountTestClean ( AATest ) :
tests = (
# raw rule clean rule
( ' mount , # foo ' , ' mount, # foo ' ) ,
( ' mount fstype = ( sysfs ) , ' , ' mount fstype=(sysfs), ' ) ,
( ' mount fstype = ( sysfs , procfs ) , ' , ' mount fstype=(procfs, sysfs), ' ) ,
( ' mount options = ( rw ) , ' , ' mount options=(rw), ' ) ,
( ' mount options = ( rw , noatime ) , ' , ' mount options=(noatime, rw), ' ) ,
2024-03-03 12:45:08 +01:00
( ' mount fstype in ( sysfs ) , ' , ' mount fstype in (sysfs), ' ) ,
( ' mount fstype in ( sysfs , procfs ) , ' , ' mount fstype in (procfs, sysfs), ' ) ,
( ' mount options in ( rw ) , ' , ' mount options in (rw), ' ) ,
( ' mount options in ( rw , noatime ) , ' , ' mount options in (noatime, rw), ' ) ,
2025-03-11 21:02:52 +01:00
( ' mount none -> /foo , ' , ' mount none -> /foo, ' ) ,
( ' mount " " -> /foo , ' , ' mount " " -> /foo, ' ) ,
( ' mount " /f /b " -> " /foo bar " , ' , ' mount " /f /b " -> " /foo bar " , ' ) ,
2024-03-03 13:09:43 +01:00
( ' umount , ' , ' umount, ' ) ,
2024-02-29 17:59:50 +00:00
( ' umount /foo , ' , ' umount /foo, ' ) ,
2024-03-03 13:09:43 +01:00
( ' remount , ' , ' remount, ' ) ,
2024-02-29 17:59:50 +00:00
( ' remount /foo , ' , ' remount /foo, ' ) ,
2025-02-09 04:35:52 -08:00
( ' priority =1 mount " " -> /foo , ' , ' priority=1 mount " " -> /foo, ' ) ,
( ' priority=0 audit mount " /f /b " -> " /foo bar " , ' , ' priority=0 audit mount " /f /b " -> " /foo bar " , ' ) ,
( ' priority = +10 umount , ' , ' priority=10 umount, ' ) ,
( ' priority=-2 deny umount /foo , ' , ' priority=-2 deny umount /foo, ' ) ,
( ' priority= 32 audit deny remount , ' , ' priority=32 audit deny remount, ' ) ,
( ' priority = -32 remount /foo , ' , ' priority=-32 remount /foo, ' ) ,
2024-02-29 17:59:50 +00:00
)
def _run_test ( self , rawrule , expected ) :
self . assertTrue ( MountRule . match ( rawrule ) )
obj = MountRule . create_instance ( rawrule )
clean = obj . get_clean ( )
raw = obj . get_raw ( )
self . assertEqual ( expected , clean , ' unexpected clean rule ' )
self . assertEqual ( rawrule . strip ( ) , raw , ' unexpected raw rule ' )
2024-03-04 09:24:58 -03:00
2024-02-29 17:59:50 +00:00
class MountLogprofHeaderTest ( AATest ) :
tests = (
2024-03-04 09:24:58 -03:00
( ' mount, ' , [ _ ( ' Operation ' ) , _ ( ' mount ' ) , _ ( ' Fstype ' ) , _ ( ' ALL ' ) , _ ( ' Options ' ) , _ ( ' ALL ' ) , _ ( ' Source ' ) , _ ( ' ALL ' ) , _ ( ' Destination ' ) , _ ( ' ALL ' ) ] ) ,
( ' mount options=(ro, nosuid) /a, ' , [ _ ( ' Operation ' ) , _ ( ' mount ' ) , _ ( ' Fstype ' ) , _ ( ' ALL ' ) , _ ( ' Options ' ) , ( ' = ' , _ ( ' nosuid ro ' ) ) , _ ( ' Source ' ) , _ ( ' /a ' ) , _ ( ' Destination ' ) , _ ( ' ALL ' ) ] ) ,
( ' mount fstype=(ext3, ext4) options=(ro, nosuid) /a -> /b, ' , [ _ ( ' Operation ' ) , _ ( ' mount ' ) , _ ( ' Fstype ' ) , ( ' = ' , _ ( ' ext3 ext4 ' ) ) , _ ( ' Options ' ) , ( ' = ' , _ ( ' nosuid ro ' ) ) , _ ( ' Source ' ) , _ ( ' /a ' ) , _ ( ' Destination ' ) , _ ( ' /b ' ) ] )
2024-02-29 17:59:50 +00:00
)
def _run_test ( self , params , expected ) :
obj = MountRule . create_instance ( params )
self . assertEqual ( obj . logprof_header ( ) , expected )
class MountIsCoveredTest ( AATest ) :
def test_is_covered ( self ) :
2024-03-04 09:24:58 -03:00
obj = MountRule ( ' mount ' , ( ' = ' , ( ' ext3 ' , ' ext4 ' ) ) , ( ' = ' , ( ' ro ' ) ) , ' /foo/b* ' , ' /b* ' )
2024-02-29 17:59:50 +00:00
tests = [
2025-05-16 10:17:14 +02:00
( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' /foo/b ' , ' /bar ' ) ,
( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' /foo/bar ' , ' /b ' ) ,
( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , [ ( ' = ' , ( ' ro ' ) ) ] , ' /foo/bar ' , ' /b ' )
2024-02-29 17:59:50 +00:00
]
for test in tests :
self . assertTrue ( obj . is_covered ( MountRule ( * test ) ) )
self . assertFalse ( obj . is_equal ( MountRule ( * test ) ) )
def test_is_covered_fs_source ( self ) :
2024-03-28 10:42:12 +01:00
obj = MountRule ( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' tmpfs ' , MountRule . ALL )
self . assertTrue ( obj . is_covered ( MountRule ( ' mount ' , ( ' = ' , [ ' ext3 ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' tmpfs ' , MountRule . ALL ) ) )
self . assertFalse ( obj . is_equal ( MountRule ( ' mount ' , ( ' = ' , [ ' ext3 ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' tmpfs ' , MountRule . ALL ) ) )
2024-02-29 17:59:50 +00:00
2024-03-28 10:42:12 +01:00
def test_is_covered_aare_1 ( self ) :
obj = MountRule ( ' mount ' , ( ' = ' , [ ' sys* ' , ' fuse.* ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' tmpfs ' , MountRule . ALL )
2024-03-07 19:27:03 +00:00
tests = [
2024-03-28 10:42:12 +01:00
( ' mount ' , ( ' = ' , [ ' sysfs ' , ' fuse.s3fs ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' tmpfs ' , MountRule . ALL ) ,
( ' mount ' , ( ' = ' , [ ' sysfs ' , ' fuse.jmtpfs ' , ' fuse.s3fs ' , ' fuse.obexfs ' , ' fuse.obexautofs ' , ' fuse.fuseiso ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' tmpfs ' , MountRule . ALL )
]
for test in tests :
self . assertTrue ( obj . is_covered ( MountRule ( * test ) ) )
self . assertFalse ( obj . is_equal ( MountRule ( * test ) ) )
2024-05-17 13:53:42 +02:00
2024-03-28 10:42:12 +01:00
def test_is_covered_aare_2 ( self ) :
obj = MountRule ( ' mount ' , ( ' = ' , [ ' ext { 3,4} ' , ' { cgroup*,fuse.*} ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' tmpfs ' , MountRule . ALL )
tests = [
( ' mount ' , ( ' = ' , [ ' ext3 ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' tmpfs ' , MountRule . ALL ) ,
( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' , ' cgroup ' , ' cgroup2 ' , ' fuse.jmtpfs ' , ' fuse.s3fs ' , ' fuse.obexfs ' , ' fuse.obexautofs ' , ' fuse.fuseiso ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' tmpfs ' , MountRule . ALL )
2024-03-07 19:27:03 +00:00
]
for test in tests :
self . assertTrue ( obj . is_covered ( MountRule ( * test ) ) )
self . assertFalse ( obj . is_equal ( MountRule ( * test ) ) )
2025-05-16 10:17:14 +02:00
def test_is_covered_options_diff_operator ( self ) :
obj = MountRule ( ' mount ' , ( ' = ' , ( ' ext3 ' , ' ext4 ' ) ) , ( ' = ' , ( ' ro ' ) ) , ' /foo/b* ' , ' /b* ' )
test_obj = MountRule ( ' mount ' , ( ' = ' , ( ' ext3 ' , ' ext4 ' ) ) , ( ' in ' , ( ' ro ' ) ) , ' /foo/b* ' , ' /b* ' )
self . assertFalse ( obj . is_covered ( test_obj ) , ' \n ' + test_obj . get_clean ( ) + ' \n should not be covered by \n ' + obj . get_clean ( ) )
def test_is_covered_options_all ( self ) :
obj = MountRule ( ' mount ' , ( ' = ' , ( ' ext3 ' , ' ext4 ' ) ) , MountRule . ALL , ' /foo/b* ' , ' /b* ' )
test_obj = MountRule ( ' mount ' , ( ' = ' , ( ' ext3 ' , ' ext4 ' ) ) , ( ' in ' , ( ' ro ' ) ) , ' /foo/b* ' , ' /b* ' )
self . assertTrue ( obj . is_covered ( test_obj ) , ' \n ' + test_obj . get_clean ( ) + ' \n should be covered by \n ' + obj . get_clean ( ) )
def test_is_covered_options_true ( self ) :
obj = MountRule ( ' mount ' , ( ' = ' , ( ' ext3 ' , ' ext4 ' ) ) , [ ( ' in ' , ( ' ro ' ) ) , ( ' = ' , ( ' rw ' , ' noexec ' ) ) , ( ' in ' , ( ' ro ' , ' nosuid ' ) ) ] , ' /foo/b* ' , ' /b* ' )
test_obj = MountRule ( ' mount ' , ( ' = ' , ( ' ext3 ' , ' ext4 ' ) ) , [ ( ' = ' , ( ' rw ' , ' noexec ' ) ) , ( ' in ' , ( ' ro ' , ' nosuid ' ) ) ] , ' /foo/b* ' , ' /b* ' )
self . assertTrue ( obj . is_covered ( test_obj ) , ' \n ' + test_obj . get_clean ( ) + ' \n should be covered by \n ' + obj . get_clean ( ) )
def test_is_equal_options_diff_operator ( self ) :
obj = MountRule ( ' mount ' , ( ' = ' , ( ' ext3 ' , ' ext4 ' ) ) , ( ' = ' , ( ' ro ' ) ) , ' /foo/b* ' , ' /b* ' )
test_obj = MountRule ( ' mount ' , ( ' = ' , ( ' ext3 ' , ' ext4 ' ) ) , ( ' in ' , ( ' ro ' ) ) , ' /foo/b* ' , ' /b* ' )
self . assertFalse ( obj . is_equal ( test_obj , True ) , ' \n ' + test_obj . get_clean ( ) + ' \n should not be equal to \n ' + obj . get_clean ( ) )
def test_is_equal_options_diff_options ( self ) :
obj = MountRule ( ' mount ' , ( ' = ' , ( ' ext3 ' , ' ext4 ' ) ) , [ ( ' in ' , ( ' ro ' ) ) , ( ' = ' , ( ' nosuid ' , ' noexec ' ) ) ] , ' /foo/b* ' , ' /b* ' )
test_obj = MountRule ( ' mount ' , ( ' = ' , ( ' ext3 ' , ' ext4 ' ) ) , ( ' in ' , ( ' ro ' ) ) , ' /foo/b* ' , ' /b* ' )
self . assertFalse ( obj . is_equal ( test_obj , True ) , ' \n ' + test_obj . get_clean ( ) + ' \n should not be equal to \n ' + obj . get_clean ( ) )
def test_is_equal_options_true ( self ) :
obj = MountRule ( ' mount ' , ( ' = ' , ( ' ext3 ' , ' ext4 ' ) ) , [ ( ' in ' , ( ' ro ' ) ) , ( ' = ' , ( ' nosuid ' , ' noexec ' ) ) ] , ' /foo/b* ' , ' /b* ' )
test_obj = MountRule ( ' mount ' , ( ' = ' , ( ' ext3 ' , ' ext4 ' ) ) , [ ( ' in ' , ( ' ro ' ) ) , ( ' = ' , ( ' nosuid ' , ' noexec ' ) ) ] , ' /foo/b* ' , ' /b* ' )
self . assertTrue ( obj . is_equal ( test_obj , True ) , ' \n ' + test_obj . get_clean ( ) + ' \n should be equal to \n ' + obj . get_clean ( ) )
2025-05-22 15:51:45 -03:00
def test_eq_cond_diff_type ( self ) :
obj = MountConditional ( ' options ' , [ ' ro ' ] , False , ' in ' , ' aare ' )
test_obj = MountConditional ( ' options ' , [ ' ro ' ] , False , ' in ' , ' list ' )
self . assertFalse ( test_obj == obj , ' cond_types should be different ' )
2025-05-16 10:17:14 +02:00
def test_eq_cond_diff_instance ( self ) :
obj = MountRule ( ' mount ' , ( ' = ' , ( ' ext3 ' , ' ext4 ' ) ) , [ ( ' in ' , ( ' ro ' ) ) , ( ' = ' , ( ' nosuid ' , ' noexec ' ) ) ] , ' /foo/b* ' , ' /b* ' )
2025-05-22 15:51:45 -03:00
test_obj = MountConditional ( ' options ' , [ ' ro ' ] , False , ' in ' , ' list ' )
2025-05-16 10:17:14 +02:00
self . assertFalse ( test_obj == obj , ' Incompatible comparison ' )
def test_covered_cond_diff_instance ( self ) :
obj = MountRule ( ' mount ' , ( ' = ' , ( ' ext3 ' , ' ext4 ' ) ) , [ ( ' in ' , ( ' ro ' ) ) , ( ' = ' , ( ' nosuid ' , ' noexec ' ) ) ] , ' /foo/b* ' , ' /b* ' )
2025-05-22 15:51:45 -03:00
test_obj = MountConditional ( ' options ' , [ ' ro ' ] , False , ' in ' , ' list ' )
2025-05-16 10:17:14 +02:00
self . assertFalse ( test_obj . is_covered ( obj ) , ' Incompatible comparison ' )
def test_covered_cond_diff_name ( self ) :
2025-05-22 15:51:45 -03:00
obj = MountConditional ( ' foo ' , [ ' ro ' ] , False , ' = ' , ' list ' )
test_obj = MountConditional ( ' options ' , [ ' ro ' ] , False , ' = ' , ' list ' )
2025-05-16 10:17:14 +02:00
self . assertFalse ( test_obj . is_covered ( obj ) , ' Incompatible comparison ' )
2024-02-29 17:59:50 +00:00
def test_is_notcovered ( self ) :
2024-03-28 10:42:12 +01:00
obj = MountRule ( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' /foo/b* ' , ' /b* ' )
2024-02-29 17:59:50 +00:00
tests = [
2025-05-16 10:17:14 +02:00
( ' mount ' , ( ' in ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' /foo/bar ' , ' /bar ' ) ,
( ' mount ' , ( ' = ' , [ ' procfs ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' /foo/bar ' , ' /bar ' ) ,
( ' mount ' , ( ' = ' , [ ' ext3 ' ] ) , ( ' = ' , ( ' rw ' ) ) , ' /foo/bar ' , ' /bar ' ) ,
2024-05-17 13:53:42 +02:00
( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , MountRule . ALL , ' /foo/b* ' , ' /bar ' ) ,
2025-05-16 10:17:14 +02:00
( ' mount ' , MountRule . ALL , ( ' = ' , ( ' ro ' ) ) , ' /foo/b* ' , ' /bar ' ) ,
( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' /invalid/bar ' , ' /bar ' ) ,
2024-05-17 13:53:42 +02:00
( ' umount ' , MountRule . ALL , MountRule . ALL , MountRule . ALL , ' /bar ' ) ,
( ' remount ' , MountRule . ALL , MountRule . ALL , MountRule . ALL , ' /bar ' ) ,
2025-05-16 10:17:14 +02:00
( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' tmpfs ' , ' /bar ' ) ,
( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' /foo/b* ' , ' /invalid ' ) ,
( ' mount ' , ( ' = ' , [ ' ext3 ' ] ) , ( ' in ' , ( ' ro ' ) ) , ' /foo/bar ' , ' /bar ' ) ,
( ' mount ' , ( ' = ' , [ ' ext3 ' ] ) , [ ( ' in ' , ( ' ro ' ) ) , ( ' = ' , ( ' rw ' ) ) ] , ' /foo/bar ' , ' /bar ' ) ,
( ' mount ' , ( ' = ' , [ ' ext3 ' ] ) , [ ( ' = ' , ( ' ro ' ) ) , ( ' in ' , ( ' rw ' ) ) ] , ' /foo/bar ' , ' /bar ' ) ,
( ' mount ' , ( ' = ' , [ ' ext3 ' ] ) , [ ( ' = ' , ( ' ro ' , ' nosuid ' ) ) , ( ' in ' , ( ' rw ' ) ) ] , ' /foo/bar ' , ' /bar ' ) ,
2024-02-29 17:59:50 +00:00
]
for test in tests :
self . assertFalse ( obj . is_covered ( MountRule ( * test ) ) )
self . assertFalse ( obj . is_equal ( MountRule ( * test ) ) )
def test_is_not_covered_fs_source ( self ) :
2024-03-28 10:42:12 +01:00
obj = MountRule ( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' tmpfs ' , MountRule . ALL )
test = ( ' mount ' , ( ' = ' , [ ' ext3 ' , ' ext4 ' ] ) , ( ' = ' , ( ' ro ' ) ) , ' procfs ' , MountRule . ALL )
2024-02-29 17:59:50 +00:00
self . assertFalse ( obj . is_covered ( MountRule ( * test ) ) )
self . assertFalse ( obj . is_equal ( MountRule ( * test ) ) )
2024-03-14 12:46:01 +00:00
def test_is_not_covered_fs_options ( self ) :
obj = MountRule ( ' mount ' , MountRule . ALL , ( ' = ' , ( ' ro ' ) ) , ' tmpfs ' , MountRule . ALL )
test = ( ' mount ' , MountRule . ALL , ( ' = ' , ( ' rw ' ) ) , ' procfs ' , MountRule . ALL )
self . assertFalse ( obj . is_covered ( MountRule ( * test ) ) )
self . assertFalse ( obj . is_equal ( MountRule ( * test ) ) )
2024-02-29 17:59:50 +00:00
setup_all_loops ( __name__ )
if __name__ == ' __main__ ' :
unittest . main ( verbosity = 1 )