# Variant RCA Report

## Summary

A materially distinct alternate trigger was confirmed on the vulnerable kernel: a malicious FUSE daemon can negotiate `FUSE_DO_READDIRPLUS` and return a `FUSE_READDIRPLUS` response containing a `struct fuse_direntplus` whose embedded `struct fuse_dirent` has `namelen=4095`. The kernel parses this through `parse_dirplusfile()`, then calls the same `fuse_emit()` and `fuse_add_dirent_to_cache()` sink used by ordinary `FUSE_READDIR`, producing the same 4120-byte copy into a 4096-byte readdir cache page and the same unprivileged `/etc/passwd` page-cache corruption primitive. This is **not a bypass** of the tested fix: the fixed `fuse.ko` adds a sink-level `reclen > PAGE_SIZE` guard in `fuse_add_dirent_to_cache()`, so it blocks both the original `FUSE_READDIR` path and the `FUSE_READDIRPLUS` alternate parser path.

## Fix Coverage / Assumptions

The effective fix relies on the invariant that no serialized directory entry larger than one page may be copied into the FUSE readdir cache. It enforces that invariant at the common sink, `fs/fuse/readdir.c:fuse_add_dirent_to_cache()`, immediately after computing:

```c
size_t reclen = FUSE_DIRENT_SIZE(dirent);
```

The tested fixed module adds the equivalent of:

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

This explicitly covers every caller that reaches the cache insertion sink through `fuse_emit()`, including:

- `parse_dirfile()` for `FUSE_READDIR` replies, and
- `parse_dirplusfile()` for `FUSE_READDIRPLUS` replies.

The fix does **not** validate `FUSE_DIRENTPLUS_SIZE()` itself against `PAGE_SIZE`; however, the overflow root cause is not the larger wire-format `direntplus` record. The overflow occurs when the embedded `struct fuse_dirent` is passed to `fuse_add_dirent_to_cache()` and copied as `FUSE_DIRENT_SIZE(dirent)`. Because the guard is at that common sink, it closes this alternate path. The fix assumption appears complete for this specific root cause: all readdir-cache insertions flow through `fuse_add_dirent_to_cache()`.

## Variant / Alternate Trigger

The tested alternate trigger is `FUSE_READDIRPLUS` instead of ordinary `FUSE_READDIR`.

Exact runtime entry/message path:

1. A local attacker-controlled FUSE daemon opens `/dev/fuse` and mounts a FUSE filesystem in the VM harness.
2. During `FUSE_INIT`, the daemon advertises `FUSE_DO_READDIRPLUS`.
3. During `getdents64()` on a trigger directory, the kernel issues `FUSE_READDIRPLUS`.
4. The malicious daemon replies with a `struct fuse_direntplus` whose embedded `dirent.namelen` is `4095`.
5. The kernel parses the response in `parse_dirplusfile()` and calls `fuse_emit()`.
6. `fuse_emit()` calls `fuse_add_dirent_to_cache()` because `FOPEN_CACHE_DIR` is set.
7. Vulnerable `fuse_add_dirent_to_cache()` copies `FUSE_DIRENT_SIZE(dirent) == 4120` bytes into one 4096-byte page.

Specific code anchors:

- `fs/fuse/readdir.c:32-88` — `fuse_add_dirent_to_cache()` computes `reclen`, advances to a new page if `offset + reclen > PAGE_SIZE`, then performs `memcpy(addr + offset, dirent, reclen)`.
- `fs/fuse/readdir.c:114-123` — `fuse_emit()` calls `fuse_add_dirent_to_cache()` when `FOPEN_CACHE_DIR` is set.
- `fs/fuse/readdir.c:293-326` — `parse_dirplusfile()` parses `FUSE_READDIRPLUS` records and invokes `fuse_emit()` with the embedded `struct fuse_dirent`.
- `fs/fuse/readdir.c:334-377` — `fuse_readdir_uncached()` selects `FUSE_READDIRPLUS` when `fuse_use_readdirplus()` is true.

The stage-specific reproducer is `bundle/vuln_variant/fuse_readdirplus_lpe.c`. It differs from the prior `FUSE_READDIR` proof by advertising `FUSE_DO_READDIRPLUS`, handling opcode `FUSE_READDIRPLUS`, building a `struct fuse_direntplus` response, and treating any fallback ordinary `FUSE_READDIR` as an error for evidence clarity.

