# CVE-2026-31694 RCA Report

## Summary

CVE-2026-31694 is an out-of-bounds write in the Linux kernel FUSE readdir cache. A malicious userspace FUSE daemon can return a `FUSE_READDIR` entry with a server-controlled `namelen=4095`; the kernel serializes this as a 4120-byte directory entry and copies it into a single 4096-byte readdir cache page. The missing `reclen > PAGE_SIZE` validation lets the final 24 bytes spill into the next physical page. In this run, the vulnerability was reproduced end-to-end in a QEMU guest using the real Linux kernel/FUSE code path, and the exploit chain was extended beyond KASAN-visible corruption: an unprivileged uid 1000 process changed the page-cache contents of a root-owned, read-only `/etc/passwd` file to a passwordless-root line. A fixed `fuse.ko` negative control rejected the oversized dirent and left `/etc/passwd` unchanged.

## Impact

- **Package/component affected:** Linux kernel FUSE subsystem, specifically `fs/fuse/readdir.c` in the readdir cache path (`fuse_add_dirent_to_cache()`).
- **Affected versions:** The issue is reachable after the FUSE name length increase to `PATH_MAX - 1` / 4095, reported for Linux v6.16 through v7.0-rc and affected stable branches before the fix.
- **Risk level and consequences:** High. A local attacker able to run a malicious FUSE daemon can obtain a constrained but attacker-controlled 24-byte overwrite into an adjacent physical page. When groomed onto a page-cache page for a privileged file such as `/etc/passwd`, the primitive bypasses filesystem write permissions and can produce local privilege escalation semantics.

## Impact Parity

- **Disclosed/claimed maximum impact:** Unprivileged local privilege escalation via page-cache corruption of `/etc/passwd`.
- **Reproduced impact from this run:** Full local privilege-escalation primitive. The script boots a non-sanitized kernel in QEMU, uses the real FUSE kernel path, drops the exploit process to uid/gid 1000 before attacker-controlled `READDIR` replies and target corruption, verifies direct writes to `/etc/passwd` fail with `Read-only file system`, then shows `/etc/passwd` page-cache contents changed from `root:x:0:0:root:/root:/bin/sh` to `root::0:0:x:.:`. Two vulnerable attempts succeeded, and two fixed negative-control attempts left `/etc/passwd` unchanged.
- **Parity:** `full`.
- **Not demonstrated:** The proof stops after demonstrating the root-level page-cache corruption effect. It does not spawn an interactive root shell, but the demonstrated primitive is the disclosed LPE mechanism: an unprivileged attacker changes the trusted cached contents of a root-owned passwd file to a passwordless-root entry without write permission.

## Root Cause

`fuse_add_dirent_to_cache()` computes a serialized FUSE dirent size from a FUSE-server-controlled name length and copies that many bytes into a single page-cache page. The vulnerable logic only checks whether the record fits in the remaining space of the current page and, if not, advances to a fresh page with offset zero. It does not reject a record whose serialized length is larger than one page.

Conceptually, the vulnerable flow is:

```c
size_t reclen = FUSE_DIRENT_SIZE(dirent);   // 4120 for namelen=4095
...
offset = size & ~PAGE_MASK;
if (offset + reclen > PAGE_SIZE) {
    index++;
    offset = 0;
}
...
memcpy(addr + offset, dirent, reclen);      // copies 4120 bytes into 4096-byte page
```

For `namelen=4095`, `FUSE_DIRENT_ALIGN(24 + 4095)` is 4120. At `offset=0`, the kernel still performs `memcpy(..., 4120)` into a 4096-byte page, spilling 24 bytes into the next physical page. The exploit controls those trailing bytes and uses page allocator grooming to place the first page of `/etc/passwd` in the adjacent page frame.

The fix is to reject oversized dirents before adding them to the readdir cache, e.g. by adding a guard equivalent to:

```c
if (reclen > PAGE_SIZE)
    return;
```

The ticket identifies upstream fix commit `51a8de6c50bf947c8f534cd73da4c8f0a13e7bed` (`fuse: reject oversized dirents in page cache`). The reproduction script builds the vulnerable module without this guard and the fixed module with this guard as the negative control.

## Reproduction Steps

1. Run `bundle/repro/reproduction_steps.sh` from the repository workspace, or run it with `PRUVA_ROOT=/path/to/bundle`.
2. The script:
   - Reuses the prepared Linux source/build cache when available.
   - Builds a non-sanitized Linux kernel and vulnerable `fuse.ko` from the real source.
   - Builds a fixed `fuse.ko` with the `reclen > PAGE_SIZE` guard.
   - Builds `bundle/repro/fuse_passwd_lpe` from `bundle/repro/fuse_passwd_lpe.c`, a public-PoC-derived malicious FUSE daemon that speaks the real FUSE protocol.
   - Creates a small active ext4 root filesystem containing a root-owned, mode `0444` `/etc/passwd`.
   - Boots QEMU twice with the vulnerable module and twice with the fixed module.
   - In each guest, root performs only setup/module loading; the exploit drops to uid/gid 1000 before attacker-controlled FUSE replies and `/etc/passwd` targeting.
3. Expected evidence:
   - Vulnerable attempts show `PAGE_CACHE_CORRUPTION_CONFIRMED uid=1000 target=/etc/passwd`, `Direct write check as uid 1000: Read-only file system`, and `INIT_AFTER=root::0:0:x:.:`.
   - Fixed attempts show `INIT_RESULT_FIXED_REJECTED_OVERSIZED_DIRENT` and `INIT_AFTER=root:x:0:0:root:/root:/bin/sh` with no page-cache-corruption confirmation.

