// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause //go:build linux && amd64 package debug import ( "fmt" "syscall" "unsafe" ) // Hardware watchpoint support via x86-64 debug registers (DR0-DR3, DR7). // The kernel translates PTRACE_POKEUSER/PEEKUSER offsets inside // [offsetof(struct user, u_debugreg[0]), u_debugreg[7]] to DR0-DR7 // (arch/x86/kernel/ptrace.c, arch_ptrace). sys/user.h places u_debugreg at // 0x350: DR0-DR3 are 0x350/0x358/0x360/0x368, DR6 (status) is 0x380 and // DR7 (control) is 0x388. Offsets below 0x350 write user_regs_struct // fields (r15 at 0x0, r10 at 0x38), not debug registers. const ( drOffset = 0x350 // offsetof(struct user, u_debugreg[0]), DR0 dr6Off = 0x380 // offsetof(struct user, u_debugreg[6]), DR6 dr7Off = 0x388 // offsetof(struct user, u_debugreg[7]), DR7 ) // archWatchpointAddr resolves the address of the watchpoint that fired. // x86 delivers si_addr = the instruction pointer of the trapping access // (arch/x86/kernel/ptrace.c send_sigtrap passes regs->ip), so the watched // data address is recovered from DR6's slot bits (B0-B3, positive polarity // through PEEKUSER) and the matching DR0-DR3. func archWatchpointAddr(s *Session, siAddr uint64) uint64 { dr6, err := ptracePeekUser(s.pid, dr6Off) if err != nil { return siAddr } for slot := range 4 { if dr6&(1< 3 { return false } return s.wpSlots[slot] } // SetWatchpoint installs a hardware watchpoint on the given address. func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error { if slot < 0 || slot > 3 { return fmt.Errorf("debug: watchpoint slot must be 0-3") } if s.wpSlots[slot] { return fmt.Errorf("debug: watchpoint slot %d already in use", slot) } var lenBits uint64 switch size { case 1: lenBits = 0 case 2: lenBits = 1 case 4: lenBits = 3 case 8: lenBits = 2 default: return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8") } if err := ptracePokeUser(s.pid, drOffset+uintptr(slot*8), addr); err != nil { return fmt.Errorf("debug: set DR%d: %w", slot, err) } dr7, err := ptracePeekUser(s.pid, dr7Off) if err != nil { return fmt.Errorf("debug: read DR7: %w", err) } enableBit := uint64(1) << (2 * slot) rwBits := uint64(typ) << (16 + 4*slot) lenField := lenBits << (18 + 4*slot) mask := ^((uint64(1) << (2 * slot)) | (uint64(3) << (16 + 4*slot)) | (uint64(3) << (18 + 4*slot))) dr7 = (dr7 & mask) | enableBit | rwBits | lenField if err := ptracePokeUser(s.pid, dr7Off, dr7); err != nil { return fmt.Errorf("debug: set DR7: %w", err) } s.wpSlots[slot] = true return nil } // ClearWatchpoint removes a hardware watchpoint. func (s *Session) ClearWatchpoint(slot int) error { if slot < 0 || slot > 3 { return fmt.Errorf("debug: watchpoint slot must be 0-3") } if !s.wpSlots[slot] { return fmt.Errorf("debug: watchpoint slot %d is not in use", slot) } dr7, err := ptracePeekUser(s.pid, dr7Off) if err != nil { return err } dr7 &^= uint64(1) << (2 * slot) if err := ptracePokeUser(s.pid, dr7Off, dr7); err != nil { return err } s.wpSlots[slot] = false return nil } // ClearAllWatchpoints removes all hardware watchpoints. func (s *Session) ClearAllWatchpoints() error { for slot := range maxWatchpoints() { if s.wpSlots[slot] { if err := s.ClearWatchpoint(slot); err != nil { return err } } } return nil } func ptracePokeUser(pid int, offset uintptr, val uint64) error { const ptracePokeuser = 6 _, _, errno := syscall.Syscall6( syscall.SYS_PTRACE, uintptr(ptracePokeuser), uintptr(pid), offset, uintptr(val), 0, 0, ) if errno != 0 { return errno } return nil } func ptracePeekUser(pid int, offset uintptr) (uint64, error) { // x86 PEEKUSR writes the word to the user-space pointer in data // (arch/x86/kernel/ptrace.c uses put_user); passing 0 there fails with // EFAULT, so the word is read through a real address. const ptracePeekuser = 3 var word uint64 _, _, errno := syscall.Syscall6( syscall.SYS_PTRACE, uintptr(ptracePeekuser), uintptr(pid), offset, uintptr(unsafe.Pointer(&word)), 0, 0, ) if errno != 0 { return 0, errno } return word, nil }