Share
## https://sploitus.com/exploit?id=EFB2F7AC-AF41-59C3-A2BF-6E1F78060C05
# copy-fail-blocker

BPF-LSM mitigation for [CVE-2026-31431](https://copy.fail/) ("Copy Fail") and
similar privilege-escalation vulnerabilities that depend on userspace access
to the Linux kernel crypto API (`AF_ALG` / `algif_*`).

A small DaemonSet attaches a single BPF-LSM program to the `socket_create`
hook on every node. The program returns `-EPERM` for any `socket(AF_ALG, ...)`
call, regardless of process capabilities, namespace, or seccomp profile.

Tested on Talos Linux (which ships with `CONFIG_BPF_LSM=y` and `bpf` in the
default LSM stack since v1.10), works on any distribution with the same
kernel configuration.

## Why

CVE-2026-31431 is a logic flaw in `algif_aead` that lets an unprivileged
local user perform a 4-byte page-cache write to any setuid binary, achieving
root with a 732-byte Python script. The exploit needs nothing but
`AF_ALG` + `splice()`, both of which are reachable from any unprivileged
process by default.

The proper fix is a kernel patch (mainline `a664bf3d603d`). Until that lands
in your distribution, the attack surface can be removed by preventing
userspace from ever opening an `AF_ALG` socket. Compared to alternatives:

| Mitigation                                  | Coverage                | Reboot? | Persists? |
| ------------------------------------------- | ----------------------- | ------- | --------- |
| `module_blacklist=algif_aead` (kernel arg)  | host-wide               | yes     | yes       |
| Custom kernel without `CRYPTO_USER_API_AEAD`| host-wide               | yes     | yes       |
| Per-pod custom seccomp profile              | only labelled workloads | no      | yes       |
| **copy-fail-blocker (this project)**        | **host-wide**           | **no**  | while DS runs |

This project is the no-reboot option. Run it cluster-wide, then plan the
permanent kernel fix on your normal patch cadence.

## How it works

`bpf/blocker.c` is a 15-line BPF-LSM program:

```c
SEC("lsm/socket_create")
int BPF_PROG(block_af_alg, int family, int type, int protocol,
             int kern, int ret)
{
    if (ret)
        return ret;
    if (family == AF_ALG)   // 38
        return -EPERM;
    return 0;
}
```

The Go loader (`main.go`, ~40 lines) loads the program and attaches it via
`bpf(BPF_LINK_CREATE)`. The link is held for the lifetime of the pod. On
`SIGTERM`, the link is closed and the hook detaches.

Requires a kernel built with `CONFIG_BPF_LSM=y` and `bpf` in the active LSM
stack (`lsm=...,bpf` on the kernel command line). Talos Linux ships with
both enabled by default since v1.10.

## Install

The chart is not published as an OCI artifact (the registry path is shared
with the container image). Install from a tagged checkout:

```sh
git clone --branch v0.2.0 https://github.com/cozystack/copy-fail-blocker
cd copy-fail-blocker
helm upgrade --install copy-fail-blocker charts/copy-fail-blocker \
  --namespace kube-system
```

Or via the Makefile shortcuts:

```sh
make apply         # helm upgrade --install into kube-system
make diff          # preview changes against the cluster
make delete        # uninstall
```

The DaemonSet must run privileged (it loads BPF programs and writes to
bpffs). Place it in a namespace with the privileged Pod Security Standard,
or in `kube-system`, which is privileged by default.

## Verify

From any pod on a covered node:

```sh
python3 -c '
import socket
try:
    socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
    print("FAIL: AF_ALG socket created")
except OSError as e:
    print("OK:", e)'
```

Expected output:

```
OK: [Errno 1] Operation not permitted
```

## Build

```sh
make image                                       # docker buildx build + push
make image REGISTRY=ghcr.io/myorg TAG=v0.2.0     # custom tag
make image PUSH=0 LOAD=1                         # build locally without pushing
```

`make image` updates `charts/copy-fail-blocker/values.yaml` with the
resolved image digest so the chart always pins by digest.

Build dependencies live in the Containerfile (clang, libbpf-dev, Go). Local
host needs only `docker buildx`, `helm`, `yq` (mikefarah), `kubectl`, and
`helm-diff`.

## Configuration

`charts/copy-fail-blocker/values.yaml`:

| Key                   | Default                              | Notes                                  |
| --------------------- | ------------------------------------ | -------------------------------------- |
| `image.repository`    | `ghcr.io/cozystack/copy-fail-blocker`| Auto-updated by `make image`           |
| `image.tag`           | `v0.1.0@sha256:...`                  | Pinned by digest                       |
| `priorityClassName`   | `system-node-critical`               | Ensures the daemon survives evictions  |
| `tolerations`         | `[{operator: Exists}]`               | Runs on every node, including tainted  |
| `resources.requests`  | `5m CPU / 16Mi memory`               | Idle footprint after attach            |

## Limitations

- **The hook lives only while the pod runs.** On pod restart there is a
  short window (seconds) where `AF_ALG` is reachable again. For most
  threat models this is acceptable; if not, consider pinning the BPF link
  to bpffs (not implemented here โ€” see [issues](https://github.com/cozystack/copy-fail-blocker/issues)).
- **Anyone with `CAP_BPF` and `CAP_SYS_ADMIN`** on the host can detach the
  hook. This is not a substitute for cluster-wide privilege restrictions.
- **Does not block `algif_skcipher` / `algif_hash` / etc.** The program
  rejects the entire `AF_ALG` family, but only `algif_aead` is currently
  known to be exploitable. If a future CVE needs a finer filter (e.g. hook
  `bind()` and inspect `salg_type`), this is straightforward to add.
- **No effect on processes that already hold an open `AF_ALG` socket.**
  Existing sockets keep working until closed.

## License

Apache License 2.0 โ€” see [LICENSE](LICENSE).