Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions common/pkg/apparmor/apparmor_linux_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ profile {{.Name}} flags=(attach_disconnected,mediate_deleted) {
# Allow signals from privileged profiles and from within the same profile
signal (receive) peer=unconfined,
signal (send,receive) peer={{.Name}},
# With AppArmor 5.0+, child processes get a stacked profile.
signal (send,receive) peer={{.Name}}//&*,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you link to official docs for this syntax/change?

Also is this fully backwards compatible? What happens if the rule is loaded on apparmor 4 or earlier will the parser accept that?

@bitoku bitoku Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://gitlab.com/apparmor/apparmor/-/wikis/Release_Notes_5.0.0

I can't find the release note about this change, but apparently AI found the change that caused the issue (sorry about just putting the AI result, but I believe it'll help).

According to the AI , torvalds/linux@a9eb185 this change caused the issue. The AI summary is below.

Details

What changed

Previously, x_to_label() in security/apparmor/domain.c only checked whether the first entry in the exec transition table started with & (the stacking marker).
If the first entry wasn't a stack, stacking was skipped entirely — even if a later entry that actually matched did start with &.

The fix checks the actually matched entry for the & prefix:

  // Before: checked first entry only
  stack = rules->file->trans.table[xindex & AA_X_INDEX_MASK];
  if (*stack != '&') { /* skip stacking */ }
  // After: checks the matched entry
  new = x_table_lookup(profile, xindex, lookupname);
  if (!new || **lookupname != '&')
      break;
  stack = new;

Why this breaks container profiles

With the fix, exec'd processes inside containers now correctly receive a stacked label like cri-containerd.apparmor.d//&unconfined (or crio-default//&crun),
where previously the stacking was silently skipped and they just got cri-containerd.apparmor.d.

Existing signal/ptrace rules like signal (send,receive) peer={{.Name}} only match the bare profile name, not the stacked compound label — hence the DENIED
audit entries.

This has also the detailed explanation, but not mention about the upstream change.
containerd/containerd#12886

I confirmed the config is valid in apparmor 4 environment, but I just ran the reproducer, so not sure if it covers enough scenarios.

# Allow certain signals from OCI runtimes (podman, runc and crun)
signal (receive) peer={/usr/bin/,/usr/sbin/,}runc,
signal (receive) peer={/usr/bin/,/usr/sbin/,}crun*,
Expand Down Expand Up @@ -48,6 +50,8 @@ profile {{.Name}} flags=(attach_disconnected,mediate_deleted) {
{{if ge .Version 208095}}
# suppress ptrace denials when using 'ps' inside a container
ptrace (trace,read) peer={{.Name}},
# With AppArmor 5.0+, child processes get a stacked profile.
ptrace (trace,read) peer={{.Name}}//&*,
{{end}}
}
`
29 changes: 29 additions & 0 deletions common/pkg/apparmor/apparmor_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
package apparmor

import (
"bytes"
"os"
"strings"
"testing"
"text/template"
)

type versionExpected struct {
Expand Down Expand Up @@ -130,6 +133,32 @@ func TestInstallDefault(t *testing.T) {
checkLoaded(false)
}

func TestStackedProfileRules(t *testing.T) {
compiled, err := template.New("apparmor_profile").Parse(defaultProfileTemplate)
if err != nil {
t.Fatalf("Failed to parse template: %v", err)
}

p := profileData{
Name: "test-profile",
Version: 300000,
}

var buf bytes.Buffer
if err := compiled.Execute(&buf, p); err != nil {
t.Fatalf("Failed to execute template: %v", err)
}

output := buf.String()

if !strings.Contains(output, "signal (send,receive) peer=test-profile//&*,") {
t.Error("Missing stacked profile signal rule")
}
if !strings.Contains(output, "ptrace (trace,read) peer=test-profile//&*,") {
t.Error("Missing stacked profile ptrace rule")
}
}

func TestDefaultContent(t *testing.T) {
if _, err := os.Stat(aapath); err != nil {
t.Skip("AppArmor isn't available in this environment")
Expand Down
Loading