Title
Wine SEH exceptions report wrong access type (write instead of read) for PAGE_NOACCESS/PAGE_READONLY faults
Environment
- box86 version:
Box86 with Dynarec v0.3.9 0579f8b9 (built May 18 2026)
- Wine version tested: 5.0 (also confirmed on Wine 11.0)
- Host: RK3566 (Cortex-A55), 64-bit ARM Linux (Rocknix)
- Confirmed NOT present on native x86_64 (same Wine version, no box86/box64 involved)
Summary
When a 32-bit Windows program running under Wine+box86 accesses a page that
was allocated with PAGE_NOACCESS, the resulting EXCEPTION_ACCESS_VIOLATION
record's ExceptionInformation[0] field (the standard Win32 access-type flag:
0 = read fault, 1 = write fault, 8 = execute fault — see
EXCEPTION_RECORD docs)
is reported as 1 (write fault) even when the actual operation was a plain
read. On native x86_64 (no box86/box64), the exact same test program
correctly reports 0 (read fault).
This causes real breakage in any Windows program that uses this fairly common
technique: reserve a large region as PAGE_NOACCESS/guard pages, then use a
vectored exception handler or __try/__except to lazily commit and
fill each page on first touch (a legitimate, deliberate demand-paging
pattern used by several older-generation game engines to avoid reading
whole resource files up front). Because the handler receives the wrong
access-type flag, it can no longer distinguish "a page I expect to lazily
fault in" from "a genuine write to a read-only page," and its fault-recovery
logic takes the wrong branch — in the case that led me to this, the
handler gives up and unwinds instead of committing the page and retrying,
which the game then reports as a fatal resource-read error.
Minimal reproduction
No copyrighted software needed — this is a ~70-line, self-contained Win32
program. Compile with i686-w64-mingw32-gcc -O0 -o segvtest.exe segvtest.c
(or MSVC), run under Wine on box86 vs. native x86_64 Wine for comparison.
#include <windows.h>
#include <stdio.h>
static DWORD g_accessType = 0xFFFFFFFF;
static DWORD g_exceptionCode = 0;
static int g_handled = 0;
static void *g_page = NULL;
LONG CALLBACK VectoredHandler(EXCEPTION_POINTERS *ep)
{
EXCEPTION_RECORD *rec = ep->ExceptionRecord;
if (rec->ExceptionCode != EXCEPTION_ACCESS_VIOLATION)
return EXCEPTION_CONTINUE_SEARCH;
g_exceptionCode = rec->ExceptionCode;
if (rec->NumberParameters >= 1)
g_accessType = (DWORD)rec->ExceptionInformation[0];
g_handled = 1;
DWORD oldProtect;
VirtualProtect(g_page, 4096, PAGE_READONLY, &oldProtect);
return EXCEPTION_CONTINUE_EXECUTION;
}
int main(void)
{
setvbuf(stdout, NULL, _IONBF, 0);
g_page = VirtualAlloc(NULL, 4096, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS);
printf("Allocated PAGE_NOACCESS page at %p\n", g_page);
PVOID handle = AddVectoredExceptionHandler(1, VectoredHandler);
volatile unsigned char value;
printf("Attempting READ from PAGE_NOACCESS page...\n");
value = *(volatile unsigned char *)g_page;
if (!g_handled) {
printf("ERROR: read did not fault! value=%d\n", value);
} else {
printf("Caught exception 0x%08lx\n", g_exceptionCode);
printf("ExceptionInformation[0] (access type) = %lu\n", g_accessType);
if (g_accessType == 0)
printf("CORRECT: reported as EXCEPTION_READ_FAULT (0)\n");
else if (g_accessType == 1)
printf("*** BUG: reported as EXCEPTION_WRITE_FAULT (1) for a READ access! ***\n");
}
RemoveVectoredExceptionHandler(handle);
VirtualFree(g_page, 0, MEM_RELEASE);
printf("done.\n");
return 0;
}
Actual output (box86, ARM64/Rocknix)
Allocated PAGE_NOACCESS page at 003F0000
Attempting READ from PAGE_NOACCESS page...
Caught exception 0xc0000005
ExceptionInformation[0] (access type) = 1
*** BUG: reported as EXCEPTION_WRITE_FAULT (1) for a READ access! ***
Expected output (matches native x86_64 Wine, same Wine version, no box86)
Allocated PAGE_NOACCESS page at 00340000
Attempting READ from PAGE_NOACCESS page...
Caught exception 0xc0000005
ExceptionInformation[0] (access type) = 0
CORRECT: reported as EXCEPTION_READ_FAULT (0)
Additional context from a real-world case
I hit this while getting a 2003-era Windows game running under Wine+box86.
The game reserves a resource-file region as PAGE_NOACCESS and installs its
own SEH handler to lazily commit + ReadFile() one page at a time on first
touch — a legitimate technique, and I confirmed via WINEDEBUG=+file,+virtual,+seh
tracing that it works correctly step-by-step on native x86_64 Wine (each
fault is serviced: commit page → ReadFile 4096 bytes → mark read-only →
resume). Under box86, the very first fault for this region reports
ExceptionInformation[0]=1 instead of 0, the game's handler treats it as
an unrecoverable error, and the game surfaces a (misleading) "resource file
corrupt or unreadable" dialog. BOX86_SHOWSEGV=1 independently confirms
box86 itself sees code=2/prot=0 (SEGV_ACCERR, consistent with a page
that has zero permissions) at the correct fault address — so box86 does
correctly identify that a fault occurred and where, but appears to
mis-derive the read/write access-type bit when constructing the exception
info handed to the guest program.
I'd guess this lives in whatever code path maps the host ARM64 Data Abort
exception's ESR_EL1/WnR (write-not-read) bit — or however box86
determines read vs. write on the ARM side — into the x86-style page-fault
error-code bit that Wine's own ntdll unix-side signal handler reads to
populate ExceptionInformation[0]. I don't have a fix, but I have both
the minimal repro above and box86/box64 tuning flags I ruled out along the
way (this is not a dynarec-correctness issue — reproduces identically with
BOX86_DYNAREC=0, pure interpreter mode) if that narrows things down.
Happy to test a patch or provide more logs/traces if useful.
Title
Wine SEH exceptions report wrong access type (write instead of read) for PAGE_NOACCESS/PAGE_READONLY faults
Environment
Box86 with Dynarec v0.3.9 0579f8b9(built May 18 2026)Summary
When a 32-bit Windows program running under Wine+box86 accesses a page that
was allocated with
PAGE_NOACCESS, the resultingEXCEPTION_ACCESS_VIOLATIONrecord's
ExceptionInformation[0]field (the standard Win32 access-type flag:0= read fault,1= write fault,8= execute fault — seeEXCEPTION_RECORD docs)
is reported as
1(write fault) even when the actual operation was a plainread. On native x86_64 (no box86/box64), the exact same test program
correctly reports
0(read fault).This causes real breakage in any Windows program that uses this fairly common
technique: reserve a large region as
PAGE_NOACCESS/guard pages, then use avectored exception handler or
__try/__exceptto lazily commit andfill each page on first touch (a legitimate, deliberate demand-paging
pattern used by several older-generation game engines to avoid reading
whole resource files up front). Because the handler receives the wrong
access-type flag, it can no longer distinguish "a page I expect to lazily
fault in" from "a genuine write to a read-only page," and its fault-recovery
logic takes the wrong branch — in the case that led me to this, the
handler gives up and unwinds instead of committing the page and retrying,
which the game then reports as a fatal resource-read error.
Minimal reproduction
No copyrighted software needed — this is a ~70-line, self-contained Win32
program. Compile with
i686-w64-mingw32-gcc -O0 -o segvtest.exe segvtest.c(or MSVC), run under Wine on box86 vs. native x86_64 Wine for comparison.
Actual output (box86, ARM64/Rocknix)
Expected output (matches native x86_64 Wine, same Wine version, no box86)
Additional context from a real-world case
I hit this while getting a 2003-era Windows game running under Wine+box86.
The game reserves a resource-file region as
PAGE_NOACCESSand installs itsown SEH handler to lazily commit +
ReadFile()one page at a time on firsttouch — a legitimate technique, and I confirmed via
WINEDEBUG=+file,+virtual,+sehtracing that it works correctly step-by-step on native x86_64 Wine (each
fault is serviced: commit page →
ReadFile4096 bytes → mark read-only →resume). Under box86, the very first fault for this region reports
ExceptionInformation[0]=1instead of0, the game's handler treats it asan unrecoverable error, and the game surfaces a (misleading) "resource file
corrupt or unreadable" dialog.
BOX86_SHOWSEGV=1independently confirmsbox86 itself sees
code=2/prot=0(SEGV_ACCERR, consistent with a pagethat has zero permissions) at the correct fault address — so box86 does
correctly identify that a fault occurred and where, but appears to
mis-derive the read/write access-type bit when constructing the exception
info handed to the guest program.
I'd guess this lives in whatever code path maps the host ARM64 Data Abort
exception's
ESR_EL1/WnR(write-not-read) bit — or however box86determines read vs. write on the ARM side — into the x86-style page-fault
error-code bit that Wine's own
ntdllunix-side signal handler reads topopulate
ExceptionInformation[0]. I don't have a fix, but I have boththe minimal repro above and box86/box64 tuning flags I ruled out along the
way (this is not a dynarec-correctness issue — reproduces identically with
BOX86_DYNAREC=0, pure interpreter mode) if that narrows things down.Happy to test a patch or provide more logs/traces if useful.