[PATCH v7 0/3] MR10232: ntdll: Avoid exposing non-user rsp in normal syscall return path
When rsp is outside the kernel stack, the SIGUSR1 handler considers the context "out of syscall" and reports the real register values to wineserver. rsp pointing into the syscall frame is used for iret returns. Commit 0a5f7a710365 moved this earlier in the return path, creating a window in the regular direct return path where rsp points neither to the kernel stack, nor to the expected user stack. When a thread is interrupted by SIGUSR1 within this window, this exposes a bogus stack pointer to GetThreadContext(), breaking the garbage collector in Unity applications. Fix the regression by first switching to the real user stack (needed so we don't break things by touching other registers), then, only if an iret path is coming up, switching to the syscall frame pointer. This fixes the regression introduced by 0a5f7a710365, since GetThreadContext() will now observe the proper stack pointer during regular syscall returns. Further work would be needed to solve other corner cases and ensure that the register context returned by GetThreadContext() behaves exactly as on Windows (with no kernel-mode context info ever returned), but that is a larger/more invasive change. Fixes: 0a5f7a710365 ("ntdll: Switch to the user stack before restoring the %fs register.") Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59333 Signed-off-by: Hoshino Lina lina@lina.yt -- v7: gitlab: Run ntdll tests with win64 architecture. ntdll/tests: Add a test for bogus SP values from GetThreadContext(). https://gitlab.winehq.org/wine/wine/-/merge_requests/10232
From: Hoshino Lina <lina@lina.yt> When rsp is outside the kernel stack, the SIGUSR1 handler considers the context "out of syscall" and reports the real register values to wineserver. rsp pointing into the syscall frame is used for iret returns. Commit 0a5f7a710365 moved this earlier in the return path, creating a window in the regular direct return path where rsp points neither to the kernel stack, nor to the expected user stack. When a thread is interrupted by SIGUSR1 within this window, this exposes a bogus stack pointer to GetThreadContext(), breaking the garbage collector in Unity applications. Fix the regression by first switching to the real user stack (needed so we don't break things by touching other registers), then, only if an iret path is coming up, switching to the syscall frame pointer. This fixes the regression introduced by 0a5f7a710365, since GetThreadContext() will now observe the proper stack pointer during regular syscall returns. Further work would be needed to solve other corner cases and ensure that the register context returned by GetThreadContext() behaves exactly as on Windows (with no kernel-mode context info ever returned), but that is a larger/more invasive change. Fixes: 0a5f7a710365 ("ntdll: Switch to the user stack before restoring the %fs register.") Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59333 Signed-off-by: Hoshino Lina <lina@lina.yt> --- dlls/ntdll/unix/signal_x86_64.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/dlls/ntdll/unix/signal_x86_64.c b/dlls/ntdll/unix/signal_x86_64.c index 0c62e8a1ad4..db0a3972ef9 100644 --- a/dlls/ntdll/unix/signal_x86_64.c +++ b/dlls/ntdll/unix/signal_x86_64.c @@ -3172,7 +3172,18 @@ __ASM_GLOBAL_FUNC( __wine_syscall_dispatcher, /* push rbp-based kernel stack cfi */ __ASM_CFI(".cfi_remember_state\n\t") __ASM_CFI_CFA_IS_AT2(rcx, 0xa8, 0x01) /* frame->syscall_cfa */ - "leaq 0x70(%rcx),%rsp\n\t" /* %rsp > frame means no longer inside syscall */ + + /* switch to user stack */ + /* %rsp outside kernel stack means no longer inside syscall */ + "movq 0x88(%rcx),%rsp\n\t" + + "movl 0xb4(%rcx),%edx\n\t" /* frame->restore_flags */ + "testl $0x1,%edx\n\t" /* CONTEXT_CONTROL | CONTEXT_INTEGER */ + "jz 1f\n\t" + /* Set up rsp for iretq return paths below */ + "leaq 0x70(%rcx),%rsp\n\t" + "1:\n\t" + #ifdef __linux__ "movw 0x338(%r13),%dx\n" /* amd64_thread_data()->fs */ "testw %dx,%dx\n\t" -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10232
From: Hoshino Lina <lina@lina.yt> --- dlls/ntdll/tests/Makefile.in | 1 + dlls/ntdll/tests/threadctx.c | 148 +++++++++++++++++++++++++++++++++++ 2 files changed, 149 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..75c13e683e4 --- /dev/null +++ b/dlls/ntdll/tests/threadctx.c @@ -0,0 +1,148 @@ +/* + * 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" + +// 110 second max runtime, to avoid winetest timeouts +#define MAX_RUNTIME 110 +#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; + CONTEXT ctx; + DWORD64 stack_lo; + DWORD64 stack_hi; +}; + +struct thread t[THREADS]; + +DWORD WINAPI thread_func(LPVOID lpParameter) +{ + struct thread *self = lpParameter; + + ULONG_PTR lo, hi; + GetCurrentThreadStackLimits(&lo, &hi); + + self->stack_lo = (DWORD64)lo; + self->stack_hi = (DWORD64)hi; + + while (looping) + Sleep(0); + + return 0; +} + +static void test_context_sp(void) +{ + int loop; + bool failed = false; + DWORD timeout = GetTickCount() + MAX_RUNTIME * 1000; + + for (int i = 0; i < THREADS; i++) { + t[i].hnd = CreateThread(0, STACK_SIZE, thread_func, (LPVOID)&t[i], 0, + &t[i].tid); + ok(!!t[i].hnd, "Failed to create thread"); + if (!t[i].hnd) { + looping = false; + failed = true; + break; + } + } + + Sleep(100); + + trace("Starting %d loops of thread context fetching", LOOPS); + + for (loop = 0; !failed && 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_range = t[i].ctx.CTX_SP > t[i].stack_lo && + t[i].ctx.CTX_SP <= t[i].stack_hi; + ok(in_range, + "[%d/%d] SP=0x%llx [%llx..%llx] " + "RIP=0x%llx", + loop, LOOPS, (long long)t[i].ctx.CTX_SP, + (long long)t[i].stack_lo, (long long)t[i].stack_hi, + (long long)t[i].ctx.CTX_IP); + + if (!in_range) + failed = 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; + } + } + + trace("Completed %d/%d loops", loop, LOOPS); + + looping = false; + + for (int i = 0; i < THREADS; i++) + WaitForSingleObject(t[i].hnd, INFINITE); +} + +START_TEST(threadctx) { + test_context_sp(); +} -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10232
From: Hoshino Lina <lina@lina.yt> --- tools/gitlab/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/gitlab/test.yml b/tools/gitlab/test.yml index db77ad5111d..70d5e0afbbb 100644 --- a/tools/gitlab/test.yml +++ b/tools/gitlab/test.yml @@ -62,7 +62,7 @@ test-linux-64: extends: .wine-test variables: - INCLUDE_TESTS: "dinput ntoskrnl.exe" + INCLUDE_TESTS: "dinput ntdll ntoskrnl.exe" rules: - if: $CI_PIPELINE_SOURCE == 'merge_request_event' needs: -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10232
Added a test. The change affects x86_64, but since the gitlab test suite largely runs on i386, it's not very useful. I added at least `ntdll` to the 64bit test runner, investigating the `ntdll:exception` failure now. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10232#note_131059
participants (2)
-
Hoshino Lina -
Hoshino Lina (@hoshinolina)