From: Breno Leitao Date: Tue, 9 Jun 2026 13:15:53 +0000 (-0700) Subject: arm64/hw_breakpoint: reject unaligned watchpoints that would truncate BAS X-Git-Tag: v7.2-rc1~21^2~1 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=4cc70f75853bebac022334b6a86b953348072f74;p=thirdparty%2Flinux.git arm64/hw_breakpoint: reject unaligned watchpoints that would truncate BAS 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. GDB and similar debuggers are unaffected by the stricter check. aarch64_linux_set_debug_regs() already treats EINVAL on NT_ARM_HW_WATCH as a downgrade signal: it clears kernel_supports_any_contiguous_range, calls aarch64_downgrade_regs() to round the BAS up to a legacy 0x01/03/0f/ff mask with an aligned base, and retries -- the same fallback path that PR-20207 introduced. The new -EINVAL is therefore reachable only from a raw perf_event_open() that pairs an unaligned base with an oversized bp_len, which is precisely the bug. 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 */ Fixes: b08fb180bb88 ("arm64: Allow hw watchpoint at varied offset from base address") Signed-off-by: Breno Leitao Signed-off-by: Will Deacon --- diff --git a/arch/arm64/kernel/hw_breakpoint.c b/arch/arm64/kernel/hw_breakpoint.c index ab76b36dce820..73cce8ac83681 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) > ARM_BREAKPOINT_LEN_8) + return -EINVAL; } hw->address &= ~alignment_mask;