tests: another thread's context with its x87/SSE state, on W64 and W3

tests/wine64's unixctx case: a thread with known xmm6-xmm15 inside a Unix
call (ws2_32's gethostname), then in guest code, suspended 2000 times each
and read with CONTEXT_FLOATING_POINT; each phase ends with a
SetThreadContext changing xmm15, which the thread must see. tests/w3/
suspctx.sh: the same in guest code on W3, xmm0-xmm7, MXCSR and
CONTEXT_EXTENDED_REGISTERS, natively and in the page's runtime.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-25 10:29:40 -04:00
1 parent 52d2b58cca
commit 865c1b20d2
4 files changed
+326

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
/* Another thread's context, with its x87/SSE state, while the thread runs
* guest code (H: W3's suspend path published only the integer, control and
* segment registers, so the floating-point state came back as zeros). A
* thread spins in a loop that touches no FP register, with xmm0-xmm7 and
* MXCSR holding known values; another suspends it again and again and reads
* its context with CONTEXT_FLOATING_POINT | CONTEXT_EXTENDED_REGISTERS.
* Every context must show those values. Last, a SetThreadContext changes
* xmm7 in a suspended thread, and the thread must see the new value. */
#include <windows.h>
#include <stdio.h>
#include <string.h>
static int failures;
static void check(int ok, const char *what, double got)
{
if (ok) printf("ok %s (%.0f)\n", what, got);
else { printf("FAIL %s (%.0f)\n", what, got); failures++; }
}
/* spin(regs, stop, count): xmm0-xmm7 = regs[0..7], MXCSR = regs[16]'s
* first dword, then count++ until *stop, then regs[8..15] = xmm0-xmm7. */
void __cdecl spin(unsigned char *regs, volatile LONG *stop, volatile LONG *count);
__asm__(
".globl _spin\n"
"_spin:\n"
" push %ebx\n push %esi\n push %edi\n"
" mov 16(%esp), %ebx\n mov 20(%esp), %esi\n mov 24(%esp), %edi\n"
" movdqu 0(%ebx), %xmm0\n movdqu 16(%ebx), %xmm1\n movdqu 32(%ebx), %xmm2\n"
" movdqu 48(%ebx), %xmm3\n movdqu 64(%ebx), %xmm4\n movdqu 80(%ebx), %xmm5\n"
" movdqu 96(%ebx), %xmm6\n movdqu 112(%ebx), %xmm7\n"
" ldmxcsr 256(%ebx)\n"
"1:\n"
" lock incl (%edi)\n"
" mov (%esi), %eax\n test %eax, %eax\n jz 1b\n"
" movdqu %xmm0, 128(%ebx)\n movdqu %xmm1, 144(%ebx)\n movdqu %xmm2, 160(%ebx)\n"
" movdqu %xmm3, 176(%ebx)\n movdqu %xmm4, 192(%ebx)\n movdqu %xmm5, 208(%ebx)\n"
" movdqu %xmm6, 224(%ebx)\n movdqu %xmm7, 240(%ebx)\n"
" stmxcsr 260(%ebx)\n"
" mov $0x1f80, %eax\n mov %eax, 264(%ebx)\n ldmxcsr 264(%ebx)\n"
" pop %edi\n pop %esi\n pop %ebx\n ret\n");
static unsigned char regs[272];
static volatile LONG stop, count;
static DWORD WINAPI spinner(void *arg)
{
(void)arg;
spin(regs, &stop, &count);
return 0;
}
int main(void)
{
HANDLE t;
CONTEXT c;
unsigned char want7[16];
int i, fp = 0, xr = 0, bad = 0, taken = 0, set = 0;
const DWORD mxcsr = 0x9fc0; /* FTZ, DAZ, every exception masked */
for (i = 0; i < 128; i++) regs[i] = (unsigned char)(i * 37 + 11);
memcpy(regs + 256, &mxcsr, 4);
t = CreateThread(NULL, 0, spinner, NULL, 0, NULL);
while (count < 1000) Sleep(1);
for (i = 0; i < 2000; i++)
{
memset(&c, 0, sizeof c);
c.ContextFlags = CONTEXT_FULL | CONTEXT_FLOATING_POINT | CONTEXT_EXTENDED_REGISTERS;
if (SuspendThread(t) == (DWORD)-1) break;
if (GetThreadContext(t, &c))
{
const BYTE *fx = c.ExtendedRegisters; /* FXSAVE: MXCSR at 24, xmm0 at 160 */
DWORD mx;
memcpy(&mx, fx + 24, 4);
taken++;
if (c.ContextFlags & CONTEXT_FLOATING_POINT & ~CONTEXT_i386) fp++;
if (c.ContextFlags & CONTEXT_EXTENDED_REGISTERS & ~CONTEXT_i386)
{
xr++;
if (memcmp(fx + 160, regs, 128) || mx != mxcsr)
{
if (bad < 4) printf("bad %d: mxcsr %#lx\n", i, (unsigned long)mx);
bad++;
}
}
}
ResumeThread(t);
if (i % 16 == 0) Sleep(0);
}
/* xmm7 changed in the suspended thread */
for (i = 0; i < 16; i++) want7[i] = (unsigned char)(0xa5 ^ i);
memset(&c, 0, sizeof c);
c.ContextFlags = CONTEXT_FULL | CONTEXT_EXTENDED_REGISTERS;
if (SuspendThread(t) != (DWORD)-1 && GetThreadContext(t, &c))
{
memcpy(c.ExtendedRegisters + 160 + 7 * 16, want7, 16);
c.ContextFlags = CONTEXT_EXTENDED_REGISTERS;
set = SetThreadContext(t, &c);
ResumeThread(t);
}
InterlockedExchange(&stop, 1);
WaitForSingleObject(t, INFINITE);
check(taken == 2000, "contexts taken", taken);
check(fp == taken && xr == taken, "contexts with the floating-point state", xr);
check(bad == 0, "of them showing other xmm0-xmm7 or MXCSR than the thread's", bad);
check(set && !memcmp(regs + 128, regs, 112), "xmm0-xmm6 unchanged", count);
check(set && !memcmp(regs + 240, want7, 16), "xmm7 as SetThreadContext set it", set);
check(!memcmp(regs + 260, &mxcsr, 4), "MXCSR unchanged", 0);
printf("suspctx %d failures\n", failures);
return failures != 0;
}
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
# Another thread's context, with its x87/SSE state, while the thread runs
# guest code (suspctx.c): GetThreadContext's xmm0-xmm7 and MXCSR, and a
# SetThreadContext's xmm7, on a suspended thread. Prints "suspctx 0 failures".
# Natively (gexc run) and in the page's runtime under Node (run-node.mjs).
#
# tests/w3/suspctx.sh DIST PREFIX [gexc]
#
# DIST is a W3 Wine install, PREFIX a Wine prefix made for it (copied, never
# written). Needs i686-w64-mingw32-gcc, the x86 shared-memory shim and seed
# in web/public/gex and, for the native legs, an x86 gexc.
set -u
here=$(cd "$(dirname "$0")" && pwd)
root=$(cd "$here/../.." && pwd)
dist=$(realpath "${1:?DIST}"); prefix=$(realpath "${2:?PREFIX}")
gexc=$(realpath -m "${3:-$root/target/x86/release/gexc}")
w=${WORK:-$root/target/w3-suspctx}
rm -rf "$w"; mkdir -p "$w"
i686-w64-mingw32-gcc -O1 -o "$w/suspctx.exe" "$here/suspctx.c" || exit 1
want="suspctx 0 failures"
pass=0 fail=0
verdict() { # NAME LOG
if grep -q "^$want" "$2"; then pass=$((pass + 1)); echo "ok $1"
else fail=$((fail + 1)); echo "FAIL $1"; grep -v '^\[host\]\|gex-console-error' "$2" | tail -8 | sed 's/^/ /'; fi
}
for mode in fast; do
extra=()
if [ -x "$gexc" ]; then
rm -rf "$w/pfx-native"; cp -a "$prefix" "$w/pfx-native"; cp "$w/suspctx.exe" "$w/pfx-native/drive_c/"
(cd "$w/pfx-native/drive_c" && timeout -k 10 "${TMO:-300}" "$gexc" run \
--env WINEPREFIX="$w/pfx-native" --env WINEDEBUG=err+all,fixme-all "${extra[@]}" \
"$dist/lib/wine/i386-unix/wine" 'C:\suspctx.exe') > "$w/native-$mode.log" 2>&1
verdict "native $mode" "$w/native-$mode.log"
else
echo "skip native: no $gexc"
fi
for f in gex_browser.x86.shared.wasm seed.x86.shared.wasm; do
[ -f "$root/web/public/gex/$f" ] || { echo "skip page: no web/public/gex/$f"; continue 2; }
done
rm -rf "$w/pfx-page" "$w/t"; cp -a "$prefix" "$w/pfx-page"; mkdir -p "$w/t"
(cd "$root" && GEX_ARCH=x86 GEX_SHARED=1 timeout -k 10 "${TMO:-300}" node --max-old-space-size=8192 \
shims/browser/js/run-node.mjs --dir /w3="$dist" --dir /pfx="$w/pfx-page" --dir /t="$w/t" \
--file "/t/suspctx.exe=$w/suspctx.exe" --env WINEPREFIX=/pfx --env WINEDEBUG=err+all,fixme-all "${extra[@]}" \
--cwd /t --argv0 /w3/lib/wine/i386-unix/wine web/public/gex/seed.x86.shared.wasm \
"$dist/lib/wine/i386-unix/wine" /t/suspctx.exe) > "$w/page-$mode.log" 2>&1
verdict "page $mode" "$w/page-$mode.log"
done
echo "$pass passed, $fail failed"
[ "$fail" = 0 ]
+12
View File
@@ -38,6 +38,8 @@
# The audio case: waveOut and mmdevapi through wineoss.drv (audio.c).
# The authattrs case: crypt32 verifies a signature over authenticated
# attributes as encoded, not in DER order (authattrs.c).
# The unixctx case: another thread's context, with its SSE state, while the
# thread is inside a Unix call and while it runs guest code (unixctx.c).
# The qpcmono case: the performance counter, monotonic across the PE and
# Unix sides (qpcmono.c).
# The dwrite case: DirectWrite's glyph bounds, alpha texture and outline
@@ -426,6 +428,16 @@ else
case " ${CASES:-qpcmono} " in *" qpcmono "*)
fail=$((fail + 1)); echo "FAIL x64/qpcmono ($tag): x86_64-w64-mingw32-gcc" ;; esac
fi
# Another thread's context taken while the thread is inside a Unix call, and
# while it runs guest code (unixctx.c): its xmm6-xmm15 as the thread has them,
# though the Unix call's frame leaves the x87/SSE state in gex's context until
# something reads it, and a SetThreadContext's xmm15 reaching the thread.
if x86_64-w64-mingw32-gcc -O1 -o "$w/unixctx.exe" "$here/unixctx.c" -lws2_32; then
check unixctx 0 "$allok" "$w/unixctx.exe"
else
case " ${CASES:-unixctx} " in *" unixctx "*)
fail=$((fail + 1)); echo "FAIL x64/unixctx ($tag): x86_64-w64-mingw32-gcc" ;; esac
fi
# A skeleton's view unmapped and its addresses reused for the program's own
# code (stalebind.c): the native bindings must not outlive the view. Only
# with the native PE core, whose DLLs are skeletons (NATIVE=).
+154
View File
@@ -0,0 +1,154 @@
/* Another thread's context, taken while it is inside a Unix call (H, B1:
* the Unix call's frame leaves the x87/SSE state in gex's context until
* something reads it). A thread loops on gethostname -- ws2_32's Unix call
* -- with xmm6-xmm15 holding known values, which it never changes (they are
* callee-saved); another suspends it again and again and reads its context
* with CONTEXT_FLOATING_POINT. Every context must carry the floating-point
* state and show those values, and the loop must end with them unchanged.
* Then the same with a loop that makes no call (guest code: the suspend
* signal's own context). Each phase ends with a SetThreadContext changing
* xmm15, which the thread must see. */
#include <winsock2.h>
#include <windows.h>
#include <stdio.h>
#include <string.h>
static int failures;
static void check(int ok, const char *what, double got)
{
if (ok) printf("ok %s (%.0f)\n", what, got);
else { printf("FAIL %s (%.0f)\n", what, got); failures++; }
}
/* spin(regs, stop, fn, buf): xmm6-xmm15 = regs[0..9], then fn(buf, 256)
* (or, with no fn, a count of calls_started) until *stop, then
* regs[10..19] = xmm6-xmm15. */
void spin(unsigned char *regs, volatile LONG *stop, int (WINAPI *fn)(char *, int), char *buf);
__asm__(
".globl spin\n"
"spin:\n"
" push %rbx\n push %rsi\n push %rdi\n push %r12\n"
" sub $200, %rsp\n"
" movdqu %xmm6, 32(%rsp)\n movdqu %xmm7, 48(%rsp)\n movdqu %xmm8, 64(%rsp)\n"
" movdqu %xmm9, 80(%rsp)\n movdqu %xmm10, 96(%rsp)\n movdqu %xmm11, 112(%rsp)\n"
" movdqu %xmm12, 128(%rsp)\n movdqu %xmm13, 144(%rsp)\n movdqu %xmm14, 160(%rsp)\n"
" movdqu %xmm15, 176(%rsp)\n"
" mov %rcx, %rbx\n mov %rdx, %rsi\n mov %r8, %rdi\n mov %r9, %r12\n"
" movdqu 0(%rbx), %xmm6\n movdqu 16(%rbx), %xmm7\n movdqu 32(%rbx), %xmm8\n"
" movdqu 48(%rbx), %xmm9\n movdqu 64(%rbx), %xmm10\n movdqu 80(%rbx), %xmm11\n"
" movdqu 96(%rbx), %xmm12\n movdqu 112(%rbx), %xmm13\n movdqu 128(%rbx), %xmm14\n"
" movdqu 144(%rbx), %xmm15\n"
"1:\n"
" test %rdi, %rdi\n jz 2f\n"
" mov %r12, %rcx\n mov $256, %edx\n call *%rdi\n"
" jmp 3f\n"
"2:\n lock incl calls_started(%rip)\n"
"3:\n"
" mov (%rsi), %eax\n test %eax, %eax\n jz 1b\n"
" movdqu %xmm6, 160(%rbx)\n movdqu %xmm7, 176(%rbx)\n movdqu %xmm8, 192(%rbx)\n"
" movdqu %xmm9, 208(%rbx)\n movdqu %xmm10, 224(%rbx)\n movdqu %xmm11, 240(%rbx)\n"
" movdqu %xmm12, 256(%rbx)\n movdqu %xmm13, 272(%rbx)\n movdqu %xmm14, 288(%rbx)\n"
" movdqu %xmm15, 304(%rbx)\n"
" movdqu 32(%rsp), %xmm6\n movdqu 48(%rsp), %xmm7\n movdqu 64(%rsp), %xmm8\n"
" movdqu 80(%rsp), %xmm9\n movdqu 96(%rsp), %xmm10\n movdqu 112(%rsp), %xmm11\n"
" movdqu 128(%rsp), %xmm12\n movdqu 144(%rsp), %xmm13\n movdqu 160(%rsp), %xmm14\n"
" movdqu 176(%rsp), %xmm15\n"
" add $200, %rsp\n pop %r12\n pop %rdi\n pop %rsi\n pop %rbx\n ret\n");
static unsigned char regs[320];
static volatile LONG stop;
static char host[256];
volatile LONG calls_started;
static int WINAPI counted_gethostname(char *buf, int len)
{
InterlockedIncrement(&calls_started);
return gethostname(buf, len);
}
static DWORD WINAPI spinner(void *arg)
{
spin(regs, &stop, arg, host);
return 0;
}
/* one phase: the spinner with FN (in the Unix call) or without (guest code),
* 2000 suspended contexts, then xmm15 changed by SetThreadContext */
static void phase(const char *where, int (WINAPI *fn)(char *, int))
{
unsigned char want15[16];
char what[128];
HANDLE t;
CONTEXT c;
int i, fp = 0, bad = 0, taken = 0, set = 0;
stop = 0; calls_started = 0;
memset(regs + 160, 0, 160);
t = CreateThread(NULL, 0, spinner, fn, 0, NULL);
while (calls_started < 100) Sleep(1);
for (i = 0; i < 2000; i++)
{
memset(&c, 0, sizeof c);
c.ContextFlags = CONTEXT_FULL | CONTEXT_FLOATING_POINT;
if (SuspendThread(t) == (DWORD)-1) break;
if (GetThreadContext(t, &c))
{
taken++;
if ((c.ContextFlags & CONTEXT_FLOATING_POINT & ~CONTEXT_AMD64))
{
fp++;
if (memcmp(&c.Xmm6, regs, 160))
{
HMODULE m = NULL;
char name[MAX_PATH] = "?";
GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(void *)c.Rip, &m);
if (m) GetModuleFileNameA(m, name, sizeof name);
if (bad < 4) printf("bad %d: at %s+%#llx, mxcsr %#lx\n", i, strrchr(name, '\\') ? strrchr(name, '\\') + 1 : name,
(unsigned long long)(c.Rip - (DWORD64)m), (unsigned long)c.MxCsr);
bad++;
}
}
}
ResumeThread(t);
if (i % 16 == 0) Sleep(0);
}
for (i = 0; i < 16; i++) want15[i] = (unsigned char)(0xa5 ^ i);
memset(&c, 0, sizeof c);
c.ContextFlags = CONTEXT_FULL | CONTEXT_FLOATING_POINT;
if (SuspendThread(t) != (DWORD)-1 && GetThreadContext(t, &c))
{
memcpy(&c.Xmm15, want15, 16);
c.ContextFlags = CONTEXT_FLOATING_POINT;
set = SetThreadContext(t, &c);
ResumeThread(t);
}
InterlockedExchange(&stop, 1);
WaitForSingleObject(t, INFINITE);
CloseHandle(t);
sprintf(what, "%s: contexts taken", where);
check(taken == 2000, what, taken);
sprintf(what, "%s: contexts with the floating-point state", where);
check(fp == taken, what, fp);
sprintf(what, "%s: of them showing other xmm6-xmm15 than the thread's", where);
check(bad == 0, what, bad);
sprintf(what, "%s: xmm6-xmm14 unchanged across the loop", where);
check(!memcmp(regs + 160, regs, 144), what, calls_started);
sprintf(what, "%s: xmm15 as SetThreadContext set it", where);
check(set && !memcmp(regs + 304, want15, 16), what, set);
}
int main(void)
{
WSADATA wsa;
int i;
WSAStartup(MAKEWORD(2, 2), &wsa);
for (i = 0; i < 160; i++) regs[i] = (unsigned char)(i * 37 + 11);
check(gethostname(host, sizeof host) == 0, "gethostname works", 0);
phase("unix call", counted_gethostname);
phase("guest code", NULL);
printf("%d failures\n", failures);
return failures != 0;
}