From 091345e70492e216df74d86190f04ebaf7b33c1e Mon Sep 17 00:00:00 2001 From: EpsilonPhoenix <129551402+EpsilonPhoenix@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:07:47 +0000 Subject: [PATCH] Fix AppArmor reference fixture for Ubuntu parser --- apparmor/README.md | 13 ++++++++++++- apparmor/VERSION | 2 +- apparmor/ref/gen.py | 23 +++++++++++++++++++---- apparmor/ref/run.sh | 1 + apparmor/ref/test_gen.py | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 apparmor/ref/test_gen.py diff --git a/apparmor/README.md b/apparmor/README.md index 59c079a..9a65b10 100644 --- a/apparmor/README.md +++ b/apparmor/README.md @@ -475,7 +475,12 @@ disagreements, each where the package is stricter (`/a rw -> b`, `foo r,`, `uX`, `profile -e`, the unix, pivot_root, rlimit and link rules outside the model, `abi` without the system's abi files, and change_profile safe and unsafe on disjoint exec conditions). A disagreement outside the list fails -the test. Runtime decisions could not be compared with the kernel: loading +the test. Generated profiles whose file rules are all audited denials also +receive a separate, modelled read grant: Ubuntu 24.04 parser 4.0.1 package +revision .9 has a [reported regression](https://bugs.launchpad.net/ubuntu/+source/apparmor/+bug/2171436) +affecting that shape. The denial remains tested. Parser failures report the +exit status or signal and complete stdout and stderr; `ref/test_gen.py` checks +the fixture and diagnostics. Runtime decisions could not be compared with the kernel: loading a profile needs root, and none was used. Run on the JavaScript lane with 80 policies (2,080 queries) each, seeds @@ -524,6 +529,12 @@ of build each). ## Status +Bend 2.0.39 (2026-11-03): `scripts/check.sh apparmor` and +`VERDICT=1 scripts/check.sh apparmor` pass; the reference run accepts 20 +policies with Ubuntu 24.04 apparmor_parser 4.0.1 package revision .9, +with 53 edge agreements and 10 known disagreements. `scripts/deps.sh +apparmor` and `python3 scripts/internals.py apparmor` pass. + Bend 2.0.39 (2026-10-26, native, three packages at a time): `PROOF.bend` (4 s, 0.23 GB peak; `--verdict` 8 s, 0.29 GB peak) ALL PROOFS CHECK, with and without `--verdict`. diff --git a/apparmor/VERSION b/apparmor/VERSION index 482e997..4180912 100644 --- a/apparmor/VERSION +++ b/apparmor/VERSION @@ -1 +1 @@ -0.1.0.0 +0.1.0.1 diff --git a/apparmor/ref/gen.py b/apparmor/ref/gen.py index 699c2d7..5067ca9 100644 --- a/apparmor/ref/gen.py +++ b/apparmor/ref/gen.py @@ -570,6 +570,20 @@ def fill(p, n): top2 = Profile('top2', r.pick(['/usr/bin/t*', '/usr/bin/**']), 'enforce', False) fill(top2, 1) pol['profiles'].append(top2) + # Some 4.0.1 builds fail compiling profiles whose only file rules are + # audit deny. Keep those rules and add a separate, modelled allow rule. + def ensure_file_allow(p): + files = [x for x in p.rules if x.kind == 'file'] + if files and all(x.q['audit'] and x.q['deny'] for x in files): + x = Rule() + x.kind, x.q = 'file', {'audit': False, 'deny': False, 'owner': False} + x.text, x.letters, x.xmode, x.target = '/aa-ref-allow', 'r', None, None + x.src = '/aa-ref-allow r,' + p.rules.append(x) + for child in p.children: + ensure_file_allow(child) + for profile in pol['profiles']: + ensure_file_allow(profile) return pol def render_profile(p, depth): @@ -1029,9 +1043,9 @@ def parser_accepts(files, main, tmp): os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, 'w') as f: f.write(text) - res = subprocess.run(['apparmor_parser', '-Q', '-K', '-q', '--base', tmp, '-I', tmp, os.path.join(tmp, main)], - stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True) - return res.returncode == 0, res.stderr.strip().split('\n')[-1] if res.stderr else '' + res = subprocess.run(['apparmor_parser', '-Q', '-K', '--base', tmp, '-I', tmp, os.path.join(tmp, main)], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + return res.returncode == 0, '%s; stdout: %s; stderr: %s' % (('signal %d' % -res.returncode) if res.returncode < 0 else ('exit code %d' % res.returncode), res.stdout or '(empty)', res.stderr or '(empty)') def main(): seed = int(sys.argv[1]) if len(sys.argv) > 1 else 2026 @@ -1116,4 +1130,5 @@ def main(): if parser_bad or bad_edges: sys.exit(1) -main() +if __name__ == "__main__": + main() diff --git a/apparmor/ref/run.sh b/apparmor/ref/run.sh index 897cb1a..3e969d8 100644 --- a/apparmor/ref/run.sh +++ b/apparmor/ref/run.sh @@ -5,6 +5,7 @@ set -uo pipefail cd "$(dirname "$0")" command -v python3 > /dev/null || { echo "python3 missing"; exit 77; } +python3 test_gen.py || exit 1 read -r -a bend <<< "${BEND:-bend}" n=$(python3 gen.py "${SEED:-2026}" "${POLICIES:-20}") || { printf '%s\n' "$n"; exit 1; } got=$(for f in cases-*.bend; do "${bend[@]}" "$f" 2>&1; done) diff --git a/apparmor/ref/test_gen.py b/apparmor/ref/test_gen.py new file mode 100644 index 0000000..a4f88a5 --- /dev/null +++ b/apparmor/ref/test_gen.py @@ -0,0 +1,33 @@ +import tempfile +import unittest +from unittest.mock import patch +from types import SimpleNamespace + +import gen + + +class GeneratorTests(unittest.TestCase): + def test_seed_policy_keeps_denial_and_adds_allow(self): + r = gen.Rng(2026) + gen.random.seed(2026) + for i in range(9): + pol = gen.gen_policy(r, i) + names = ['main', 'main//c1', 'other'] + ( + ['main//h1'] if any(c.hat for c in pol['profiles'][0].children) else []) + gen.gen_queries(r, pol, names) + rules = pol['profiles'][1].rules + self.assertTrue(any(x.kind == 'file' and x.q['audit'] and x.q['deny'] for x in rules)) + self.assertTrue(any(x.kind == 'file' and not x.q['deny'] for x in rules)) + + def test_parser_diagnostics_include_signal_and_all_output(self): + result = SimpleNamespace(returncode=-11, stdout='first\nsecond\n', stderr='one\ntwo\n') + with tempfile.TemporaryDirectory() as tmp, patch('gen.subprocess.run', return_value=result): + ok, message = gen.parser_accepts({'main': ''}, 'main', tmp) + self.assertFalse(ok) + self.assertIn('signal 11', message) + self.assertIn('first\nsecond\n', message) + self.assertIn('one\ntwo\n', message) + + +if __name__ == '__main__': + unittest.main()