Skip to contentSkip to conversation

selftests/ptrace: Add PTRACE_SYSEMU_SINGLESTEP syscall-exit-stop test

PATCH In review Kees Cook 3 participants 5 messages Sep 22 – Sep 28 Kselftest

lore
Share & tools
lore

Kees Cook

author
#0 lore raw

PATCH 1/1 selftests/ptrace: Add PTRACE_SYSEMU_SINGLESTEP syscall-exit-stop test

ptrace(2) states that no syscall-exit-stop occurs when a tracee is continued with PTRACE_SYSEMU or PTRACE_SYSEMU_SINGLESTEP. arm64 gated its syscall-exit report on

flags & (_TIF_SYSCALL_TRACE | _TIF_SINGLESTEP)

and ptrace_resume() clears SYSCALL_TRACE for both SYSEMU requests while PTRACE_SYSEMU_SINGLESTEP additionally sets TIF_SINGLESTEP, so the single-step bit alone produced a stop that must not exist. Nothing in tools/testing/selftests covered this.

Detecting the extra stop is less direct than it looks. The report is emitted with step=1, so ptrace_report_syscall_exit() dispatches to user_single_step_report() rather than ptrace_report_syscall(), and it arrives as a plain SIGTRAP indistinguishable by signal from the legitimate single-step trap: neither PTRACE_O_TRACESYSGOOD nor PTRACE_GET_SYSCALL_INFO separates the two. What does separate them is the PC. The redundant report fires before the tracee has moved past the syscall instruction, so it lands on the PC of the syscall-entry stop:

	[0] sig=133 SYSCALL-STOP  pc=0x41ec28
	[1] sig=5   trap          pc=0x41ec28   <-- must not exist
	[2] sig=5   trap          pc=0x41ec2c

A fixed kernel reports pc=0x41ec2c already at stop [1]. The test therefore asserts that the first stop after the syscall-entry stop is at a different PC, which tests the consequence rather than the mechanism and does not depend on how many pseudo-step traps follow.

PTRACE_SYSEMU is used to reach the syscall-entry stop, as single- stepping there costs one stop per instruction and takes roughly 200000 stops. Architectures without a PC accessor here, or without SYSEMU support, skip rather than fail.

Build tested ARCH=arm64 with GCC aarch64-linux-gnu 16.1.0, ARCH=arm with GCC arm-linux-gnueabihf 16.1.0, and ARCH=x86_64 with GCC 16.2.0.

Tests passing on ARCH=arm64 under qemu-system-aarch64, and the same test fails as expected on v7.3-rc2 without the fix[1]:

	# entry stop pc=0x41ec28, next stop sig=5 pc=0x41ec28
	# Expected next_pc (4320296) != entry_pc (4320296)
	not ok 1 sysemu.no_syscall_exit_stop

An AArch32 build of the same test on an arm64 kernel reproduces the failure identically, covering the is_compat_task() path. It also passes on x86_64, which already uses generic entry, so the test does not report a false positive against a correct implementation.

---
 tools/testing/selftests/ptrace/Makefile       |   3 +-
 .../selftests/ptrace/sysemu_singlestep.c      | 176 ++++++++++++++++++
 tools/testing/selftests/ptrace/.gitignore     |   1 +
 3 files changed, 179 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/ptrace/sysemu_singlestep.c
tools/testing/selftests/ptrace/Makefile +2 -1
index c5e0b76ba6ac..5284d79e9b71 100644
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0-only
 CFLAGS += -std=c99 -pthread -Wall $(KHDR_INCLUDES)
 
-TEST_GEN_PROGS := get_syscall_info set_syscall_info peeksiginfo vmaccess get_set_sud
+TEST_GEN_PROGS := get_syscall_info set_syscall_info peeksiginfo vmaccess get_set_sud \
+		  sysemu_singlestep
 
 include ../lib.mk
tools/testing/selftests/ptrace/sysemu_singlestep.c new file +176 -0

176 lines not on this page · raw message

tools/testing/selftests/ptrace/.gitignore +1 -0
index f6be8efd57ea..792c81f804c0 100644
@@ -4,3 +4,4 @@ get_set_sud
 peeksiginfo
 vmaccess
 set_syscall_info
