Skip to content

CVE-2026-31694: Verified Repro With Script Download

CVE-2026-31694: Linux kernel FUSE readdir cache out-of-bounds write

CVE-2026-31694 is verified against the affected target. Vulnerability class: Privilege Escalation. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00221.

REPRO-2026-00221 Privilege Escalation Variant found Jul 3, 2026 CVE entry ↗ .txt
Severity
HIGH
CVSS
7.8
Confidence
HIGH
Reproduced in
63m 5s
Tool calls
393
Spend
$18.52
01 · Overview

What Is CVE-2026-31694?

CVE-2026-31694 is a high-severity out-of-bounds write in the Linux kernel's FUSE readdir cache, in fs/fuse/readdir.c, that lets a malicious FUSE server overflow a page-cache page by 24 bytes. Pruva reproduced it (reproduction REPRO-2026-00221).

02 · Severity & CVSS

CVE-2026-31694 Severity & CVSS Score

CVE-2026-31694 is rated high severity, with a CVSS base score of 7.8 out of 10.

HIGH threat level
7.8 / 10 CVSS base

High — serious impact or readily exploitable. Prioritize remediation.

How to Reproduce CVE-2026-31694

$ pruva-verify REPRO-2026-00221
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00221/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-31694

Privilege escalation — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

malicious FUSE server READDIR reply with namelen=4095 and payload root::0:0:x:.:\n#######

Attack chain
  1. uid 1000 FUSE daemon
  2. getdents64
  3. fuse_readdir_uncached
  4. fuse_emit
  5. fuse_add_dirent_to_cache
  6. memcpy oversized dirent into readdir cache page
  7. /etc/passwd page-cache first line changed
Runnable proof: reproduction_steps.sh
Captured evidence: qemu fixed attempt1qemu fixed attempt2qemu lpe fixed attempt1qemu lpe fixed attempt2
Variants tested

A malicious FUSE daemon can trigger the same oversized-dirent readdir-cache overflow through FUSE_READDIRPLUS by returning a direntplus record whose embedded dirent has namelen=4095. The alternate path reproduces on the vulnerable module but is blocked by the tested sink-level reclen > PAGE_SIZE fix, so it is not a fi…

How the agent worked 938 events · 393 tool calls · 1h 3m
1h 3mDuration
393Tool calls
250Reasoning steps
938Events
4Dead-ends
Agent activity over 1h 3m
Support
15
Hypothesis
2
Repro
697
Judge
106
Variant
113
0:0063:05

Root Cause and Exploit Chain for CVE-2026-31694

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.

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.

  • 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:

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:

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:

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:

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:

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:

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.

Variant Analysis & Alternative Triggers for CVE-2026-31694

Versions: 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.

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:

size_t reclen = FUSE_DIRENT_SIZE(dirent);

The tested fixed module adds the equivalent of:

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-88fuse_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-123fuse_emit() calls fuse_add_dirent_to_cache() when FOPEN_CACHE_DIR is set.
  • fs/fuse/readdir.c:293-326parse_dirplusfile() parses FUSE_READDIRPLUS records and invokes fuse_emit() with the embedded struct fuse_dirent.
  • fs/fuse/readdir.c:334-377fuse_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.

  • 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:

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:

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.

