nfs_getnickauth post-increment bug: auth_str points 4 bytes into 8-byte kmalloc β OOB heap read into RPC request + kfree of non-base pointer
Summary
nfs_syscalls.c:1115 nickp=kmalloc(2*NFSX_UNSIGNED=8). :1116 *nickp++=txdr_unsigned(RPCAKN_NICKNAME) β writes P[0..3] nickp advances to P+4. :1117 *nickp=txdr_unsigned(nickname) writes P[4..7] nickp stays P+4. :1118 *auth_str=(char*)nickp β BUG: returns P+4 not P with :1119 auth_len=8. nfsm_rpchead (nfsm_subs.c:195-211) bcopy(auth_len=8 bytes from P+4) reads P[4..7] valid + P[8..11] 4 bytes OOB kernel heap into RPC request mbuf transmitted to NFS server = info leak. nfs_socket.c:1237-1238 kfree(auth_str==P+4) non-base pointer = slab corruption/panic. NFSMNT_KERB mounted client any unprivileged user triggers on every RPC after nickname established. Fix: nickp[0]/nickp[1] no post-increment *auth_str=nickp base.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0825 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df0825_harness.c | trigger-source | KLD module that calls real nfs_getnickauth() with planted nickname to prove the off-by-4 | 5.4 KB | view raw |
| Makefile | build-config | KLD module Makefile | 355 B | β download |
| build.sh | build-script | Builds the KLD module in-guest | 598 B | view raw |
| run.sh | run-script | Loads the module and shows output | 474 B | view raw |
| fix.diff | suggested-fix | Replace *nickp++/*nickp with nickp[0]/nickp[1] to preserve base pointer | 533 B | view raw |
| build.log | build-log | Final successful module build (baseline) | 996 B | view raw |
| run.log | run-log | Baseline run #1 (unpatched, BUG CONFIRMED) | 7.4 KB | view raw |
| run.2.log | run-log | Baseline runs 2 & 3 (consistent off-by-4) | 2.0 KB | view raw |
| fix_build.log | build-log | Full fixed-kernel nativekernel build log | 5.6 MB | β download |
| fix_run.log | run-log | Fixed-kernel run (bug GONE, auth_str aligned) | 1002 B | view raw |
| panic.txt | panic-signature | Deferred slab corruption panic from kfree(P+4) | 386 B | view raw |
| env.txt | environment | Guest uname, cc version, INVARIANTS config | 396 B | view raw |
| VERDICT.md | verdict | Full narrative analysis | 5.8 KB | β raw |
| README.md | readme | Human reproduction guide | 2.4 KB | β raw |
| manifest.json | manifest | This file | 3.3 KB | 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-0825 β nfs_getnickauth post-increment bug
Finding: nfs_getnickauth (sys/vfs/nfs/nfs_syscalls.c:1115-1119) has a
post-increment bug that leaves auth_str pointing 4 bytes into the 8-byte
kmalloc'd auth buffer instead of at its start.
How to reproduce
./build.sh # builds the df0825_harness.ko KLD module in-guest
./run.sh # loads the module; nfs_getnickauth is called with a planted nickname
Expected output
Unpatched (bug present, kernel #0):
DF-0825: auth_str = 0xfffff8008d680c84 β off-by-4 (ends in 4) DF-0825: auth_str[0..3] = 0xdeadbeef β nickname, NOT RPCAKN_NICKNAME DF-0825: auth_str[4..7] = 0x00000000 β OOB heap DF-0825: *** BUG CONFIRMED ***
After kfree(auth_str), a deferred slab panic fires within ~30s:
panic: assertion "(((intptr_t)chunk ^ (intptr_t)z) & ZoneMask) == 0"
failed in chunk_mark_free at kern_slaballoc.c:1675
Patched (fix applied, kernel #1):
DF-0825: auth_str = 0xfffff8008d382270 β base (ends in 0) DF-0825: auth_str[0..3] = 0x00000001 β RPCAKN_NICKNAME β DF-0825: auth_str[4..7] = 0xdeadbeef β nickname β DF-0825: OK β auth_str points at allocation base (bug fixed)
No panic; guest stays alive.
Harness design
The KLD module (df0825_harness.c) constructs a minimal fake struct nfsmount
with a nickname entry (uid 1001, nickname 0xDEADBEEF) and calls the real
in-kernel nfs_getnickauth() function. It inspects where auth_str points and
demonstrates:
1. auth_str is 4 bytes past the allocation base (the bug)
2. auth_str[0] = nickname instead of RPCAKN_NICKNAME
3. auth_str[4..7] reads 4 bytes OOB past the allocation (info leak)
4. kfree(auth_str) frees a non-base pointer β deferred slab corruption panic
Files
| File | Description |
|---|---|
df0825_harness.c |
KLD module harness source |
Makefile |
Module build file |
build.sh |
Builds the module in-guest |
run.sh |
Loads the module and shows output |
fix.diff |
Standalone git-apply-able fix |
VERDICT.md |
Full narrative analysis |
build.log |
Final successful module build (baseline) |
run.log |
Baseline run (unpatched, bug confirmed) |
run.2.log |
Stress run 2 |
fix_build.log |
Full fixed-kernel build log |
fix_run.log |
Fixed-kernel run (bug gone) |
panic.txt |
Slab corruption panic signature |
env.txt |
Guest environment |
manifest.json |
Machine-readable catalog |
DF-0825 β nfs_getnickauth post-increment bug
Verdict: REPRODUCED β confirmed OOB heap read + deferred slab-corruption panic; fix VALIDATED
Bug mechanism (confirmed by source trace + live kernel harness)
Root cause: nfs_getnickauth() in sys/vfs/nfs/nfs_syscalls.c:1115-1119 has a
classic post-increment bug. The code allocates an 8-byte buffer and writes two
4-byte XDR words using *nickp++, which advances nickp past the first word.
It then assigns the post-increment pointer (not the base) to *auth_str:
nickp = (u_int32_t *)kmalloc(2 * NFSX_UNSIGNED, M_TEMP, M_WAITOK); // P = base, 8 bytes
*nickp++ = txdr_unsigned(RPCAKN_NICKNAME); // writes P[0..3], nickp β P+4
*nickp = txdr_unsigned(nuidp->nu_nickname); // writes P[4..7], nickp stays P+4
*auth_str = (char *)nickp; // BUG: auth_str = P+4, not P
*auth_len = 2 * NFSX_UNSIGNED; // auth_len = 8
Consequence 1 β 4-byte OOB heap read (info leak): The caller
nfs_request() (sys/vfs/nfs/nfs_socket.c:1233-1235) passes auth_str and
auth_len=8 to nfsm_rpchead(). In the RPCAUTH_KERB4 branch
(sys/vfs/nfs/nfsm_subs.c:195-211), bcopy(auth_str, info.bpos, i) copies 8
bytes starting at P+4 β i.e. P[4..7] (valid, the nickname) + P[8..11] (4 bytes
past the 8-byte allocation = OOB kernel heap). These 4 OOB bytes are placed
into the NFS RPC credential mbuf and transmitted to the NFS server.
Consequence 2 β kfree of non-base pointer β slab corruption β panic: After
building the RPC header, nfs_request() at nfs_socket.c:1237-1238 does
kfree(auth_str, M_TEMP) where auth_str == P+4. On the default GENERIC kernel
(INVARIANTS ON), this corrupts the slab allocator's chunk tracking. The
corruption is detected deferred by the periodic slab_cleanup() GC timer,
which calls chunk_mark_free() and trips:
panic: assertion "(((intptr_t)chunk ^ (intptr_t)z) & ZoneMask) == 0"
failed in chunk_mark_free at kern_slaballoc.c:1675
Live reproduction (deterministic kernel harness)
A KLD module (df0825_harness.c) constructs a minimal struct nfsmount with a
planted nickname entry (uid 1001, nickname 0xDEADBEEF) and calls the real
nfs_getnickauth() (kernel symbol 0xffffffff80809570). It then inspects
where auth_str actually points.
Unpatched #0 kernel β 3 runs, all show the bug:
DF-0825: auth_str = 0xfffff8008d680c84 β last nibble 4 = off-by-4 from base DF-0825: auth_str[0..3] = 0xdeadbeef β nickname, NOT RPCAKN_NICKNAME DF-0825: auth_str[4..7] = 0x00000000 β OOB heap (4 bytes past alloc) DF-0825: *** BUG CONFIRMED ***
The auth_str pointer consistently ends in nibble 4 across all 3 runs
(0x...c84, 0x...6a4, 0x...914) β proving it is always base+4, never the base.
After the harness runs kfree(auth_str) (= kfree(P+4)), the deferred slab
panic fires:
panic: assertion "(((intptr_t)chunk ^ (intptr_t)z) & ZoneMask) == 0"
failed in chunk_mark_free at kern_slaballoc.c:1675
chunk_mark_free() β slab_cleanup() β slotimer_callback() β softclock_handler()
Patched #1 kernel (fix applied) β bug is GONE:
DF-0825: auth_str = 0xfffff8008d382270 β last nibble 0 = allocation base DF-0825: auth_str[0..3] = 0x00000001 β RPCAKN_NICKNAME β DF-0825: auth_str[4..7] = 0xdeadbeef β nickname β DF-0825: OK β auth_str points at allocation base (bug fixed)
Guest stays alive after 15+ seconds; no deferred slab panic.
Reachability assessment
nfs_getnickauth is reached when an NFS client mount has NFSMNT_KERB set
(mount -o kerb) AND a nickname entry exists for the calling uid. The nickname
is established via nfs_savenickauth() (nfs_socket.c:1499-1501) after the
server responds to an RPC with a RPCAUTH_KERB4 verifier.
Important: NFSKERB (the compile-time Kerberos v4 switch) is never
defined in any shipped DragonFlyBSD kernel config (grep -rn "define NFSKERB"
sys/ β empty). The code compiles with stub 2-byte key typedefs and no-op
encryption. The #ifdef NFSKERB / XXX / #endif block at nfs_syscalls.c:1136
contains a literal XXX placeholder, confirming this path was never fully
implemented for modern use.
However, the buggy code at lines 1115-1119 is NOT behind #ifdef NFSKERB β
it is unconditionally compiled into every GENERIC kernel. And nfs_getnickauth
IS called at runtime whenever an NFSMNT_KERB mount is active. The function is
a live kernel symbol (nm confirms T nfs_getnickauth at 0xffffffff80809570).
An unprivileged user on a client with a -o kerb NFS mount, after the nickname
exchange completes, triggers this bug on every subsequent NFS RPC.
Impact
- OOB heap read: 4 bytes of uninitialized/freed kernel heap memory leaked into the NFS RPC auth credential, potentially transmitted to a remote NFS server (info leak).
- Corrupted RPC auth: The credential data sent to the server is shifted by 4 bytes (wrong auth type/sequence β auth failures or misparse).
- Slab corruption β panic:
kfree(P+4)corrupts the slab allocator; under INVARIANTS (default GENERIC) this is detected by the periodic slab GC and causes a kernel panic (local DoS). - No uid=0 escalation path: This is an OOB read + data corruption primitive, not a write primitive. The 4 OOB bytes are read (leaked), not written through. There is no attacker-controlled write here β no escalation chain.
Fix
fix.diff replaces the post-increment writes with array-index writes that
preserve the base pointer:
- *nickp++ = txdr_unsigned(RPCAKN_NICKNAME);
- *nickp = txdr_unsigned(nuidp->nu_nickname);
+ nickp[0] = txdr_unsigned(RPCAKN_NICKNAME);
+ nickp[1] = txdr_unsigned(nuidp->nu_nickname);
*auth_str = (char *)nickp; // now correctly = base
Validated on a single-fix kernel (#1): the off-by-4 is gone, auth_str is properly aligned, auth_str[0] = RPCAKN_NICKNAME, and no panic occurs.
Fix verification
fixedVALIDATED: the fix.diff (nickp[0]/nickp[1] replacing nickp++/nickp) was applied to sys/vfs/nfs/nfs_syscalls.c, a single-fix kernel was built (make -j6 nativekernel, rc=0, kernel sha256 f45ef964...), installed as #1, and rebooted. The SAME harness on the unpatched #0 baseline showed auth_str off-by-4 (0x...c84, auth_str[0]=0xdeadbeef nickname) and a deferred slab panic. On the patched #1 kernel auth_str is aligned to base (0x...270, auth_str[0]=0x00000001 RPCAKN_NICKNAME, auth_str[4..7]=0xdeadbeef nickname) and no panic occurs after 15s. Fix closes the bug.
BEFORE (baseline #0): auth_str=0xfffff8008d680c84 [nibble 4=off-by-4], auth_str[0]=0xdeadbeef [nickname, WRONG], auth_str[4..7]=0x00000000 [OOB heap read] -> panic: assertion failed in chunk_mark_free at kern_slaballoc.c:1675. AFTER (fixed #1): auth_str=0xfffff8008d382270 [nibble 0=base], auth_str[0]=0x00000001 [RPCAKN_NICKNAME, CORRECT], auth_str[4..7]=0xdeadbeef [nickname, CORRECT] -> no panic, guest alive after 15s.
Confirmed kernel references
- sys/vfs/nfs/nfs_syscalls.c:1115
- sys/vfs/nfs/nfs_syscalls.c:1116
- sys/vfs/nfs/nfs_syscalls.c:1117
- sys/vfs/nfs/nfs_syscalls.c:1118
- sys/vfs/nfs/nfs_syscalls.c:1119
- sys/vfs/nfs/nfs_socket.c:1212
- sys/vfs/nfs/nfs_socket.c:1237
- sys/vfs/nfs/nfs_socket.c:1238
- sys/vfs/nfs/nfsm_subs.c:195
- sys/vfs/nfs/nfsm_subs.c:206
- sys/kern/kern_slaballoc.c:1675
Detail
Exploit chain
none -- this is an OOB read (4 bytes leaked into RPC auth credential) + data corruption primitive, not a write primitive. The 4 OOB bytes are read/leaked, not written through, so there is no slab grooming or pointer-corruption escalation path. The kfree(P+4) slab corruption manifests as a deferred panic (local DoS), not a controllable write. No uid=0 chain derivable from a read-only OOB + offset free.
Evidence (decisive lines)
BASELINE #0 (3 runs, consistent): auth_str=0xfffff8008d680c84 (nibble 4=off-by-4), auth_str[0]=0xdeadbeef (nickname, NOT RPCAKN_NICKNAME=1), auth_str[4..7]=0x00000000 (OOB heap). After kfree(P+4): panic 'assertion (((intptr_t)chunk ^ (intptr_t)z) & ZoneMask) == 0 failed in chunk_mark_free at kern_slaballoc.c:1675' via slab_cleanup->slotimer_callback->softclock_handler. PATCHED #1: auth_str=0xfffff8008d382270 (nibble 0=base), auth_str[0]=0x00000001 (RPCAKN_NICKNAME), auth_str[4..7]=0xdeadbeef (nickname), no panic after 15s.
PoC changes
Authored df0825_harness.c (KLD module that calls the real nfs_getnickauth with a planted nickname to deterministically prove the off-by-4), Makefile (module build with opt_nfs.h generation), build.sh, run.sh, fix.diff (nickp[0]/nickp[1] replacing nickp++/nickp), VERDICT.md, README.md, manifest.json. No pre-existing PoC files existed in findings/poc/DF-0825/ -- all created from scratch.
Verified recommended fix
Replace nickp++ = txdr_unsigned(RPCAKN_NICKNAME) and nickp = txdr_unsigned(nuidp->nu_nickname) with nickp[0] = txdr_unsigned(RPCAKN_NICKNAME) and nickp[1] = txdr_unsigned(nuidp->nu_nickname) at sys/vfs/nfs/nfs_syscalls.c:1116-1117. This preserves nickp as the allocation base so auth_str = (char)nickp at :1118 correctly returns the buffer start. Matches the finding proposal's intent (capture base pointer before increment). Full git-apply-able diff in findings/poc/DF-0825/fix.diff.
Verdict
REPRODUCED. The post-increment bug in nfs_getnickauth (sys/vfs/nfs/nfs_syscalls.c:1116) is confirmed: nickp++ advances nickp to P+4, then auth_str=(char*)nickp at :1118 returns P+4 instead of the kmalloc base P. A KLD harness calling the real nfs_getnickauth (kernel symbol 0xffffffff80809570) with a planted nickname deterministically shows auth_str ending in nibble 4 (off-by-4) across 3 runs, with auth_str[0]=nickname instead of RPCAKN_NICKNAME and auth_str[4..7] reading 4 bytes OOB past the 8-byte allocation. The caller kfree(auth_str==P+4) corrupts the slab; under INVARIANTS (default GENERIC) the periodic slab_cleanup() GC detects it and panics: 'assertion (((intptr_t)chunk ^ (intptr_t)z) & ZoneMask) == 0 failed in chunk_mark_free at kern_slaballoc.c:1675'. The buggy lines 1115-1119 are NOT behind #ifdef NFSKERB (which is never defined in any shipped config) and nfs_getnickauth is a live kernel symbol called from nfs_request when NFSMNT_KERB is set.
No comments yet.