Skip to content

2026.8.24: runtime_t lacks file execmod, so 6.12.100's overlayfs mounter check breaks in-image AOT/text-relocation code loading #537

Description

@yasyf

Summary

Bottlerocket 2026.8.24 (kernel 6.12.100) includes 82544d36b172 ("selinux: fix overlayfs mmap() and mprotect() access checks", CVE-2026-46054). That commit adds a second FILE__EXECMOD check in selinux_file_mprotect(), evaluated against the overlay mounter's credentials and the lower inode rather than the task's.

On Bottlerocket the mounter is containerd, system_u:system_r:runtime_t:s0:c0.c1023. The shipped policy grants:

allow runtime_s global:file { execute execute_no_trans };   # no execmod
allow container_s global:file execmod;                       # container_s = {container_t, control_t, super_t}

runtime_t is not in container_s and holds no execmod against any type, so this check fails unconditionally — for every container, for every file in every image, independent of the file's label. Any workload that mmaps a file shipped in its image, dirties it via COW, then mprotects it executable now gets EACCES.

policy/mcs and policy/mls place no constraint on execmod, and rules.cil carries (neverallow host_s global (files (relax))), so the grant cannot be added by a downstream policy module — host_s includes runtime_t.

Affected

  • Affected: 2026.8.24 (aws-k8s-1.33-standard), kernel 6.12.100.
  • Not affected: 2026.8.10, kernel 6.12.95 — that build does not contain the commit. selinux_mmap_backing_file and selinux_backing_file_alloc are absent from its /proc/kallsyms and present on 2026.8.24, which is a version-independent way to test a given build.

The revert of this patch was added in bottlerocket-kernel-kit#496 ("Undo patches that break dotnet workloads") and deleted in bottlerocket-kernel-kit#526 while bumping 6.12.95 → 6.12.100. The 6.12.97 follow-up 9fe595fad54d narrows the mounter path for the process-class EXECMEM check — which is why the dotnet symptom in #496 went away — but leaves the file-class EXECMOD check untouched.

Reproducer

A static binary in a container with a default container_t label, run against a file baked into the image:

int fd = open(argv[1], O_RDONLY);
size_t n = 65536;
char *p = mmap(NULL, n, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
p[0] ^= 1;                                                    /* COW fault -> vma->anon_vma set */
if (mprotect(p, n, PROT_READ|PROT_EXEC) != 0) perror("mprotect");   /* EACCES on 6.12.100 */

Controls that still pass on 6.12.100, isolating the case:

probe 6.12.95 6.12.100
anonymous, dirtied, then PROT_EXEC ok ok
same file mapped clean, then PROT_EXEC ok ok
same file mapped PROT_EXEC at mmap() time ok ok
memfd, dirtied, then PROT_EXEC ok ok
file mapped MAP_PRIVATE, dirtied, then PROT_EXEC ok EACCES

The failure appears only when the file is on an overlayfs lower layer. Copying it to an emptyDir and repeating the sequence succeeds.

Denial:

avc: denied { execmod } for pid=642369 comm="riza"
  path="/root/.cache/.../rizapy-0.2.0.cwasm" dev="nvme0n1p1" ino=218528986
  scontext=system_u:system_r:runtime_t:s0:c0.c1023
  tcontext=system_u:object_r:cache_t:s0 tclass=file permissive=0

Note the shape: a container-visible path= with an scontext of containerd and a dev=/ino= on the host data volume — the signature of the backing-file check.

Impact

Silent, image-independent breakage of any runtime that patches mapped code in place: wasmtime AOT (.cwasm) loading, ELF objects with DT_TEXTREL, .NET ReadyToRun. JIT engines that compile into anonymous memory (V8, JVM, LuaJIT) are unaffected, as is ordinary ld.so loading, since runtime_s does hold execute.

The workload sees only EACCES from mprotect. Nothing in Kubernetes surfaces the AVC, and there is no action a workload can take from inside the container to fix it. In our case a code-execution service crashlooped on every node the new AMI reached; the only workable mitigation was moving the affected file onto a non-overlay volume.

We could not find this documented in the 2026.8.24 release notes or an existing issue.

Requested

Grant execmod to the container runtime domain in the Bottlerocket policy, mirroring the existing container_s grant, so the mounter check can pass where the task check already does. Failing that, document it as a breaking change in the release notes along with the volume workaround.

Separately, and for upstream rather than here: it may be worth asking whether FILE__EXECMOD belongs under the mounter check at all. 9fe595fad54d removed PROCESS__EXECMEM from that path on the reasoning that it "doesn't pertain to the file itself". For a private, COW-dirtied mapping the modified pages are anonymous and grant the mounter no additional access to the file's contents, so the same argument appears to apply.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions