selftests/x86: Add shadow stack uprobe CALL test

Add coverage for entry uprobes installed on CALL instructions while user
shadow stack is enabled. The test puts an entry uprobe on a helper whose
first instruction is a relative CALL, then verifies that the call/return
sequence completes without SIGSEGV.

This catches regressions where x86 uprobe CALL emulation updates the
regular user stack but leaves the CET shadow stack stale.

Signed-off-by: David Windsor <dwindsor@gmail.com>
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Link: https://patch.msgid.link/b957039191118c5eba97d01d80c494b859f115a6.1782777969.git.dwindsor@gmail.com
This commit is contained in:
David Windsor 2026-06-29 20:13:34 -04:00 committed by Peter Zijlstra
parent abf08854d2
commit 5166973b20

View File

@ -873,6 +873,86 @@ static int test_uretprobe(void)
return err;
}
/* Keep the CALL first so the function address is exactly the probed CALL. */
extern void uprobe_call_trigger(void);
asm (".pushsection .text\n"
".global uprobe_call_target\n"
".type uprobe_call_target, @function\n"
"uprobe_call_target:\n"
" ret\n"
".size uprobe_call_target, .-uprobe_call_target\n"
".global uprobe_call_trigger\n"
".type uprobe_call_trigger, @function\n"
"uprobe_call_trigger:\n"
" call uprobe_call_target\n"
" ret\n"
".size uprobe_call_trigger, .-uprobe_call_trigger\n"
".popsection\n"
);
/* If CALL emulation misses the shadow stack update, this exits via SIGSEGV. */
static int test_uprobe_call(void)
{
const size_t attr_sz = sizeof(struct perf_event_attr);
const char *file = "/proc/self/exe";
int fd = -1, type, err = 1;
struct perf_event_attr attr;
struct sigaction sa = {};
ssize_t offset;
type = determine_uprobe_perf_type();
if (type < 0) {
if (type == -ENOENT)
printf("[SKIP]\tUprobe on CALL test, uprobes are not available\n");
return 0;
}
offset = get_uprobe_offset(uprobe_call_trigger);
if (offset < 0)
return 1;
sa.sa_sigaction = segv_gp_handler;
sa.sa_flags = SA_SIGINFO;
if (sigaction(SIGSEGV, &sa, NULL))
return 1;
/* Setup entry uprobe through perf event interface. */
memset(&attr, 0, attr_sz);
attr.size = attr_sz;
attr.type = type;
attr.config = 0;
attr.config1 = (__u64)(unsigned long)file;
attr.config2 = offset;
fd = syscall(__NR_perf_event_open, &attr, 0 /* pid */, -1 /* cpu */,
-1 /* group_fd */, PERF_FLAG_FD_CLOEXEC);
if (fd < 0)
goto out;
if (sigsetjmp(jmp_buffer, 1))
goto out;
if (ARCH_PRCTL(ARCH_SHSTK_ENABLE, ARCH_SHSTK_SHSTK))
goto out;
/*
* This either segfaults and goes through sigsetjmp above
* or succeeds and we're good.
*/
uprobe_call_trigger();
printf("[OK]\tUprobe on CALL test\n");
err = 0;
out:
ARCH_PRCTL(ARCH_SHSTK_DISABLE, ARCH_SHSTK_SHSTK);
signal(SIGSEGV, SIG_DFL);
if (fd >= 0)
close(fd);
return err;
}
void segv_handler_ptrace(int signum, siginfo_t *si, void *uc)
{
/* The SSP adjustment caused a segfault. */
@ -1071,6 +1151,12 @@ int main(int argc, char *argv[])
goto out;
}
if (test_uprobe_call()) {
ret = 1;
printf("[FAIL]\tuprobe on CALL test\n");
goto out;
}
return ret;
out: