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

pagertab[] has 7 slots but enum obj_type has 8 values β€” an OBJT_MARKER (7) object dispatched to any pager op indexes one struct pagerops* past the array (latent uncontrolled indirect call; DF-0944 family)

Field Value
ID DF-2866
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:L/A:H
CWE CWE-129 / CWE-822
File sys/vm/vm_pager.c
Lines 152-160 (enum vm_object.h:126; dispatchers vm_pager.h:134/150/168)
Area vm
Confidence likely
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

vm_object.h defines 8 object types (OBJT_MARKER == 7) but pagertab[] provides only 7 entries. Every dispatcher (vm_pager_deallocate and the get_page/put_pages/has_page inlines) indexes pagertab[object->type] with no bounds check. OBJT_MARKER objects are real, in-tree, stack-allocated list-scan markers; all current scan sites check the type before dispatching, so the defect is latent β€” but any path that hands a marker, or any object whose u_char type field is corrupted, to a dispatcher reads one pointer past the array and calls through it. Demonstrated on the stock guest: pagertab[7] reads NULL on this layout and vm_pager_has_page(marker) faults at 0x18; with a non-zero word after the array the same path is a call through an uncontrolled pointer.

Proof of contest

VERIFIED via KLD (synthetic trigger by design β€” no in-tree path dispatches a marker): stage 1 dumps pagertab[0..7] (index 7 = OOB); stage 2 vm.pgtmark_fire=1 β†’ Fatal trap 12 ... VA 0x18 ... movq 0x18(%rax),%rax β†’ panic. Fix (an eighth slot &deadpagerops) validated on rebuilt kernel #1: dispatch returns FALSE, no panic, clean unload.

Validated hunk in findings/poc/DF-2866/fix.diff.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of vm_pager.c (GLM 5.3); KLD panic + fix validation same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2866 Β· 13 files
FileTypeDescriptionSize
README.md β€” 3.1 KB ↓ raw
VERDICT.md β€” 3.1 KB ↓ raw
pgtmark.c β€” 3.1 KB view raw
build.sh β€” 444 B view raw
run.sh β€” 859 B view raw
build.log β€” 5.6 KB view raw
run.log β€” 703 B view raw
run.patched.log β€” 777 B view raw
panic.txt β€” 2.2 KB view raw
fix.diff β€” 3.6 KB view raw
fix_build.log β€” 5.6 MB ↓ download
env.txt β€” 270 B view raw
verdict.json β€” 4.9 KB view raw

DF-2866 β€” pagertab[] has 7 slots but obj_type has 8 values: OBJT_MARKER dispatch reads past the array

sys/vm/vm_pager.c:152-160 + sys/vm/vm_object.h:118-127 + sys/vm/vm_pager.h:128-169.

The bug

enum obj_type { OBJT_DEFAULT, OBJT_SWAP, OBJT_VNODE, OBJT_DEVICE,
                OBJT_MGTDEVICE, OBJT_PHYS, OBJT_DEAD,
                OBJT_MARKER };               /* == 7 */

struct pagerops *pagertab[] = { ... 7 entries, 0..6 ... };

Every dispatcher indexes pagertab[object->type] with no bounds check: vm_pager_deallocate() (vm_pager.c:340) and the vm_pager_get_page/vm_pager_put_pages/vm_pager_has_page inlines (vm_pager.h:134/150/168). An object with type == OBJT_MARKER (7) reads one struct pagerops * past the end of pagertab and calls through it (pgo_haspage at +24).

OBJT_MARKER objects are real, in-tree, kernel-stack-allocated fake objects used as list-scan markers (sys/vm/vm_object.c:1847/1960, sys/vm/swap_pager.c:2171, sys/vm/vm_swapcache.c:220). All current scan sites check type == OBJT_MARKER before dispatching, so the bug is latent β€” but any future (or present, un-audited) path that hands such an object, or any object whose type byte is corrupted, to a pager dispatch gets an uncontrolled indirect call. Same defect family as DF-0944 (vm_fault.c MGTDEVICE NULL page deref).

PoC