CVE-2026-31694 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:001:21
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-31694 · REPRO-20
0:03
0:05
web search
0:06
web search
0:08
web search
0:10
0:26
0:27
0:37
0:37
extract_facts
no facts extracted
0:38
0:38
0:38
supportrepro
0:52
0:54
0:54
0:54
0:55
0:55
0:55
0:58
0:58
1:04
1:04
$ls -la /data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/.pruva/ 2>/dev/null; echo "---KERNELS---"; ls -la /data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/kernels/ 2>/dev/null; ls -la /data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/kernels/*/ 2>/dev/null; echo "---QEMU---"; which qemu-system-x86_64 2>/dev/null; echo "---REPRO---"; cat /data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/repro.c
{"cwd":"/data/pruva/runs/faf1cf12-e2da-4977-9356-d1d6c2a36dfd","duration_ms":11,"exit_code":0,"stderr":"","stdout":"total 0\ndrwxr-xr-x 1 vscode vscode   22 Jul  2 21:46 .\ndrwxr-xr-x 1 vscode vscode 1370 Jul  3 10:05 ..\ndrwxr-xr-x 1 vscode vscode   60 Jul  3 13:05 proof-carry\n---KERNELS---\ntotal 0\ndrwxr-xr-x 1 vscode vscode   50 Jul  3 09:29 .\ndrwxr-xr-x 1 vscode vscode 1370 Jul  3 10:05 ..\ndrwxr-xr-x 1 vscode vscode  100 Jul  3 09:23 bond\ndrwxr-xr-x 1 vscode vscode  210 Jul  3 10:05 bond7rc2\ndrwxr-xr-x 1 vscode vscode  470 Jul  2 21:40 v7.0.10\ndrwxr-xr-x 1 vscode vscode  462 Jul  2 21:40 v7.0.9\n/data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/kernels/bond/:\ntotal 333696\ndrwxr-xr-x 1 vscode vscode       100 Jul  3 09:23 .\ndrwxr-xr-x 1 vscode vscode        50 Jul  3 09:29 ..\n-rw-r--r-- 1 vscode vscode       219 Jul  3 09:12 BUILT_MARKER\n-rw-r--r-- 1 vscode vscode  21840896 Jul  3 09:11 bzImage\n-rw-r--r-- 1 vscode vscode 208920576 Jul  3 09:11 fixed-rootfs.img\n-rw-r--r-- 1 vscode vscode 208969728 Jul  3 09:23 vuln-rootfs.img\n\n/data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/kernels/bond7rc2/:\ntotal 801532\ndrwxr-xr-x 1 vscode vscode   … [truncated]
1:04
1:10
1:10
$echo "=== linux-src version ==="; head -5 /data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/linux-src/Makefile 2>/dev/null; echo "=== linux-src-7rc2 version ==="; head -5 /data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/linux-src-7rc2/Makefile 2>/dev/null; echo "=== proof-carry ==="; find /data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/.pruva/proof-carry -type f 2>/dev/null; echo "=== QEMU check ==="; which qemu-system-x86_64 2>/dev/null || echo "no qemu"; dpkg -l | grep -i qemu 2>/dev/null | head -5; echo "=== KVM ==="; ls -la /dev/kvm 2>/dev/null || echo "no /dev/kvm"
{"cwd":"/data/pruva/runs/faf1cf12-e2da-4977-9356-d1d6c2a36dfd","duration_ms":14,"exit_code":0,"stderr":"","stdout":"=== linux-src version ===\n# SPDX-License-Identifier: GPL-2.0\nVERSION = 6\nPATCHLEVEL = 18\nSUBLEVEL = 18\nEXTRAVERSION =\n=== linux-src-7rc2 version ===\n# SPDX-License-Identifier: GPL-2.0\nVERSION = 7\nPATCHLEVEL = 0\nSUBLEVEL = 0\nEXTRAVERSION = -rc2\n=== proof-carry ===\n/data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/.pruva/proof-carry/latest_confirmed/repro/reproduction_steps.sh\n/data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/.pruva/proof-carry/latest_confirmed/repro/runtime_manifest.json\n/data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/.pruva/proof-carry/latest_confirmed/repro/validation_verdict.json\n/data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/.pruva/proof-carry/latest_confirmed/repro/rca_report.md\n/data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/.pruva/proof-carry/latest_confirmed/logs/reproduction_steps.log\n/data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/.pruva/proof-carry/latest_confirmed/logs/qemu_var_vuln.log\n/data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-… [truncated]
1:10
$echo "=== FUSE readdir.c in linux-src ==="; cat /data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/linux-src/fs/fuse/readdir.c 2>/dev/null | head -200; echo "=== FUSE_NAME_MAX in linux-src ==="; grep -rn "FUSE_NAME_MAX" /data/pruva/project-cache/6cba72b6-b104-4ebf-be7b-01f748e87ccf/linux-src/include/ 2>/dev/null | head -20
0.7s
{"cwd":"/data/pruva/runs/faf1cf12-e2da-4977-9356-d1d6c2a36dfd","duration_ms":684,"exit_code":0,"stderr":"","stdout":"=== FUSE readdir.c in linux-src ===\n/*\n  FUSE: Filesystem in Userspace\n  Copyright (C) 2001-2018  Miklos Szeredi <miklos@szeredi.hu>\n\n  This program can be distributed under the terms of the GNU GPL.\n  See the file COPYING.\n*/\n\n\n#include \"fuse_i.h\"\n#include <linux/iversion.h>\n#include <linux/posix_acl.h>\n#include <linux/pagemap.h>\n#include <linux/highmem.h>\n\nstatic bool fuse_use_readdirplus(struct inode *dir, struct dir_context *ctx)\n{\n\tstruct fuse_conn *fc = get_fuse_conn(dir);\n\tstruct fuse_inode *fi = get_fuse_inode(dir);\n\n\tif (!fc->do_readdirplus)\n\t\treturn false;\n\tif (!fc->readdirplus_auto)\n\t\treturn true;\n\tif (test_and_clear_bit(FUSE_I_ADVISE_RDPLUS, &fi->state))\n\t\treturn true;\n\tif (ctx->pos == 0)\n\t\treturn true;\n\treturn false;\n}\n\nstatic void fuse_add_dirent_to_cache(struct file *file,\n\t\t\t\t     struct fuse_dirent *dirent, loff_t pos)\n{\n\tstruct fuse_inode *fi = get_fuse_inode(file_inode(file));\n\tsize_t reclen = FUSE_DIRENT_SIZE(dirent);\n\tpgoff_t index;\n\tstruct page *page;\n\tloff_t size;\n\tu64 version;\… [truncated]
1:21

