bottlerocket-os / bottlerocket-os/bottlerocket-kernel-kit
2026.8.24: runtime_t lacks file execmod, so 6.12.100's overlayfs mounter check breaks in-image AOT/text-relocation code loading
- Dominant language
- Rust
- Stars
- 13
- Forks
- 38
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 21
Description
## Summary
Bottlerocket 2026.8.24 (kernel 6.12.100) includes [`82544d36b172`](https://github.com/torvalds/linux/commit/82544d36b1729153c8aeb179e84750f0c085d3b1) ("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 `mprotect`s 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`](https://github.com/torvalds/linux/commit/9fe595fad54d4ac6a402edb3f60bec859d52cea6) 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:
```c
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.
Contributor guide
Research direction
Inspect the SELinux policy areas named in policy/mcs, policy/mls, and rules.cil, comparing the existing container_s execmod grant with runtime_t and its neverallow constraints. Reproduce the MAP_PRIVATE, COW-dirtied file mapping with the shown C program on affected kernels, then verify either the policy change or release-note documentation and the emptyDir workaround.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c, linux
- Domain
- operating-systems, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100