feat(platform): add full Windows x86_64 PE32+ cross-compilation support - #446
Draft
Guikingone wants to merge 2 commits into
Draft
feat(platform): add full Windows x86_64 PE32+ cross-compilation support#446Guikingone wants to merge 2 commits into
Guikingone wants to merge 2 commits into
Conversation
Guikingone
marked this pull request as draft
July 1, 2026 10:12
Guikingone
force-pushed
the
feat/windows-pe
branch
2 times, most recently
from
July 1, 2026 15:43
1a6888e to
4b7fd88
Compare
Guikingone
force-pushed
the
feat/windows-pe
branch
2 times, most recently
from
July 2, 2026 17:30
aa5e6be to
2ac8b78
Compare
Guikingone
added a commit
to Guikingone/elephc
that referenced
this pull request
Jul 6, 2026
…es builtin Reconstruct PR illegalstudio#446's feature set on top of current main. Windows x86_64 PE target: - Platform::Windows with the MSx64 ABI (rcx/rdx/r8/r9, 32-byte shadow, 16-byte alignment) and ~40 Win32 shim wrappers converting SysV->MSx64 (WriteFile/ReadFile, sockets, file/dir ops, VirtualAlloc/Heap, BCryptGenRandom, ...). - Syscall->shim transform wired into the pipeline + runtime cache (Windows-only) so PE binaries contain no raw Linux syscalls; unmapped syscalls route to __rt_unsupported_syscall instead of a silent trap. - Entry wrapper (MSx64->SysV), MinGW-w64 assembler/linker, kernel32/msvcrt/winmm/ ws2_32/bcrypt/shlwapi link set. --target windows-x86_64 (alias x86_64-pc-windows-gnu); .exe / .dll outputs. CI windows-pe-cross-compile job (MinGW + Wine) wired into the gate. Fix the implicit end-of-main exit code on Windows: emit_exit and emit_exit_with_result_reg now load the code into edi and call the __rt_sys_exit (ExitProcess) shim directly, identical to an explicit exit($n), instead of returning through the MinGW CRT. The CRT's exit() reaches the same rdi-consuming shim, so the old return path left rdi holding leftover data and exited with a garbage nonzero code. random_bytes(int $length): string: - Cryptographically secure random byte string on every supported target (arc4random_buf / getrandom / BCryptGenRandom), fatal on entropy failure or a constant length below 1. - Expressed as a single-source registry home file (src/builtins/math/random_bytes.rs) instead of the legacy hand-maintained tables; excluded from the legacy runtime-callable wrapper as an EIR-only builtin. Builtin docs + registry JSON regenerated.
Guikingone
force-pushed
the
feat/windows-pe
branch
from
July 6, 2026 08:54
2ac8b78 to
5ec3017
Compare
Guikingone
added a commit
to Guikingone/elephc
that referenced
this pull request
Jul 6, 2026
…es builtin Reconstruct PR illegalstudio#446's feature set on top of current main. Windows x86_64 PE target: - Platform::Windows with the MSx64 ABI (rcx/rdx/r8/r9, 32-byte shadow, 16-byte alignment) and ~40 Win32 shim wrappers converting SysV->MSx64 (WriteFile/ReadFile, sockets, file/dir ops, VirtualAlloc/Heap, BCryptGenRandom, ...). - Syscall->shim transform wired into the pipeline + runtime cache (Windows-only) so PE binaries contain no raw Linux syscalls; unmapped syscalls route to __rt_unsupported_syscall instead of a silent trap. - Entry wrapper (MSx64->SysV), MinGW-w64 assembler/linker, kernel32/msvcrt/winmm/ ws2_32/bcrypt/shlwapi link set. --target windows-x86_64 (alias x86_64-pc-windows-gnu); .exe / .dll outputs. CI windows-pe-cross-compile job (MinGW + Wine) wired into the gate. Fix the implicit end-of-main exit code on Windows: emit_exit and emit_exit_with_result_reg now load the code into edi and call the __rt_sys_exit (ExitProcess) shim directly, identical to an explicit exit($n), instead of returning through the MinGW CRT. The CRT's exit() reaches the same rdi-consuming shim, so the old return path left rdi holding leftover data and exited with a garbage nonzero code. random_bytes(int $length): string: - Cryptographically secure random byte string on every supported target (arc4random_buf / getrandom / BCryptGenRandom), fatal on entropy failure or a constant length below 1. - Expressed as a single-source registry home file (src/builtins/math/random_bytes.rs) instead of the legacy hand-maintained tables; excluded from the legacy runtime-callable wrapper as an EIR-only builtin. Builtin docs + registry JSON regenerated.
Guikingone
force-pushed
the
feat/windows-pe
branch
from
July 6, 2026 09:25
5ec3017 to
c8aa4df
Compare
Guikingone
added a commit
to Guikingone/elephc
that referenced
this pull request
Jul 6, 2026
…es builtin Reconstruct PR illegalstudio#446's feature set on top of current main. Windows x86_64 PE target: - Platform::Windows with the MSx64 ABI (rcx/rdx/r8/r9, 32-byte shadow, 16-byte alignment) and ~40 Win32 shim wrappers converting SysV->MSx64 (WriteFile/ReadFile, sockets, file/dir ops, VirtualAlloc/Heap, BCryptGenRandom, ...). - Syscall->shim transform wired into the pipeline + runtime cache (Windows-only) so PE binaries contain no raw Linux syscalls; unmapped syscalls route to __rt_unsupported_syscall instead of a silent trap. - Entry wrapper (MSx64->SysV), MinGW-w64 assembler/linker, kernel32/msvcrt/winmm/ ws2_32/bcrypt/shlwapi link set. --target windows-x86_64 (alias x86_64-pc-windows-gnu); .exe / .dll outputs. CI windows-pe-cross-compile job (MinGW + Wine) wired into the gate. Fix the implicit end-of-main exit code on Windows: emit_exit and emit_exit_with_result_reg now load the code into edi and call the __rt_sys_exit (ExitProcess) shim directly, identical to an explicit exit($n), instead of returning through the MinGW CRT. The CRT's exit() reaches the same rdi-consuming shim, so the old return path left rdi holding leftover data and exited with a garbage nonzero code. random_bytes(int $length): string: - Cryptographically secure random byte string on every supported target (arc4random_buf / getrandom / BCryptGenRandom), fatal on entropy failure or a constant length below 1. - Expressed as a single-source registry home file (src/builtins/math/random_bytes.rs) instead of the legacy hand-maintained tables; excluded from the legacy runtime-callable wrapper as an EIR-only builtin. Builtin docs + registry JSON regenerated.
Guikingone
force-pushed
the
feat/windows-pe
branch
from
July 6, 2026 10:20
c8aa4df to
2d7dcca
Compare
Guikingone
added a commit
to Guikingone/elephc
that referenced
this pull request
Jul 6, 2026
…es builtin Reconstruct PR illegalstudio#446's feature set on top of current main. Windows x86_64 PE target: - Platform::Windows with the MSx64 ABI (rcx/rdx/r8/r9, 32-byte shadow, 16-byte alignment) and ~40 Win32 shim wrappers converting SysV->MSx64 (WriteFile/ReadFile, sockets, file/dir ops, VirtualAlloc/Heap, BCryptGenRandom, ...). - Syscall->shim transform wired into the pipeline + runtime cache (Windows-only) so PE binaries contain no raw Linux syscalls; unmapped syscalls route to __rt_unsupported_syscall instead of a silent trap. - Entry wrapper (MSx64->SysV), MinGW-w64 assembler/linker, kernel32/msvcrt/winmm/ ws2_32/bcrypt/shlwapi link set. --target windows-x86_64 (alias x86_64-pc-windows-gnu); .exe / .dll outputs. CI windows-pe-cross-compile job (MinGW + Wine) wired into the gate. Fix the implicit end-of-main exit code on Windows: emit_exit and emit_exit_with_result_reg now load the code into edi and call the __rt_sys_exit (ExitProcess) shim directly, identical to an explicit exit($n), instead of returning through the MinGW CRT. The CRT's exit() reaches the same rdi-consuming shim, so the old return path left rdi holding leftover data and exited with a garbage nonzero code. random_bytes(int $length): string: - Cryptographically secure random byte string on every supported target (arc4random_buf / getrandom / BCryptGenRandom), fatal on entropy failure or a constant length below 1. - Expressed as a single-source registry home file (src/builtins/math/random_bytes.rs) instead of the legacy hand-maintained tables; excluded from the legacy runtime-callable wrapper as an EIR-only builtin. Builtin docs + registry JSON regenerated.
Guikingone
force-pushed
the
feat/windows-pe
branch
from
July 6, 2026 11:20
2d7dcca to
85541c3
Compare
Guikingone
added a commit
to Guikingone/elephc
that referenced
this pull request
Jul 6, 2026
…es builtin Reconstruct PR illegalstudio#446's feature set on top of current main. Windows x86_64 PE target: - Platform::Windows with the MSx64 ABI (rcx/rdx/r8/r9, 32-byte shadow, 16-byte alignment) and ~40 Win32 shim wrappers converting SysV->MSx64 (WriteFile/ReadFile, sockets, file/dir ops, VirtualAlloc/Heap, BCryptGenRandom, ...). - Syscall->shim transform wired into the pipeline + runtime cache (Windows-only) so PE binaries contain no raw Linux syscalls; unmapped syscalls route to __rt_unsupported_syscall instead of a silent trap. - Entry wrapper (MSx64->SysV), MinGW-w64 assembler/linker, kernel32/msvcrt/winmm/ ws2_32/bcrypt/shlwapi link set. --target windows-x86_64 (alias x86_64-pc-windows-gnu); .exe / .dll outputs. CI windows-pe-cross-compile job (MinGW + Wine) wired into the gate. Fix the implicit end-of-main exit code on Windows: emit_exit and emit_exit_with_result_reg now load the code into edi and call the __rt_sys_exit (ExitProcess) shim directly, identical to an explicit exit($n), instead of returning through the MinGW CRT. The CRT's exit() reaches the same rdi-consuming shim, so the old return path left rdi holding leftover data and exited with a garbage nonzero code. random_bytes(int $length): string: - Cryptographically secure random byte string on every supported target (arc4random_buf / getrandom / BCryptGenRandom), fatal on entropy failure or a constant length below 1. - Expressed as a single-source registry home file (src/builtins/math/random_bytes.rs) instead of the legacy hand-maintained tables; excluded from the legacy runtime-callable wrapper as an EIR-only builtin. Builtin docs + registry JSON regenerated.
Guikingone
force-pushed
the
feat/windows-pe
branch
from
July 6, 2026 14:33
c60d605 to
b8483f1
Compare
Guikingone
added a commit
to Guikingone/elephc
that referenced
this pull request
Jul 6, 2026
…es builtin Reconstruct PR illegalstudio#446's feature set on top of current main. Windows x86_64 PE target: - Platform::Windows with the MSx64 ABI (rcx/rdx/r8/r9, 32-byte shadow, 16-byte alignment) and ~40 Win32 shim wrappers converting SysV->MSx64 (WriteFile/ReadFile, sockets, file/dir ops, VirtualAlloc/Heap, BCryptGenRandom, ...). - Syscall->shim transform wired into the pipeline + runtime cache (Windows-only) so PE binaries contain no raw Linux syscalls; unmapped syscalls route to __rt_unsupported_syscall instead of a silent trap. - Entry wrapper (MSx64->SysV), MinGW-w64 assembler/linker, kernel32/msvcrt/winmm/ ws2_32/bcrypt/shlwapi link set. --target windows-x86_64 (alias x86_64-pc-windows-gnu); .exe / .dll outputs. CI windows-pe-cross-compile job (MinGW + Wine) wired into the gate. Fix the implicit end-of-main exit code on Windows: emit_exit and emit_exit_with_result_reg now load the code into edi and call the __rt_sys_exit (ExitProcess) shim directly, identical to an explicit exit($n), instead of returning through the MinGW CRT. The CRT's exit() reaches the same rdi-consuming shim, so the old return path left rdi holding leftover data and exited with a garbage nonzero code. random_bytes(int $length): string: - Cryptographically secure random byte string on every supported target (arc4random_buf / getrandom / BCryptGenRandom), fatal on entropy failure or a constant length below 1. - Expressed as a single-source registry home file (src/builtins/math/random_bytes.rs) instead of the legacy hand-maintained tables; excluded from the legacy runtime-callable wrapper as an EIR-only builtin. Builtin docs + registry JSON regenerated. Callable-invoker ABI on Windows x86_64: - The descriptor-based runtime callable invoker read its descriptor from a hardcoded rdi, but on Windows the caller passes it in the MSx64 first arg (rcx); read it from int_arg_reg_name(target, 0) instead. - The invoker clone helpers passed arguments to the __rt_array_clone_shallow / __rt_array_to_mixed / __rt_hash_clone_shallow / __rt_hash_to_mixed / __rt_array_new runtime helpers via int_arg_reg_name, which returns the target user-call ABI (rcx/rdx on Windows); those helpers use the System V AMD64 ABI on every x86_64 target and read rdi/rsi. Add runtime_int_arg_reg_name (SysV on all x86_64, platform ABI on AArch64) and route the four helper call sites through it. No-op on Linux and AArch64.
Guikingone
force-pushed
the
feat/windows-pe
branch
from
July 6, 2026 16:16
c7c3d92 to
719394b
Compare
Guikingone
added a commit
to Guikingone/elephc
that referenced
this pull request
Jul 6, 2026
…es builtin Reconstruct PR illegalstudio#446's feature set on top of current main. Windows x86_64 PE target: - Platform::Windows with the MSx64 ABI (rcx/rdx/r8/r9, 32-byte shadow, 16-byte alignment) and ~40 Win32 shim wrappers converting SysV->MSx64 (WriteFile/ReadFile, sockets, file/dir ops, VirtualAlloc/Heap, BCryptGenRandom, ...). - Syscall->shim transform wired into the pipeline + runtime cache (Windows-only) so PE binaries contain no raw Linux syscalls; unmapped syscalls route to __rt_unsupported_syscall instead of a silent trap. - Entry wrapper (MSx64->SysV), MinGW-w64 assembler/linker, kernel32/msvcrt/winmm/ ws2_32/bcrypt/shlwapi link set. --target windows-x86_64 (alias x86_64-pc-windows-gnu); .exe / .dll outputs. CI windows-pe-cross-compile job (MinGW + Wine) wired into the gate. Fix the implicit end-of-main exit code on Windows: emit_exit and emit_exit_with_result_reg now load the code into edi and call the __rt_sys_exit (ExitProcess) shim directly, identical to an explicit exit($n), instead of returning through the MinGW CRT. The CRT's exit() reaches the same rdi-consuming shim, so the old return path left rdi holding leftover data and exited with a garbage nonzero code. random_bytes(int $length): string: - Cryptographically secure random byte string on every supported target (arc4random_buf / getrandom / BCryptGenRandom), fatal on entropy failure or a constant length below 1. - Expressed as a single-source registry home file (src/builtins/math/random_bytes.rs) instead of the legacy hand-maintained tables; excluded from the legacy runtime-callable wrapper as an EIR-only builtin. Builtin docs + registry JSON regenerated. Callable-invoker ABI on Windows x86_64: - The descriptor-based runtime callable invoker read its descriptor from a hardcoded rdi, but on Windows the caller passes it in the MSx64 first arg (rcx); read it from int_arg_reg_name(target, 0) instead. - The invoker clone helpers passed arguments to the __rt_array_clone_shallow / __rt_array_to_mixed / __rt_hash_clone_shallow / __rt_hash_to_mixed / __rt_array_new runtime helpers via int_arg_reg_name, which returns the target user-call ABI (rcx/rdx on Windows); those helpers use the System V AMD64 ABI on every x86_64 target and read rdi/rsi. Add runtime_int_arg_reg_name (SysV on all x86_64, platform ABI on AArch64) and route the four helper call sites through it. No-op on Linux and AArch64.
Guikingone
force-pushed
the
feat/windows-pe
branch
from
July 6, 2026 20:19
719394b to
0240020
Compare
Guikingone
added a commit
to Guikingone/elephc
that referenced
this pull request
Jul 6, 2026
…es builtin Reconstruct PR illegalstudio#446's feature set on top of current main. Windows x86_64 PE target: - Platform::Windows with the MSx64 ABI (rcx/rdx/r8/r9, 32-byte shadow, 16-byte alignment) and ~40 Win32 shim wrappers converting SysV->MSx64 (WriteFile/ReadFile, sockets, file/dir ops, VirtualAlloc/Heap, BCryptGenRandom, ...). - Syscall->shim transform wired into the pipeline + runtime cache (Windows-only) so PE binaries contain no raw Linux syscalls; unmapped syscalls route to __rt_unsupported_syscall instead of a silent trap. - Entry wrapper (MSx64->SysV), MinGW-w64 assembler/linker, kernel32/msvcrt/winmm/ ws2_32/bcrypt/shlwapi link set. --target windows-x86_64 (alias x86_64-pc-windows-gnu); .exe / .dll outputs. CI windows-pe-cross-compile job (MinGW + Wine) wired into the gate. Fix the implicit end-of-main exit code on Windows: emit_exit and emit_exit_with_result_reg now load the code into edi and call the __rt_sys_exit (ExitProcess) shim directly, identical to an explicit exit($n), instead of returning through the MinGW CRT. The CRT's exit() reaches the same rdi-consuming shim, so the old return path left rdi holding leftover data and exited with a garbage nonzero code. random_bytes(int $length): string: - Cryptographically secure random byte string on every supported target (arc4random_buf / getrandom / BCryptGenRandom), fatal on entropy failure or a constant length below 1. - Expressed as a single-source registry home file (src/builtins/math/random_bytes.rs) instead of the legacy hand-maintained tables; excluded from the legacy runtime-callable wrapper as an EIR-only builtin. Builtin docs + registry JSON regenerated. Callable-invoker ABI on Windows x86_64: - The descriptor-based runtime callable invoker read its descriptor from a hardcoded rdi, but on Windows the caller passes it in the MSx64 first arg (rcx); read it from int_arg_reg_name(target, 0) instead. - The invoker clone helpers passed arguments to the __rt_array_clone_shallow / __rt_array_to_mixed / __rt_hash_clone_shallow / __rt_hash_to_mixed / __rt_array_new runtime helpers via int_arg_reg_name, which returns the target user-call ABI (rcx/rdx on Windows); those helpers use the System V AMD64 ABI on every x86_64 target and read rdi/rsi. Add runtime_int_arg_reg_name (SysV on all x86_64, platform ABI on AArch64) and route the four helper call sites through it. No-op on Linux and AArch64.
Guikingone
force-pushed
the
feat/windows-pe
branch
from
July 6, 2026 21:28
0240020 to
c0f1476
Compare
Guikingone
added a commit
to Guikingone/elephc
that referenced
this pull request
Jul 6, 2026
…es builtin Reconstruct PR illegalstudio#446's feature set on top of current main. Windows x86_64 PE target: - Platform::Windows with the MSx64 ABI (rcx/rdx/r8/r9, 32-byte shadow, 16-byte alignment) and ~40 Win32 shim wrappers converting SysV->MSx64 (WriteFile/ReadFile, sockets, file/dir ops, VirtualAlloc/Heap, BCryptGenRandom, ...). - Syscall->shim transform wired into the pipeline + runtime cache (Windows-only) so PE binaries contain no raw Linux syscalls; unmapped syscalls route to __rt_unsupported_syscall instead of a silent trap. - Entry wrapper (MSx64->SysV), MinGW-w64 assembler/linker, kernel32/msvcrt/winmm/ ws2_32/bcrypt/shlwapi link set. --target windows-x86_64 (alias x86_64-pc-windows-gnu); .exe / .dll outputs. CI windows-pe-cross-compile job (MinGW + Wine) wired into the gate. Fix the implicit end-of-main exit code on Windows: emit_exit and emit_exit_with_result_reg now load the code into edi and call the __rt_sys_exit (ExitProcess) shim directly, identical to an explicit exit($n), instead of returning through the MinGW CRT. The CRT's exit() reaches the same rdi-consuming shim, so the old return path left rdi holding leftover data and exited with a garbage nonzero code. random_bytes(int $length): string: - Cryptographically secure random byte string on every supported target (arc4random_buf / getrandom / BCryptGenRandom), fatal on entropy failure or a constant length below 1. - Expressed as a single-source registry home file (src/builtins/math/random_bytes.rs) instead of the legacy hand-maintained tables; excluded from the legacy runtime-callable wrapper as an EIR-only builtin. Builtin docs + registry JSON regenerated. Callable-invoker ABI on Windows x86_64: - The descriptor-based runtime callable invoker read its descriptor from a hardcoded rdi, but on Windows the caller passes it in the MSx64 first arg (rcx); read it from int_arg_reg_name(target, 0) instead. - The invoker clone helpers passed arguments to the __rt_array_clone_shallow / __rt_array_to_mixed / __rt_hash_clone_shallow / __rt_hash_to_mixed / __rt_array_new runtime helpers via int_arg_reg_name, which returns the target user-call ABI (rcx/rdx on Windows); those helpers use the System V AMD64 ABI on every x86_64 target and read rdi/rsi. Add runtime_int_arg_reg_name (SysV on all x86_64, platform ABI on AArch64) and route the four helper call sites through it. No-op on Linux and AArch64.
Guikingone
force-pushed
the
feat/windows-pe
branch
2 times, most recently
from
July 7, 2026 07:11
a1b7fa8 to
b0d4620
Compare
Guikingone
added a commit
to Guikingone/elephc
that referenced
this pull request
Jul 7, 2026
…es builtin Reconstruct PR illegalstudio#446's feature set on top of current main. Windows x86_64 PE target: - Platform::Windows with the MSx64 ABI (rcx/rdx/r8/r9, 32-byte shadow, 16-byte alignment) and ~40 Win32 shim wrappers converting SysV->MSx64 (WriteFile/ReadFile, sockets, file/dir ops, VirtualAlloc/Heap, BCryptGenRandom, ...). - Syscall->shim transform wired into the pipeline + runtime cache (Windows-only) so PE binaries contain no raw Linux syscalls; unmapped syscalls route to __rt_unsupported_syscall instead of a silent trap. - Entry wrapper (MSx64->SysV), MinGW-w64 assembler/linker, kernel32/msvcrt/winmm/ ws2_32/bcrypt/shlwapi link set. --target windows-x86_64 (alias x86_64-pc-windows-gnu); .exe / .dll outputs. CI windows-pe-cross-compile job (MinGW + Wine) wired into the gate. Fix the implicit end-of-main exit code on Windows: emit_exit and emit_exit_with_result_reg now load the code into edi and call the __rt_sys_exit (ExitProcess) shim directly, identical to an explicit exit($n), instead of returning through the MinGW CRT. The CRT's exit() reaches the same rdi-consuming shim, so the old return path left rdi holding leftover data and exited with a garbage nonzero code. random_bytes(int $length): string: - Cryptographically secure random byte string on every supported target (arc4random_buf / getrandom / BCryptGenRandom), fatal on entropy failure or a constant length below 1. - Expressed as a single-source registry home file (src/builtins/math/random_bytes.rs) instead of the legacy hand-maintained tables; excluded from the legacy runtime-callable wrapper as an EIR-only builtin. Builtin docs + registry JSON regenerated. Callable-invoker ABI on Windows x86_64: - The descriptor-based runtime callable invoker read its descriptor from a hardcoded rdi, but on Windows the caller passes it in the MSx64 first arg (rcx); read it from int_arg_reg_name(target, 0) instead. - The invoker clone helpers passed arguments to the __rt_array_clone_shallow / __rt_array_to_mixed / __rt_hash_clone_shallow / __rt_hash_to_mixed / __rt_array_new runtime helpers via int_arg_reg_name, which returns the target user-call ABI (rcx/rdx on Windows); those helpers use the System V AMD64 ABI on every x86_64 target and read rdi/rsi. Add runtime_int_arg_reg_name (SysV on all x86_64, platform ABI on AArch64) and route the four helper call sites through it. No-op on Linux and AArch64. # Conflicts: # CHANGELOG.md # CLAUDE.md # docs/internals/builtins/math/mt_rand.md # docs/internals/builtins/math/rand.md # docs/internals/builtins/math/random_int.md # src/codegen/abi/mod.rs # src/codegen/builtins/arrays/call_user_func_array.rs # src/codegen/builtins/system/exit.rs # src/codegen/callable_dispatch.rs # src/codegen/prescan.rs # src/codegen/runtime_callable_invoker.rs # src/codegen_support/abi/bootstrap.rs # src/codegen_support/platform/windows_transform.rs # src/codegen_support/runtime/arrays/random_bytes.rs # src/codegen_support/runtime/emitters.rs # src/pipeline.rs
Guikingone
force-pushed
the
feat/windows-pe
branch
2 times, most recently
from
July 8, 2026 07:58
d2bb042 to
d31d8bf
Compare
Guikingone
force-pushed
the
feat/windows-pe
branch
from
July 23, 2026 17:42
24c5d66 to
1db31d8
Compare
Guikingone
force-pushed
the
feat/windows-pe
branch
23 times, most recently
from
July 27, 2026 07:57
536a403 to
3c3e6ed
Compare
…php compliance gaps
Windows x86_64 (PE32+) joins the target list: a MinGW-GNU-ABI backend with MSx64
calling-convention codegen, Win32 shims standing in for the Linux syscalls the
runtime issues, an import table, and a parity gate that runs the codegen suite
against the new target.
Auditing that target against php-src turned up defects that were never
Windows-specific, so they are fixed here as well. Each is verified against php 8.5.6
and locked by a fixture that fails without the fix.
Silent data loss and memory disclosure:
sprintf("%s", $s) copied its argument into a 128-byte stack buffer to
NUL-terminate it for snprintf, truncating every string past 127 bytes and cutting
short at any NUL byte php allows inside a string. %s is now rendered natively.
Any sprintf conversion wider than 127 characters copied adjacent stack memory into
the result, because snprintf reports the length it *would* have written. Comparing
lengths hides this; only a content hash shows it.
filemtime() and filesize() read the stat buffer without checking whether stat()
had written it, so a missing path returned whatever the stack held. Both now
report false, as php does.
array_splice($a, 1, -1) corrupted the array outright: the negative length passed
the upper clamp and became the capacity handed to the allocator.
php semantics that C's formatter does not share, all of which snprintf was deciding:
precision is ignored on %d/%u/%c and empties %x/%X/%o, the space flag has no
meaning, %b and the %'X pad character do not exist in C, and %N$ names an argument
without advancing the sequential counter.
Negative lengths, which four functions read as "select nothing" because -1 doubled
as an in-band "argument omitted" sentinel: substr, substr_replace, array_slice and
array_splice now read them as php does, and the sentinel is gone.
The temporary directory was the "/tmp" literal on every POSIX target, ignoring
TMPDIR, which macOS always sets. sys_get_temp_dir(), tmpfile() and tempnam() now
resolve it through one runtime helper, and tempnam() falls back to the system
directory instead of returning a path to a file it never created.
Also: tls:// and ssl:// opened in plaintext, random_int() truncated its range to 32
bits, fopen's append mode did not append, and SIGPIPE killed the process instead of
failing the write.
The first full CI run of this branch turned four more defects up, each visible on
exactly one platform and hidden on the others.
The zval pack/unpack bridge had stopped being compiled at all. Resolving the merge
that brought this runtime in dropped `mod zval;` and the ten `zval::emit_*` calls,
replacing them with the new win32 entries. Nothing warned: an undeclared module is
not dead code, it is absent, so the zero-warning build stayed clean while every
helper it defines left the runtime object and `__rt_zval_pack` went undefined at
link.
sprintf formatted php integers at 32 bits on x86_64. The mini format named a bare
conversion, so snprintf read half the value and sprintf("%u", -1) rendered
4294967295. The AArch64 arm has written the "ll" modifier since it was first
emitted; no fixture used a value above 2**32, so nothing caught it.
sprintf rendered oversized conversions from clobbered registers on linux-aarch64.
A result wider than the 128-byte scratch is rendered a second time, and the
argument registers are caller-saved. Apple's ABI passes every variadic argument on
the stack, so the existing stack copy carried the re-render on macOS and hid this
completely; AAPCS64 passes the first in x3, or d0 for a double, so Linux formatted
whatever the first call had left behind -- the right number of bytes, the wrong
ones.
random_int dropped its lower bound on windows: random_int(-100, -50) returned
values in [0, 50]. The x86_64 lowering held min in r9 across the sampler call, and
r9 is caller-saved in both SysV and MSx64, so that only worked by accident of which
registers the sampler touched. On Linux the entropy draw is a bare syscall, which
clobbers rcx and r11; on windows it is rewritten into a call reaching
BCryptGenRandom, which clobbers r9. The AArch64 arm already spilled it.
Five fixtures asserted POSIX rules php does not apply on windows -- TMPDIR against
GetTempPath, /var/tmp probe paths, and the eval constants, which passed there only
because eval answered with the host's values regardless of the target. Each now
names what php defines for its own platform. compiled_binary_size measured `test`
rather than `test.exe` and turned the missing file into a zero, reporting a link
that never happened as a bridge that was not linked.
The crypto-transport fixtures asserted two things at once -- that the handshake
completed, and that the encrypted stream then reads and writes correctly -- so a
failure could not say which half broke. A fixture now asserts only that
stream_socket_client() hands back a stream, which is the half php guarantees in
the transport factory, separating the two failure modes.
Two defects the fixtures could not have caught, found while instrumenting them.
The handshake stream_socket_client() runs on a crypto transport had no bound at
all. Against a peer that completes the TCP connect and then says nothing, rustls
waits on a read the descriptor has no timeout for, and the process blocks forever
-- php returns false after default_socket_timeout. A hung client costs far more
than a failing one: it takes a whole CI shard down with it. php gets this for free
because the stream carries the timeout and crypto is negotiated through the
stream; elephc attaches rustls to a raw descriptor, so it is set explicitly.
microtime(true) reported an instant unrelated to the clock on macos,
intermittently, and the fixture guarding it -- `$t > 1000000000` -- passed under
both faults. The AArch64 helper asked the Darwin trap to fill a timeval, which it
does not do: that is libsystem's job, and the trap returns 0, so the struct was
read back holding whatever the stack carried. Routing through libc, as the x86_64
arm already did, exposed the second fault: Darwin's suseconds_t is 32 bits, so a
64-bit load of tv_usec folded in padding libc does not clear, putting the result
4294.967296 seconds ahead whenever it held 1. The replacement fixture requires
agreement with time(), which samples the same clock.
A second fixture covers the write half of the encrypted path on its own, asserting
fwrite()'s byte count without reading back. With the connect half already
separated, the three data fixtures stop being one indivisible signal: whichever of
connect, write and read is at fault, exactly one fixture fails and names it.
The concat arena also had no capacity at all. It is a flat 64 KiB buffer with an
offset, shared by every string-producing helper, and both read paths wrote into it
without ever asking whether the bytes fit. fread($f, 200000) and
stream_get_contents() on anything past roughly 72 KiB ran straight off the end.
Nothing reported it: the returned string was correct, because it came back from
the overflowed region, while whatever followed the buffer was destroyed -- the
stream itself, in the observed cases, so fclose() rejected it as "unknown given"
where php closes it cleanly. Which neighbour dies depends on the program's layout,
which is what made it look intermittent.
A read that does not fit now takes its own heap block, and stream_get_contents
grows its accumulation onto the heap, doubling so a stream of n bytes costs O(n)
copying. Callers cannot tell: a heap-backed string is the same pointer/length
pair, and __rt_decref_any validates against the managed heap window, so it
releases those blocks and goes on ignoring arena slices. The arena cursor only
moves when the bytes actually landed in the arena.
…urps Two independent paths took an owned heap block from __rt_stream_get_contents and then dropped the only reference to it, leaking one block per call for the life of the process. Both were invisible below 64 KiB: the helper returns a borrowed slice of the concat arena while the stream fits in it, and only hands back an owned __rt_heap_alloc block once it does not. stream_get_contents() is typed `string|false`, so its result is boxed into a Mixed cell inside the lowering. The boxing copies rather than adopts -- __rt_mixed_from_value tag 1 goes through __rt_str_persist, which always allocates -- so the slurped block was left with no owner and, because no EIR value names it, no release either. A 500 KB stream stranded one 524288-byte block per read. The six file_get_contents() URL branches (http/https/ftp, both arches) have the same shape one level down: each slurps the response, persists it for ownership, and then drops the slurped pointer. A 200 KB body cost ~256 KiB per read. Both fixes release the pre-copy pointer through __rt_heap_free_safe once the copy is complete. That separates the two shapes exactly rather than heuristically: _concat_buf and _heap_buf are distinct .comm objects, so an arena pointer can never satisfy its managed-heap window test and passes through untouched. The arena-sized fixtures are the negative controls for that -- freeing a borrowed slice would be a wild free, not a leak. The release is safe here only because no EIR value names the pre-copy pointer. It must not be copied to a builtin typed plain `string`: fread() is exactly that, and there the EIR release already reclaims the block, so a second free would be a double free. Fixtures assert `leak summary: clean` under --heap-debug and check the bytes, not just the length -- releasing before the copy completed would still yield the right length. Verified failing before and passing after: 4 blocks / 2097216 B and 1 block / 262076 B. docs/specs/mixed-string-ownership-debts.md records the wider investigation these two fixes came out of: the still-open Mixed->Str cast leak (~one source-sized block per call across a family of builtins), why a uniform fix there is a use-after-free rather than a leak fix, four other pre-existing defects found on the way -- including silent data corruption on dynamic phar:// reads and an unbounded arena copy in bounded stream_get_contents -- and the measurement pitfalls that make this area easy to get wrong. Claims in it are tagged measured or read so the unexecuted ones stay visible.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #379.
Summary
This pull request adds an experimental
windows-x86_64backend that producesstandalone PE32+ executables and brings the compiler, runtime, bridge crates,
Magician/eval, TLS, and web mode onto native Windows validation paths.
The implementation uses the GNU/MinGW Windows ABI. The accepted
x86_64-pc-windows-msvcspelling is a CLI compatibility alias only; it does notclaim MSVC ABI output. The branch is rebased on the current
mainand remainsone squashed commit. Windows stays experimental until every strict gate below
is green on the same revision.
What is included
Target, PE/COFF, and toolchain support
windows-x86_64/x86_64-pc-windows-gnutarget selection.GNU Windows ABI.
argc/argv, process exit, exceptions,callbacks, fibers, refcounted values, unwind records, and loader mitigations.
running under Rust's native MSVC host toolchain.
and the PE load-configuration directory can be emitted as one validated unit.
Windows ABI and runtime
and Microsoft x64 ABI imports, including shadow space, integer/floating
register classes, stack arguments, variadics, callbacks, and indirect calls.
metadata, directory iteration, streams, processes, clocks, entropy, sockets,
DNS, synchronization, and console/file I/O.
realpath/stat/lstat/fstatmetadata, readonly/link/executable modes, reparse-point probing, dangling
symlinks,
tempnam,tmpfile, loopbacksocketpair,stream_isatty, andstream timeout behavior.
links through
GetFinalPathNameByHandleW, materializes all sevenCreateFileWarguments in their exact MSx64 slots (including directorybackup semantics), and avoids issuing reparse-point control requests for
ordinary files and directories.
tempnam()contract for prefix basenames, Windows pathseparators, the 63-byte prefix limit, and the
falsefailure sentinel.chown()/chgrp()visible with Windows failure semantics whileomitting
lchown()/lchgrp()exactly as php-src does withoutHAVE_LCHOWN.Direct calls, namespace fallback, user redeclarations, first-class callables,
dynamic dispatch, Reflection, eval,
function_exists(), andis_callable()all use the same target-aware catalog.
fopenbase-mode and suffix surface, Windowstemporary-file deletion, complete 64-bit Winsock handles, and the correct
close order for filters, TLS sessions, stream state, CRT descriptors, and
sockets.
ordinary I/O and zlib, bzip2, iconv, and user-filter paths;
fseek()andrewind()use the same handle mapping.onCreate, filter callbacks, parameters, andonCloseuse the same object.z_streamlayout and 32-bituLongfields for one-shotzlib operations, and recognizes PHP's exact uppercase
INF,-INF, andNANserialized float tokens before falling back to MSVCRTstrtod.__FILE__,__DIR__, closure names, and EIR source metadata.the runtime can negotiate; Windows omits Unix-domain transports and all
targets omit unavailable SSLv2/SSLv3 aliases.
longresults before 64-bit consumers and derivesPHP_EOL,DIRECTORY_SEPARATOR, andPATH_SEPARATORfrom the target.Processes, shell behavior, eval, and web
proc_open()descriptor inheritance and sparsedescriptor behavior,
proc_close(),proc_terminate(), andproc_get_status(). Windows reportscached: false, while Unix preservesPHP's post-exit cached transition.
synchronous reads to the bytes reported by
PeekNamedPipe, preventing bothstartup races and accidental blocking.
order, ignores the keys like php-src, and preserves raw numeric environment
entries while skipping empty converted values.
correctly through CRT
argvconstruction.randomness, time, string, callable, and target-specific builtin behavior.
--webas a PE program with exported ABI validation, HTTPsuccess/error responses, Ctrl-C shutdown, watchdog termination, and clean
--max-requestsexit.TLS and stream contexts
paths.
and enterprise trust anchors remain visible; other targets retain the
deterministic bundled root set.
ElephcTlsClientOptionsstructure(options layout ABI v1) shared by HTTPS connects and STARTTLS attachment.
verify_peer,verify_peer_name, andallow_self_signed, pluspeer_name, additivecafile/capath, and combinablelocal_cert/local_pk.local_pkas optional when the key is present inlocal_cert, andignores
local_pkwithout a client certificate, matching php-src.allow_self_signedto a valid depth-zero genuinely self-signedleaf. It never accepts an arbitrary untrusted leaf, never bypasses hostname
validation, and always verifies TLS 1.2/1.3 handshake signatures.
0and"0"arefalse; any other non-zero integer is true).
enforces supported TLS 1.2/1.3 client masks, and reuses the source TLS
configuration/session cache when
session_streamis provided.terminal
UnexpectedEofread to the same response EOF behavior php-srcenables through
SSL_OP_IGNORE_UNEXPECTED_EOF, and rejects oversized,truncated, or unrewindable response streams with balanced handle cleanup.
Rust, preserving retryable
WouldBlock/TimedOutresults without markingstreams as EOF.
Final php-src audit
The implementation was audited against the current official php-src Windows,
stream, TLS, process, shell, and filesystem paths:
win32/ioutil.cwin32/sockets.cwin32/build/config.w32.h.inmain/streams/xp_socket.cmain/streams/plain_wrapper.cmain/php_open_temporary_file.cext/standard/streamsfuncs.cext/standard/filestat.cext/standard/proc_open.cext/standard/exec.cext/openssl/xp_ssl.cThe last comparison found and closed the remaining gaps around native Windows
trust roots, TLS policy independence and true self-signed handling, client key
fallback, STARTTLS crypto-method/session propagation, complete HTTPS I/O,
target-aware
lchown/lchgrpvisibility, namespace and callable resolution,tempnam()failure/prefix semantics,proc_open()command/environmentmarshalling,
proc_get_status().cached, shell escaping, stream-filter registerpreservation and object lifetime, bounded process-pipe reads, LLP64 zlib
layout, serialized non-finite floats, loopback socket-pair fixtures,
canonical-path sharing/reparse handling, terminal TLS EOF, Windows stream
seeking, deterministic process-filter line endings, and target-path
normalization. The resulting CI investigation also
closed an AArch64 runtime-callable stack-padding regression without changing
the Windows shadow-space contract, and split a redundant composite eval
failure-path regression into focused cases so each native scenario retains its
own timeout budget.
The audit also removed the obsolete Wine allow-list/known-failures snapshots
and their exemption commands. The retained aggregate verifier accepts only
exact native shard coverage with no failing testcase, validates the expected
JUnit report count inside the verifier, and emits an explicit cardinality
diagnostic instead of counting CLI argument tokens in the workflow shell.
What the first full CI run exposed
Earlier runs on this branch had all been cancelled, so the complete matrix had
never seen this code. The first full run returned 14 red checks across 6
distinct defect classes. Every one is fixed in this revision, and each was
visible on exactly one platform while the others hid it:
mod zval;dropped by the merge resolutionsprintfmissing thelllength modifiersprintfoverflow re-render losing its variadicrandom_intlosing itsminboundsyscallclobbers onlyrcx/r11; the Windows rewrite turns it into acall, which destroysr9fclose()on a TLS stream faulting#[ignore], outbound network)The TLS fault was the last to close and was not where it appeared to be. Connect,
write, and reads all succeed on x86-64; only closing failed. On that target the
descriptor lives in
raxwhile__rt_user_filter_release_fdreads it fromrdi,and
lower_fclosewas the one call site in the function that did not stage theargument. It worked by accident, because the TLS teardown's own map lookup leaves
the descriptor in
rdi— but only while it finds no session to close. Once it does,it loads the session handle into
rdiand calls the bridge, and the Rust closeleaves
rdiholding whatever it likes; the filter table was then indexed with thatvalue. AArch64 was unaffected because it passes the argument in
x0, which theteardown restores.
Two further defects were found while investigating the above and are fixed here:
__rt_freadandstream_get_contentswrote past the end of the fixed 64 KiBconcat arena. The returned bytes were correct — read back from the overflowed
region — while whatever followed the buffer was destroyed, so the observable
casualty changed from program to program. Reads that do not fit now take an owned
block, sized from what the stream can actually supply rather than from the
requested length, since
fread($f, $n)reads up to$nbytes.microtime(true)returned a value roughly 2000× too large on macOS. Darwin'sgettimeofdaytrap returns 0 without filling the caller'stimeval, andsuseconds_tis 32 bits there; both are now handled.CI and acceptance gates
Cross-compilation gate
Windows PE Cross-Compile Testsand the independentWindows PE LLVM/LLD Testsrun on Ubuntu with MinGW-w64 and Wine. Together they:binaries.
Native bridge gate
Windows Native PE & Bridge Testsruns on Windows Server 2025. It:windows_peintegration module; andStrict native codegen gate
The runnable
ci-profile codegen inventory is split into 16 deterministicpartitions and executed directly on Windows Server 2025. There is no allowlist
or known-failure exemption: each runnable fixture must appear exactly once and
every shard must succeed. The aggregate Windows gates are required by the
top-level
Build & Testjob.The same top-level gate explicitly waits for both ordinary and Magician/eval
codegen matrices on macOS AArch64, Linux x86_64, and Linux AArch64, so a
separately sharded eval failure cannot be hidden behind a green aggregate.
Validation
The complete CI matrix is green on this revision: 123 jobs, no failures, across
macOS AArch64, Linux x86-64, Linux AArch64, and native Windows Server 2025, including
all 16 strict native codegen shards, both eval matrices, and the aggregate gates.
Focused local validation performed during development includes:
regressions;
execution of the affected fixtures as PE32+ binaries under Wine; and
git diff --check, ancestry, squash, and unmerged-file checks.Closing the disclosed gaps
The four items previously disclosed here have been worked through; what changed and what
did not is recorded below rather than quietly dropped.
Windows ACLs behind
is_readable()/is_writable()— closed.__rt_sys_accesswasGetFileAttributesW-only, and its own docblock admitted that "R_OK succeeds whenever thepath exists". It now performs php-src's check from
TSRM/tsrm_win32.c: a cachedimpersonation token,
GetFileSecurityW,MapGenericMask, thenAccessCheck, with php'sexact
FILE_GENERIC_READ/FILE_GENERIC_WRITEmasks.advapi32joins both theproduction and test-harness link sets.
The check fails open by construction. Every failure — no token, a failed security
query, a descriptor larger than the inline buffer, a failed
AccessCheck— returns asentinel that the gate treats exactly like "granted", so the DACL verdict can only ever
turn a yes into a no and never invents a denial. That matters more than it sounds: a
fail-closed design would make
is_readable()return false for every path, permanently andinvisibly, under a restricted token (a service, AppContainer, or low integrity). The
worst case here equals the previous behaviour.
Reaching that guarantee required dropping the heap from the path entirely. An earlier
draft allocated the security descriptor and treated a null return as its fallback, but
__rt_heap_allocnever returns null — it diverges through__rt_heap_exhausted, which onWindows issues Linux syscall numbers that neither print nor exit before falling off the end
of the helper. The fallback branch was unreachable. The descriptor now lives in a
fixed-size stack buffer, and an oversized descriptor takes the same fail-open path.
_elephc_web_handlernon-volatile registers — closed. The handler is anextern "C" fn()the bridge's Rust server loop calls directly, so it owes that caller theplatform's callee-saved contract; the register allocator does not discharge it, because its
pools deliberately exclude the registers involved (AArch64 allocates x21-x28, x86_64 only
rbx).
abi::nested_call_reghands out x19 and r12 regardless, hand-written runtime routinesuse r14/r15 as scratch, and float spills stage through xmm15. The handler now preserves
those itself, plus rdi/rsi and xmm6-xmm15 on Windows, where MSx64 makes them callee-saved
and elephc's SysV-internal convention treats them as scratch. AArch64 was violating the same
contract through x19 and is fixed with it.
Oversized-read heap blocks — closed for
fread(). This needed two independentomissions, which is why fixing either alone measured as no change at all. The EIR already
emitted a
releasefor these values and the backend discarded it, becausevalue_is_scratch_stringtreats any non-Freshruntime call as arena scratch; and theblock carried heap kind 0, which every
__rt_decref_anyin the runtime's own read pathsskips as raw. Arena slices stay safe under the release:
_concat_bufand_heap_bufareseparate
.commobjects, so an arena pointer can never satisfy__rt_heap_free_safe'smanaged-heap window test, making the discrimination exact rather than heuristic.
Still open: a
stream_get_contents()result boxed into a Mixed cell strands its grownaccumulation buffer.
__rt_mixed_from_valuecopies the bytes out, so the original block isorphaned before any release refers to it. That belongs to the boxing path.
Marginal
evalfixtures — closed. Two fixtures that compile and link a PHP program atrun time sat against the global 60s
slow-timeout. They now carry the same documentedper-test override the four heavier fixtures above them already use.
Known gaps
stream_get_contents()result boxed into a Mixed cell still strands its accumulationbuffer, as described above.
__rt_heap_exhausteddoes not terminate correctly on Windows: it issues Linux syscallnumbers, so heap exhaustion falls off the end of the helper rather than reporting and
exiting. Pre-existing and independent of this branch's work; found while reviewing the
ACL fallback and worth fixing on its own.
Promotion criteria
Windows can move into the first-class supported-target matrix only when one
revision satisfies all of the following:
green;
reduced semantics that the project claims to support.
On this revision the first three are met, and the php-src audit is recorded above.
The last is not:
is_readable()/is_writable()still ignore Windows ACLs, which isexactly the kind of reduced semantics that criterion exists to catch. Promotion is
the maintainer's call, and this branch does not claim it — documentation and
diagnostics continue to label the target experimental.