Skip to content

fix(jit/win64): platform call conv for closure indirect calls - #2

Merged
darmie merged 3 commits into
mainfrom
windows-async-callconv-fix
Jul 29, 2026
Merged

fix(jit/win64): platform call conv for closure indirect calls#2
darmie merged 3 commits into
mainfrom
windows-async-callconv-fix

Conversation

@darmie

@darmie darmie commented Jul 29, 2026

Copy link
Copy Markdown
Member

Fixes the Windows-only async garbage: JIT-executed async functions returned garbage on x86_64-pc-windows-msvc (e.g. double(21) → 174116032 instead of 42) across async_execution_tests and promise_combinator_tests.

Root cause: the CallClosure indirect-call site hard-coded CallConv::SystemV. The async poll fn is reached via CreateClosureCallClosure, and on Windows the callee is compiled WindowsFastcall — so args landed in SysV registers while the callee read MS x64 registers, making poll_fn read a garbage state-machine pointer. Same root cause and fix as the sibling indirect-call site (which already switched to the module default).

Fix: build the signature from module.make_signature() so it carries the platform default calling convention, matching the callee. No-op on Unix (default is SystemV there).

Verifying via the Windows CI job on this PR.

darmie added 3 commits July 29, 2026 10:58
The CallClosure handler built its indirect-call signature with a hard-
coded CallConv::SystemV. On x86_64-pc-windows-msvc the callee is compiled
WindowsFastcall, so arguments landed in SysV registers (rdi, rsi) while
the callee read MS x64 registers (rcx, rdx). The async poll fn is reached
via CreateClosure -> CallClosure, so every JIT-executed async function on
Windows read a garbage state-machine pointer and returned garbage
(async_execution_tests / promise_combinator_tests: double(21) -> garbage
instead of 42).

Build the signature from module.make_signature() so it carries the
platform default calling convention, matching the callee — the same fix
already applied to the sibling indirect-call site. No-op on Unix (default
is SystemV there); async + effect suites still green on macOS/Linux.
…vention

The real cause of the Windows async garbage: the krio_adapter-generated
async entry (`generate_promise_entry`), sync entry (`generate_sync_entry`),
and poll fn (`reshape_to_poll_abi`) inherited HirFunction::new's default
`CallingConvention::Fast`. On x86_64 Fast lowers to Cranelift's Fast, which
happens to match `extern "C"` (SystemV) on Unix — so Linux/macOS worked —
but on x86_64-pc-windows-msvc it does NOT match Win64. The runtime calls
these functions through `extern "C"` transmutes, so on Windows the entry
read its first argument (and the poll fn its state-machine pointer) from
the wrong register, returning garbage for every JIT-executed async
function.

Pin the three runtime-facing generated functions to `CallingConvention::C`
(= platform default = WindowsFastcall on Windows). No-op on Unix.

Complements the CallClosure indirect-call fix in the same branch (inner-
promise polls).
…ntion

Regression guard for the Windows async-garbage fix: the async promise
entry and sync entry must carry CallingConvention::C (platform default),
not HirFunction::new's Fast default, or JIT-executed async functions read
arguments from the wrong registers on x86_64-pc-windows-msvc.
@darmie
darmie merged commit 0c72e30 into main Jul 29, 2026
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