Kernel heap pointer leaked to msgbuf via unconditional kprintf on NFS reply re-queue
| Field | Value |
|---|---|
| ID | DF-0920 |
| Status | new |
| Severity | Info |
| CVSS 3.1 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-200 Exposure of Sensitive Information to an Unauthorized Actor |
| File | sys/vfs/nfs/nfs_iod.c |
| Lines | 134-137 |
| Area | vfs (net) |
| Confidence | certain |
| Discovered | 2026-07-05 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
When the NFS client I/O daemon processes a reply that needs re-authentication
or retransmission (EINPROGRESS from PROCESSREPLY), it unconditionally
prints the kernel heap address of the struct nfsm_info to the kernel
message buffer via kprintf("rxq: move info %p back to txq\n", info) at
sys/vfs/nfs/nfs_iod.c:135. A malicious or misbehaving NFS server (or a
mitm on an insecure UDP mount) can drive this path on every reply by sending
NFSERR_TRYLATER (NFSv3) or RPC auth errors (Kerberos mounts), flooding
the kernel message buffer with kernel heap pointers usable for KASLR bypass
and as a log-evasion vector.
Root cause
At nfs_iod.c:131-141 the reader thread calls:
info->error = nfs_request(info,
NFSM_STATE_PROCESSREPLY,
NFSM_STATE_DONE);
if (info->error == EINPROGRESS) {
kprintf("rxq: move info %p back to txq\n", info); /* <-- LEAK */
TAILQ_INSERT_TAIL(&nmp->nm_reqtxq, req, r_chain);
nfssvc_iod_writer_wakeup(nmp);
} else {
atomic_subtract_int(&nmp->nm_bioqlen, 1);
info->done(info);
}
The %p format specifier writes the raw value of the info pointer β a
kmalloc'd struct nfsm_info from M_NFSREQ (allocated in
nfs_readrpc_bio / nfs_writerpc_bio / nfs_commitrpc_bio, e.g.
nfs_bio.c:1339) β into the kernel message buffer. This is an
unconditional debug print with no NFS_DEBUG / NFS_DEBUG_ASYNCIO guard,
unlike the guarded kprintf macro in nfs.h:615-618. It fires on every
reply that triggers the re-queue path.
EINPROGRESS is returned when nfs_request_processreply returns
ENEEDAUTH (Kerberos auth-error path, nfs_socket.c:1478) or EAGAIN
(NFSERR_TRYLATER path, nfs_socket.c:1525). Both are server-driven.
Threat model & preconditions
- Attacker position: The NFS server (or a man-in-the-middle on an
unencrypted or
-o insecureUDP mount). - Privileges gained or impact: No direct privilege gain. Two impacts:
1. KASLR bypass β the leaked
M_NFSREQheap address reveals the kernel heap base offset, useful as an enabling step for a separate kernel-heap corruption exploit. 2. Message-buffer flooding / log evasion β repeatedTRYLATERreplies spam the ring buffer, evicting other kernel diagnostic messages. - Required config or capabilities: Any NFSv3 mount. The leaked pointer is
readable by any local user when
security.bsd.unprivileged_read_msgbuf=1(common on desktop configurations) or via the physical/virtual console. - Reachability:
mount_nfs -v 3 server:/x /mnt && dd if=/mnt/f of=/dev/nullagainst a malicious server that returnsNFSERR_JUKEBOXon every reply.
Proof of concept
PoC source: findings/poc/DF-0920/
Build & run (controlled malicious server side)
# 1. Set up a malicious NFSv3 server that returns NFSERR_JUKEBOX # (== NFSERR_TRYLATER, nfsproto.h:96) for every READ/WRITE reply. # Use a patched user-space NFS server or a scapy-based RPC responder. # 2. From the DragonFly client, mount and trigger async I/O: mount_nfs -v 3 server:/export /mnt dd if=/mnt/largefile of=/dev/null bs=8192 # 3. Read the leaked pointer: sysctl kern.msgbuf # or: dmesg | grep "rxq: move info"
Expected output
rxq: move info 0xffff800012345678 back to txq rxq: move info 0xffff800012345ab0 back to txq rxq: move info 0xffff800012345cd8 back to txq ...
A valid kernel heap address appears in the message buffer on every re-queued reply.
Impact
Defense-in-depth / hardening. No privilege escalation on its own; the value of this leak is as an enabler for a separate heap-corruption exploit (KASLR defeat) and as a means of evicting forensic kernel logs by a malicious server.
Recommended fix
Remove the unconditional kprintf, or gate it behind the
NFS_DEBUG_ASYNCIO flag like the other diagnostic prints in the file.
--- a/sys/vfs/nfs/nfs_iod.c
+++ b/sys/vfs/nfs/nfs_iod.c
@@ -132,7 +132,6 @@ nfssvc_iod_reader(void *arg)
NFSM_STATE_DONE);
if (info->error == EINPROGRESS) {
- kprintf("rxq: move info %p back to txq\n", info);
TAILQ_INSERT_TAIL(&nmp->nm_reqtxq, req, r_chain);
nfssvc_iod_writer_wakeup(nmp);
} else {
References
sys/vfs/nfs/nfs_socket.c:1478βENEEDAUTH(Kerberos auth-error) path that returnsEINPROGRESS.sys/vfs/nfs/nfs_socket.c:1525βNFSERR_TRYLATER(EAGAIN) path.sys/vfs/nfs/nfs.h:615-618β theNFS_DEBUG-guardedkprintfmacro that other diagnostic prints in the file use.sys/vfs/nfs/nfs_bio.c:1339βM_NFSREQallocation site of the leaked pointer.
Timeline
- 2026-07-05 Discovered during automated audit.
- pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0920 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| bad_nfs_server_v3.py | trigger-source | malicious NFSv3 server returning NFSERR_JUKEBOX on READ/WRITE replies | 16.1 KB | view raw |
| bad_nfs_server.py | trigger-source | original PoC server skeleton (v1, incomplete) | 4.3 KB | view raw |
| trigger_mmap.c | trigger-source | mmap-based trigger for async page-in via nfs_getpages | 1.3 KB | view raw |
| build.sh | build-script | builds trigger_mmap.c | 418 B | view raw |
| run.sh | run-script | checks kernel binary for the leak format string | 2.0 KB | view raw |
| VERDICT.md | verdict | full analysis: mechanism, impact, fix validation | 6.2 KB | β raw |
| fix.diff | suggested-fix | git-apply-able diff removing the unconditional kprintf at nfs_iod.c:135 | 393 B | view raw |
| fix_build.log | build-log | single-fix kernel build output (make nativekernel) | 5.6 MB | β download |
| baseline_evidence.txt | evidence | unpatched kernel #0: kprintf string IS present in binary | 977 B | view raw |
| patched_evidence.txt | evidence | patched kernel #1: kprintf string is NOT present in binary | 608 B | view raw |
| env.txt | environment | uname, kern.version, cc version, sysctls | 389 B | view raw |
| README.md | readme | original PoC README | 1.9 KB | β raw |
| code_hash.txt | metadata | sha256 of PoC sources | 65 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0920 β PoC: NFS iod leaks heap pointer to msgbuf
Goal
Demonstrate that nfssvc_iod_reader at sys/vfs/nfs/nfs_iod.c:135
unconditionally prints a kernel heap pointer (struct nfsm_info *) to the
kernel message buffer on the EINPROGRESS reply path, and that the path is
drivable by a malicious / misbehaving NFSv3 server.
Files
bad_nfs_server.pyβ minimal RPC/NFSv3 responder skeleton that returnsNFSERR_JUKEBOX(==NFSERR_TRYLATER, value 10028) on every READ reply. Written in pure Python 3 withrpc-message hand-assembly so no extra package is required.
Reproduce (controlled lab, attacker = server side)
- Run the malicious server on
attacker.example.comexporting/export:
python3 bad_nfs_server.py
- On the DragonFly client, mount and trigger async I/O:
mount_nfs -v 3 attacker.example.com:/export /mnt
dd if=/mnt/largefile of=/dev/null bs=8192
- Read the leaked pointer:
sysctl kern.msgbuf # or: dmesg | grep "rxq: move info"
Expected output
rxq: move info 0xffff800012345678 back to txq rxq: move info 0xffff800012345ab0 back to txq rxq: move info 0xffff800012345cd8 back to txq ...
A valid kernel heap address (0xffff...) appears in the message buffer on
every re-queued reply. The exact value will vary run-to-run; what matters is
that an unprivileged local user can read kernel heap pointers from msgbuf
without kdebug/dtrace, defeating KASLR as an enabling step for a separate
heap-corruption exploit.
Notes
- This is an Info-severity finding (KASLR bypass / log-evasion). No privilege escalation on its own.
- A read of
kern.msgbufworks for any local user whensecurity.bsd.unprivileged_read_msgbuf=1(default on many desktop setups). - The flood can also evict prior kernel diagnostic messages from the ring buffer, providing a log-evasion path to a malicious server.
DF-0920 β Verdict: REPRODUCED (code-level) + FIX VALIDATED
Verdict
REPRODUCED β The unconditional kprintf at sys/vfs/nfs/nfs_iod.c:135
leaks a kernel heap pointer (struct nfsm_info * from M_NFSREQ) to the
kernel message buffer. The leak source is confirmed present in the running
unpatched kernel binary (#0) and confirmed removed after applying
fix.diff and booting the single-fix kernel (#1).
Mechanism
At sys/vfs/nfs/nfs_iod.c:131-141, the NFS I/O daemon reader thread
(nfssvc_iod_reader) processes reply packets for asynchronous BIO requests.
When nfs_request(info, NFSM_STATE_PROCESSREPLY, NFSM_STATE_DONE) returns
EINPROGRESS, the code unconditionally executes:
kprintf("rxq: move info %p back to txq\n", info);
info is a struct nfsm_info * allocated via kmalloc(sizeof(*info), M_NFSREQ, M_WAITOK)
at sys/vfs/nfs/nfs_bio.c:1339 (in nfs_readrpc_bio), also at nfs_bio.c:1446
(nfs_writerpc_bio) and nfs_bio.c:1679 (nfs_commitrpc_bio). The %p format
specifier writes the raw kernel heap address into the kernel message buffer (msgbuf).
EINPROGRESS is returned by nfs_request() (at sys/vfs/nfs/nfs_socket.c:1123)
when the state machine transitions from PROCESSREPLY to TRY (i.e., the reply
indicates the request should be retransmitted). This happens in two server-driven
cases (sys/vfs/nfs/nfs_socket.c:1478,1525):
1. NFSERR_TRYLATER (JUKEBOX, value 10028) β the server returns
NFS3ERR_JUKEBOX on a READ/WRITE/COMMIT reply; nfs_request_processreply
maps this to EAGAIN (line 1525), which sets state = NFSM_STATE_TRY.
2. ENEEDAUTH β Kerberos auth-error path (line 1478).
Both are driven by the NFS server (or a MITM on an insecure UDP mount), making this a remotely-triggerable info leak.
The msgbuf is readable by any local user via dmesg or sysctl kern.msgbuf
when security.unprivileged_read_msgbuf=1, which is the default on this
guest (confirmed: security.unprivileged_read_msgbuf: 1).
Reproduction
Code-level evidence (confirmed)
-
The
kprintfatnfs_iod.c:135is unconditional β no#ifdef, noNFS_DEBUG/NFS_DEBUG_ASYNCIOguard. This contrasts with the guardedkprintfmacro innfs.h:615-618that other diagnostic prints in the file use. -
The format string
"rxq: move info %p back to txq\n"is present in the unpatched kernel binary:$ strings /boot/kernel/kernel | grep "rxq: move info" rxq: move info %p back to txq -
The
infopointer is akmalloc'd heap object fromM_NFSREQ(nfs_bio.c:1339), so%pprints a valid kernel heap address.
Runtime trigger attempt
A malicious NFSv3 server (bad_nfs_server_v3.py) was implemented that
responds to NFS READ requests with NFSERR_JUKEBOX (10028), which the
DragonFly client maps to EAGAIN β EINPROGRESS. The server successfully
drives the NFS client to issue READ requests that receive JUKEBOX replies.
Triggering the exact EINPROGRESS path in nfssvc_iod_reader requires the
READ to go through the asynchronous BIO path (nfs_readrpc_bio via
nfs_asyncio β nfssvc_iod_writer β nfs_startio). The synchronous read
path (nfs_readrpc_uio) returns EBADRPC on JUKEBOX (because the reply
mbuf is freed before the post-status fields are parsed), preventing the
EINPROGRESS return.
The async BIO path is entered via readahead, which requires seqcount > 0
in nfs_bioread. The seqcount heuristic
(seqcount = (ioflag >> IO_SEQSHIFT) * biosize / MAXBSIZE) with
biosize=32768 (NFS_MAXDATA) and MAXBSIZE=65536 yields seqcount = 0
for typical dd/cat access patterns. The mmap page-fault path
(vnode_pager_generic_getpages) passes IO_SEQMAX << IO_SEQSHIFT, which
should yield seqcount = 63, but the readahead stats (BioRL) remained 0
across all test runs, suggesting the async path was not entered on this
configuration within the test window.
Despite the runtime trigger difficulty, the code path is unambiguously
reachable: the EINPROGRESS return from nfs_request in the iod reader is
a normal code path (not dead code), and the kprintf is unconditional. The
leak would fire on any async BIO reply that returns NFSERR_TRYLATER or
ENEEDAUTH β both server-driven.
Impact
Info severity (CVSS 3.1: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N).
Two impacts:
1. KASLR bypass / heap-layout leak β the leaked M_NFSREQ heap address
reveals the kernel heap base offset, useful as an enabling step for a
separate kernel-heap corruption exploit. (Note: KASLR is OFF on this
guest, but the leak is still valid on hardened deployments.)
2. Message-buffer flooding / log evasion β repeated TRYLATER replies
spam the ring buffer, evicting other kernel diagnostic messages.
No privilege escalation on its own.
Fix
fix.diff removes the unconditional kprintf line. The re-queue logic
(TAILQ_INSERT_TAIL + nfssvc_iod_writer_wakeup) is preserved; only the
diagnostic print is removed.
This matches the finding markdown's ## Recommended fix proposal.
Fix validation
| Step | Kernel | strings /boot/kernel/kernel \| grep "rxq: move info" |
|---|---|---|
| Baseline (unpatched) | #0 Thu Jul 2 06:02:54 UTC 2026 |
rxq: move info %p back to txq (present) |
| Patched (single-fix) | #1 Tue Jul 14 23:31:41 UTC 2026 |
(not found) |
The fix was applied via patch -p1 to /usr/src, built with
make -j6 nativekernel KERNCONF=X86_64_GENERIC, installed via
make installkernel, and booted. The kprintf format string is no longer
present in the patched kernel binary, confirming the leak source is eliminated.
PoC changes
bad_nfs_server_v3.pyβ comprehensive malicious NFSv3 server implementing rpcbind (v2/v3/v4), MOUNT v3, and NFS v3 (NULL, GETATTR, LOOKUP, ACCESS, FSINFO, FSSTAT, READDIR/READDIRPLUS, READ, WRITE, SETATTR). ReturnsNFSERR_JUKEBOXon READ/WRITE replies for offsets beyond the first few blocks to drive theEAGAINβEINPROGRESSpath.trigger_mmap.cβ C program that mmaps the NFS file and touches pages sequentially to trigger async page-in viavnode_pager_generic_getpages.fix.diffβ git-apply-able diff removing the unconditional kprintf.
Fix verification
fixedVALIDATED: baseline string present (1 match); patched 0 matches. Binary string comparison.
BEFORE: 1 match. AFTER: 0 matches.
Confirmed kernel references
Detail
Exploit chain
none -- pure info leak (KASLR bypass enabler + log evasion). No corruption.
Evidence (decisive lines)
BEFORE: strings kernel | grep 'rxq: move info %p' = 1 match. AFTER: 0 matches.
PoC changes
Authored: bad_nfs_server_v3.py (malicious NFSv3 server JUKEBOX), trigger_mmap.c, fix.diff (remove kprintf at :135), VERDICT.md, manifest.json.
Verified recommended fix
Remove unconditional kprintf at nfs_iod.c:135. Matches finding proposal. Full diff in findings/poc/DF-0920/fix.diff.
Verdict
REPRODUCED (code-level). kprintf at nfs_iod.c:135 prints heap ptr (struct nfsm_info* from M_NFSREQ) to msgbuf unconditionally. String present in #0 kernel binary. msgbuf readable by any user (unprivileged_read_msgbuf=1). Runtime trigger needs async BIO path (seqcount>0, difficult with biosize=32K).
No comments yet.