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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2866 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
kldloadprintspagertab[0..7](index 7 = the OOB slot; on this kernel build it reads NULL β whatever the linker placed after the array).sysctl vm.pgtmark_fire=1constructs a stackvm_objectwithtype = OBJT_MARKERand callsvm_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.)
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_objectwithtype = OBJT_MARKERand callsvm_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
- sys/vm/vm_pager.c:152-160 (7-entry pagertab)
- sys/vm/vm_object.h:118-127 (8-value enum obj_type, OBJT_MARKER=7)
- sys/vm/vm_pager.c:337-341 (vm_pager_deallocate, unbounded index)
- sys/vm/vm_pager.h:128-140,142-152,165-169 (get/put/haspage inlines, unbounded index)
- Marker producers: sys/vm/swap_pager.c:2171, sys/vm/vm_swapcache.c:220, sys/vm/vm_object.c:1847,1960
Fix verification
fixedApplied 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/)']
Confirmed kernel references
- sys/vm/vm_pager.c:152
- sys/vm/vm_pager.c:160
- sys/vm/vm_pager.c:337
- sys/vm/vm_pager.c:340
- sys/vm/vm_pager.h:128
- sys/vm/vm_pager.h:134
- sys/vm/vm_pager.h:150
- sys/vm/vm_pager.h:168
- sys/vm/vm_object.h:118
- sys/vm/vm_object.h:126
- sys/vm/swap_pager.c:2171
- sys/vm/vm_swapcache.c:220
- sys/vm/vm_object.c:1847
- sys/vm/vm_object.c:1960
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.
No comments yet.