Skip to content

Commit a03d582

Browse files
authored
Mitigate CVE-2026-43284 + CVE-2026-43500 (DirtyFrag LPE) on all cluster nodes (#554)
**Impact:** All cluster nodes (base infra, ARC runners, BuildKit, GPU, H100/B200) — blocks a container-escape LPE exploitable by any unprivileged CI workflow **Risk:** low ## What Adds a privileged DaemonSet that blacklists the `esp4`, `esp6`, and `rxrpc` kernel modules via `/etc/modprobe.d`, defensively unloads any that are already loaded, and drops the page cache to evict potentially poisoned pages. ## Why DirtyFrag (CVE-2026-43284 + CVE-2026-43500) is a pair of Linux kernel page-cache write LPEs in the IPv4/IPv6 datagram zero-copy path. `__ip_append_data()` fails to set `SKBFL_SHARED_FRAG` on splice-attached pages, allowing ESP (xfrm) and RxRPC to perform in-place crypto over them — mutating the page cache of read-only files like `/usr/bin/su` and `/etc/passwd`. The flaw crosses container boundaries via the shared page cache, so on a multi-tenant CI cluster any unprivileged workflow code can escalate to root on the host. AWS released patched AL2023 kernels on 2026-05-09 (6.1.170+ / 6.12.83+), but as of 2026-05-11 no patched EKS-optimized AMI exists yet (latest v20260505 ships kernel 6.1.168 / 6.12.80). This DaemonSet blocks the attack surface until nodes can be rolled to a patched AMI. ## How - Follows the exact same pattern as the existing `algif-mitigation.yaml` (CVE-2026-31431): ConfigMap script + privileged initContainer via `nsenter` into host namespaces + sleep-infinity main container - `modprobe.d` `install` directives redirect module loading to `/bin/false` — more reliable than `blacklist` which only prevents autoload, not explicit `modprobe` - Page cache drop (`echo 3 > /proc/sys/vm/drop_caches`) is best-effort: mapped pages held by running processes survive until the process exits or the node reboots - Idempotent with marker file at `/var/lib/dirtyfrag-mitigation/.configured`; re-verifies module state on pod restart - `system-node-critical` priority + `tolerations: [operator: Exists]` ensures it runs on every node type including GPU nodes with custom taints - Temporary mitigation — annotated with TODO(CVE-2026-43284) for removal once patched AMIs ship ## Changes - **`base/kubernetes/dirtyfrag-mitigation.yaml`** — New DaemonSet (ConfigMap + initContainer + ServiceAccount) that writes modprobe.d blacklist, unloads vulnerable modules, and drops page cache on every node - **`base/kubernetes/kustomization.yaml`** — Wire the new DaemonSet into the base kustomize build - **`clusters.yaml`** — Add TODO breadcrumb alongside the existing algif-mitigation TODO tracking AMI version for removal - **`modules/buildkit/scripts/python/generate_buildkit.py`** — Add TODO breadcrumb for DirtyFrag removal in BuildKit EC2NodeClass AMI selector - **`modules/nodepools/scripts/python/generate_nodepools.py`** — Add TODO breadcrumb for DirtyFrag removal in nodepool EC2NodeClass AMI selector - **`modules/pypi-cache/kubernetes/ec2nodeclass.yaml.tpl`** — Add TODO breadcrumb for DirtyFrag removal in pypi-cache EC2NodeClass AMI selector ## Notes - Remove this DaemonSet (and all TODO breadcrumbs) once all nodes are running a patched AL2023 AMI with kernel 6.1.170+ or 6.12.83+. Track: ALAS2023-2026-1694 / ALAS2023-2026-1695. - The `rxrpc` module is not built into the AL2023 default kernel — blacklisting it here is purely defensive per AWS bulletin 2026-027-AWS. - Node resource footprint: 10m CPU / 32Mi memory (requests) for the sleep container; the initContainer runs once and exits. ## Testing - Verify the DaemonSet is healthy on a test cluster: `kubectl get ds dirtyfrag-mitigation -n kube-system` — desired/ready counts should match node count - Confirm the modprobe.d file was written: `kubectl logs -n kube-system -l app=dirtyfrag-mitigation -c disable-dirtyfrag` should show "Mitigation Applied" - Verify modules are blocked: `ssh` to a node and run `modprobe esp4` — should fail with `/bin/false` - `just lint` and `just test` pass with zero errors --------- Signed-off-by: Jean Schmidt <contato@jschmidt.me>
1 parent 858e1c3 commit a03d582

6 files changed

Lines changed: 233 additions & 0 deletions

File tree

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# DirtyFrag Mitigation DaemonSet (CVE-2026-43284 + CVE-2026-43500)
2+
#
3+
# DirtyFrag = two related Linux kernel page-cache write LPEs in the IPv4/IPv6
4+
# datagram zero-copy path. __ip_append_data() fails to set SKBFL_SHARED_FRAG
5+
# on splice-attached pages, then ESP (xfrm) and RxRPC perform in-place crypto
6+
# over them — mutating the page cache of read-only files like /usr/bin/su
7+
# and /etc/passwd.
8+
# - CVE-2026-43284: xfrm-ESP variant. Requires unshare(CLONE_NEWUSER|
9+
# CLONE_NEWNET) to acquire CAP_NET_ADMIN in a user namespace.
10+
# - CVE-2026-43500: RxRPC variant. Requires no privileges — but rxrpc.ko
11+
# is not built into the AL2023 default kernel (blacklisted defensively
12+
# per AWS bulletin 2026-027-AWS).
13+
#
14+
# Crosses container boundaries via the shared page cache, so on a multi-tenant
15+
# CI cluster any unprivileged workflow code can escalate to root on the node.
16+
#
17+
# AWS Security Bulletin: https://aws.amazon.com/security/security-bulletins/2026-027-aws/
18+
# ALAS2023-2026-1694 (kernel 6.1, fixed in 6.1.170-210.320.amzn2023)
19+
# ALAS2023-2026-1695 (kernel 6.12, fixed in 6.12.83-113.160.amzn2023)
20+
# Released 2026-05-09.
21+
#
22+
# AS OF 2026-05-11, no patched EKS-optimized AMI exists yet (latest v20260505
23+
# ships kernel 6.1.168 / 6.12.80 — both pre-patch).
24+
#
25+
# This DaemonSet writes /etc/modprobe.d/disable-dirtyfrag.conf to block
26+
# on-demand loading of esp4, esp6, and rxrpc, then defensively rmmod's any
27+
# that have already loaded, then drops the page cache to evict any
28+
# DirtyFrag-poisoned pages of read-only files.
29+
#
30+
# Runs on EVERY node (base infra + Karpenter ARC + BuildKit + GPU + H100/B200).
31+
# Same shape as algif-mitigation.yaml.
32+
#
33+
# REMOVE this DaemonSet once all nodes are running a patched AL2023 AMI
34+
# (kernel 6.1.170+ or 6.12.83+).
35+
36+
---
37+
apiVersion: v1
38+
kind: ConfigMap
39+
metadata:
40+
name: dirtyfrag-mitigation-script
41+
namespace: kube-system
42+
data:
43+
disable-dirtyfrag.sh: |
44+
#!/bin/bash
45+
set -euo pipefail
46+
47+
echo "=== DirtyFrag Mitigation (CVE-2026-43284 + CVE-2026-43500) ==="
48+
echo "Node: $(hostname 2>/dev/null || echo 'unknown')"
49+
echo "Date: $(date)"
50+
echo ""
51+
52+
MARKER_FILE="/var/lib/dirtyfrag-mitigation/.configured"
53+
MODPROBE_FILE="/etc/modprobe.d/disable-dirtyfrag.conf"
54+
MODULES="esp4 esp6 rxrpc"
55+
56+
# =========================================================================
57+
# Idempotency — re-verify on pod restart, skip rewrite if still in place
58+
# =========================================================================
59+
if [ -f "$MARKER_FILE" ] && [ -f "$MODPROBE_FILE" ]; then
60+
echo "Already configured. Re-checking modules are not loaded..."
61+
for mod in $MODULES; do
62+
if lsmod | awk '{print $1}' | grep -qx "$mod"; then
63+
echo " WARNING: $mod is loaded despite blacklist. Unloading..."
64+
modprobe -r "$mod" || echo " rmmod $mod failed (module in use); reboot required for full mitigation."
65+
else
66+
echo " $mod not loaded — mitigation effective."
67+
fi
68+
done
69+
exit 0
70+
fi
71+
72+
# =========================================================================
73+
# Step 1 — install modprobe.d blacklist
74+
# =========================================================================
75+
echo "[1/3] Writing $MODPROBE_FILE..."
76+
mkdir -p "$(dirname "$MODPROBE_FILE")"
77+
cat > "$MODPROBE_FILE" <<'EOF'
78+
# CVE-2026-43284 + CVE-2026-43500 ("DirtyFrag") — page-cache write LPE mitigation.
79+
# Redirects on-demand loading of esp4, esp6, and rxrpc to /bin/false so the
80+
# modules are never inserted into the running kernel.
81+
# Managed by base/kubernetes/dirtyfrag-mitigation.yaml DaemonSet.
82+
# Remove this file (and the DaemonSet) once nodes are running a patched
83+
# AL2023 kernel (6.1.170+ or 6.12.83+).
84+
install esp4 /bin/false
85+
install esp6 /bin/false
86+
install rxrpc /bin/false
87+
EOF
88+
echo " Wrote blacklist."
89+
90+
# =========================================================================
91+
# Step 2 — unload modules if currently loaded (defensive, each individually)
92+
# =========================================================================
93+
echo "[2/3] Checking if vulnerable modules are loaded..."
94+
for mod in $MODULES; do
95+
if lsmod | awk '{print $1}' | grep -qx "$mod"; then
96+
echo " $mod loaded. Unloading (modprobe -r)..."
97+
if modprobe -r "$mod"; then
98+
echo " Unloaded $mod."
99+
else
100+
echo " WARNING: rmmod $mod failed. Module may be in use. New attempts"
101+
echo " will be blocked from now on, but the vulnerable code path"
102+
echo " remains reachable until the node reboots."
103+
fi
104+
else
105+
echo " $mod not loaded."
106+
fi
107+
done
108+
109+
# =========================================================================
110+
# Step 3 — drop page cache to evict any DirtyFrag-poisoned pages
111+
# =========================================================================
112+
# Best-effort: only evicts unmapped clean pages. Pages currently mapped by
113+
# running processes (libc, sshd, /etc/passwd held open, etc.) cannot be
114+
# evicted this way and will retain any prior poisoning until the node
115+
# reboots or the holding process exits.
116+
echo "[3/3] Dropping page cache (echo 3 > /proc/sys/vm/drop_caches)..."
117+
echo 3 > /proc/sys/vm/drop_caches || echo " WARNING: drop_caches failed."
118+
119+
# Marker — written last so a partial run re-attempts on next pod start
120+
mkdir -p "$(dirname "$MARKER_FILE")"
121+
date -Iseconds 2>/dev/null > "$MARKER_FILE" || date > "$MARKER_FILE"
122+
123+
echo ""
124+
echo "=== Mitigation Applied ==="
125+
echo "Future kernel attempts to load esp4/esp6/rxrpc will be redirected to /bin/false."
126+
127+
---
128+
apiVersion: apps/v1
129+
kind: DaemonSet
130+
metadata:
131+
name: dirtyfrag-mitigation
132+
namespace: kube-system
133+
labels:
134+
app: dirtyfrag-mitigation
135+
app.kubernetes.io/name: dirtyfrag-mitigation
136+
app.kubernetes.io/component: node-configuration
137+
spec:
138+
selector:
139+
matchLabels:
140+
app: dirtyfrag-mitigation
141+
142+
updateStrategy:
143+
type: RollingUpdate
144+
rollingUpdate:
145+
maxUnavailable: "25%"
146+
147+
template:
148+
metadata:
149+
labels:
150+
app: dirtyfrag-mitigation
151+
152+
spec:
153+
hostNetwork: true
154+
hostPID: true
155+
156+
priorityClassName: system-node-critical
157+
158+
# Run on ALL nodes (base infra + Karpenter-managed: ARC, BuildKit, GPU, H100, B200)
159+
tolerations:
160+
- operator: Exists
161+
162+
# NO nodeSelector — must run on every node in the cluster
163+
164+
serviceAccountName: dirtyfrag-mitigation
165+
166+
initContainers:
167+
- name: disable-dirtyfrag
168+
image: public.ecr.aws/amazonlinux/amazonlinux:2023
169+
command: ["/bin/bash", "-c"]
170+
args:
171+
# hostPID gives access to host's PID 1 root filesystem.
172+
# nsenter runs the script in the host's mount/UTS/IPC/net namespaces
173+
# so modprobe, lsmod, and writes to /etc/modprobe.d hit the host.
174+
# stdin redirection reads the script from the container's ConfigMap
175+
# mount before nsenter switches namespaces.
176+
- /proc/1/root/usr/bin/nsenter -t 1 -m -u -i -n -- /bin/bash < /scripts/disable-dirtyfrag.sh
177+
178+
securityContext:
179+
privileged: true
180+
181+
volumeMounts:
182+
- name: scripts
183+
mountPath: /scripts
184+
readOnly: true
185+
186+
containers:
187+
- name: sleep
188+
image: public.ecr.aws/docker/library/alpine:3.19
189+
command:
190+
- /bin/sh
191+
- -c
192+
- "echo 'DirtyFrag mitigation applied. Sleeping...'; sleep infinity"
193+
194+
resources:
195+
requests:
196+
cpu: 10m
197+
memory: 32Mi
198+
limits:
199+
cpu: 50m
200+
memory: 64Mi
201+
202+
volumes:
203+
- name: scripts
204+
configMap:
205+
name: dirtyfrag-mitigation-script
206+
defaultMode: 0755
207+
208+
---
209+
apiVersion: v1
210+
kind: ServiceAccount
211+
metadata:
212+
name: dirtyfrag-mitigation
213+
namespace: kube-system
214+
labels:
215+
app: dirtyfrag-mitigation

osdc/base/kubernetes/kustomization.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ resources:
1414
- git-cache/
1515
- registry-mirror-config.yaml
1616
- algif-mitigation.yaml
17+
- dirtyfrag-mitigation.yaml
1718
# image-cache-janitor is deployed via its own deploy.sh (builds custom image)
1819
# nodelocaldns is deployed via its own deploy.sh (resolves kube-dns ClusterIP at apply time)
1920
# NOTE: Namespaces for modules (arc-runners, arc-systems, buildkit, etc.)

osdc/clusters.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ defaults:
2727
# TODO(CVE-2026-31431): when bumping to a kernel 6.12.85+ AL2023 AMI,
2828
# remove osdc/base/kubernetes/algif-mitigation.yaml.
2929
# https://explore.alas.aws.amazon.com/CVE-2026-31431.html
30+
# TODO(CVE-2026-43284): when bumping to a kernel with the DirtyFrag fix
31+
# (6.1.170+ or 6.12.83+ on AL2023), remove
32+
# osdc/base/kubernetes/dirtyfrag-mitigation.yaml.
33+
# https://aws.amazon.com/security/security-bulletins/2026-027-aws/
3034
base_node_ami_version: "v20260318"
3135
eks_version: "1.35"
3236
single_nat_gateway: false

osdc/modules/buildkit/scripts/python/generate_buildkit.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,10 @@ def _nodepool_block(arch, instance_type, cpu_limit, memory_limit_gi):
348348
# rotation picks up a kernel 6.12.85+ AMI, remove
349349
# osdc/base/kubernetes/algif-mitigation.yaml.
350350
# https://explore.alas.aws.amazon.com/CVE-2026-31431.html
351+
# TODO(CVE-2026-43284): al2023@latest tracks the newest AL2023 AMI; once node
352+
# rotation picks up a kernel with the DirtyFrag fix (6.1.170+ or 6.12.83+),
353+
# remove osdc/base/kubernetes/dirtyfrag-mitigation.yaml.
354+
# https://aws.amazon.com/security/security-bulletins/2026-027-aws/
351355
amiSelectorTerms:
352356
- alias: al2023@latest
353357

osdc/modules/nodepools/scripts/python/generate_nodepools.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ def generate_nodepool_yaml(nodepool_def, module_name, defs_dir=None):
162162
# kernel 6.12.85+ AMI. Once rolled out across all nodes, remove
163163
# osdc/base/kubernetes/algif-mitigation.yaml.
164164
# https://explore.alas.aws.amazon.com/CVE-2026-31431.html
165+
# TODO(CVE-2026-43284): the AL2023 aliases / name globs below already track
166+
# @latest, so node rotation picks up the fix automatically once AWS ships a
167+
# kernel with the DirtyFrag fix (6.1.170+ or 6.12.83+). Once rolled out
168+
# across all nodes, remove osdc/base/kubernetes/dirtyfrag-mitigation.yaml.
169+
# https://aws.amazon.com/security/security-bulletins/2026-027-aws/
165170
if is_gpu:
166171
ami_family_block = " amiFamily: AL2023"
167172
ami_selector_block = """ amiSelectorTerms:

osdc/modules/pypi-cache/kubernetes/ec2nodeclass.yaml.tpl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ spec:
1212
# rotation picks up a kernel 6.12.85+ AMI, remove
1313
# osdc/base/kubernetes/algif-mitigation.yaml.
1414
# https://explore.alas.aws.amazon.com/CVE-2026-31431.html
15+
# TODO(CVE-2026-43284): al2023@latest tracks the newest AL2023 AMI; once node
16+
# rotation picks up a kernel with the DirtyFrag fix (6.1.170+ or 6.12.83+),
17+
# remove osdc/base/kubernetes/dirtyfrag-mitigation.yaml.
18+
# https://aws.amazon.com/security/security-bulletins/2026-027-aws/
1519
amiSelectorTerms:
1620
- alias: al2023@latest
1721

0 commit comments

Comments
 (0)