WSAPatch — Binary Patcher for WSABuilds on Windows 11 26H1

WSAPatch

**Pure MASM x64 · Zero CRT · 8.5 KB · Windows 11 26H1** *Fixes `STATUS_STOWED_EXCEPTION` crash in WSABuilds 2407.40000.4.0* *Two precise binary patches to `WsaClient.exe` — anchor-based scan, no fixed offsets* *Android apps run stably after patch — deep link registration silently skipped*

Table of Contents


The Problem

WSA (WSABuilds / MustardChef 2407.40000.4.0) crashes within a few seconds to ~40 seconds on Windows 11 26H1 (build 10.0.28000+). The subsystem loads, apps launch — then WsaClient.exe silently dies.

Property Value
Target binary WSABuilds / MustardChef 2407.40000.4.0 — WsaClient.exe
Affected OS Windows 11 26H1 (build 10.0.28000+)
Exception code STATUS_STOWED_EXCEPTION (0xC000027B)
Root HRESULT 0x80070005E_ACCESSDENIED
Faulting API AppUriHandlerRegistrationManager.UpdateAsync()
Effect of patch Android apps run stably; URI handler registration silently skipped

Everything starts fine. WSA loads, Android apps launch. Then WsaClient.exe calls AppUriHandlerRegistrationManager.UpdateAsync() to register URI scheme handlers (deep links) — the API returns E_ACCESSDENIED, the WinRT exception propagates uncaught, and combase!RoFailFastWithErrorContext terminates the process.


Debugging Session

Step 1 — Capturing the crash dump

WER (Windows Error Reporting) was set to capture a full dump on WsaClient.exe crash. Initial triage with CDB:

.ecxr
k 50
!analyze -v

Output:

Faulting module:  combase!RoFailFastWithErrorContextInternal2
Stowed exception: ResultCode = 0x80070005 (E_ACCESSDENIED)
                  ExceptionAddress = WsaClient+0x82AEB

STATUS_STOWED_EXCEPTION is a WinRT mechanism: when a WinRT exception crosses the ABI boundary without being caught, the runtime stores it ("stows" it) and later calls RoFailFastWithErrorContext. The stored HRESULT here was E_ACCESSDENIED.

Step 2 — Identifying the WinRT interface

The stowed exception address led to a vtable pointer. Inspecting the interface GUID:

da WsaClient+0x35f788
dq WsaClient+0x316d58 L1
ln poi(WsaClient+0x316d58)
dq WsaClient+0x4ad278 L1
ln poi(WsaClient+0x4ad278)

Result:

Symbol Value
GUID {D54DAC97-CB39-5F1F-883E-01853730BD6D}
Interface IAppUriHandlerRegistrationManagerStatics
Method consume_Windows_System_IAppUriHandlerRegistrationManagerStatics<...>::GetDefault()

Step 3 — Finding the two crash points

Two independent code paths both lead to the fatal handler:

Point A — HRESULT dispatcher at WsaClient+0xC93D

A 13-entry dispatch table maps known error codes to handlers. One entry checks for E_ACCESSDENIED:

; WsaClient+0xC93D
cmp  ebx, 80070005h    ; E_ACCESSDENIED
jne  +0x19             ; 75 19 — skip to next entry
; falls through to fatal handler if matched

Point B — directly after IAsyncOperation::GetResults() at WsaClient+0x81EE5

; WsaClient+0x81EE5
call qword ptr [rdi+40h]   ; GetResults() → eax = 0x80070005
test eax, eax              ; SF=1 (HRESULT failure bit)
js   +0x26                 ; 78 26 — jump to error path
; should fall through to normal epilogue

Full crash call chain