## Impact

- **Package/component affected:** Linux kernel FUSE subsystem, `fs/fuse/readdir.c` readdir cache path.
- **Affected versions as tested:** vulnerable module built from the prepared Linux 6.18.18 source snapshot without the `reclen > PAGE_SIZE` guard; fixed negative-control module built from the same source/config with the guard inserted.
- **Risk level and consequences:** High on vulnerable builds. The alternate `FUSE_READDIRPLUS` path gives the same local privilege-escalation primitive as the parent issue: an unprivileged attacker-controlled FUSE daemon can corrupt the page-cache contents of a root-owned, read-only `/etc/passwd` file.

## Impact Parity

- **Disclosed/claimed maximum impact for the parent:** unprivileged local privilege escalation via page-cache corruption of `/etc/passwd`.
- **Reproduced impact from this variant run:** full LPE primitive on the vulnerable module through `FUSE_READDIRPLUS`: the exploit drops to uid/gid 1000 before malicious replies and target corruption, direct append to `/etc/passwd` fails with `Read-only file system`, and `/etc/passwd` page-cache contents change to `root::0:0:x:.:`.
- **Parity:** `full` on the vulnerable alternate trigger; `none` on the fixed module because the tested fix blocks the alternate trigger.
- **Not demonstrated:** an interactive root shell was not spawned. The proof stops at deterministic page-cache corruption of `/etc/passwd`, matching the parent LPE primitive.

## Root Cause

The same underlying bug can be reached because `FUSE_READDIRPLUS` eventually reuses the same cache insertion function as ordinary `FUSE_READDIR`. `parse_dirplusfile()` computes the wire record length using `FUSE_DIRENTPLUS_SIZE(direntplus)` to parse a `struct fuse_direntplus`, but when it emits a directory entry it passes the embedded `struct fuse_dirent` to `fuse_emit()`. `fuse_emit()` then calls `fuse_add_dirent_to_cache()`, which computes `FUSE_DIRENT_SIZE(dirent)` from the embedded attacker-controlled `namelen` and performs the vulnerable copy.

For `namelen=4095`, `FUSE_DIRENT_SIZE(dirent)` is 4120. In the vulnerable sink, the existing check only moves the insertion offset to the start of the next page when `offset + reclen > PAGE_SIZE`; it does not reject `reclen > PAGE_SIZE`. A 4120-byte `memcpy()` therefore writes 24 bytes beyond the end of the target cache page.

The ticket identifies the upstream fix as `51a8de6c50bf947c8f534cd73da4c8f0a13e7bed` (`fuse: reject oversized dirents in page cache`). The tested fixed module applies the same sink-level guard, and runtime evidence shows that this guard blocks the `FUSE_READDIRPLUS` alternate trigger.

## Reproduction Steps

1. Run `bundle/vuln_variant/reproduction_steps.sh` from the workspace, or set `PRUVA_ROOT=/path/to/bundle` and run it from any directory.
2. The script:
   - builds `bundle/vuln_variant/fuse_readdirplus_lpe` from the stage-specific source,
   - boots the prepared non-KASAN kernel in QEMU,
   - tests the `FUSE_READDIRPLUS` variant against the vulnerable `fuse.ko`,
   - tests the same variant against the fixed `fuse.ko`, and
   - writes logs under `bundle/logs/vuln_variant/`.
3. Expected evidence:
   - Vulnerable run: `received FUSE_READDIRPLUS`, `PAGE_CACHE_CORRUPTION_CONFIRMED uid=1000 target=/etc/passwd`, `VARIANT_AFTER=root::0:0:x:.:`, and `VARIANT_RESULT_READDIRPLUS_LPE_CONFIRMED`.
   - Fixed run: `received FUSE_READDIRPLUS`, `VARIANT_AFTER=root:x:0:0:root:/root:/bin/sh`, and `VARIANT_RESULT_FIXED_REJECTED_READDIRPLUS_OVERSIZED_DIRENT`, with no `PAGE_CACHE_CORRUPTION_CONFIRMED`.