pgtmark.c β€” KLD harness, two stages:

  1. kldload prints pagertab[0..7] (index 7 = the OOB slot; on this kernel build it reads NULL β€” whatever the linker placed after the array).
  2. sysctl vm.pgtmark_fire=1 constructs a stack vm_object with type = OBJT_MARKER and calls vm_pager_has_page() β€” exactly what a dispatcher-reaching path would do.
./build.sh
./run.sh

Expected (stock kernel): stage 2 faults loading 0x18(%rax) β€” pagertab[7] == NULL -> pgo_haspage β€” and panics. With the fix (OBJT_MARKER slot = &deadpagerops) it returns FALSE and prints NO PANIC.

Observed (stock kernel, run.log + panic.txt)

PGTMARK: pagertab[7] = 0   <-- OOB SLOT
PGTMARK: dispatching OBJT_MARKER(7) object via vm_pager_has_page(); pagertab[7] = 0
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x18
instruction pointer = pgtmark_fire_sysctl.part.0+0x53:  movq 0x18(%rax),%rax

NULL on this link layout β†’ clean fault/panic. A layout where a non-zero .data word follows the array yields a call through an attacker-uncontrolled pointer instead β€” uncontrolled dispatch either way.

The trigger is synthetic (KLD-constructed marker) because no in-tree path currently reaches the dispatch with a marker β€” that is what "latent" means here; the PoC proves the dispatch primitive and the missing bounds, not an existing attacker path.

Fix

fix.diff (hunk 1): give pagertab an eighth slot, &deadpagerops /* OBJT_MARKER */, making every dispatch through a marker-object type land in the dead pager (returns FAIL/AGAIN/FALSE) instead of off the end of the array. (Hunks 2-6 of the same diff are the DF-2865 cmpxchg reservation fix β€” the two were validated on one combined kernel build.)

VERDICT.md
↓ download raw

DF-2866 β€” VERDICT

Status: reproduced (dispatch primitive, stock kernel) / fixed (patched kernel). Impact: panic (demonstrated: uncontrolled indirect call through an out-of-bounds pagerops *). Latent in-tree reachability β€” no current path dispatches a marker; the PoC constructs one, proving the missing bounds check is real and what it does when hit.

What was run

KLD harness pgtmark.c on the stock INVARIANTS guest kernel (see ../DF-2865/env.txt).

Stage 1 β€” the OOB slot itself (run.log)

PGTMARK: &pagertab=0xffffffff81120fc0 OBJT_MARKER=7 (pagertab has 7 legal slots 0..6)
PGTMARK: pagertab[0..6] = <7 valid pagerops pointers>
PGTMARK: pagertab[7] = 0   <-- OOB SLOT

pagertab[7] is not a pagerops β€” it is whatever the linker placed after the array (zero on this build).

Stage 2 β€” dispatching an OBJT_MARKER object (run.log + panic.txt)

PGTMARK: dispatching OBJT_MARKER(7) object via vm_pager_has_page(); pagertab[7] = 0
Fatal user address access from kernel mode from sysctl ...
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x18
instruction pointer = pgtmark_fire_sysctl.part.0+0x53:  movq 0x18(%rax),%rax

The inlined vm_pager_has_page() (vm_pager.h:168) loaded pagertab[7] into %rax and dereferenced +0x18 (pgo_haspage) β€” kernel-mode fault at address 0x18, guest down at db>. With a non-zero word after the array (different link layout) the same path is a call through an uncontrolled pointer instead of a NULL fault β€” the bounds check is missing either way.

Honesty notes

  • The trigger is synthetic: a KLD builds a stack vm_object with type = OBJT_MARKER and calls vm_pager_has_page(). All three in-tree marker scan sites (sys/vm/vm_object.c:1847,1960; sys/vm/swap_pager.c:2180; sys/vm/vm_swapcache.c:728) currently check for markers before operating, so nothing in-tree reaches the dispatch with one today. The finding is a latent OOB-dispatch hazard (the DF-0944 family), rated Low.
  • The demonstrated primitive is nevertheless exact: 8-value enum, 7-entry table, unbounded index β€” any marker (or type-corrupted object) reaching any of the four dispatchers panics or worse.

Patched kernel (fix.diff hunk 1: &deadpagerops slot for OBJT_MARKER)

Combined build with DF-2865's fix (fix_build.log); stage 2 on the patched kernel (run.patched.log):

