From: Hoshino Lina <lina@lina.yt> This tests whether kernel-mode SP/IP values leak when __wine_syscall_dispatcher is interrupted by SIGUSR1, and whether NtGetContextThred()/NtSetContextThread() behave properly when interrupted by another thread. --- dlls/ntdll/tests/Makefile.in | 1 + dlls/ntdll/tests/threadctx.c | 246 +++++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 dlls/ntdll/tests/threadctx.c diff --git a/dlls/ntdll/tests/Makefile.in b/dlls/ntdll/tests/Makefile.in index 42d663777ea..177b9c7e35c 100644 --- a/dlls/ntdll/tests/Makefile.in +++ b/dlls/ntdll/tests/Makefile.in @@ -25,6 +25,7 @@ SOURCES = \ testdll.c \ testdll.spec \ thread.c \ + threadctx.c \ threadpool.c \ time.c \ unwind.c \ diff --git a/dlls/ntdll/tests/threadctx.c b/dlls/ntdll/tests/threadctx.c new file mode 100644 index 00000000000..10bc982000d --- /dev/null +++ b/dlls/ntdll/tests/threadctx.c @@ -0,0 +1,246 @@ +/* + * Unit test suite for ntdll thread context behavior + * + * Copyright 2026 Hoshino Lina + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + */ + +#include <stdarg.h> +#include <stdbool.h> + +#include "ntstatus.h" +#define WIN32_NO_STATUS +#include "windef.h" +#include "winbase.h" +#include "winternl.h" +#include "wine/test.h" + +// 90 second max runtime, to avoid winetest timeouts +#define MAX_RUNTIME 90 +#define THREADS 50 +#define STACK_SIZE 0x10000 +#define LOOPS 2000 + +#ifdef __x86_64__ +#define CTX_SP Rsp +#define CTX_IP Rip +#endif +#ifdef __i386__ +#define CTX_SP Esp +#define CTX_IP Eip +#endif +#ifdef __arm__ +#define CTX_SP Sp +#define CTX_IP Pc +#endif +#ifdef __aarch64__ +#define CTX_SP Sp +#define CTX_IP Pc +#endif + +static volatile bool looping = true; + +struct thread { + DWORD tid; + bool stopped; + HANDLE hnd; + HANDLE ready; + CONTEXT ctx; + DWORD64 stack_lo; + DWORD64 stack_hi; + NTSTATUS err; + bool complete; +}; + +struct thread t[THREADS]; + +static HMODULE hntdll; +static HMODULE hKernel32; +static NTSTATUS (WINAPI *pNtGetContextThread)(HANDLE,CONTEXT*); +static NTSTATUS (WINAPI *pNtSetContextThread)(HANDLE,CONTEXT*); +static void (WINAPI *pGetCurrentThreadStackLimits)(PULONG_PTR,PULONG_PTR); + +DWORD WINAPI thread_func(LPVOID lpParameter) +{ + struct thread *self = lpParameter; + + ULONG_PTR lo, hi; + pGetCurrentThreadStackLimits(&lo, &hi); + + self->stack_lo = (DWORD64)lo; + self->stack_hi = (DWORD64)hi; + + SetEvent(self->ready); + + while (looping) { + CONTEXT c; + NTSTATUS status; + + /* Play with the FP context. These exercise the FP restore syscall + * path, which was seen to tickle a particular bug. + */ + c.ContextFlags = CONTEXT_FLOATING_POINT; + status = pNtGetContextThread( GetCurrentThread(), &c ); + if (status) + ok(!status, "Failed to get context: 0x%lx\n", (long)status); + c.ContextFlags = CONTEXT_FLOATING_POINT; + status = pNtSetContextThread( GetCurrentThread(), &c ); + if (status) + ok(!status, "Failed to set context: 0x%lx\n", (long)status); + } + + self->complete = true; + CloseHandle(self->ready); + + return 0; +} + +static bool is_pe_map(DWORD64 addr) +{ + /* This check only makes sense on Wine, where fake kernel mode exists. */ + if (!winetest_platform_is_wine) + return true; + + /* See server/mapping.c for the upper limits. */ + return (addr >= 0x60000000 && addr < 0x7c000000) || + (addr >= 0x600000000000 && addr < 0x700000000000) || +#ifdef _WIN64 + (addr >= 0x140000000 && addr < 0x141000000); +#else + (addr >= 0x400000 && addr < 0x1400000); +#endif +} + +static void test_context_sp(void) +{ + int i; + int loop; + bool failed_sp = false; + bool failed_ip = false; + DWORD timeout = GetTickCount() + MAX_RUNTIME * 1000; + hntdll = GetModuleHandleA("ntdll.dll"); + hKernel32 = GetModuleHandleA("kernel32.dll"); + +#define X(f) p##f = (void*)GetProcAddress(hntdll, #f) + X(NtGetContextThread); + X(NtSetContextThread); +#undef X +#define X(f) p##f = (void*)GetProcAddress(hKernel32, #f) + X(GetCurrentThreadStackLimits); +#undef X + + if (!pNtGetContextThread) { + win_skip("NtGetContextThread not available.\n"); + return; + } + if (!pNtSetContextThread) { + win_skip("NtSetContextThread not available.\n"); + return; + } + if (!pGetCurrentThreadStackLimits) { + win_skip("GetCurrentThreadStackLimits not available.\n"); + return; + } + + trace("Starting %d threads\n", THREADS); + for (i = 0; i < THREADS; i++) { + t[i].ready = CreateEventA(NULL, TRUE, FALSE, NULL); + ok(!!t[i].ready, "Failed to create event\n"); + if (!t[i].ready) { + looping = false; + break; + } + + t[i].hnd = CreateThread(0, STACK_SIZE, thread_func, (LPVOID)&t[i], 0, + &t[i].tid); + ok(!!t[i].hnd, "Failed to create thread\n"); + if (!t[i].hnd) { + looping = false; + break; + } + + WaitForSingleObject(t[i].ready, INFINITE); + } + trace("Started %d threads\n", i); + + trace("Starting %d loops of thread context fetching\n", LOOPS); + + for (loop = 0; looping && loop < LOOPS && GetTickCount() < timeout; + loop++) { + for (int i = 0; i < THREADS; i++) { + if (SuspendThread(t[i].hnd) != (DWORD)-1) { + t[i].ctx.ContextFlags = CONTEXT_INTEGER | CONTEXT_CONTROL; + + if (GetThreadContext(t[i].hnd, &t[i].ctx)) { + bool in_sp_range = t[i].ctx.CTX_SP > t[i].stack_lo && + t[i].ctx.CTX_SP <= t[i].stack_hi; + bool in_ip_range = is_pe_map(t[i].ctx.CTX_IP); + + if (!in_sp_range) { + trace("[%d/%d:%d] SP=0x%llx [%llx..%llx]\n", loop, + LOOPS, i, (long long)t[i].ctx.CTX_SP, + (long long)t[i].stack_lo, + (long long)t[i].stack_hi); + failed_sp = true; + } + + if (!in_ip_range) { + trace("[%d/%d:%d] IP=0x%llx\n", loop, LOOPS, i, + (long long)t[i].ctx.CTX_IP); + failed_ip = true; + } + + t[i].stopped = true; + } else { + ResumeThread(t[i].hnd); + } + } + } + + for (int i = 0; i < THREADS; i++) { + if (t[i].stopped) + ResumeThread(t[i].hnd); + + t[i].stopped = false; + } + + if (failed_sp && failed_ip) + break; + } + + trace("Completed %d/%d loops\n", loop, LOOPS); + + looping = false; + + for (int i = 0; i < THREADS; i++) + WaitForSingleObject(t[i].hnd, INFINITE); + + ok(!failed_sp, "Invalid SP value detected\n"); + + /* This is known broken on i386 */ +#ifdef __i386__ + todo_wine +#endif + ok(!failed_ip, "Invalid IP value detected\n"); + + for (int i = 0; i < THREADS; i++) + ok(t[i].complete, "Thread %d died unexpectedly\n", i); +} + +START_TEST(threadctx) { + test_context_sp(); +} -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10419