// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause //go:build linux && arm64 package debug import ( "fmt" "syscall" "unsafe" ) // Hardware watchpoint support via arm64 debug registers (DBGWVR/DBGWCR). // Accessed via PTRACE_GETREGSET/SETREGSET with NT_ARM_HW_WATCH. // WatchpointType selects what triggers the watchpoint. type WatchpointType int const ( WatchWrite WatchpointType = 1 WatchRead WatchpointType = 3 ) // maxWatchpoints reports the number of hardware watchpoint slots the // architecture provides: DBGWVR0-DBGWCR15. func maxWatchpoints() int { return 16 } // hwWatchState mirrors the kernel's struct user_hwdebug_state. type hwWatchState struct { DbgInfo uint32 _pad [4]byte DbgRegs [16]hwWatchReg } type hwWatchReg struct { Addr uint64 Ctrl uint64 } // ntArmHWWatch is NT_ARM_HW_WATCH (0x403), the watchpoint regset // (include/uapi/linux/elf.h; 0x402 is NT_ARM_HW_BREAK). Watchpoints and // breakpoints live in different regsets with the same struct shape, so the // constant is named for what it arms to keep a future edit from arming // breakpoints instead. const ntArmHWWatch = 0x403 // archWatchpointAddr resolves the address of the watchpoint that fired: // the arm64 kernel already reports the watched data address as si_addr. func archWatchpointAddr(s *Session, siAddr uint64) uint64 { return siAddr } func (s *Session) FindFreeWatchpointSlot() int { for i := range maxWatchpoints() { if !s.wpSlots[i] { return i } } return -1 } func (s *Session) IsWatchpointSlotUsed(slot int) bool { if slot < 0 || slot >= maxWatchpoints() { 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 >= maxWatchpoints() { return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints()-1) } if s.wpSlots[slot] { return fmt.Errorf("debug: watchpoint slot %d already in use", slot) } state, err := s.getHWWatchState() if err != nil { return fmt.Errorf("debug: read watchpoint state: %w", err) } // MDSCR_EL1 packs (debug_arch << 8) | num_slots into dbg_info, so only // the low byte counts slots. if uint32(slot) >= state.DbgInfo&0xff { return fmt.Errorf("debug: slot %d exceeds available watchpoints (%d)", slot, state.DbgInfo&0xff) } state.DbgRegs[slot].Addr = addr // DBGWCR bits 3-4 select the access type: 01 load, 10 store, 11 either // (ARM DDI 0487, DBGWCR_EL1 watchpoint type field). ctrl := uint64(1) // enable switch typ { case WatchWrite: ctrl |= 2 << 3 // store only case WatchRead: ctrl |= 3 << 3 // load+store } var bas uint64 switch size { case 1: bas = 0x01 case 2: bas = 0x03 case 4: bas = 0x0F case 8: bas = 0xFF default: return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8") } ctrl |= bas << 5 state.DbgRegs[slot].Ctrl = ctrl if err := s.setHWWatchState(state); err != nil { return fmt.Errorf("debug: set watchpoint: %w", err) } s.wpSlots[slot] = true return nil } func (s *Session) ClearWatchpoint(slot int) error { if slot < 0 || slot >= maxWatchpoints() { return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints()-1) } if !s.wpSlots[slot] { return fmt.Errorf("debug: watchpoint slot %d is not in use", slot) } state, err := s.getHWWatchState() if err != nil { return err } state.DbgRegs[slot].Addr = 0 state.DbgRegs[slot].Ctrl = 0 if err := s.setHWWatchState(state); err != nil { return err } s.wpSlots[slot] = false return nil } 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 (s *Session) getHWWatchState() (*hwWatchState, error) { var state hwWatchState iovec := syscall.Iovec{ Base: (*byte)(unsafe.Pointer(&state)), Len: uint64(unsafe.Sizeof(state)), } _, _, errno := syscall.Syscall6( syscall.SYS_PTRACE, uintptr(syscall.PTRACE_GETREGSET), uintptr(s.pid), uintptr(ntArmHWWatch), uintptr(unsafe.Pointer(&iovec)), 0, 0, ) if errno != 0 { return nil, errno } return &state, nil } func (s *Session) setHWWatchState(state *hwWatchState) error { iovec := syscall.Iovec{ Base: (*byte)(unsafe.Pointer(state)), Len: uint64(unsafe.Sizeof(*state)), } _, _, errno := syscall.Syscall6( syscall.SYS_PTRACE, uintptr(syscall.PTRACE_SETREGSET), uintptr(s.pid), uintptr(ntArmHWWatch), uintptr(unsafe.Pointer(&iovec)), 0, 0, ) if errno != 0 { return errno } return nil }