Artifacts and Evidence for CVE-2026-31694

Scripts, logs, diffs, and output captured during the reproduction.

bundle/ticket.md1.3 KB
bundle/ticket.json2.5 KB
bundle/repro/build_initramfs.sh1.9 KB
bundle/repro/fix.patch0.4 KB
bundle/repro/fuse-vuln.ko748.8 KB
bundle/repro/fuse-fixed.ko748.8 KB
bundle/repro/fuse_evil.c19.0 KB
bundle/repro/fuse_evil835.5 KB
bundle/repro/validation_verdict.json0.9 KB
bundle/repro/fuse_passwd_lpe.c17.0 KB
bundle/repro/build_lpe_initramfs.sh1.9 KB
bundle/repro/fuse-nokasan-vuln.ko399.7 KB
bundle/repro/fuse-nokasan-fixed.ko399.9 KB
bundle/repro/runtime_manifest.json1.0 KB
bundle/repro/passwd.seed0.1 KB
bundle/repro/init.rootfs0.9 KB
bundle/logs/reproduction_steps.log10.1 KB
bundle/logs/qemu_vuln_attempt1.log30.1 KB
bundle/logs/qemu_vuln_attempt2.log30.1 KB
bundle/logs/qemu_vuln_attempt3.log30.4 KB
bundle/logs/qemu_vuln_attempt4.log30.2 KB
bundle/logs/qemu_vuln_attempt5.log30.1 KB
bundle/logs/qemu_fixed_attempt1.log25.1 KB
bundle/logs/qemu_fixed_attempt2.log0.3 KB
bundle/logs/test_lpe_vuln.log24.7 KB
bundle/logs/test_lpe_v709.log29.4 KB
bundle/logs/test_lpe_v709b.log29.1 KB
bundle/logs/test_lpe_kasan_target.log25.2 KB
bundle/logs/test_lpe_fixed.log25.0 KB
bundle/logs/test_active_rootfs_vuln.log25.6 KB
bundle/logs/test_nokasan_vuln.log25.1 KB
bundle/logs/qemu_lpe_vuln_attempt1.log25.4 KB
bundle/logs/qemu_lpe_vuln_attempt2.log25.3 KB
bundle/logs/qemu_lpe_fixed_attempt1.log24.9 KB
bundle/logs/qemu_lpe_fixed_attempt2.log24.9 KB
bundle/logs/vuln_variant/readdirplus_variant.log6.6 KB
bundle/logs/vuln_variant/qemu_readdirplus_vuln.log26.4 KB
bundle/logs/vuln_variant/qemu_readdirplus_fixed.log25.8 KB
bundle/logs/vuln_variant/fixed_version.txt1.1 KB
bundle/logs/vuln_variant/artifact_hashes.txt0.5 KB
bundle/vuln_variant/fuse_readdirplus_lpe.c18.6 KB
bundle/vuln_variant/runtime_manifest.json1.1 KB
bundle/vuln_variant/fuse-readdirplus-vuln.ko399.7 KB
bundle/vuln_variant/fuse-readdirplus-fixed.ko399.9 KB
bundle/vuln_variant/passwd.seed0.1 KB
bundle/vuln_variant/init.readdirplus1.0 KB
bundle/vuln_variant/patch_analysis.md6.7 KB
bundle/vuln_variant/variant_manifest.json4.1 KB
bundle/vuln_variant/validation_verdict.json2.4 KB
bundle/vuln_variant/source_identity.json2.0 KB
bundle/vuln_variant/root_cause_equivalence.json1.3 KB
bundle/repro/rca_report.md9.7 KB
bundle/repro/reproduction_steps.sh15.6 KB
bundle/vuln_variant/reproduction_steps.sh10.7 KB
bundle/vuln_variant/rca_report.md10.5 KB
08 · How to Fix

How to Fix CVE-2026-31694

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-31694 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-31694

Does CVE-2026-31694 require remote access to exploit?

No. It requires local access to run a malicious FUSE daemon (i.e., mount a FUSE filesystem); it is a local privilege-escalation primitive, not a remotely triggerable vulnerability.

How severe is CVE-2026-31694?

It is rated high severity: a local attacker able to run a malicious FUSE daemon obtains a constrained but attacker-controlled 24-byte overwrite into an adjacent physical page, which can be groomed onto a privileged file's page-cache page for local privilege escalation.

How can I reproduce CVE-2026-31694?

Download the verified script from this page and run it in an isolated environment inside a QEMU guest running an affected kernel (reachable from v6.16 through v7.0-rc). Run a malicious FUSE daemon that returns a FUSE_READDIR entry with namelen=4095, groom the overflow onto the page-cache page backing /etc/passwd as an unprivileged uid 1000 process, and confirm a fixed fuse.ko rejects the oversized dirent and leaves the file unchanged.
11 · References

References for CVE-2026-31694

Authoritative sources for CVE-2026-31694 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.