Skip to content

Windows: start the generator with shadow stacks off too, and log where it got (#779) - #780

Open
peiikai wants to merge 1 commit into
beta_6from
fix-779-arm-lasgen
Open

peiikai wants to merge 1 commit into
beta_6from
fix-779-arm-lasgen

Conversation

@peiikai

@peiikai peiikai commented Nov 14, 2026

Copy link
Copy Markdown
Owner

For #779: on Windows on ARM (Snapdragon X2, Windows 11 26H1) the game code generator lasgen.exe (x64, run under Prism) stops with 0xC0000409 even though it is started with Control Flow Guard off (#337).

What we found

  • The sdk and main generators take the same path. Both get to unicorn's first jump table emulation (Module.jump_table → emu_start) within seconds. main prints its game version line before that, but sdk prints nothing before it, so gen_sdk.log stays empty. main was most likely stopped by the runtime when sdk failed (gamebuild.c stops the rest after a failure). So this is not something only the sdk path does.
  • The shipped unicorn.dll is an MSVC build that imports longjmp and __intrinsic_setjmp from VCRUNTIME140. Disassembly of the VCRUNTIME140.dll in the windows-arm64 test build shows its longjmp:
    • when CFG is active (__guard_check_icall_fptr is not the no-op), it runs int 0x29 (fast fail 13) if the jmp_buf frame is 0. That is Game files setup fails: "The game code didn't build, make sure there is 2GB free" #337.
    • then it runs rdsspq: if a shadow stack is present it always goes through RtlUnwindEx, even with frame 0. That unwinds through unicorn's generated code, which has no unwind data, and fails. With no shadow stack and frame 0, it just restores the registers.
  • PE headers in the test build: lasgen.exe has CFG off (pe_no_cfg.py) and is not CETCOMPAT. VCRUNTIME140*.dll are CFG and CETCOMPAT. unicorn, capstone and python312 are neither. So pe_no_cfg.py needs no change.

Change

  • spawn_child (Windows) now starts the generator with PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF as well as CFG off. If Windows refuses the pair, it falls back to CFG off alone, then to a plain start. plat_spawn_no_cfg reports both results, and the log says which one applied.
  • gen.py logs a first line before any work, e.g. sdk: started (x64 on arm64 (emulated), Control Flow Guard off, shadow stacks off, strict shadow stacks off), and a line before its first unicorn run. A failure's last line, which helper_failed copies into lasrecomp.log, then shows how far it got and which mitigations were really on.
  • test_windows_processes.py also checks that the child has shadow stacks off. The test stubs follow the new signature.

Caveat

This can't be tested here: we have no Windows on ARM machine. The shadow stack theory fits the longjmp code, but lasgen.exe isn't CETCOMPAT, so on x64 Windows it only gets a shadow stack if one is forced system-wide. Whether 26H1's emulation enables one is unconfirmed. If this build still fails, the new first log line will say whether CFG or shadow stacks were on in the generator anyway (for example, a policy not applied under emulation). That points to the next fix: replacing unicorn's longjmp import.

Tested: test_gamebuild_resume.py on macOS. The Windows build and test_windows_processes.py (x64 and ARM64) run in CI. test_gamebuild_ios.py can't run locally (no zig here).

🤖 Generated with Claude Code

…e it got (#779)

On Windows on ARM (x64 emulation, Windows 11 26H1) the game code generator still
stops with 0xC0000409 although it runs with Control Flow Guard off. Both
generators reach unicorn's first jump table emulation within seconds; the sdk
one just has no log line before it, so its log stays empty.

unicorn leaves its generated code with VCRUNTIME140's longjmp from a setjmp
that saved no frame. That longjmp fast-fails for it when CFG is on (#337),
and when a shadow stack is present it always unwinds with RtlUnwindEx, which
can't walk the generated code. So the generator is now also started with
user-mode shadow stacks off (falling back to CFG off alone where Windows
refuses that policy).

gen.py now logs a first line with the CPU it runs on (emulated or not) and
whether CFG and shadow stacks are on in its process, and a line before its
first unicorn run, so a failure's last log line shows how far it got.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant