WSAPatch — Binary Patcher for WSABuilds on Windows 11 26H1

Table of Contents
- The Problem
- Debugging Session
- Full crash call chain
- Why E_ACCESSDENIED on 26H1
- The Two Patches
- Anchor-based scan
- Usage
- Project layout
- Technical highlights
- Building from source
- Requirements
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 | 0x80070005 — E_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
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 19 — JNE rel8 +0x19 |
| Replacement | EB 19 — JMP 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 26 — JS rel8 +0x26 |
| Replacement | 90 90 — NOP 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:
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