From 534e2c4e8ee8f71aca0dea48342f854508df354edab68fab7e0abbc7026d729b Mon Sep 17 00:00:00 2001 From: Christian Boltz Date: Tue, 28 May 2024 19:51:09 +0000 Subject: [PATCH] Accepting request 1177403 from home:cboltz - add utils-relax-mount-rules.diff and utils-relax-mount-rules-2.diff: Relax handling of mount rules in utils to avoid errors when parsing valid profiles - add teardown-unconfined.diff to fix aa-teardown for 'unconfined' profiles (boo#1225457) OBS-URL: https://build.opensuse.org/request/show/1177403 OBS-URL: https://build.opensuse.org/package/show/security:apparmor/apparmor?expand=0&rev=409 --- apparmor.changes | 9 + apparmor.spec | 12 +- teardown-unconfined.diff | 21 ++ utils-relax-mount-rules-2.diff | 182 ++++++++++++++++ utils-relax-mount-rules.diff | 366 +++++++++++++++++++++++++++++++++ 5 files changed, 589 insertions(+), 1 deletion(-) create mode 100644 teardown-unconfined.diff create mode 100644 utils-relax-mount-rules-2.diff create mode 100644 utils-relax-mount-rules.diff diff --git a/apparmor.changes b/apparmor.changes index 4b54f64..006a976 100644 --- a/apparmor.changes +++ b/apparmor.changes @@ -1,3 +1,12 @@ +------------------------------------------------------------------- +Tue May 28 19:34:43 UTC 2024 - Christian Boltz + +- add utils-relax-mount-rules.diff and utils-relax-mount-rules-2.diff: + Relax handling of mount rules in utils to avoid errors when + parsing valid profiles +- add teardown-unconfined.diff to fix aa-teardown for 'unconfined' + profiles (boo#1225457) + ------------------------------------------------------------------- Tue May 28 12:20:59 UTC 2024 - Christian Boltz diff --git a/apparmor.spec b/apparmor.spec index 9f03d2c..fbb86f1 100644 --- a/apparmor.spec +++ b/apparmor.spec @@ -88,9 +88,16 @@ Patch10: tools-fix-redefinition.diff # make test-aa-notify a bit more relaxed to allow different argparse wording on Leap 15.5 (merged upstream 2024-05-06 (4.0 and master) https://gitlab.com/apparmor/apparmor/-/merge_requests/1226) Patch11: test-aa-notify.diff -# Fix aa-remove-unknown for 'unconfined' profiles (submitted upstream 2024-05-25 https://gitlab.com/apparmor/apparmor/-/merge_requests/1240) +# Fix aa-remove-unknown for 'unconfined' profiles (merged upstream 2024-05-28 in 4.0 and master https://gitlab.com/apparmor/apparmor/-/merge_requests/1240) Patch12: aa-remove-unknown-fix-unconfined.diff +# Fix aa-teardown for 'unconfined' profiles (submitted upstream 2024-05-28 https://gitlab.com/apparmor/apparmor/-/merge_requests/1242) +Patch13: teardown-unconfined.diff + +# Relax handling of mount rules in utils to avoid errors when parsing valid profiles (both patches taken from upstream 4.0 branch 2024-05-28) +Patch14: utils-relax-mount-rules.diff +Patch15: utils-relax-mount-rules-2.diff + PreReq: sed BuildRoot: %{_tmppath}/%{name}-%{version}-build BuildRequires: autoconf @@ -361,6 +368,9 @@ mv -v profiles/apparmor.d/usr.lib.apache2.mpm-prefork.apache2 profiles/apparmor/ %patch -P 10 -p1 %patch -P 11 -p1 %patch -P 12 -p1 +%patch -P 13 -p1 +%patch -P 14 -p1 +%patch -P 15 -p1 %build export SUSE_ASNEEDED=0 diff --git a/teardown-unconfined.diff b/teardown-unconfined.diff new file mode 100644 index 0000000..5708bd9 --- /dev/null +++ b/teardown-unconfined.diff @@ -0,0 +1,21 @@ +commit f497afbe1364b45540a6582870e5a76f1ada7a2b +Author: Christian Boltz +Date: Tue May 28 21:13:47 2024 +0200 + + Fix aa-teardown for `unconfined` profiles + + Fixes: https://bugzilla.opensuse.org/show_bug.cgi?id=1225457 + +diff --git a/parser/rc.apparmor.functions b/parser/rc.apparmor.functions +index f66fea422..099ab67d3 100644 +--- a/parser/rc.apparmor.functions ++++ b/parser/rc.apparmor.functions +@@ -253,7 +253,7 @@ remove_profiles() { + retval=0 + # We filter child profiles as removing the parent will remove + # the children +- sed -e "s/ (\(enforce\|complain\))$//" "$SFS_MOUNTPOINT/profiles" | \ ++ sed -e "s/ (\(enforce\|complain\|unconfined\))$//" "$SFS_MOUNTPOINT/profiles" | \ + LC_COLLATE=C sort | grep -v // | { + while read -r profile ; do + printf "%s" "$profile" > "$SFS_MOUNTPOINT/.remove" diff --git a/utils-relax-mount-rules-2.diff b/utils-relax-mount-rules-2.diff new file mode 100644 index 0000000..94b34c9 --- /dev/null +++ b/utils-relax-mount-rules-2.diff @@ -0,0 +1,182 @@ +commit 1f4bba0448563b7d1fe4d86c230556ebf8d3805b +Author: Maxime Bélair +Date: Mon May 20 11:09:04 2024 +0200 + + Cherry-pick: 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 + + (cherry picked from commit 60acc4a4053ddb3718b9a2f5ceb1ef02fea3a226) + + Signed-off-by: Maxime Bélair + +diff --git a/utils/apparmor/rule/mount.py b/utils/apparmor/rule/mount.py +index abfa2b75e..b2d73a27b 100644 +--- a/utils/apparmor/rule/mount.py ++++ b/utils/apparmor/rule/mount.py +@@ -25,15 +25,18 @@ _ = init_translation() + + # TODO : Apparmor remount logs are displayed as mount (with remount flag). Profiles generated with aa-genprof are therefore mount rules. It could be interesting to make them remount rules. + +-flags_keywords = [ +- # keep in sync with parser/mount.cc mnt_opts_table! +- 'ro', 'r', 'read-only', 'rw', 'w', 'suid', 'nosuid', 'dev', 'nodev', 'exec', 'noexec', 'sync', 'async', 'remount', +- 'mand', 'nomand', 'dirsync', 'symfollow', 'nosymfollow', 'atime', 'noatime', 'diratime', 'nodiratime', 'bind', 'B', +- 'move', 'M', 'rbind', 'R', 'verbose', 'silent', 'loud', 'acl', 'noacl', 'unbindable', 'make-unbindable', 'runbindable', +- 'make-runbindable', 'private', 'make-private', 'rprivate', 'make-rprivate', 'slave', 'make-slave', 'rslave', 'make-rslave', +- 'shared', 'make-shared', 'rshared', 'make-rshared', 'relatime', 'norelatime', 'iversion', 'noiversion', 'strictatime', +- 'nostrictatime', 'lazytime', 'nolazytime', 'user', 'nouser', +- '([A-Za-z0-9])', ++flags_bind_mount = {'B', 'bind', 'R', 'rbind'} ++flags_change_propagation = { ++ 'remount', 'unbindable', 'shared', 'private', 'slave', 'runbindable', 'rshared', 'rprivate', 'rslave', ++ 'make-unbindable', 'make-shared', 'make-private', 'make-slave', 'make-runbindable', 'make-rshared', 'make-rprivate', ++ 'make-rslave' ++} ++# keep in sync with parser/mount.cc mnt_opts_table! ++flags_keywords = list(flags_bind_mount) + list(flags_change_propagation) + [ ++ 'ro', 'r', 'read-only', 'rw', 'w', 'suid', 'nosuid', 'dev', 'nodev', 'exec', 'noexec', 'sync', 'async', 'mand', ++ 'nomand', 'dirsync', 'symfollow', 'nosymfollow', 'atime', 'noatime', 'diratime', 'nodiratime', 'move', 'M', ++ 'verbose', 'silent', 'loud', 'acl', 'noacl', 'relatime', 'norelatime', 'iversion', 'noiversion', 'strictatime', ++ 'nostrictatime', 'lazytime', 'nolazytime', 'user', 'nouser', '([A-Za-z0-9])', + ] + join_valid_flags = '|'.join(flags_keywords) + +@@ -112,6 +115,7 @@ class MountRule(BaseRule): + self.is_options_equal = options[0] if not self.all_options else None + + self.source, self.all_source = self._aare_or_all(source, 'source', is_path=False, log_event=log_event) ++ self.dest, self.all_dest = self._aare_or_all(dest, 'dest', is_path=False, log_event=log_event) + + if not self.all_fstype and self.is_fstype_equal not in ('=', 'in'): + raise AppArmorBug(f'Invalid is_fstype_equal : {self.is_fstype_equal}') +@@ -120,11 +124,14 @@ class MountRule(BaseRule): + if self.operation != 'mount' and not self.all_source: + raise AppArmorException(f'Operation {self.operation} cannot have a source') + +- flags_forbidden_with_source = {'remount', 'unbindable', 'shared', 'private', 'slave', 'runbindable', 'rshared', 'rprivate', 'rslave'} +- if self.operation == 'mount' and not self.all_source and not self.all_options and flags_forbidden_with_source & self.options != set(): +- raise AppArmorException(f'Operation {flags_forbidden_with_source & self.options} cannot have a source. Source = {self.source}') ++ if self.operation == 'mount' and not self.all_options and flags_change_propagation & self.options != set(): ++ if not (self.all_source or self.all_dest): ++ raise AppArmorException(f'Operation {flags_change_propagation & self.options} cannot specify a source. Source = {self.source}') ++ elif not self.all_fstype: ++ raise AppArmorException(f'Operation {flags_change_propagation & self.options} cannot specify a fstype. Fstype = {self.fstype}') + +- self.dest, self.all_dest = self._aare_or_all(dest, 'dest', is_path=False, log_event=log_event) ++ if self.operation == 'mount' and not self.all_options and flags_bind_mount & self.options != set() and not self.all_fstype: ++ raise AppArmorException(f'Bind mount rules cannot specify a fstype. Fstype = {self.fstype}') + + self.can_glob = not self.all_source and not self.all_dest and not self.all_options + +diff --git a/utils/test/test-mount.py b/utils/test/test-mount.py +index 7f88ff7db..cb1a89630 100644 +--- a/utils/test/test-mount.py ++++ b/utils/test/test-mount.py +@@ -43,12 +43,12 @@ class MountTestParse(AATest): + ('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, '' )), + ('mount fstype=(ext3, ext4) /a -> /b,', MountRule('mount', ('=', ['ext3', 'ext4']), MountRule.ALL, '/a', '/b', False, False, False, '' )), +- ('mount fstype=(ext3, ext4) options=(ro, rbind) /a -> /b,', MountRule('mount', ('=', ['ext3', 'ext4']), ('=', ('ro', 'rbind')), '/a', '/b', False, False, False, '' )), +- ('mount fstype=(ext3, ext4) options=(ro, rbind) /a -> /b, #cmt', MountRule('mount', ('=', ['ext3', 'ext4']), ('=', ('ro', 'rbind')), '/a', '/b', False, False, False, ' #cmt')), +- ('mount fstype=({ext3,ext4}) options in (ro, rbind) /a -> /b,', MountRule('mount', ('=', ['{ext3,ext4}']), ('in', ('ro', 'rbind')), '/a', '/b', False, False, False, '' )), +- ('mount fstype in (ext3, ext4) options=(ro, rbind) /a -> /b, #cmt', MountRule('mount', ('in', ['ext3', 'ext4']), ('=', ('ro', 'rbind')), '/a', '/b', False, False, False, ' #cmt')), +- ('mount fstype in (ext3, ext4) option in (ro, rbind) /a, #cmt', MountRule('mount', ('in', ['ext3', 'ext4']), ('in', ('ro', 'rbind')), '/a', MountRule.ALL, False, False, False, ' #cmt')), +- ('mount fstype=(ext3, ext4) option=(ro, rbind) /a -> /b, #cmt', MountRule('mount', ('=', ['ext3', 'ext4']), ('=', ('ro', 'rbind')), '/a', '/b', False, False, False, ' #cmt')), ++ ('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')), + ('mount options=(rw, rbind) {,/usr}/lib{,32,64,x32}/modules/ -> /tmp/snap.rootfs_*{,/usr}/lib/modules/,', + MountRule('mount', MountRule.ALL, ('=', ('rw', 'rbind')), '{,/usr}/lib{,32,64,x32}/modules/', + '/tmp/snap.rootfs_*{,/usr}/lib/modules/', +@@ -68,6 +68,17 @@ class MountTestParse(AATest): + expected.raw_rule = rawrule.strip() + self.assertTrue(obj.is_equal(expected, True)) + ++ 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') ++ + + class MountTestParseInvalid(AATest): + tests = ( +@@ -143,6 +154,20 @@ class MountTestParseInvalid(AATest): + with self.assertRaises(AppArmorException): + MountRule('remount', MountRule.ALL, MountRule.ALL, '/foo', MountRule.ALL) + ++ 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') ++ + + class MountTestGlob(AATest): + def test_glob(self): +diff --git a/utils/test/test-parser-simple-tests.py b/utils/test/test-parser-simple-tests.py +index 451af7d22..60a738aed 100644 +--- a/utils/test/test-parser-simple-tests.py ++++ b/utils/test/test-parser-simple-tests.py +@@ -85,16 +85,6 @@ exception_not_raised = ( + 'mount/bad_1.sd', + 'mount/bad_2.sd', + +- # not checked/detected: "make-*" mount opt and an invalid src +- 'mount/bad_opt_17.sd', +- 'mount/bad_opt_18.sd', +- 'mount/bad_opt_19.sd', +- 'mount/bad_opt_20.sd', +- 'mount/bad_opt_21.sd', +- 'mount/bad_opt_22.sd', +- 'mount/bad_opt_23.sd', +- 'mount/bad_opt_24.sd', +- + 'profile/flags/flags_bad10.sd', + 'profile/flags/flags_bad11.sd', + 'profile/flags/flags_bad12.sd', +@@ -324,19 +314,6 @@ unknown_line = ( + 'bare_include_tests/ok_85.sd', + 'bare_include_tests/ok_86.sd', + +- # Mount with flags in {remount, [r]unbindable, [r]shared, [r]private, [r]slave} does not support a source +- 'mount/ok_opt_68.sd', +- 'mount/ok_opt_69.sd', +- 'mount/ok_opt_70.sd', +- 'mount/ok_opt_71.sd', +- 'mount/ok_opt_72.sd', +- 'mount/ok_opt_73.sd', +- 'mount/ok_opt_74.sd', +- 'mount/ok_opt_75.sd', +- +- # options=slave with /** src (first rule in the test causes exception) +- 'mount/ok_opt_84.sd', +- + # According to spec mount should be in the form fstype=... options=... and NOT in the form options=... fstype=... + 'mount/ok_opt_combo_3.sd', + 'mount/ok_opt_combo_2.sd', diff --git a/utils-relax-mount-rules.diff b/utils-relax-mount-rules.diff new file mode 100644 index 0000000..dbcf2a7 --- /dev/null +++ b/utils-relax-mount-rules.diff @@ -0,0 +1,366 @@ +commit eee50538da9a240bc151f26c6cff309808d33590 +Author: Georgia Garcia +Date: Wed May 8 12:58:42 2024 +0000 + + Merge MountRule: Relaxing constraints on fstype and completing AARE support + + - Before this commit, fstype had to match a known fs. However, having and maintaining the exhaustive list of fstypes proved challenging (see !1195 and !1176). Therefore, we add support for any filesystem name. + - Completing AARE support for fstype (brace expressions like ext{3,4} are now supported). + + MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1198 + Approved-by: Christian Boltz + Merged-by: Christian Boltz + + + (cherry picked from commit baa8b67248f3467cde40683600d7a945b05f9a3b) + + dad5ee28 MountRule: Relaxing constraints on fstype and completing AARE support + + Co-authored-by: Christian Boltz + +diff --git a/utils/apparmor/rule/mount.py b/utils/apparmor/rule/mount.py +index f62c08e4b..abfa2b75e 100644 +--- a/utils/apparmor/rule/mount.py ++++ b/utils/apparmor/rule/mount.py +@@ -23,19 +23,7 @@ from apparmor.translations import init_translation + + _ = init_translation() + +-# TODO : +-# - match correctly AARE on every field +-# - Find the actual list of supported filesystems. This one comes from /proc/filesystems. We also blindly accept fuse.* +-# - Support path that begin by { (e.g. {,/usr}/lib/...) This syntax is not a valid AARE but is used by usr.lib.snapd.snap-confine.real in Ubuntu and will currently raise an error in genprof if these lines are not modified. +-# - Apparmor remount logs are displayed as mount (with remount flag). Profiles generated with aa-genprof are therefore mount rules. It could be interesting to make them remount rules. +- +-valid_fs = [ +- 'sysfs', 'tmpfs', 'bdevfs', 'procfs', 'cgroup', 'cgroup2', 'cpuset', 'devtmpfs', 'configfs', 'debugfs', 'tracefs', +- 'securityfs', 'sockfs', 'bpf', 'npipefs', 'ramfs', 'hugetlbfs', 'devpts', 'ext3', 'ext2', 'ext4', 'squashfs', +- 'vfat', 'ecryptfs', 'fuseblk', 'fuse', 'fusectl', 'efivarfs', 'mqueue', 'store', 'autofs', 'binfmt_misc', 'overlay', +- 'none', 'bdev', 'proc', 'pipefs', 'pstore', 'btrfs', 'xfs', '9p', 'resctrl', 'zfs', 'iso9660', 'udf', 'ntfs3', +- 'nfs', 'cifs', 'overlayfs', 'aufs', 'rpc_pipefs', 'msdos', 'nfs4', +-] ++# TODO : Apparmor remount logs are displayed as mount (with remount flag). Profiles generated with aa-genprof are therefore mount rules. It could be interesting to make them remount rules. + + flags_keywords = [ + # keep in sync with parser/mount.cc mnt_opts_table! +@@ -48,7 +36,6 @@ flags_keywords = [ + '([A-Za-z0-9])', + ] + join_valid_flags = '|'.join(flags_keywords) +-join_valid_fs = '|'.join(valid_fs) + + sep = r'\s*[\s,]\s*' + +@@ -106,27 +93,18 @@ class MountRule(BaseRule): + + self.operation = operation + +- self.fstype, self.all_fstype, unknown_items = check_and_split_list(fstype[1] if fstype != self.ALL else fstype, valid_fs, self.ALL, type(self).__name__, 'fstype') +- +- if unknown_items: +- for it in unknown_items: +- +- # Several filesystems use fuse internally and are referred as fuse. (e.g. fuse.jmtpfs, fuse.s3fs, fuse.obexfs). +- # Since this list seems to evolve too fast for a fixed list to work in practice, we just accept fuse.* +- # See https://github.com/libfuse/libfuse/wiki/Filesystems and, https://doc.ubuntu-fr.org/fuse +- if it.startswith('fuse.') and len(it) > 5: +- continue +- +- it = AARE(it, is_path=False) +- found = False +- for fs in valid_fs: +- if self._is_covered_aare(it, self.all_fstype, AARE(fs, False), self.all_fstype, 'fstype'): +- found = True +- break +- if not found: +- raise AppArmorException(_('Passed unknown fstype keyword to %s: %s') % (type(self).__name__, ' '.join(unknown_items))) +- +- self.is_fstype_equal = fstype[0] if not self.all_fstype else None ++ if fstype == self.ALL or fstype[1] == self.ALL: ++ self.all_fstype = True ++ self.fstype = None ++ self.is_fstype_equal = None ++ else: ++ self.all_fstype = False ++ for it in fstype[1]: ++ l, unused = parse_aare(it, 0, 'fstype') ++ if l != len(it): ++ raise AppArmorException(f'Invalid aare : {it}') ++ self.fstype = fstype[1] ++ self.is_fstype_equal = fstype[0] + + self.options, self.all_options, unknown_items = check_and_split_list(options[1] if options != self.ALL else options, flags_keywords, self.ALL, type(self).__name__, 'options') + if unknown_items: +@@ -173,7 +151,7 @@ class MountRule(BaseRule): + + if r['fstype'] is not None: + is_fstype_equal = r['fstype_equals_or_in'] +- fstype = strip_parenthesis(r['fstype']).replace(',', ' ').split() ++ fstype = parse_aare_list(strip_parenthesis(r['fstype']), 'fstype') + else: + is_fstype_equal = None + fstype = cls.ALL +@@ -316,6 +294,38 @@ class MountRuleset(BaseRuleset): + '''Class to handle and store a collection of Mount rules''' + + ++ ++def parse_aare(s, offset, param): ++ parsed = '' ++ brace_count = 0 ++ for i, c in enumerate(s[offset:], start=offset): ++ if c in [' ', ',', '\t'] and brace_count == 0: ++ break ++ parsed += c ++ if c == '{': ++ brace_count += 1 ++ elif c == '}': ++ brace_count -= 1 ++ if brace_count < 0: ++ raise AppArmorException(f"Unmatched closing brace in {param}: {s[offset:]}") ++ offset = i ++ ++ if brace_count != 0: ++ raise AppArmorException(f"Unmatched opening brace in {param}: {s[offset:]}") ++ ++ return offset + 1, parsed ++ ++ ++def parse_aare_list(s, param): ++ res = [] ++ offset = 0 ++ while offset <= len(s): ++ offset, part = parse_aare(s, offset, param) ++ if part.translate(' ,\t') != '': ++ res.append(part) ++ return res ++ ++ + def wrap_in_with_spaces(value): + ''' wrap 'in' keyword in spaces, and leave everything else unchanged ''' + +diff --git a/utils/test/test-mount.py b/utils/test/test-mount.py +index e37c287c7..7f88ff7db 100644 +--- a/utils/test/test-mount.py ++++ b/utils/test/test-mount.py +@@ -20,7 +20,7 @@ from common_test import AATest, setup_all_loops + from apparmor.common import AppArmorException, AppArmorBug + from apparmor.translations import init_translation + +-from apparmor.rule.mount import MountRule, valid_fs ++from apparmor.rule.mount import MountRule + + _ = init_translation() + +@@ -31,34 +31,34 @@ class MountTestParse(AATest): + # Rule Operation Filesystem Options Source Destination Audit Deny Allow Comment + ('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, '' )), +- ('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 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 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, '' )), +- ('mount fstype=(ext3, ext4) options=(ro),', MountRule('mount', ('=', ('ext3', 'ext4')), ('=', ('ro')), MountRule.ALL, MountRule.ALL, False, False, False, '' )), ++ ('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, '' )), +- ('mount fstype=(ext3, ext4) /a -> /b,', MountRule('mount', ('=', ('ext3', 'ext4')), MountRule.ALL, '/a', '/b', False, False, False, '' )), +- ('mount fstype=(ext3, ext4) options=(ro, rbind) /a -> /b,', MountRule('mount', ('=', ('ext3', 'ext4')), ('=', ('ro', 'rbind')), '/a', '/b', False, False, False, '' )), +- ('mount fstype=(ext3, ext4) options=(ro, rbind) /a -> /b, #cmt', MountRule('mount', ('=', ('ext3', 'ext4')), ('=', ('ro', 'rbind')), '/a', '/b', False, False, False, ' #cmt')), +- ('mount fstype=(ext3, ext4) options in (ro, rbind) /a -> /b,', MountRule('mount', ('=', ('ext3', 'ext4')), ('in', ('ro', 'rbind')), '/a', '/b', False, False, False, '' )), +- ('mount fstype in (ext3, ext4) options=(ro, rbind) /a -> /b, #cmt', MountRule('mount', ('in', ('ext3', 'ext4')), ('=', ('ro', 'rbind')), '/a', '/b', False, False, False, ' #cmt')), +- ('mount fstype in (ext3, ext4) option in (ro, rbind) /a, #cmt', MountRule('mount', ('in', ('ext3', 'ext4')), ('in', ('ro', 'rbind')), '/a', MountRule.ALL, False, False, False, ' #cmt')), +- ('mount fstype=(ext3, ext4) option=(ro, rbind) /a -> /b, #cmt', MountRule('mount', ('=', ('ext3', 'ext4')), ('=', ('ro', 'rbind')), '/a', '/b', False, False, False, ' #cmt')), ++ ('mount fstype=(ext3, ext4) /a -> /b,', MountRule('mount', ('=', ['ext3', 'ext4']), MountRule.ALL, '/a', '/b', False, False, False, '' )), ++ ('mount fstype=(ext3, ext4) options=(ro, rbind) /a -> /b,', MountRule('mount', ('=', ['ext3', 'ext4']), ('=', ('ro', 'rbind')), '/a', '/b', False, False, False, '' )), ++ ('mount fstype=(ext3, ext4) options=(ro, rbind) /a -> /b, #cmt', MountRule('mount', ('=', ['ext3', 'ext4']), ('=', ('ro', 'rbind')), '/a', '/b', False, False, False, ' #cmt')), ++ ('mount fstype=({ext3,ext4}) options in (ro, rbind) /a -> /b,', MountRule('mount', ('=', ['{ext3,ext4}']), ('in', ('ro', 'rbind')), '/a', '/b', False, False, False, '' )), ++ ('mount fstype in (ext3, ext4) options=(ro, rbind) /a -> /b, #cmt', MountRule('mount', ('in', ['ext3', 'ext4']), ('=', ('ro', 'rbind')), '/a', '/b', False, False, False, ' #cmt')), ++ ('mount fstype in (ext3, ext4) option in (ro, rbind) /a, #cmt', MountRule('mount', ('in', ['ext3', 'ext4']), ('in', ('ro', 'rbind')), '/a', MountRule.ALL, False, False, False, ' #cmt')), ++ ('mount fstype=(ext3, ext4) option=(ro, rbind) /a -> /b, #cmt', MountRule('mount', ('=', ['ext3', 'ext4']), ('=', ('ro', 'rbind')), '/a', '/b', False, False, False, ' #cmt')), + ('mount options=(rw, rbind) {,/usr}/lib{,32,64,x32}/modules/ -> /tmp/snap.rootfs_*{,/usr}/lib/modules/,', + MountRule('mount', MountRule.ALL, ('=', ('rw', 'rbind')), '{,/usr}/lib{,32,64,x32}/modules/', + '/tmp/snap.rootfs_*{,/usr}/lib/modules/', + False, False, False, '' )), + ('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 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 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, '' )), + ) + +@@ -72,7 +72,6 @@ class MountTestParse(AATest): + class MountTestParseInvalid(AATest): + tests = ( + ('mount fstype=,', AppArmorException), +- ('mount fstype=(foo),', AppArmorException), + ('mount fstype=(),', AppArmorException), + ('mount options=(),', AppArmorException), + ('mount option=(invalid),', AppArmorException), +@@ -90,7 +89,7 @@ class MountTestParseInvalid(AATest): + + def test_diff_non_mountrule(self): + exp = namedtuple('exp', ('audit', 'deny')) +- obj = MountRule('mount', ('=', 'ext4'), MountRule.ALL, MountRule.ALL, MountRule.ALL) ++ obj = MountRule('mount', ('=', ['ext4']), MountRule.ALL, MountRule.ALL, MountRule.ALL) + with self.assertRaises(AppArmorBug): + obj.is_equal(exp(False, False), False) + +@@ -98,9 +97,25 @@ class MountTestParseInvalid(AATest): + with self.assertRaises(AppArmorBug): + MountRule('mount', ('ext3', 'ext4'), MountRule.ALL, MountRule.ALL, MountRule.ALL) # fstype[0] should be '=' or 'in' + +- def test_diff_invalid_fstype_keyword(self): +- with self.assertRaises(AppArmorException): +- MountRule('mount', ('=', 'invalidfs'), MountRule.ALL, MountRule.ALL, MountRule.ALL) # fstype[0] should be '=' or 'in' ++ def test_diff_invalid_fstype_aare(self): ++ tests = [ ++ 'mount fstype=({unclosed_regex),', ++ 'mount fstype=({closed}twice}),', ++ ] ++ ++ for t in tests: ++ with self.assertRaises(AppArmorException): ++ MountRule.create_instance(t) ++ ++ def test_diff_invalid_fstype_aare_2(self): ++ fslists = [ ++ ['invalid_{_regex'], ++ ['ext4', 'invalid_}_regex'], ++ ['ext4', '{invalid} {regex}'] ++ ] ++ for fslist in fslists: ++ with self.assertRaises(AppArmorException): ++ MountRule('mount', ('=', fslist), MountRule.ALL, MountRule.ALL, MountRule.ALL) + + def test_diff_invalid_options_equals_or_in(self): + with self.assertRaises(AppArmorBug): +@@ -111,7 +126,7 @@ class MountTestParseInvalid(AATest): + MountRule('mount', MountRule.ALL, ('=', 'invalid'), MountRule.ALL, MountRule.ALL) # fstype[0] should be '=' or 'in' + + def test_diff_fstype(self): +- obj1 = MountRule('mount', ('=', 'ext4'), MountRule.ALL, MountRule.ALL, MountRule.ALL) ++ obj1 = MountRule('mount', ('=', ['ext4']), MountRule.ALL, MountRule.ALL, MountRule.ALL) + obj2 = MountRule('mount', MountRule.ALL, MountRule.ALL, MountRule.ALL, MountRule.ALL) + self.assertFalse(obj1.is_equal(obj2, False)) + +@@ -129,14 +144,6 @@ class MountTestParseInvalid(AATest): + MountRule('remount', MountRule.ALL, MountRule.ALL, '/foo', MountRule.ALL) + + +-class MountTestFilesystems(AATest): +- def test_fs(self): +- with open('/proc/filesystems') as f: +- for line in f: +- fs_name = line.split()[-1] +- self.assertTrue(fs_name in valid_fs, '/proc/filesystems contains %s which is not listed in MountRule valid_fs' % fs_name) +- +- + class MountTestGlob(AATest): + def test_glob(self): + globList = [( +@@ -199,49 +206,58 @@ class MountIsCoveredTest(AATest): + def test_is_covered(self): + obj = MountRule('mount', ('=', ('ext3', 'ext4')), ('=', ('ro')), '/foo/b*', '/b*') + tests = [ +- ('mount', ('=', ('ext3', 'ext4')), ('=', ('ro')), '/foo/b', '/bar'), +- ('mount', ('=', ('ext3', 'ext4')), ('=', ('ro')), '/foo/bar', '/b') ++ ('mount', ('=', ['ext3', 'ext4']), ('=', ('ro')), '/foo/b', '/bar'), ++ ('mount', ('=', ['ext3', 'ext4']), ('=', ('ro')), '/foo/bar', '/b') + ] + 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): +- 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))) ++ 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))) + +- def test_is_covered_regex(self): +- obj = MountRule('mount', ('=', ('sys*', 'fuse.*')), ('=', ('ro')), 'tmpfs', MountRule.ALL) ++ def test_is_covered_aare_1(self): ++ obj = MountRule('mount', ('=', ['sys*', 'fuse.*']), ('=', ('ro')), 'tmpfs', MountRule.ALL) + tests = [ +- ('mount', ('=', ('sysfs', 'fuse.s3fs')), ('=', ('ro')), 'tmpfs', MountRule.ALL), +- ('mount', ('=', ('sysfs', 'fuse.jmtpfs', 'fuse.s3fs', 'fuse.obexfs', 'fuse.obexautofs', 'fuse.fuseiso')), ('=', ('ro')), 'tmpfs', MountRule.ALL) ++ ('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))) ++ 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) + ] + for test in tests: + self.assertTrue(obj.is_covered(MountRule(*test))) + self.assertFalse(obj.is_equal(MountRule(*test))) + + def test_is_notcovered(self): +- obj = MountRule('mount', ('=', ('ext3', 'ext4')), ('=', ('ro')), '/foo/b*', '/b*') ++ obj = MountRule('mount', ('=', ['ext3', 'ext4']), ('=', ('ro')), '/foo/b*', '/b*') + tests = [ +- ('mount', ('in', ('ext3', 'ext4')), ('=', ('ro')), '/foo/bar', '/bar' ), +- ('mount', ('=', ('procfs', 'ext4')), ('=', ('ro')), '/foo/bar', '/bar' ), +- ('mount', ('=', ('ext3')), ('=', ('rw')), '/foo/bar', '/bar' ), +- ('mount', ('=', ('ext3', 'ext4')), MountRule.ALL, '/foo/b*', '/bar' ), ++ ('mount', ('in', ['ext3', 'ext4']), ('=', ('ro')), '/foo/bar', '/bar' ), ++ ('mount', ('=', ['procfs', 'ext4']), ('=', ('ro')), '/foo/bar', '/bar' ), ++ ('mount', ('=', ['ext3']), ('=', ('rw')), '/foo/bar', '/bar' ), ++ ('mount', ('=', ['ext3', 'ext4']), MountRule.ALL, '/foo/b*', '/bar' ), + ('mount', MountRule.ALL, ('=', ('ro')), '/foo/b*', '/bar' ), +- ('mount', ('=', ('ext3', 'ext4')), ('=', ('ro')), '/invalid/bar', '/bar' ), ++ ('mount', ('=', ['ext3', 'ext4']), ('=', ('ro')), '/invalid/bar', '/bar' ), + ('umount', MountRule.ALL, MountRule.ALL, MountRule.ALL, '/bar' ), + ('remount', MountRule.ALL, MountRule.ALL, MountRule.ALL, '/bar' ), +- ('mount', ('=', ('ext3', 'ext4')), ('=', ('ro')), 'tmpfs', '/bar' ), +- ('mount', ('=', ('ext3', 'ext4')), ('=', ('ro')), '/foo/b*', '/invalid'), ++ ('mount', ('=', ['ext3', 'ext4']), ('=', ('ro')), 'tmpfs', '/bar' ), ++ ('mount', ('=', ['ext3', 'ext4']), ('=', ('ro')), '/foo/b*', '/invalid'), + ] + 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): +- obj = MountRule('mount', ('=', ('ext3', 'ext4')), ('=', ('ro')), 'tmpfs', MountRule.ALL) +- test = ('mount', ('=', ('ext3', 'ext4')), ('=', ('ro')), 'procfs', MountRule.ALL) ++ obj = MountRule('mount', ('=', ['ext3', 'ext4']), ('=', ('ro')), 'tmpfs', MountRule.ALL) ++ test = ('mount', ('=', ['ext3', 'ext4']), ('=', ('ro')), 'procfs', MountRule.ALL) + self.assertFalse(obj.is_covered(MountRule(*test))) + self.assertFalse(obj.is_equal(MountRule(*test))) + +diff --git a/utils/test/test-parser-simple-tests.py b/utils/test/test-parser-simple-tests.py +index 40f61ef85..451af7d22 100644 +--- a/utils/test/test-parser-simple-tests.py ++++ b/utils/test/test-parser-simple-tests.py +@@ -324,9 +324,6 @@ unknown_line = ( + 'bare_include_tests/ok_85.sd', + 'bare_include_tests/ok_86.sd', + +- # mount with fstype using AARE +- 'mount/ok_12.sd', +- + # Mount with flags in {remount, [r]unbindable, [r]shared, [r]private, [r]slave} does not support a source + 'mount/ok_opt_68.sd', + 'mount/ok_opt_69.sd',