# CVE-2026-41579 — Root Cause Analysis

## Summary

CVE-2026-41579 is a low-severity host filesystem integrity issue in opencontainers/runc. When runc prepares a container rootfs, the functions `setupPtmx` and `setupDevSymlinks` operate on path strings under the bundle rootfs before `pivot_root(2)` occurs. If the container image has `/dev` as a symlink that points outside the rootfs (for example to a host directory controlled by the attacker), `filepath.Join(rootfs, "/dev/ptmx")` resolves through the symlink and runc deletes or re-creates files on the host. A malicious image can therefore trick runc into removing an existing file named `ptmx` and creating a small fixed set of device symlinks in an attacker-chosen host directory.

## Impact

- **Package / component:** opencontainers/runc
- **Affected versions:** prior to 1.3.6, 1.4.0-rc.1 through 1.4.3, and 1.5.0-rc.1 through 1.5.0-rc.3
- **Risk level:** low (per upstream advisory)
- **Consequences:** Arbitrary deletion of a host file named `ptmx` and creation of a limited set of hardcoded symlinks in a host directory reachable via a malicious `/dev` symlink. Not exploitable under Docker, but exploitable via other runc-based runtimes that do not mask `/dev` with a top-level read-only layer.

## Impact Parity

- **Disclosed / claimed maximum impact:** Arbitrary file deletion and symlink creation on the host filesystem through a malicious container image (`/dev` symlink).
- **Reproduced impact from this run:** Vulnerable runc deleted a decoy file named `ptmx` and replaced it with a symlink in an attacker-controlled directory; fixed runc left the decoy untouched.
- **Parity:** `full` for the documented filesystem-integrity impact. The reproduction does not demonstrate privilege escalation or code execution, which is consistent with the advisory's low-severity rating.

## Root Cause

The bug is in runc's rootfs preparation code. Before the container pivots into its rootfs, `setupPtmx` and `setupDevSymlinks` use `filepath.Join(rootfs, "/dev/...")` and then call `os.Remove` / `os.Symlink`. Because the operations happen before `pivot_root`, a `/dev` entry in the image that is a symlink to an attacker-controlled host directory is followed, causing the operations to affect the host path instead of the container rootfs.

Upstream fix commit:

- `opencontainers/runc@864db8042dbb` — "rootfs: make /dev initialisation code fd-based"

The fix rewrites the `/dev` setup code to operate on file descriptors relative to the opened rootfs directory, so symlinks in the image cannot redirect the operations to host paths.

## Reproduction Steps

The reproduction is implemented in `bundle/repro/reproduction_steps.sh`. At a high level it:

1. Verifies Docker is available.
2. Downloads the vulnerable runc release binary (`v1.3.5`) and the fixed release binary (`v1.3.6`).
3. Builds a minimal OCI rootfs from the official `busybox` image.
4. Builds two privileged Docker images (`repro-runc-vuln` and `repro-runc-fixed`) that each contain one runc binary and the rootfs.
5. Inside a privileged container, replaces `/bundle/rootfs/dev` with a symlink to `/controlled_dev` and creates a decoy `/controlled_dev/ptmx`.
6. Generates an OCI bundle with `runc spec`, disables the terminal, and sets the command to `/bin/true`.
7. Runs `runc run cve-ptmx-test -b /bundle`.
8. Checks whether the decoy file was deleted.

Expected evidence:

- **Vulnerable (1.3.5):** the `ptmx` decoy is removed and `/controlled_dev` contains symlinks such as `ptmx -> pts/ptmx`, `core -> /proc/kcore`, `fd -> /proc/self/fd`, etc.
- **Fixed (1.3.6):** the `ptmx` decoy remains untouched and runc does not create host symlinks.

## Evidence

- `bundle/logs/repro_vuln.log` — vulnerable runc 1.3.5 deletes the decoy and creates host symlinks.
- `bundle/logs/repro_fixed.log` — fixed runc 1.3.6 preserves the decoy.
- `bundle/logs/build_repro-runc-vuln.log` — Docker build log for the vulnerable image.
- `bundle/logs/build_repro-runc-fixed.log` — Docker build log for the fixed image.
- `bundle/repro/runtime_manifest.json` — runtime evidence manifest produced by the script.

Key excerpts:

Vulnerable run:

```text
RUN_VERSION: runc version 1.3.5
BEFORE: /controlled_dev/ptmx present?
-rw-r----    1 root     root            10 ... ptmx
...
AFTER: /controlled_dev contents:
-rw-r--r--    ... ptmx
RESULT: decoy deleted
```

Fixed run:

```text
RUN_VERSION: runc version 1.3.6
BEFORE: /controlled_dev/ptmx present?
-rw-r--r--    ... ptmx
...
AFTER: /controlled_dev contents:
-rw-r--r--    ... ptmx
RESULT: decoy preserved
```

## Recommendations / Next Steps

- Upgrade runc to a patched version: **1.3.6**, **1.4.3**, or **1.5.0** (or later).
- Higher-level runtimes that consume runc should ensure container images cannot ship a `/dev` symlink that resolves to a host path, or rely on the patched runc version.
- Regression tests should include a rootfs where `/dev` is a symlink to a controlled host directory and verify that `setupPtmx`/`setupDevSymlinks` do not operate on the host path.

## Additional Notes

- The script is idempotent: it re-downloads only missing binaries, rebuilds the Docker images each run, and uses unique container names.
- The reproduction uses the real `runc` CLI binary and the real OCI bundle execution path (`runc run`), not a reimplemented parser or mocked environment.
- The Docker-in-Docker privileged container is required in this sandbox because the host environment lacks `CAP_SYS_ADMIN` and a writable cgroup hierarchy; inside the privileged container runc has the capabilities needed to create a genuine container.
- No sanitizer or crash is involved; the proof relies on the filesystem state difference between the vulnerable and fixed versions.
