When exactly are DR6's bits set (or cleared for DR6/7's bit 13)? Before the exception vector is attempted or after it succeeds? What if it leads to another fault (not triple fault)? Are the bits still set if a general protection fault occurs while trying to parse #DB or #BP?
Last edited by superfury on 2025-10-27, 11:00. Edited 1 time in total.
What about the clearing of bit 13 of DR7 (General Detect Enable)? Is it still cleared if the fault handler (#DB) throws another fault before finishing it's invocation (page fault or whatever), before reaching the debugger code's initial eip execution? Because that would give the attacker effectively control of the debugger registers (as the fault handler's EIP points to the mov to the debugger register, which DR7.GD is supposed to catch)? Or is that only updated once #DB successfully completes?
Yes, bit 13 of DR7 is cleared in the microcode routine that implements single-stepping and breakpoints, before entering the microcode routine that implements the interrupt.
Doesn't that give the attacker DR access if the #DB handling (before it completes fully) throws a fault? As the fault would return to the MOV Dn instruction, which can access the registers now due to BD being cleared before the interrupt handling?
https://gitlab.arm.com/arm-reference-solution … 86/debug.c#L330
That clearly says it's cleared upon successful entry to the #DB handler, thus preventing the debugging issue. So the debugger handler's start has been reached to clear DR7.BD, otherwise it remains set?
Or was it changed in later CPUs? Imagine the following: a kernel debugger tries to MOV into DRn. Because DR7.BD is set, it causes a #DB exception. Return point is the prohibited MOV (it's a fault after all). The CPU clears DR7.BD. Then it raises a page/segmentation fault before the #DB interrupt completes. The segmentation/page fault does it's thing and returns. But it returns to the kernel mode instruction that isn't allowed the register access. But since DR7.BD isn't set, it obtains read/write access even though it's not in the #DB handler!
If DR7.BD is cleared after successfull invocation of #DB, this issue doesn't happen.
Or did something change on later CPUs? Since the 80386 BD bit is different from Pentium+ versions of it (it has a different definition on 486 or Pentium-class CPUs I think). On the 386, it's a kind of SMM-related bit, while on Pentium+(or 486+), it's a different behaving bit.
Edit: Double checked it. On the 386 at least, it detects the ICE-386 using the debug registers, while on Pentium it's a specially debug-privileged #DB handler.
It seems it has been changed indeed:
The 486 reports the same as the 386 for the DR6 bit 13 (DR7 bit 13 isn't documented in either, only as '0').
The Pentium however, documents the following (on the DR7 GD bit):
1The GD bit enables the debug register protection condition that is flagged by BD of DR6. Note 2that GD is cleared at entry to the debug exception handler by the processor. This allows the 3handler free access to the debug registers.
So that clearly clears said bit once the handler fully completes it's interrupt handling.
Edit: Just modified the clearing of the DR7 BD bit to happen when the interrupt completes successfully (no faulting happens while invoking the exception handler).
This effectively is performed before the next instruction is handled.
Although it isn't documented (afaik), I've implemented the same for the INT3 instruction (only when it completes fully without any exceptions handling the INT 3 interrupt).
Edit: It made me remember that I still need to handle interrupt completion for task switches too (if triggered by an interrupt). Perhaps that should happen at the new tasks's commit point (before loading LDTR)? Or after pushing the error code?
Edit: Implemented proper interrupt completion when an interrupt uses a task gate. So now it performs the interrupt completion (that's handling stuff like #DB's DR7.BD bit) when the interrupt handler is a task gate. The interrupt is considered completed when either:
- No error code is pushed and the 'commit point' of the task switch is reached (committing to the new task, before the LDTR is loaded).
- An error code has been pushed after the task switch.
Both will invoke the newly added function that handles an interrupt completion, just like any normal protected mode or real mode interrupt already did.
Said function notes the special 'we've executed an interrupt' case to the CPU (for the main handling to terminate correctly) and for #DB interrupts (and also for #BP in current UniPCemu) call the #DB and #BP execution start handler. Said execution start handlers both simply do the same right now: clear the DR7.BD bit to allow the #DB and #BP handlers to access the debugger registers.