leftmatch_fb() ignores diff-encoded DFA states, so profile attachments silently fail (e.g. Ubuntu's firefox profile)
Summary
aa_dfa_leftmatch() → leftmatch_fb() (security/apparmor/match.c), which find_attach() uses to match a profile's attachment (xmatch), takes the default transition as state = def[state] and moves on to the next character. It does not handle MATCH_FLAG_DIFF_ENCODE. match_char(), used by aa_dfa_match() and friends, does handle it: on a miss in a diff-encoded state it follows def[state] and retries the same character.
When apparmor_parser emits a differentially encoded xmatch DFA (the default whenever the kernel advertises policy/diff_encode), the attachment can therefore end in a non-accepting state for a path that matches the pattern. No conflict is involved, so nothing is audited. The exec simply stays unconfined.
File rules are not affected because they are matched with match_char(). Only profile attachment is affected.
Real-world impact
Ubuntu's /etc/apparmor.d/firefox:
profile firefox /{usr/lib/firefox{,-esr,-beta,-devedition,-nightly},opt/firefox}/firefox{,-esr,-bin} flags=(unconfined) {
userns,
...
}This profile exists so that Firefox installed as a .deb (for example from packages.mozilla.org) may create user namespaces. Because the attachment never matches, /usr/lib/firefox/firefox-bin runs unconfined. With kernel.apparmor_restrict_unprivileged_userns=1 it is then moved to unprivileged_userns and denied sys_admin:
apparmor="AUDIT" operation="userns_create" info="Userns create - transitioning profile" profile="unconfined" comm="firefox-bin" target="unprivileged_userns" execpath="/usr/lib/firefox/firefox-bin"
apparmor="DENIED" operation="capable" class="cap" profile="unprivileged_userns" comm="firefox-bin" capability=21 capname="sys_admin"As a result, Firefox's content sandbox runs without user-namespace isolation. Any other profile whose attachment compiles to a diff-encoded DFA is affected in the same way.
Reproducer (run as root)
#!/bin/sh
set -e
D=${1:-/tmp/aa-repro}
PAT='{usr/lib/firefox{,-esr,-beta,-devedition,-nightly},opt/firefox}/firefox{,-esr,-bin}'
for v in enc noenc; do
mkdir -p "$D/$v/usr/lib/firefox"
cp /usr/bin/dash "$D/$v/usr/lib/firefox/firefox-bin"
printf 'abi <abi/5.0>,\ninclude <tunables/global>\nprofile repro_%s %s/%s/%s flags=(unconfined) { }\n' \
"$v" "$D" "$v" "$PAT" > "$D/$v.profile"
done
apparmor_parser -r -T "$D/enc.profile"
apparmor_parser -r -T -O no-diff-encode "$D/noenc.profile"
for v in enc noenc; do
printf '%-6s -> ' "$v"
"$D/$v/usr/lib/firefox/firefox-bin" -c 'cat /proc/$$/attr/current; :' | tr -d '\0'; echo
done
apparmor_parser -R "$D/enc.profile" "$D/noenc.profile"
rm -rf "$D"Expected output:
enc -> repro_enc (unconfined)
noenc -> repro_noenc (unconfined)Actual output:
enc -> unconfined
noenc -> repro_noenc (unconfined)The two profiles differ only in -O no-diff-encode.
Further observations
- Simpler patterns attach:
/plain/path,/a/{b,c}/x,/a/x{,-y,-z}and/a/*all attach normally. Larger or nested alternations fail once the DFA ends up diff-encoded. Examples:/{X/usr/lib/fire{fox,dog},opt/firefox}/firefox-binandX/{usr/lib/firefox{,-esr},opt/firefox}/firefox-bin. Each of those attaches when compiled with-O no-diff-encode. - Not a conflict: no
"conflicting profile attachments"audit is ever emitted. - Simpler sequential alternations attach: for example
unix-chkpwd(/{,usr/}{,s}bin/unix_chkpwd) attaches as expected. - File rules unaffected (from the code): they go through
aa_dfa_match()/match_char(), which handle diff encoding.
Analysis
leftmatch_fb(), equivalence-class branch (the plain branch is the same):
wb->history[wb->pos] = state;
pos = base_idx(base[state]) + equiv[(u8) *str++];
if (check[pos] == state)
state = next[pos];
else
state = def[state]; /* <- diff-encoded states need a retry */versus match_char():
if ((check)[pos] != (state)) {
(state) = (def)[(state)];
if (b & MATCH_FLAG_DIFF_ENCODE)
continue; /* retry same char in the default state */
break;
}Suggested fix (untested sketch)
--- a/security/apparmor/match.c
+++ b/security/apparmor/match.c
@@ static aa_state_t leftmatch_fb(struct aa_dfa *dfa, aa_state_t start,
- aa_state_t state = start, pos;
+ aa_state_t state = start;
@@
while (*str) {
unsigned int adjust;
+ u8 c = equiv[(u8) *str++];
wb->history[wb->pos] = state;
- pos = base_idx(base[state]) + equiv[(u8) *str++];
- if (check[pos] == state)
- state = next[pos];
- else
- state = def[state];
+ match_char(state, def, base, next, check, c);
if (is_loop(wb, state, &adjust)) {
@@
while (*str) {
unsigned int adjust;
+ u8 c = (u8) *str++;
wb->history[wb->pos] = state;
- pos = base_idx(base[state]) + (u8) *str++;
- if (check[pos] == state)
- state = next[pos];
- else
- state = def[state];
+ match_char(state, def, base, next, check, c);
if (is_loop(wb, state, &adjust)) {The is_loop() history and the longest-left count logic should also be reviewed for diff-encoded default chains.
Workarounds
- Compile policy with
Optimize=no-diff-encodein/etc/apparmor/parser.conf, then reload all profiles. - Alternatively, give the affected profile a simpler attachment, for example
/usr/lib/firefox/firefox{,-bin}, which attaches here.
Environment
- Ubuntu 26.04.1 LTS (resolute)
- Kernel: Ubuntu
linuxsource 7.0.0-38.38 (upstream 7.0.14)./proc/version_signature:Ubuntu 7.0.0-38.38-generic 7.0.14. The build is local, withCONFIG_SECURITY_APPARMOR*identical to the 7.0.0-38-generic config, andsecurity/apparmor/is unmodified. - apparmor / libapparmor1 5.0.2-0ubuntu1~26.04.1, AppArmor parser 5.0.2
- Kernel features:
policy/diff_encode= yes,domain/computed_longest_left,domain/attach_conditions - firefox 158.0.1~build1 (packages.mozilla.org .deb)