* [REGRESSION] 32-bit ARM's BKPT instruction no longer works
@ 2026-06-21 19:15 slipher
2026-06-21 20:19 ` Russell King (Oracle)
0 siblings, 1 reply; 4+ messages in thread
From: slipher @ 2026-06-21 19:15 UTC (permalink / raw)
To: linux-kernel@vger.kernel.org
Cc: stable@vger.kernel.org, regressions@lists.linux.dev,
linus.walleij@linaro.org, rmk+kernel@armlinux.org.uk
Consider the C program for 32-bit ARM architectures:
int main() {
__asm__ __volatile__ ("BKPT");
return 0;
}
Expected behavior is that this raises SIGTRAP. Since Linux 6.10 this no
longer happens; instead execution perpetually resumes at the same
instruction, using 100% of CPU. It does not matter whether GDB is
attached. I have tested with an armv7l CPU, but I imagine any other
variants with the BKPT instruction would be equally affected.
I believe the culprit to be commit
c3f89986fde7bb9ccc86a901bf28e1f7d69fc3b3 "ARM: 9391/2: hw_breakpoint:
Handle CFI breakpoints". The commit defines the method-of-entry code 3
as "ARM_ENTRY_CFI_BREAKPOINT", but this is the code used for any BKPT
instruction - see
https://developer.arm.com/documentation/ddi0379/a/Debug-Register-Reference/Control-and-status-registers/Debug-Status-and-Control-Register--DSCR-?lang=en
"Method of Debug Entry (MOE), bits [5:2]". If the CFI option is disabled
in the kernel config, hw_breakpoint_pending() returns 0 indicating the
breakpoint was handled, but takes no action. So breakpoints cannot be
used by user-space code, regardless of how CONFIG_CFI is set. The blog
post
https://www.jwhitham.org/2015/04/the-mystery-of-fifteen-millisecond.html
gives a nice overview of the control flow in older, working kernels.
The following Systemtap script can be used to demonstrate that the
ARM_ENTRY_CFI_BREAKPOINT path is used, when running the above C program.
probe kernel.function("hw_breakpoint_pending").call {
printf("hw_breakpoint_pending entered\n");
}
probe kernel.function("hw_breakpoint_pending").return {
printf("hw_breakpoint_pending returned %d\n", $return);
}
// these are not called
probe kernel.function("watchpoint_handler") {
printf("watchpoint_handler\n");
}
probe kernel.function("breakpoint_handler") {
printf("breakpoint_handler\n");
}
Tested on a 7.0.12 kernel, the output is:
hw_breakpoint_pending entered
hw_breakpoint_pending returned 0
hw_breakpoint_pending entered
hw_breakpoint_pending returned 0
hw_breakpoint_pending entered
hw_breakpoint_pending returned 0
...
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [REGRESSION] 32-bit ARM's BKPT instruction no longer works
2026-06-21 19:15 [REGRESSION] 32-bit ARM's BKPT instruction no longer works slipher
@ 2026-06-21 20:19 ` Russell King (Oracle)
2026-06-21 21:53 ` slipher
0 siblings, 1 reply; 4+ messages in thread
From: Russell King (Oracle) @ 2026-06-21 20:19 UTC (permalink / raw)
To: slipher
Cc: linux-kernel@vger.kernel.org, stable@vger.kernel.org,
regressions@lists.linux.dev, linus.walleij@linaro.org
On Sun, Jun 21, 2026 at 07:15:27PM +0000, slipher wrote:
> Consider the C program for 32-bit ARM architectures:
>
> int main() {
> __asm__ __volatile__ ("BKPT");
> return 0;
> }
>
>
> Expected behavior is that this raises SIGTRAP. Since Linux 6.10 this no
> longer happens; instead execution perpetually resumes at the same
> instruction, using 100% of CPU. It does not matter whether GDB is
> attached. I have tested with an armv7l CPU, but I imagine any other
> variants with the BKPT instruction would be equally affected.
Looking at the code, I doubt this has ever cleanly raised SIGTRAP (can
you check whether it does in kernels without c3f89986fde please?)
What I suspect instead is you get an "Unhandled ... abort" instead
and the program forcefully killed as hw_breakpoint_pending() would
have ARM_DSCR_MOE(dscr) == 3, and the switch() would set ret = 1.
That triggers the fault handlers in arch/arm/mm/fault.c to
complain bitterly, and forced a SIGTRAP to the program to kill it
off. No resumption from an unhandled trap is expected.
BKPT was added in later versions of the architecture. In order for
32-bit ARM to have functional breakpoints with older versions of the
architecture, we had to invent our own breakpoint instruction using
what was available in the reference manuals of the time - and this
needed to be maintained in a forwards compatible manner. Sadly, Arm
Ltd were late to the party.
The ARM mode breakpoint instructions that were chosen were 0xe7f001f0
and 0xde01 for Thumb. These cause a SIGTRAP with a TRAP_BRKPT code.
--
RMK's Patch system: https://www.armlinux.org.uk/developer/patches/
FTTP is here! 80Mbps down 10Mbps up. Decent connectivity at last!
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [REGRESSION] 32-bit ARM's BKPT instruction no longer works
2026-06-21 20:19 ` Russell King (Oracle)
@ 2026-06-21 21:53 ` slipher
2026-06-21 22:41 ` Russell King (Oracle)
0 siblings, 1 reply; 4+ messages in thread
From: slipher @ 2026-06-21 21:53 UTC (permalink / raw)
To: Russell King (Oracle)
Cc: linux-kernel@vger.kernel.org, stable@vger.kernel.org,
regressions@lists.linux.dev
On Sunday, June 21st, 2026 at 3:19 PM, Russell King (Oracle) <linux@armlinux.org.uk> wrote:
> On Sun, Jun 21, 2026 at 07:15:27PM +0000, slipher wrote:
> > Consider the C program for 32-bit ARM architectures:
> >
> > int main() {
> > __asm__ __volatile__ ("BKPT");
> > return 0;
> > }
> >
> >
> > Expected behavior is that this raises SIGTRAP. Since Linux 6.10 this no
> > longer happens; instead execution perpetually resumes at the same
> > instruction, using 100% of CPU. It does not matter whether GDB is
> > attached. I have tested with an armv7l CPU, but I imagine any other
> > variants with the BKPT instruction would be equally affected.
>
> Looking at the code, I doubt this has ever cleanly raised SIGTRAP (can
> you check whether it does in kernels without c3f89986fde please?)
>
> What I suspect instead is you get an "Unhandled ... abort" instead
> and the program forcefully killed as hw_breakpoint_pending() would
> have ARM_DSCR_MOE(dscr) == 3, and the switch() would set ret = 1.
> That triggers the fault handlers in arch/arm/mm/fault.c to
> complain bitterly, and forced a SIGTRAP to the program to kill it
> off. No resumption from an unhandled trap is expected.
I have tested with a 6.6 kernel. All of that is correct, as detailed in
the aforementioned blog post, except the last sentence. The switch does
set ret = 1, thereby passing on the exception. The kernel complains,
with such lines in dmesg output:
[ 1547.164526] Unhandled prefetch abort: breakpoint debug exception (0x222) at 0x0001051c
Indeed, it is not clean or efficient; the blog
(https://www.jwhitham.org/2015/04/the-mystery-of-fifteen-millisecond.html)
even has a proposed patch to improve the performance when raising
SIGTRAP. However, it is possible to catch the signal, and even resume
with something like this:
#include <ucontext.h>
#include <signal.h>
#include <stdio.h>
void handl(int a, siginfo_t *b, void *uc) {
puts("caught SIGTRAP");
((ucontext_t*)uc)->uc_mcontext.arm_pc += 4;
}
int main() {
struct sigaction s;
s.sa_flags = SA_SIGINFO;
s.sa_sigaction = handl;
sigemptyset(&s.sa_mask);
sigaction(SIGTRAP, &s, 0);
puts("start");
__asm__ __volatile__("BKPT");
puts("resumed");
return 0;
}
Re-testing, I realized there is a huge caveat: SIGTRAP is *not* raised
when running under a debugger! If GDB is attached, either of the C
programs above will repeatedly resume at the faulting instruction on
Linux 6.6, just as they will with the latest kernels. So the regression
only affects the perhaps-obscure case of using BKPT without any
intention of attaching a debugger, unless that worked in even-earlier
versions of Linux.
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [REGRESSION] 32-bit ARM's BKPT instruction no longer works
2026-06-21 21:53 ` slipher
@ 2026-06-21 22:41 ` Russell King (Oracle)
0 siblings, 0 replies; 4+ messages in thread
From: Russell King (Oracle) @ 2026-06-21 22:41 UTC (permalink / raw)
To: slipher
Cc: linux-kernel@vger.kernel.org, stable@vger.kernel.org,
regressions@lists.linux.dev
On Sun, Jun 21, 2026 at 09:53:17PM +0000, slipher wrote:
>
> On Sunday, June 21st, 2026 at 3:19 PM, Russell King (Oracle) <linux@armlinux.org.uk> wrote:
>
> > On Sun, Jun 21, 2026 at 07:15:27PM +0000, slipher wrote:
> > > Consider the C program for 32-bit ARM architectures:
> > >
> > > int main() {
> > > __asm__ __volatile__ ("BKPT");
> > > return 0;
> > > }
> > >
> > >
> > > Expected behavior is that this raises SIGTRAP. Since Linux 6.10 this no
> > > longer happens; instead execution perpetually resumes at the same
> > > instruction, using 100% of CPU. It does not matter whether GDB is
> > > attached. I have tested with an armv7l CPU, but I imagine any other
> > > variants with the BKPT instruction would be equally affected.
> >
> > Looking at the code, I doubt this has ever cleanly raised SIGTRAP (can
> > you check whether it does in kernels without c3f89986fde please?)
> >
> > What I suspect instead is you get an "Unhandled ... abort" instead
> > and the program forcefully killed as hw_breakpoint_pending() would
> > have ARM_DSCR_MOE(dscr) == 3, and the switch() would set ret = 1.
> > That triggers the fault handlers in arch/arm/mm/fault.c to
> > complain bitterly, and forced a SIGTRAP to the program to kill it
> > off. No resumption from an unhandled trap is expected.
>
> I have tested with a 6.6 kernel. All of that is correct, as detailed in
> the aforementioned blog post, except the last sentence. The switch does
> set ret = 1, thereby passing on the exception. The kernel complains,
> with such lines in dmesg output:
>
> [ 1547.164526] Unhandled prefetch abort: breakpoint debug exception (0x222) at 0x0001051c
This message is printed at Alert level. It's just not supposed to
happen, and if anyone sees it, it means someone cocked up in the kernel
and didn't provide the code to handle a fault that can be generated.
In these situations, the kernel's response is to try and keep the system
running by delivering a signal that should result in the process being
terminated. In this case, the hardware breakpoint code tells the
generic code to deliver a SIGTRAP / TRAP_HWBKPT, and this will be
delivered by force_sig_fault() after the noisy kernel message has been
produced.
force_sig_fault() will unblock the signal and set the handler to
default if it was blocked or ignored. The default action for SIGTRAP
should be to generate a coredump and terminate the program.
> Indeed, it is not clean or efficient; the blog
> (https://www.jwhitham.org/2015/04/the-mystery-of-fifteen-millisecond.html)
> even has a proposed patch to improve the performance when raising
> SIGTRAP. However, it is possible to catch the signal, and even resume
> with something like this:
>
>
> #include <ucontext.h>
> #include <signal.h>
> #include <stdio.h>
>
> void handl(int a, siginfo_t *b, void *uc) {
> puts("caught SIGTRAP");
> ((ucontext_t*)uc)->uc_mcontext.arm_pc += 4;
> }
>
> int main() {
> struct sigaction s;
> s.sa_flags = SA_SIGINFO;
> s.sa_sigaction = handl;
> sigemptyset(&s.sa_mask);
> sigaction(SIGTRAP, &s, 0);
> puts("start");
> __asm__ __volatile__("BKPT");
> puts("resumed");
> return 0;
> }
>
> Re-testing, I realized there is a huge caveat: SIGTRAP is *not* raised
> when running under a debugger! If GDB is attached, either of the C
> programs above will repeatedly resume at the faulting instruction on
> Linux 6.6, just as they will with the latest kernels. So the regression
> only affects the perhaps-obscure case of using BKPT without any
> intention of attaching a debugger, unless that worked in even-earlier
> versions of Linux.
... and while it's repeatedly raising the same fault, it's flooding the
kernel console with Alert level messages telling you the fault hasn't
been handled even on older kernels... yet you seem to be under the
impression that this is supposed to work.
You are testing something that has never been tested before, and are
hitting behaviour that isn't _supposed_ to be clean.
That said, the change of behaviour is wrong. If
hw_breakpoint_cfi_handler() doesn't understand the reason its been
called, it should cause the old behaviour (where the alert message
is printed) to be actioned.
The issue over whether BKPT should correctly raise a SIGTRAP that
is appropriately handled is an entirely separate issue, which I
would regard as a feature request rather than a regression.
Let me put it slightly differently. BKPT in userspace hasn't been
supported by the kernel, and the behaviour you've seen from the
kernel is incidental to the kernel's abort handling - it is not
by design.
Architecturally, BKPT is used with JTAG debuggers, causing the
processor to enter debug mode so a JTAG debugger can do its
stuff. There was some discussion ten years ago whether LLVM
should use BKPT for setting software breakpoints, and it seems
they decided against it because of interfering with JTAG
debuggers. See https://reviews.llvm.org/D16853?id=46899#347119
Also see the linked discussion from that post, where using BKPT
was discussed with gdb. Basically, if a hardware JTAG debugger is
connected, BKPT goes straight to the hardware debugger not the
kernel. However, note that the sourceware discussion is talking
about Thumb2 rather than ARM, but the same will apply there.
In essence, the decision was to stick with the UDF instructions
for software breakpoints handled by the kernel, and leave BKPT
for hardware JTAG debuggers. Consequently, explicitly executing
BKPT without a hardware JTAG debugger is unexpected, the results
of which are not guaranteed.
Indeed, under older architectures, you'll get an undefined
instruction exception and the program killed by a SIGILL not a
SIGTRAP, because BKPT isn't architecturally defined there.
--
RMK's Patch system: https://www.armlinux.org.uk/developer/patches/
FTTP is here! 80Mbps down 10Mbps up. Decent connectivity at last!
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-06-21 22:41 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-21 19:15 [REGRESSION] 32-bit ARM's BKPT instruction no longer works slipher
2026-06-21 20:19 ` Russell King (Oracle)
2026-06-21 21:53 ` slipher
2026-06-21 22:41 ` Russell King (Oracle)
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox