[PATCH] arm64/hw_breakpoint: reject unaligned watchpoints that would truncate BAS

Will Deacon will at kernel.org
Wed Jun 10 05:30:17 PDT 2026


On Mon, Jun 08, 2026 at 05:14:22AM -0700, Breno Leitao wrote:
> On Fri, May 29, 2026 at 03:53:58PM +0100, Will Deacon wrote:
> > Hi Breno,
> > 
> > Thanks for sending this out.
> > 
> > On Thu, Apr 30, 2026 at 02:40:10AM -0700, Breno Leitao wrote:
> > > hw_breakpoint_arch_parse() positions the BAS bit pattern in
> > > hw->ctrl.len with
> > > 
> > > 	offset = hw->address & alignment_mask;	/* 0..7 */
> > > 	hw->ctrl.len <<= offset;
> > > 
> > > ctrl.len is an 8-bit bitfield (struct arch_hw_breakpoint_ctrl::len is
> > > u32 :8), so the shift silently drops any bits past bit 7.  For
> > > non-compat AArch64 watchpoints the offset is unbounded relative to
> > > ctrl.len: a perf_event_open(PERF_TYPE_BREAKPOINT) caller asking for
> > > HW_BREAKPOINT_W with bp_addr=page+1 and bp_len=HW_BREAKPOINT_LEN_8
> > > ends up with 0xff << 1 = 0x1fe, stored as 0xfe.  The kernel programs
> > > WCR.BAS=0xfe and the hardware watches bytes [1..7] instead of the
> > > requested [1..8] -- the eighth byte is silently dropped.  The
> > > syscall still returns success, leaving userspace to discover the
> > > gap by empirical probing.
> > > 
> > > The same class affects HW_BREAKPOINT_LEN_{2,4} when offset pushes the
> > > high BAS bit past bit 7 (e.g. LEN_4 with offset=5 yields 0xe0
> > > instead of 0x1e0).  No memory-safety impact -- the value is masked
> > > into 8 bits before encoding -- but debuggers and perf users observe
> > > missed events on bytes they thought they were watching.
> > > 
> > > The AArch32 branch immediately above already rejects unrepresentable
> > > (offset, len) combinations via an explicit switch.  Mirror that for
> > > the non-compat branch by checking that the shifted pattern fits in
> > > the BAS field, returning -EINVAL when it does not.
> > > 
> > > Reproducer:
> > > 
> > >   struct perf_event_attr a = {
> > >       .type = PERF_TYPE_BREAKPOINT, .size = sizeof(a),
> > >       .bp_type = HW_BREAKPOINT_W,
> > >       .bp_addr = (uintptr_t)(buf + 1),
> > >       .bp_len = HW_BREAKPOINT_LEN_8,
> > >       .exclude_kernel = 1, .exclude_hv = 1,
> > >   };
> > >   int fd = perf_event_open(&a, 0, -1, -1, 0);
> > >   /* before this fix: succeeds, watches 7 bytes (buf+1..buf+7)   */
> > >   /* after  this fix: fails with EINVAL                          */
> > > 
> > > Signed-off-by: Breno Leitao <leitao at debian.org>
> > > Fixes: b08fb180bb88 ("arm64: Allow hw watchpoint at varied offset from base address")
> > 
> > Oh man, this has been broken for nearly a decade :/
> > 
> > I think we probably should've stuck with the old behaviour of rejecting
> > unaligned base addresses, but it's too late now. Damn.
> > 
> > >  arch/arm64/kernel/hw_breakpoint.c | 9 +++++++++
> > >  1 file changed, 9 insertions(+)
> > > 
> > > diff --git a/arch/arm64/kernel/hw_breakpoint.c b/arch/arm64/kernel/hw_breakpoint.c
> > > index ab76b36dce820..b8a1402119f3a 100644
> > > --- a/arch/arm64/kernel/hw_breakpoint.c
> > > +++ b/arch/arm64/kernel/hw_breakpoint.c
> > > @@ -559,6 +559,15 @@ int hw_breakpoint_arch_parse(struct perf_event *bp,
> > >  		else
> > >  			alignment_mask = 0x7;
> > >  		offset = hw->address & alignment_mask;
> > > +
> > > +		/*
> > > +		 * BAS is an 8-bit field in WCR/BCR; the shift below would
> > > +		 * silently drop the high bits of ctrl.len when offset + len
> > > +		 * exceeds 8, programming hardware to watch fewer bytes than
> > > +		 * the user requested.
> > > +		 */
> > > +		if (((u32)hw->ctrl.len << offset) > 0xff)
> > 
> > nit: Use ARM_BREAKPOINT_LEN_8 instead of 0xff
> > 
> > > +			return -EINVAL;
> > >  	}
> > 
> > I must confess, I'm very nervous about breaking userspace here. If GDB
> > is triggering this path, then this patch will change an unreliable
> > watchpoint into a hard error (which probably means GDB exits). Have you
> > looked to see what GDB and/or any other debuggers do?
> > 
> > I had a quick peek and found the bugzilla entry which motivated the
> > buggy change in the first place:
> > 
> > https://sourceware.org/bugzilla/show_bug.cgi?id=20207
> > 
> > and it looks like the aarch64_align_watchpoint() function does try to
> > spill into multiple watchpoints, so perhaps your patch is ok. I'd
> > appreciate your opinion, though.
> 
> It won't, for two independent reasons.

Sorry, not sure I understand you here when you say "it won't". GDB won't
spill or something else?

> The new -EINVAL is unreachable from GDB; only a raw perf_event_open() passing
> an unaligned base with an oversized bp_len hits it, which is the bug.

Why isn't it reachable via hw_break_set() => {ptrace_hbp_set_addr(),
ptrace_hbp_set_ctrl()} ?

> Second, even if a debugger did hand the kernel such a request, GDB
> already treats EINVAL on NT_ARM_HW_WATCH as a downgrade signal, not a
> fatal error. aarch64_linux_set_debug_regs() catches it, clears
> kernel_supports_any_contiguous_range, calls aarch64_downgrade_regs()
> (which rounds the BAS up to a legacy 0x01/03/0f/ff mask and aligns the
> base down), and retries. That fallback is exactly the PR-20207 path.
> 
> Confirmed on a Grace box: with the patch applied (under virtme-ng), the
> reproducer's unaligned LEN_8 now returns -EINVAL while aligned LEN_8
> still succeeds, and GDB still inserts the watchpoint and it still fires
> on every write.

Thanks for testing it, that's somewhat reassuring.

> GDB in fact downgrades on the current kernel independently of this patch, so
> behaviour is unchanged for it.

That sounds like a bug?

Will



More information about the linux-arm-kernel mailing list