# REPRO-2026-00221: Linux kernel FUSE readdir cache out-of-bounds write ## Summary Status: published Severity: high Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00221 CVE: CVE-2026-31694 ## Package Name: Unknown Ecosystem: Unknown Affected: Unknown Fixed: Unknown ## Root Cause # 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. ## Reproduction Details Reproduced: 2026-07-03T18:44:47.877Z Duration: 3785 seconds Tool calls: 393 Turns: Unknown Handoffs: 3 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00221 pruva-verify CVE-2026-31694 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00221&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00221/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-31694 - Source: https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git ## Artifacts - bundle/repro/fix.patch (patch, 422 bytes) - bundle/repro/rca_report.md (analysis, 9938 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 15977 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 10917 bytes) - bundle/vuln_variant/rca_report.md (analysis, 10781 bytes) - bundle/ticket.md (ticket, 1303 bytes) - bundle/ticket.json (other, 2519 bytes) - bundle/repro/build_initramfs.sh (other, 1970 bytes) - bundle/repro/fuse-vuln.ko (other, 766792 bytes) - bundle/repro/fuse-fixed.ko (other, 766728 bytes) - bundle/repro/fuse_evil.c (other, 19507 bytes) - bundle/repro/fuse_evil (other, 855576 bytes) - bundle/repro/validation_verdict.json (other, 876 bytes) - bundle/repro/fuse_passwd_lpe.c (other, 17410 bytes) - bundle/repro/build_lpe_initramfs.sh (other, 1896 bytes) - bundle/repro/fuse-nokasan-vuln.ko (other, 409272 bytes) - bundle/repro/fuse-nokasan-fixed.ko (other, 409448 bytes) - bundle/repro/runtime_manifest.json (other, 1060 bytes) - bundle/repro/passwd.seed (other, 71 bytes) - bundle/repro/init.rootfs (other, 925 bytes) - bundle/logs/reproduction_steps.log (log, 10327 bytes) - bundle/logs/qemu_vuln_attempt1.log (log, 30870 bytes) - bundle/logs/qemu_vuln_attempt2.log (log, 30777 bytes) - bundle/logs/qemu_vuln_attempt3.log (log, 31150 bytes) - bundle/logs/qemu_vuln_attempt4.log (log, 30957 bytes) - bundle/logs/qemu_vuln_attempt5.log (log, 30848 bytes) - bundle/logs/qemu_fixed_attempt1.log (log, 25747 bytes) - bundle/logs/qemu_fixed_attempt2.log (log, 303 bytes) - bundle/logs/test_lpe_vuln.log (log, 25315 bytes) - bundle/logs/test_lpe_v709.log (log, 30143 bytes) - bundle/logs/test_lpe_v709b.log (log, 29804 bytes) - bundle/logs/test_lpe_kasan_target.log (log, 25761 bytes) - bundle/logs/test_lpe_fixed.log (log, 25621 bytes) - bundle/logs/test_active_rootfs_vuln.log (log, 26228 bytes) - bundle/logs/test_nokasan_vuln.log (log, 25710 bytes) - bundle/logs/qemu_lpe_vuln_attempt1.log (log, 25959 bytes) - bundle/logs/qemu_lpe_vuln_attempt2.log (log, 25955 bytes) - bundle/logs/qemu_lpe_fixed_attempt1.log (log, 25510 bytes) - bundle/logs/qemu_lpe_fixed_attempt2.log (log, 25509 bytes) - bundle/logs/vuln_variant/readdirplus_variant.log (log, 6748 bytes) - bundle/logs/vuln_variant/qemu_readdirplus_vuln.log (log, 27019 bytes) - bundle/logs/vuln_variant/qemu_readdirplus_fixed.log (log, 26411 bytes) - bundle/logs/vuln_variant/fixed_version.txt (other, 1085 bytes) - bundle/logs/vuln_variant/artifact_hashes.txt (other, 494 bytes) - bundle/vuln_variant/fuse_readdirplus_lpe.c (other, 19054 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1081 bytes) - bundle/vuln_variant/fuse-readdirplus-vuln.ko (other, 409272 bytes) - bundle/vuln_variant/fuse-readdirplus-fixed.ko (other, 409448 bytes) - bundle/vuln_variant/passwd.seed (other, 71 bytes) - bundle/vuln_variant/init.readdirplus (other, 995 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 6899 bytes) - bundle/vuln_variant/variant_manifest.json (other, 4174 bytes) - bundle/vuln_variant/validation_verdict.json (other, 2490 bytes) - bundle/vuln_variant/source_identity.json (other, 2085 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 1307 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00221 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00221/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00221 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev