fix(runc): apply AppArmor profile after finalizeNamespace in standard_init (strict) - #5552
Conversation
…_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
left a comment
There was a problem hiding this comment.
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.
Summary
Reorders the strict-mode runc v1.4.2 patch
0003-standard_init_linux-change-AppArmor-profile-as-late-so the AppArmor profile change (andNoNewPrivileges) happen afterfinalizeNamespacerunssetuid(2), mirroring the ordering already used insetns_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):
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 onexecveonceNO_NEW_PRIVSis set, and Kubernetes pods withallowPrivilegeEscalation: falseset NNP.In
standard_init_linux.go(container creation,runc:[2:INIT]), patch0003applied the profile right aftersyncParentReady, i.e. beforefinalizeNamespacecallssetuid(2). That relabels only the calling thread tocri-containerd.apparmor.d, while the Go runtime's sibling threads stay undersnap.microk8s.daemon-containerd. The subsequentsetuid(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 doesfinalizeNamespace→ApplyProfile→ NNP, which is whykubectl execworks but pod creation does not. This change makesstandard_initfollow 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 toexecve; all threads share one profile during thesetuidhandshake, 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'sdocker-supportinterface.Change
build-scripts/components/runc/strict-patches/v1.4.2/0003-...patch: move theApplyProfile+NNPblock from aftersyncParentReadyto afterfinalizeNamespace/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
git amcleanly on a freshv1.4.2checkout.go build ./libcontainer/succeeds;gofmtclean.rtmin+1denials on eithersnap.microk8s.daemon-containerdorcri-containerd.apparmor.d, and thatkubectl execstill works.Related
Complements the
containerd-profilesignal rule change in #5550 / #5551, which is retained as defense-in-depth on the send side. Refs #5543.