+sysemu_singlestep
Signature · 1 line
2.34.1
Reviewed-by on v1 1/1
#1 lore raw
Quoted 11 lines · Kees Cook

ptrace(2) states that no syscall-exit-stop occurs when a tracee is continued with PTRACE_SYSEMU or PTRACE_SYSEMU_SINGLESTEP. arm64 gated its syscall-exit report on

flags & (_TIF_SYSCALL_TRACE | _TIF_SINGLESTEP)

and ptrace_resume() clears SYSCALL_TRACE for both SYSEMU requests while PTRACE_SYSEMU_SINGLESTEP additionally sets TIF_SINGLESTEP, so the single-step bit alone produced a stop that must not exist. Nothing in tools/testing/selftests covered this.

IOW, this test-case veirifies that in the sequence like the generic code

	step = report_single_step(work);
	if (step || work & SYSCALL_WORK_SYSCALL_TRACE)
		arch_ptrace_report_syscall_exit(regs, step);

report_single_step() returns false, right? SYSCALL_TRACE is cleared by the arch neutral code...

Quoted 9 lines
---
 tools/testing/selftests/ptrace/Makefile       |   3 +-
 .../selftests/ptrace/sysemu_singlestep.c      | 176 ++++++++++++++++++
 tools/testing/selftests/ptrace/.gitignore     |   1 +
 3 files changed, 179 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/ptrace/sysemu_singlestep.c
#2 lore raw
In reply to Kees Cook
Quoted 5 lines · Kees Cook

ptrace(2) states that no syscall-exit-stop occurs when a tracee is continued with PTRACE_SYSEMU or PTRACE_SYSEMU_SINGLESTEP. arm64 gated its syscall-exit report on

flags & (_TIF_SYSCALL_TRACE | _TIF_SINGLESTEP)

Hi Kees,

The original fix description is a bit off: on arm64 there is no extra syscall-exit-stop; instead, there's an additional pseudo single-step injection.