flowchart TD START[WsaClient.exe startup] --> CALL[AppUriHandlerRegistrationManager.UpdateAsync] CALL --> GETRES["IAsyncOperation::GetResults()\n→ returns 0x80070005 E_ACCESSDENIED"] GETRES --> TSTEAX["TEST EAX, EAX\nJS +0x26 ← Point B\njumps to error path"] TSTEAX --> CHKHR["winrt::check_hresult(0x80070005)\nthrows C++ exception"] CHKHR --> UNWIND["Stack unwind through catch-all block\n@ WsaClient+0x30dc5b"] UNWIND --> DISPATCH["HRESULT dispatcher\nCMP EBX, 80070005h\nJNE +0x19 ← Point A\nfalls through to fatal path"] DISPATCH --> RFFC["combase!RoFailFastWithErrorContext\n(0x80070005)"] RFFC --> RAISE["KERNELBASE!RaiseFailFastException"] RAISE --> CRASH["STATUS_STOWED_EXCEPTION\n0xC000027B → process terminated"]

Why E_ACCESSDENIED on 26H1

AppUriHandlerRegistrationManager.UpdateAsync() registers URI scheme handlers (deep links) for MSIX/UWP/Android apps at runtime. WSA calls it at startup so Windows knows which Android app should handle a given URI scheme.

At some point Microsoft tightened the capability requirements for this API — it now requires restrictedAppUriHandlerHost or equivalent package trust level. WSA's AppxManifest.xml declares windows.appUriHandler and runFullTrust, but that is no longer sufficient on 26H1.

Affected builds Windows 11 26H1 (10.0.28000+)
Unaffected builds Windows 11 22H2, 23H2 — API succeeds or code path unreached
Root cause API capability requirement tightened mid-2025
Impact of skipping Deep links declared statically in AppxManifest.xml remain registered; only the dynamic runtime update is omitted

The Two Patches

Both patches are located by anchor-based scanning — pre-bytes and post-bytes bracket the site — making the patcher robust against minor linker layout differences.

Patch 1 — Bypass E_ACCESSDENIED in HRESULT dispatcher

Turns a conditional branch into an unconditional one: the dispatcher always skips to the non-fatal path, so E_ACCESSDENIED is never routed to the fatal handler.

File offset 0x0C943
RVA 0xD543
Original 75 19JNE rel8 +0x19
Replacement EB 19JMP rel8 +0x19
Pre-anchor 81 FB 05 00 07 80 (CMP EBX, 80070005h)
Post-anchor 19 (jump offset byte)

Patch 2 — Ignore HRESULT from GetResults()

Removes the signed-jump that redirected to the error path. Execution falls through to the normal function epilogue, stack canary check and ret — no exception thrown, no crash.

File offset 0x81EE7
RVA 0x82AE7
Original 78 26JS rel8 +0x26
Replacement 90 90NOP NOP
Pre-anchor 85 C0 (TEST EAX, EAX)
Post-anchor 48 8B 4D (next instruction prefix)

Anchor-based scan

Each patch site is described by a PATCH_DESC struct (48 bytes):

Field Size Description
name_ptr 8 B Pointer to ANSI description string
pre[8] 8 B Anchor bytes immediately before the patch site
pre_len 4 B Valid bytes in pre[]
orig_bytes[4] 4 B Original bytes at patch site
repl_bytes[4] 4 B Replacement bytes
patch_len 4 B Number of bytes to patch (1–4)
post[8] 8 B Anchor bytes after orig/repl
post_len 4 B Valid bytes in post[] (0 = skip post check)

ScanPatch (implemented in scan.asm) walks the file image byte-by-byte:

flowchart LR START[i = 0] --> PRE{data at i\nmatches pre?} PRE -->|No| NEXT[i++] NEXT --> START PRE -->|Yes| POST{post_len > 0?} POST -->|Yes| POSTCHK{data at i+pre+patch\nmatches post?} POSTCHK -->|No| NEXT POSTCHK -->|Yes| ORIG{patch site\nmatches orig?} POST -->|No| ORIG ORIG -->|Yes| NEEDS[ST_NEEDS_PATCH\nreturn i] ORIG -->|No| REPL{patch site\nmatches repl?} REPL -->|Yes| ALREADY[ST_ALREADY_PATCHED\nreturn i] REPL -->|No| NEXT

The result is one of three states: ST_NEEDS_PATCH, ST_ALREADY_PATCHED, or ST_NOT_FOUND. If any patch returns ST_NOT_FOUND, the file is not written — unsupported version, no damage.


Usage

Run from an elevated command prompt (Administrator):

WSAPatch.exe [path\to\WsaClient.exe]