## Evidence

Current-run artifacts:

- `bundle/vuln_variant/reproduction_steps.sh` — stage-specific vulnerable/fixed variant test.
- `bundle/vuln_variant/fuse_readdirplus_lpe.c` — malicious READDIRPLUS FUSE daemon/helper.
- `bundle/vuln_variant/runtime_manifest.json` — runtime stack and proof artifact manifest.
- `bundle/logs/vuln_variant/readdirplus_variant.log` — consolidated current-run evidence.
- `bundle/logs/vuln_variant/qemu_readdirplus_vuln.log` — vulnerable QEMU evidence.
- `bundle/logs/vuln_variant/qemu_readdirplus_fixed.log` — fixed negative-control evidence.

Key vulnerable evidence from the idempotency run:

```text
VARIANT_ROLE=vuln
VARIANT_KERNEL=6.18.18
VARIANT_BEFORE=root:x:0:0:root:/root:/bin/sh
[fuse] INIT: advertising FUSE_DO_READDIRPLUS for variant path
[fuse] received FUSE_READDIRPLUS offset=0; holding reply for grooming
[fuse] replying to FUSE_READDIRPLUS with namelen=4095 plus_reclen=4248 embedded_dirent_reclen=4120
[+] Direct write check as uid 1000: Read-only file system
[+] CORRUPTED /etc/passwd first_line=root::0:0:x:.:
[+] PAGE_CACHE_CORRUPTION_CONFIRMED uid=1000 target=/etc/passwd
VARIANT_EXPLOIT_RC=0
VARIANT_AFTER=root::0:0:x:.:
VARIANT_RESULT_READDIRPLUS_LPE_CONFIRMED
```

Key fixed evidence:

```text
VARIANT_ROLE=fixed
VARIANT_KERNEL=6.18.18
VARIANT_BEFORE=root:x:0:0:root:/root:/bin/sh
[fuse] INIT: advertising FUSE_DO_READDIRPLUS for variant path
[fuse] received FUSE_READDIRPLUS offset=0; holding reply for grooming
[fuse] replying to FUSE_READDIRPLUS with namelen=4095 plus_reclen=4248 embedded_dirent_reclen=4120
[+] Warmup: 0/5 (0%)
[-] No warmup hits.
VARIANT_EXPLOIT_RC=1
VARIANT_AFTER=root:x:0:0:root:/root:/bin/sh
VARIANT_RESULT_FIXED_REJECTED_READDIRPLUS_OVERSIZED_DIRENT
```

Both mandatory verification runs completed without crashing. Both returned script exit code 1, which is the designed outcome for “alternate trigger on vulnerable module, no fixed-version bypass.”

## Recommendations / Next Steps

- Keep the fix at the common cache insertion sink (`fuse_add_dirent_to_cache()`), not only in the ordinary `FUSE_READDIR` parser, because `FUSE_READDIRPLUS` and any future parser that calls `fuse_emit()` share the same sink.
- Add regression coverage for both `FUSE_READDIR` and `FUSE_READDIRPLUS` replies with `namelen=4095` and `FOPEN_CACHE_DIR` enabled.
- Consider adding a defensive WARN/test assertion that every readdir-cache insertion satisfies `FUSE_DIRENT_SIZE(dirent) <= PAGE_SIZE` before copying.
- Audit future FUSE directory-entry emitters for direct calls to `fuse_add_dirent_to_cache()` or equivalent page-cache serialization logic.

## Additional Notes

- **Idempotency confirmation:** `bundle/vuln_variant/reproduction_steps.sh` was executed twice consecutively. Both runs completed cleanly, reproduced the `FUSE_READDIRPLUS` alternate trigger on the vulnerable module, and showed the fixed module blocking it.
- **Threat model scope:** The Linux kernel documentation treats exploitable kernel bugs as security bugs and routes them to `security@kernel.org`. This variant crosses the same local kernel trust boundary as the parent: an unprivileged local process controls a FUSE server response that is consumed by privileged kernel code.
- **Conclusion:** This is a validated alternate vulnerable-version trigger, not a patch bypass. The tested sink-level fix is complete for the discovered `FUSE_READDIRPLUS` path.