As sashiko pointed out, as commit ac2081cdc4d9 ("arm64: ptrace:
Consistently use pseudo-singlestep exceptions") described, for PTRACE_SYSEMU_SINGLESTEP, arm64 still needs the pseudo single-step because its hardware single-step state machine is fast-forwarded over the SVC instruction.

static void report_syscall_exit(struct pt_regs *regs) {
...

   if (test_thread_flag(TIF_SINGLESTEP)) {
       /*
        * Signal a pseudo-step exception since we are stepping but
        * tracer modifications to the registers may have rewound the
        * state machine.
        */
       ptrace_report_syscall_exit(regs, 1);
          -> user_single_step_report(regs);
    }
}

https://sashiko.dev/#/patchset/20260922035510.1090299-1-ruanjinjie%40huawei.com

Quoted 53 lines

and ptrace_resume() clears SYSCALL_TRACE for both SYSEMU requests while PTRACE_SYSEMU_SINGLESTEP additionally sets TIF_SINGLESTEP, so the single-step bit alone produced a stop that must not exist. Nothing in tools/testing/selftests covered this.

Detecting the extra stop is less direct than it looks. The report is emitted with step=1, so ptrace_report_syscall_exit() dispatches to user_single_step_report() rather than ptrace_report_syscall(), and it arrives as a plain SIGTRAP indistinguishable by signal from the legitimate single-step trap: neither PTRACE_O_TRACESYSGOOD nor PTRACE_GET_SYSCALL_INFO separates the two. What does separate them is the PC. The redundant report fires before the tracee has moved past the syscall instruction, so it lands on the PC of the syscall-entry stop:

	[0] sig=133 SYSCALL-STOP  pc=0x41ec28
	[1] sig=5   trap          pc=0x41ec28   <-- must not exist
	[2] sig=5   trap          pc=0x41ec2c

A fixed kernel reports pc=0x41ec2c already at stop [1]. The test therefore asserts that the first stop after the syscall-entry stop is at a different PC, which tests the consequence rather than the mechanism and does not depend on how many pseudo-step traps follow.

PTRACE_SYSEMU is used to reach the syscall-entry stop, as single- stepping there costs one stop per instruction and takes roughly 200000 stops. Architectures without a PC accessor here, or without SYSEMU support, skip rather than fail.

Build tested ARCH=arm64 with GCC aarch64-linux-gnu 16.1.0, ARCH=arm with GCC arm-linux-gnueabihf 16.1.0, and ARCH=x86_64 with GCC 16.2.0.

Tests passing on ARCH=arm64 under qemu-system-aarch64, and the same test fails as expected on v7.3-rc2 without the fix[1]:

	# entry stop pc=0x41ec28, next stop sig=5 pc=0x41ec28
	# Expected next_pc (4320296) != entry_pc (4320296)
	not ok 1 sysemu.no_syscall_exit_stop

An AArch32 build of the same test on an arm64 kernel reproduces the failure identically, covering the is_compat_task() path. It also passes on x86_64, which already uses generic entry, so the test does not report a false positive against a correct implementation.

---
 tools/testing/selftests/ptrace/Makefile       |   3 +-
 .../selftests/ptrace/sysemu_singlestep.c      | 176 ++++++++++++++++++
 tools/testing/selftests/ptrace/.gitignore     |   1 +
 3 files changed, 179 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/ptrace/sysemu_singlestep.c

[...]

Quoted 10 lines
tools/testing/selftests/ptrace/sysemu_singlestep.c new file +1 -0
+TEST_HARNESS_MAIN
tools/testing/selftests/ptrace/.gitignore +1 -0
index f6be8efd57ea..792c81f804c0 100644
@@ -4,3 +4,4 @@ get_set_sud
 peeksiginfo
 vmaccess
 set_syscall_info
+sysemu_singlestep
Signature · 2 lines
Best regards,
Jinjie
author
#3 lore raw
Quoted 15 lines · Jinjie Ruan
Quoted 5 lines · Kees Cook

ptrace(2) states that no syscall-exit-stop occurs when a tracee is continued with PTRACE_SYSEMU or PTRACE_SYSEMU_SINGLESTEP. arm64 gated its syscall-exit report on

flags & (_TIF_SYSCALL_TRACE | _TIF_SINGLESTEP)

Hi Kees,

The original fix description is a bit off: on arm64 there is no extra syscall-exit-stop; instead, there's an additional pseudo single-step injection.

Quoted 5 lines

As sashiko pointed out, as commit ac2081cdc4d9 ("arm64: ptrace:
Consistently use pseudo-singlestep exceptions") described, for PTRACE_SYSEMU_SINGLESTEP, arm64 still needs the pseudo single-step because its hardware single-step state machine is fast-forwarded over the SVC instruction.

Ah! Okay. So this test should be tweaked to deal with that? Feel free to snag/update this test for the series. We didn't have anything testing syscall exit stops, so it seemed reasonable to get it written.

-Kees

Signature · 1 line
Kees Cook
#4 lore raw
Quoted 16 lines · Kees Cook
Quoted 15 lines · Jinjie Ruan
Quoted 5 lines · Kees Cook

ptrace(2) states that no syscall-exit-stop occurs when a tracee is continued with PTRACE_SYSEMU or PTRACE_SYSEMU_SINGLESTEP. arm64 gated its syscall-exit report on

flags & (_TIF_SYSCALL_TRACE | _TIF_SINGLESTEP)

Hi Kees,

The original fix description is a bit off: on arm64 there is no extra syscall-exit-stop; instead, there's an additional pseudo single-step injection.

Quoted 7 lines
Quoted 5 lines

As sashiko pointed out, as commit ac2081cdc4d9 ("arm64: ptrace:
Consistently use pseudo-singlestep exceptions") described, for PTRACE_SYSEMU_SINGLESTEP, arm64 still needs the pseudo single-step because its hardware single-step state machine is fast-forwarded over the SVC instruction.

Ah! Okay. So this test should be tweaked to deal with that? Feel free to

Yes, I understand that these two cases should be distinguished.

Quoted 2 lines

snag/update this test for the series. We didn't have anything testing syscall exit stops, so it seemed reasonable to get it written.

I agree with adding a syscall exit stop test case to check whether it violates what the man page describes.

At the same time, I think we could also add a test case to verify the problems that may arise from arm64's own missing pseudo single-step.

Quoted 3 lines

-Kees

Signature · 2 lines
Best regards,
Jinjie