Without a path argument, WSAPatch discovers WsaClient.exe automatically via the registry:

HKLM\SYSTEM\CurrentControlSet\Services\WsaService\ImagePath

FindWsaPath (reg.asm) reads the ImagePath value, strips WsaService.exe and the WsaService\ directory component, then appends \WsaClient\WsaClient.exe. Supports both REG_SZ and REG_EXPAND_SZ (calls ExpandEnvironmentStringsA).

A .bak backup is created before any write. Running on an already-patched file prints Already patched and exits cleanly (idempotent).

Example output — fresh patch:

WsaClient.exe patcher for Windows 11 26H1
==========================================
[*] No path given - searching via WsaService registry...
[*] Target: C:\wsa\WsaClient\WsaClient.exe
[*] Path found via registry

[*] Read 0x4DFC00 bytes

Patch 1: Bypass E_ACCESSDENIED check (JNE->JMP)
  [+] Found at 0xC93D, patching 0xC943: 75  -> EB

Patch 2: Ignore HRESULT from GetResults() (JS->NOP NOP)
  [+] Found at 0x81EE5, patching 0x81EE7: 78 26  -> 90 90

[*] Backup saved to *.bak
[*] Done. 2 patches applied.

Project layout

WSAPatch/
├── x64/
│   ├── consts.inc   Win32 constants, PATCH_DESC struct definition
│   ├── globals.inc  Exported data variable declarations
│   ├── io.asm       OutStr / OutHex / OutByte (WriteFile, no CRT)
│   ├── scan.asm     ScanPatch — anchor-based binary scan
│   ├── reg.asm      FindWsaPath — registry path discovery
│   └── patch.asm    Entry point, patch table, orchestration
└── build.ps1        ML64 + LINK64, /NODEFAULTLIB, UAC manifest
Module File Responsibility
Entry / orchestration patch.asm mainCRTStartup, patch loop, file I/O, backup, output
Console output io.asm OutStr, OutHex, OutByte — raw WriteFile, no CRT
Binary scan scan.asm ScanPatch — anchor-based pattern search, returns state + offset
Registry discovery reg.asm FindWsaPath — reads WsaService\ImagePath, derives WsaClient.exe path
Constants / structs consts.inc PATCH_DESC layout, Win32 constants, scan states
Build script build.ps1 ML64 + LINK64, /NODEFAULTLIB, UAC manifest injection

Technical highlights

Feature Detail
Pure MASM x64 Zero CRT, zero C runtime — only kernel32.dll + advapi32.dll
8.5 KB binary No imports beyond Win32, no startup overhead
Anchor-based scan Finds patch sites by surrounding byte patterns, not fixed offsets — robust across minor linker layout differences
Full unwind tables Proper .pushreg / .allocstack / .endprolog in every proc frame — correct stack walking under WER and debuggers
UAC manifest /MANIFESTUAC:level='requireAdministrator' via linker flag — elevation prompt on launch
Idempotent Detects already-patched bytes (ST_ALREADY_PATCHED), never double-patches
Registry autodiscovery Reads HKLM\SYSTEM\CurrentControlSet\Services\WsaService\ImagePath, strips service exe, appends client path
No file written on failure If any patch returns ST_NOT_FOUND, the file is not touched — safe on unsupported versions

Building from source

Requires Visual Studio 2026 / Build Tools v18 (MASM x64 + LINK):

.\build.ps1

Output: bin\WSAPatch.exe — verified no CRT imports by the build script (dumpbin /imports).

The build script calls ML64.exe for each .asm file, then LINK64.exe with /NODEFAULTLIB and an embedded UAC manifest (/MANIFESTUAC:level='requireAdministrator').


Requirements

Target binary WSABuilds / MustardChef 2407.40000.4.0 (WsaClient.exe, verified by anchor scan)
OS Windows 11 26H1 (tested on build 10.0.28000.2113)
Elevation Administrator required (UAC manifest embedded)
Build tools Visual Studio 2026 / Build Tools v18 — ML64.exe + LINK64.exe

WSAPatch — Pure MASM x64, zero external dependencies.
kvc.pl | [email protected] | GitHub