PGTMARK: pagertab[7] = 0xffffffff81121380   <-- now &deadpagerops (== pagertab[6])
PGTMARK: dispatching OBJT_MARKER(7) object via vm_pager_has_page(); pagertab[7] = 0xffffffff81121380
PGTMARK: vm_pager_has_page returned 0 -- NO PANIC (fixed kernel)

pagertab[7] now resolves to &deadpagerops; the dead pager's pgo_haspage returns FALSE. Guest stayed up, module unloaded.

Kernel references

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied fix.diff (hunk 1 = this fix; hunks 2-6 = DF-2865) to /usr/src in-guest, make -j6 nativekernel KERNCONF=X86_64_GENERIC + installkernel, rebooted into kernel #1. Exact same fire sysctl: pagertab[7] resolves to &deadpagerops (== pagertab[6]), vm_pager_has_page returns 0, 'NO PANIC', module unloads, guest stays up.

["findings/poc/DF-2866/run.patched.log: 'PGTMARK: pagertab[7] = 0xffffffff81121380' + 'returned 0 -- NO PANIC (fixed kernel)' + 'UNLOAD-OK'", 'findings/poc/DF-2866/panic.txt: baseline stock-kernel Fatal trap 12 at 0x18 for contrast', 'findings/poc/DF-2866/fix.diff: git-apply-able (verified --check against pristine sys/)']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Wed Sep 2 14:41:38 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

["run.log: 'PGTMARK: pagertab[7] = 0   <-- OOB SLOT' then 'dispatching OBJT_MARKER(7) ... pagertab[7] = 0' and ssh death", "panic.txt: 'Fatal trap 12: page fault while in kernel mode / fault virtual address = 0x18 / Stopped at pgtmark_fire_sysctl.part.0+0x53: movq 0x18(%rax),%rax'", "run.patched.log (kernel #1 with fix.diff): 'pagertab[7] = 0xffffffff81121380   <-- now &deadpagerops (== pagertab[6])' + 'vm_pager_has_page returned 0 -- NO PANIC (fixed kernel)' + clean kldunload", 'build.log / fix_build.log: module build and full nativekernel build logs']

PoC changes

PoC authored from scratch (no seed). Iterations: (1) removed a speculative pre-dereference of pagertab[7]+24 from the kprintf so any fault lands inside the dispatcher where it belongs; (2) fire hooked through SYSCTL_PROC handler instead of plain SYSCTL_INT so the dispatch runs on demand, not at load.

Verified recommended fix

Add the missing eighth slot '&deadpagerops / OBJT_MARKER (list-scan marker; never paged) /' to pagertab[] so any marker-typed object dispatched to a pager op lands in the dead pager instead of one past the array - see fix.diff hunk 1.

Verdict

Reproduced the dispatch primitive on the stock INVARIANTS guest: enum obj_type has 8 values (OBJT_MARKER==7, vm_object.h:118-127) but pagertab[] has only 7 entries (vm_pager.c:152-160), and every dispatcher (vm_pager_deallocate vm_pager.c:340; vm_pager_get_page/put_pages/has_page inlines vm_pager.h:134/150/168) indexes pagertab[object->type] unbounded. KLD stage 1 dumped pagertab[7] (the OOB slot: NULL on this link layout - whatever the linker placed after the array); stage 2 called vm_pager_has_page() on a type=OBJT_MARKER object, producing 'Fatal trap 12: page fault while in kernel mode, fault virtual address = 0x18, movq 0x18(%rax),%rax' - the inline loaded pagertab[7] and dereferenced ->pgo_haspage at +24; guest down at db>. IN-TREE REACHABILITY IS LATENT: all three marker producers (vm_object.c:1847/1960, swap_pager.c:2171/2180, vm_swapcache.c:220/728) check the type before dispatching, so the PoC constructs the marker itself (synthetic trigger) - the finding is the missing bounds/eighth slot (DF-0944 family), rated Low; on a layout where a non-zero word follows the array the same path is a call through an uncontrolled pointer. fix.diff hunk 1 (eighth slot = &deadpagerops) validated on rebuilt kernel #1: dispatch returns FALSE, 'NO PANIC', clean unload.