## Evidence

Primary current-run artifacts:

- `bundle/repro/reproduction_steps.sh` — self-contained reproduction script.
- `bundle/repro/runtime_manifest.json` — runtime evidence manifest written by the script.
- `bundle/repro/validation_verdict.json` — structured verdict written by the script.
- `bundle/logs/reproduction_steps.log` — consolidated build and proof log.
- `bundle/logs/qemu_lpe_vuln_attempt1.log`
- `bundle/logs/qemu_lpe_vuln_attempt2.log`
- `bundle/logs/qemu_lpe_fixed_attempt1.log`
- `bundle/logs/qemu_lpe_fixed_attempt2.log`
- `bundle/repro/fuse_passwd_lpe.c` and `bundle/repro/fuse_passwd_lpe` — malicious FUSE daemon/helper used in the guest.
- `bundle/repro/fuse-nokasan-vuln.ko` and `bundle/repro/fuse-nokasan-fixed.ko` — vulnerable and fixed FUSE modules used for comparison.

Key vulnerable evidence from the second verified run in `bundle/logs/reproduction_steps.log`:

```text
Primary proof build is non-sanitized: CONFIG_KASAN is disabled
INIT_ROLE=vuln
INIT_KERNEL=6.18.18
INIT_ROOTFS_ACTIVE=/dev/vda
INIT_BEFORE=root:x:0:0:root:/root:/bin/sh
[+] Running exploit logic as uid=1000 gid=1000 target=/etc/passwd
[+] Direct write check as uid 1000: Read-only file system
[+] Warmup: 3/5 (60%)
[*] LPE 1/200 ... [+] CORRUPTED /etc/passwd first_line=root::0:0:x:.:
HIT!
[+] PAGE_CACHE_CORRUPTION_CONFIRMED uid=1000 target=/etc/passwd
INIT_EXPLOIT_RC=0
INIT_AFTER=root::0:0:x:.:
INIT_RESULT_PAGE_CACHE_LPE_CONFIRMED
VULNERABLE attempt 1: unprivileged page-cache corruption of /etc/passwd confirmed
```

The second vulnerable attempt produced the same successful markers:

```text
INIT_ROLE=vuln
INIT_BEFORE=root:x:0:0:root:/root:/bin/sh
[+] Direct write check as uid 1000: Read-only file system
[+] PAGE_CACHE_CORRUPTION_CONFIRMED uid=1000 target=/etc/passwd
INIT_AFTER=root::0:0:x:.:
INIT_RESULT_PAGE_CACHE_LPE_CONFIRMED
VULNERABLE attempt 2: unprivileged page-cache corruption of /etc/passwd confirmed
```

Fixed negative-control evidence:

```text
INIT_ROLE=fixed
INIT_BEFORE=root:x:0:0:root:/root:/bin/sh
[+] Running exploit logic as uid=1000 gid=1000 target=/etc/passwd
[+] Direct write check as uid 1000: Read-only file system
[+] Warmup: 0/5 (0%)
[-] No warmup hits.
INIT_EXPLOIT_RC=1
INIT_AFTER=root:x:0:0:root:/root:/bin/sh
INIT_RESULT_FIXED_REJECTED_OVERSIZED_DIRENT
FIXED attempt 1: oversized dirent failed closed; /etc/passwd unchanged
```

The second fixed attempt also failed closed and left `/etc/passwd` unchanged. The script summary was:

```text
Vulnerable page-cache-corruption attempts: 2/2
Fixed negative-control attempts: 2/2
CVE-2026-31694 CONFIRMED: non-sanitized vulnerable kernel permits uid 1000 to corrupt the page-cache contents of root-owned read-only /etc/passwd; fixed module blocks it.
```

## Recommendations / Next Steps

- Apply the upstream fix that rejects dirents whose serialized length exceeds `PAGE_SIZE` before caching them.
- Backport the `reclen > PAGE_SIZE` guard to all supported kernels that include the FUSE name length increase and have vulnerable readdir cache logic.
- Add regression tests for FUSE `READDIR` replies with maximum name lengths, including records that exceed a page after alignment.
- Consider hardening and auditing other page-cache or direct-map write paths where attacker-controlled data can cross a page boundary into an adjacent physical page.
- Treat KASAN-only testing as insufficient for this exploit class: the primary exploit target is a page-cache page and the important observable impact is privileged file-content corruption in the kernel page cache.

## Additional Notes

- **Idempotency confirmation:** `bundle/repro/reproduction_steps.sh` was executed twice consecutively after the shell bug was fixed. Both runs exited 0 and produced the same 2/2 vulnerable and 2/2 fixed outcomes.
- **Primary proof mode:** The proof is intentionally non-sanitized (`CONFIG_KASAN` disabled) and uses product/runtime kernel execution through QEMU and the real FUSE boundary.
- **Privilege boundary:** The process opens `/dev/fuse` and mounts during privileged harness setup, then calls `setresgid(1000,1000,1000)` and `setresuid(1000,1000,1000)` before sending malicious FUSE `READDIR` replies and grooming `/etc/passwd`.
- **Scope:** The proof demonstrates the disclosed LPE primitive by changing the trusted page-cache contents of root-owned `/etc/passwd`. It does not persist the change to disk or spawn an interactive root shell, to keep the run deterministic and self-contained.
