set_dbregs() stores DR7 with architecturally-reserved bits 63:32 unvalidated; the value is loaded by cpu_heavy_restore during a context switch, raising a ring-0 #GP (fatal trap, panic) from an unprivileged ptrace() of the attacker's own child
Summary
set_dbregs validates only the R/W fields and (non-root) the dr0-3 addresses, then stores the raw 64-bit dr7 into pcb_dr7; cpu_heavy_restore merges only bits 10-15 before the MOV, so reserved bits 63:32 flow straight into it - under KVM (kvm_set_dr rejects them unconditionally) and on Intel bare metal this is a ring-0 #GP inside the context switch, outside the doreti_iret redirect, falling to trap_fatal. Reproduced as uid 1001 on the stock kernel: PT_SETDBREGS(dr7=0xdeadbeef00000000) accepted then PT_CONTINUE panics at cpu_heavy_restore+0x1ab (movl %eax,%dr7), guest down; baseline dr7=0 clean. Deterministic unpriv local DoS on all virtualized deployments + Intel bare metal (AMD ignores the writes). reject-high-bits fix validated on a rebuilt kernel (EINVAL, legit values unregressed). Ceiling is DoS - clean fault, no write primitive, ddb console-only.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5657 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.7 KB | β raw | |
| VERDICT.md | β | 4.7 KB | β raw | |
| poc.c | β | 2.2 KB | view raw | |
| build.sh | β | 167 B | view raw | |
| run.sh | β | 258 B | view raw | |
| build.log | β | 94 B | view raw | |
| run.log | β | 368 B | view raw | |
| run.2.log | β | 1.0 KB | view raw | |
| panic.txt | β | 579 B | view raw | |
| env.txt | β | 341 B | view raw | |
| fix.diff | β | 862 B | view raw | |
| fix_build.log | β | 664 B | view raw | |
| fix_run.log | β | 798 B | view raw | |
| manifest.json | β | 1.6 KB | view raw | |
| verdict.json | β | 5.2 KB | view raw |
DF-5657 β pc64 set_dbregs() stores DR7 with reserved bits 63:32 unvalidated β ring-0 #GP inside cpu_heavy_restore on context switch β fatal kernel trap (panic) from an unprivileged ptrace() of the attacker's own child
File under audit (owner file): sys/platform/pc64/x86_64/procfs_machdep.c (procfs_write_dbregs β set_dbregs plumbing)
Root cause: sys/platform/pc64/x86_64/machdep.c set_dbregs() + sys/platform/pc64/x86_64/swtch.s cpu_heavy_restore()
TL;DR
An unprivileged user (uid 1001) calls ptrace(PT_SETDBREGS) on its own stopped
child with dbregs.dr[7] = 0xdeadbeef00000000. set_dbregs() validates only
the R/W fields (bits 16β29) and the dr0βdr3 addresses vs VM_MAX_USER_ADDRESS;
it stores the raw 64-bit dr7 into pcb->pcb_dr7 and sets PCB_DBREGS
(machdep.c:3215-3222). On the next context switch into the tracee,
cpu_heavy_restore() reloads %dr7 masking ONLY bits 10β15
(andq $~0x0000fc00), so the reserved bits 63:32 flow straight into
movq %rax,%dr7 (swtch.s:597-617). On Intel CPUs β and unconditionally under
KVM, where the host kvm_set_dr() injects #GP for any set bit in DR7[63:32] β
this raises a ring-0 #GP in the middle of a context switch. The fault is not at
doreti_iret, so trap.c's kernel-mode T_PROTFLT redirect does not apply; the
trap falls through to trap_fatal() (trap.c:770) β fatal trap, all CPUs
stopped, system down.
Reproduced on the stock guest kernel; fix (reject dr7 with bits 63:32 set in set_dbregs) validated on a rebuilt kernel: same PoC now gets EINVAL, guest stays up, legitimate dr7 values (0, 0x401) still accepted.
Reproduce
Guest: dfbsd-qemu/vm.sh up (QEMU/KVM), then as unprivileged user (maxx):
./build.sh # cc -O -o /tmp/poc poc.c ./run.sh # baseline (dr7=0) then attack (dr7=0xdeadbeef00000000)
Expected (stock kernel):
- baseline: PT_SETDBREGS(dr7=0) ACCEPTED, child resumes, guest stays up;
- attack: PT_SETDBREGS(dr7=0xdeadbeef00000000) ACCEPTED, then on
PT_CONTINUE the kernel panics β serial console shows:
Fatal trap 9: general protection fault while in kernel mode β¦
Stopped at cpu_heavy_restore+0x1ab: movl %eax,%dr7 (see panic.txt);
ssh dies; vm.sh status β down.
On the fix-patched kernel: attack run prints PT_SETDBREGS: Invalid argument,
guest stays up (see fix_run.log).
Impact
Unprivileged local kernel panic / DoS (deterministic under KVM and on Intel bare metal; AMD bare metal ignores writes to DR7[63:32] and is not affected by this particular variant). No memory-corruption primitive: the fault is a clean
GP(0) taken to ddb β the ddb prompt is console-only and not reachable from an
ssh session, so the ceiling is denial of service.
DF-5657 VERDICT β reproduced (kernel panic from unprivileged local user), fix validated
What was tested
sys/platform/pc64/x86_64/procfs_machdep.c is the owner file; its
procfs_write_dbregs() (procfs_machdep.c:97-101) and the ptrace compat
plumbing (kern_ptrace PT_SETDBREGS β procfs_dodbregs,
sys/kern/sys_process.c:531-560) both funnel into
set_dbregs() in sys/platform/pc64/x86_64/machdep.c:3133.
Root cause (path:line)
set_dbregs()(machdep.c:3166-3169) validates only the four R/W fields of dr7 (rejects the 0b10 I/O-breakpoint pattern) and machdep.c:3189-3213 bounds dr0βdr3 to user space when enabled (gated oncaps_priv_check(SYSCAP_RESTRICTEDROOT)). Nothing checks the architecturally-reserved bits.pcb->pcb_dr7 = dbregs->dr[7];(machdep.c:3220) stores the raw 64-bit user value andpcb->pcb_flags |= PCB_DBREGS(machdep.c:3222) arms the reload path.- On the next context switch into the tracee,
cpu_heavy_restore(sys/platform/pc64/x86_64/swtch.s:597-617) reloads the debug registers:%dr7 β (current_dr7 & 0x0000fc00) | (pcb_dr7 & ~0x0000fc00). Only bits 10β15 (incl. the GD bit) are masked; bits 63:32 pass through raw intomovq %rax,%dr7. - DR7 bits 63:32 are reserved: Intel raises #GP(0) on
MOV DR7with them set, and under KVM the hostkvm_set_dr()(Linux arch/x86/kvm/x86.c) unconditionally returns 1 (β injected #GP)if (val & 0xffffffff00000000ULL)for DR7 β independent of host vendor. - The #GP lands in ring 0 inside the context-switch path. The kernel-mode
T_PROTFLT handler only redirects faults whose RIP is
doreti_iret(trap.c:675-681, matching exception.S IDTVEC(prot)'s special case at exception.S:351-371). A fault at swtch.s is not at doreti_iret, has no pcb_onfault, so it falls through totrap_fatal(frame, 0)(trap.c:770).
Observation (decisive)
Stock guest kernel, unprivileged uid 1001:
[+] PT_SETDBREGS(dr7=0xdeadbeef00000000) ACCEPTED by kernel
[*] PT_CONTINUE -> tracee switches in -> movq %rax,%dr7
<ssh dies>
vm.sh status -> down
serial: Fatal trap 9: general protection fault while in kernel mode
current process = 1136 <- the traced child
Stopped at cpu_heavy_restore+0x1ab: movl %eax,%dr7
db> (all CPUs stopped)
Baseline control on the same kernel: dr7=0 accepted, tracee resumed, guest healthy (run.log). The panic is caused specifically by the unvalidated reserved bits of dr7 surviving into the context-switch DR7 load.
Exploitability ceiling
This is a clean #GP(0) fatal trap, not a memory-corruption primitive: no attacker-controlled write, no state survives the trap (system stops in ddb / reboots). The ddb prompt is on the serial console only β an ssh-local attacker cannot drive it. Under KVM and on Intel bare metal the DoS is deterministic; AMD bare metal ignores writes to DR7[63:32] (no fault) and is not affected by this variant. Ceiling: unprivileged local kernel panic (DoS). Impact class: panic/dos β no uid0 route exists for this bug.
Note the GD bit (dr7 bit 13) is accidentally defused by the swtch.s
0x0000fc00 mask (taken from the in-CPU dr7, always GD=0), so the classic
"debug-register-access #DB in kernel" escalation of this family does NOT
apply here β only the reserved-high-bits #GP does.
Fix validation
fix.diff adds to set_dbregs() (machdep.c, before the R/W loop):
if (dbregs->dr[7] & 0xFFFFFFFF00000000LU)
return (EINVAL);
- Rebuilt the guest kernel with only this change
(
make nativekernel KERNCONF=X86_64_GENERIC, BUILD_RC=0), installed, rebooted (#1: Fri Oct 9 11:27:30 UTC 2026). - Exact same PoC on patched kernel:
PT_SETDBREGS: Invalid argument, guest stays up (fix_run.log). - Regression on patched kernel: dr7=0 and dr7=0x401 still accepted, tracee resumes, no panic (fix_run.log).
- Guest reset to clean snapshot afterwards.
Distinction from known findings
- DF-3258 / DF-0993: dbregs dr[8..15] read-side uninit leak β different direction, different sink; not re-reported.
- DF-1315: guest DR persistence across #VMEXIT (vkernel host interaction) β different mechanism entirely.
- DF-5335: bad-%ss iretq fault machine (sigreturn/set_regs) β different fault site (doreti_iret) and different unvalidated field (tf_ss); not re-reported.
- The vkernel64 twin implements set_dbregs as ENOSYS (cpu_regs.c:788-792), so no twin divergence weakens this analysis.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff (reject dr7 with bits 63:32 set in set_dbregs) applied to the guest /usr/src, kernel rebuilt with make nativekernel KERNCONF=X86_64_GENERIC (BUILD_RC=0), installed, rebooted. Baseline behavior on stock kernel reproduced first (fatal trap 9 at cpu_heavy_restore+0x1ab). On the patched kernel the exact PoC gets PT_SETDBREGS: Invalid argument and the guest stays up; regression runs with dr7=0x0000000000000000 and dr7=0x0000000000000401 are still accepted and resume cleanly. Bad behavior gone, no functional regression.
fix_run.log (attack -> EINVAL, regressions accepted, vm.sh status up); fix_build.log (patch applied hunk#1 @3153, BUILD_RC=0, uname #1 Fri Oct 9 11:27:30 UTC 2026); fix.diff (git-applyable, machdep.c set_dbregs).
Confirmed kernel references
- sys/platform/pc64/x86_64/procfs_machdep.c:97
- sys/platform/pc64/x86_64/machdep.c:3133
- sys/platform/pc64/x86_64/machdep.c:3166
- sys/platform/pc64/x86_64/machdep.c:3220
- sys/platform/pc64/x86_64/machdep.c:3222
- sys/platform/pc64/x86_64/swtch.s:597
- sys/platform/pc64/x86_64/swtch.s:612
- sys/platform/pc64/x86_64/swtch.s:617
- sys/platform/pc64/x86_64/trap.c:675
- sys/platform/pc64/x86_64/trap.c:770
- sys/kern/sys_process.c:531
- sys/vfs/procfs/procfs_dbregs.c:66
Detail
Exploit chain
unprivileged user fork()s a child, PT_TRACE_ME + raise(SIGSTOP); parent waitpid(), ptrace(PT_SETDBREGS) with dr[7]=0xdeadbeef00000000 (passes the R/W-field loop because bits 16-29 are 0 and the address checks because no breakpoint is enabled); kernel stores raw dr7 into pcb_dr7 and sets PCB_DBREGS; ptrace(PT_CONTINUE) schedules the tracee; cpu_heavy_restore loads %dr7 with the reserved high bits intact; KVM/Intel raise #GP(0) in ring 0 inside the context switch; trap_fatal() -> fatal trap 9, all CPUs stopped, system down. (DoS only - no memory write primitive, no uid0 escalation path.)
Evidence (decisive lines)
panic.txt: 'Fatal trap 9 ... Stopped at cpu_heavy_restore+0x1ab: movl %eax,%dr7', current process 1136 (uid-1001 tracee). run.2.log: PT_SETDBREGS(dr7=0xdeadbeef00000000) ACCEPTED then guest down. run.log: baseline dr7=0 accepted, no crash, guest up. fix_run.log: patched kernel rejects attack with EINVAL, accepts dr7=0/0x401, guest up. fix_build.log: nativekernel BUILD_RC=0 with fix.diff.
PoC changes
No seed PoC existed for this pass-2 finding; written fresh. Guest-side trivia: PT_SETDBREGS is 38 on pc64 (sys/platform/pc64/include/ptrace.h: PT_FIRSTMACH+6) and struct dbreg is dr[16] u64s, so the PoC passes its own 128-byte struct directly.
Verified recommended fix
Reject DR7 values with reserved high bits in set_dbregs(): if (dbregs->dr[7] & 0xFFFFFFFF00000000LU) return (EINVAL); (fix.diff)
Verdict
set_dbregs() (sys/platform/pc64/x86_64/machdep.c:3133, reached from procfs_machdep.c:97-101 and PT_SETDBREGS via sys/kern/sys_process.c:531) validates only dr7's R/W fields and dr0-dr3 addresses; it stores the raw 64-bit dr[7] into pcb_dr7 (machdep.c:3220). cpu_heavy_restore() reloads %dr7 masking only bits 10-15 (swtch.s:612-617), so reserved bits 63:32 flow into 'movq %rax,%dr7'. On Intel (and deterministically under KVM via the host kvm_set_dr check) that raises a ring-0 #GP mid-context-switch; the fault is not at doreti_iret so trap.c:675-681 does not apply and it falls to trap_fatal() (trap.c:770). Verified as uid 1001 on the stock guest: ptrace(PT_SETDBREGS, dr7=0xdeadbeef00000000) is accepted, PT_CONTINUE panics the kernel at cpu_heavy_restore+0x1ab (movl %eax,%dr7) with current process = the traced child; baseline dr7=0 does not crash. Clean fatal trap, no write primitive; ceiling is unprivileged local kernel panic/DoS (no uid0 route; ddb prompt is console-only). Fix (reject dr7 with bits 63:32 set in set_dbregs) validated on a rebuilt kernel: same PoC now gets EINVAL, guest stays up, dr7=0/0x401 still accepted.
No comments yet.