β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0825

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0825 Β· 17 files
FileTypeDescriptionSize
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
README.md readme Human reproduction guide
↓ download 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
VERDICT.md verdict Full narrative analysis
↓ download raw

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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Jul 11 01:40:12 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC

Confirmed kernel references

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.