Skip to content

fix(runc): apply AppArmor profile after finalizeNamespace in standard_init (strict) - #5552

Merged
ktsakalozos-canonical merged 3 commits into
strictfrom
fix/runc-init-order-strict
Jun 30, 2026
Merged

fix(runc): apply AppArmor profile after finalizeNamespace in standard_init (strict)#5552
ktsakalozos-canonical merged 3 commits into
strictfrom
fix/runc-init-order-strict

Conversation

@ktsakalozos-canonical

Copy link
Copy Markdown
Contributor

Summary

Reorders the strict-mode runc v1.4.2 patch 0003-standard_init_linux-change-AppArmor-profile-as-late- so the AppArmor profile change (and NoNewPrivileges) happen after finalizeNamespace runs setuid(2), mirroring the ordering already used in setns_init_linux.

Background

This fixes the second AppArmor denial seen when deploying workloads such as metallb in strict mode on AppArmor 4.x hosts (e.g. Plucky 25.10):

profile="snap.microk8s.daemon-containerd" comm="runc:[2:INIT]"
requested_mask="receive" denied_mask="receive" signal=rtmin+1
peer="cri-containerd.apparmor.d"

Root cause

The strict patches switch the container's AppArmor profile immediately (aa_change_profile) instead of on exec (aa_change_onexec) — this is required because the kernel forbids gaining a profile on execve once NO_NEW_PRIVS is set, and Kubernetes pods with allowPrivilegeEscalation: false set NNP.

In standard_init_linux.go (container creation, runc:[2:INIT]), patch 0003 applied the profile right after syncParentReady, i.e. before finalizeNamespace calls setuid(2). That relabels only the calling thread to cri-containerd.apparmor.d, while the Go runtime's sibling threads stay under snap.microk8s.daemon-containerd. The subsequent setuid(2) triggers glibc's NPTL setxid broadcast (SIGRTMIN+1) across the two profiles, which AppArmor 4.x denies → pod creation fails.

setns_init_linux.go (container exec) already does finalizeNamespaceApplyProfile → NNP, which is why kubectl exec works but pod creation does not. This change makes standard_init follow the same proven ordering.

Why Docker's containerd is unaffected

The Docker snap uses stock upstream runc with aa_change_onexec, which defers the transition to execve; all threads share one profile during the setuid handshake, so the cross-profile signal never occurs. Only microk8s's immediate-relabel patches create the split, so this is fixed here rather than in snapd's docker-support interface.

Change

  • build-scripts/components/runc/strict-patches/v1.4.2/0003-...patch: move the ApplyProfile + NNP block from after syncParentReady to after finalizeNamespace/pdeath.Restore(). NNP still follows the profile change (the kernel forbids switching profile once NNP is set).

Only the currently-built version (v1.4.2) is touched.

Verification

  • All three strict patches git am cleanly on a fresh v1.4.2 checkout.
  • go build ./libcontainer/ succeeds; gofmt clean.
  • Pending: strict snap build + metallb repro on an AppArmor 4.x host to confirm no rtmin+1 denials on either snap.microk8s.daemon-containerd or cri-containerd.apparmor.d, and that kubectl exec still works.

Related

Complements the containerd-profile signal rule change in #5550 / #5551, which is retained as defense-in-depth on the send side. Refs #5543.

…_init

Reorders the strict-mode runc v1.4.2 patch so the AppArmor profile change
(and NoNewPrivileges) happen after finalizeNamespace runs setuid(2),
mirroring setns_init_linux.

Because the strict patches switch the profile immediately
(aa_change_profile) instead of on exec, relabelling before setuid left the
Go runtime's sibling threads under snap.microk8s.daemon-containerd while
the calling thread moved to cri-containerd.apparmor.d. glibc's NPTL setxid
broadcast (SIGRTMIN+1) then crossed two AppArmor profiles and was denied
by AppArmor 4.x, breaking pod creation for workloads with
allowPrivilegeEscalation: false (e.g. metallb).

Doing setuid(2) while all threads share one profile keeps the broadcast
intra-profile and resolves the denials without any snapd changes.
The upload_sarifs_matrix job runs codeql-action/upload-sarif, which
requires security-events: write. With no permissions block declared the
token fell back to the restricted default and uploads failed with
"Resource not accessible by integration". Add a job-scoped permissions
block granting the minimal scopes needed (contents/actions read,
security-events write).

@louiseschmidtgen louiseschmidtgen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

Context around the approval:

The problem: With our strict-snap patches switching the AppArmor profile immediately (aa_change_profile) instead of on-exec, calling ApplyProfile before setuid splits the Go runtime's sibling threads across two profiles. The kernel only changes the profile on the current thread (domain.c#L1433 — it doesn't broadcast to siblings. So when glibc's NPTL does the setuid(2) setxid broadcast (SIGRTMIN+1) across all threads, that signal crosses two profiles and AppArmor 4.x denies it → pod creation fails (e.g. metallb with allowPrivilegeEscalation: false).

The fix: Do the setuid broadcast first, while all threads still share one profile, then apply the locked-down profile + NNP to the current thread. The post-switch split is harmless because we have nothing left to broadcast to the siblings.

Why we can't just match upstream: Upstream defers the switch to exec, so the ordering never bites them. In a strict snap we need the profile active earlier than that, so this reordering is the best we can do.

@ktsakalozos-canonical
ktsakalozos-canonical merged commit 1f46348 into strict Jun 30, 2026
22 checks passed
@ktsakalozos-canonical
ktsakalozos-canonical deleted the fix/runc-init-order-strict branch June 30, 2026 08:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants