Skip to content

Wine SEH exceptions report wrong access type (write instead of read) for PAGE_NOACCESS/PAGE_READONLY faults #1063

Description

@Kryptikone316

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions