Skip to content

Windows: fork answers -1, so system* always spawns - #1

Merged
aoinoikaz merged 8 commits into
windows-pe32-ntcallfrom
windows-spawn
Nov 3, 2026
Merged

aoinoikaz merged 8 commits into
windows-pe32-ntcallfrom
windows-spawn

Conversation

@aoinoikaz

Copy link
Copy Markdown

On Windows, fork cloned the process with RtlCloneUserProcess (lib/windows/fork.c): on x64 a heaven's gate into 64-bit ntdll and a write into the child's ntdll at an address from two hardcoded RVAs, on ARM64 (no wow64cpu.dll) a plain clone nothing had measured. Wine has no RtlCloneUserProcess, so system*'s spawn branch ran only under Wine.

  • fork answers -1 with ENOSYS from lib/stub/fork.c, which answered 0, the child's answer. This also changes the GNU/Hurd list. fork.c, wow64gate.c, wow64resolve.c and scaffold/boot/fork-test.scm are removed.
  • kaem.windows.run names the files the tree has: five of its paths were gone, and seven files it needs, spawn.c among them, were missing.
  • nixpkgs' libc fixes become commits (nixpkgs 839e2de5d856, unchanged at 510ce503d505): ntoab, ioctl3, vfprintf, vsnprintf. The printf fix keeps va_arg8 and notes l per conversion; the commits say why. strpbrk is already fixed. GNU Mes has none of the four as of 746a8209.
  • A Windows workflow builds stage0-pe32's chain and mes-m2 from kaem.windows.run, then runs scaffold/spawn.scm, on windows-latest and windows-11-arm.

Not covered: MesCC's own M1 and hex2 calls and the MesCC-built C library (nova-nix issue 358). system* still splits PATH on : and looks for a bare name without .exe, so a name with no / is not found on Windows.

Refs nova-nix issue 357.

The ntdll layer moved to lib/windows/ntdll.c and include/windows/ntdll.h,
with include/windows/ntcall.h and lib/windows/x86-mes-m2/ntlow.c and
ntcall.c beside it (a417554, f917901, 7e5bfdc), and the WOW64 files to
lib/windows/ (dcd1876).  7baa813 split argv.M1 and defs.M1 out of
crt1.M1 into lib/windows/x86-mes/ and moved pe-end.M1 there, and spawn.c
(266ab1f) and filetime.c (00fbf64) were added.  kaem.windows.run named
five paths that no longer exist, so M2-Planet stopped at the first it
could not open, and none of the seven files added since, among them
spawn.c, which core:spawn calls, and defs.M1, without which M1 cannot
assemble crt1.M1.

* kaem.windows.run: Compile include/windows/ntdll.h and ntcall.h,
lib/windows/x86-mes-m2/ntlow.c, ntcall.c and filetime.c, and
lib/windows/ntdll.c, wow64gate.c, wow64resolve.c and spawn.c.  Assemble
with lib/windows/x86-mes/defs.M1, argv.M1 and pe-end.M1.
lib/windows/fork.c cloned the process with RtlCloneUserProcess.  Wine's
ntdll has no such export, so under Wine fork answered -1 and system* took
its spawn branch; on Windows, every M1 and hex2 call MesCC makes would take
the clone instead.  On x64 the clone's child runs only after a heaven's
gate into 64-bit ntdll and a write of four zero bytes into the child's
ntdll, at an address computed from two RVAs measured on one Windows build.
ARM64 emulates x86 with xtajit.dll and has no wow64cpu.dll, so there fork
took the plain clone, which its own comment says was reasoned through
rather than measured.

The Windows lists now name lib/stub/fork.c, as they do for the other calls
this port has no answer for.  That stub answered 0, which tells a caller
it is the child.  It answers -1 with ENOSYS now, and gives no __mes_debug
notice, since kaem.windows.run's M2-Planet list has no __mes_debug;
lib/stub/__raise.c is the same plain form.  This also changes the stub for
the GNU/Hurd list, the only other one that names it, which has no spawn
for core:spawn either.

* lib/stub/fork.c (fork): Set errno to ENOSYS and return -1, with no
__mes_debug notice.
* build-aux/configure-lib.sh (libc_SOURCES) [windows]: Name lib/stub/fork.c
instead of lib/windows/fork.c, wow64gate.c and wow64resolve.c.
* kaem.windows.run: Likewise.
* lib/windows/fork.c, lib/windows/wow64gate.c, lib/windows/wow64resolve.c,
scaffold/boot/fork-test.scm: Remove.
* include/windows/ntdll.h (__walloc16, __qadd64): Remove declarations.
* mes/module/mes/posix.mes (system*): Update comment.
* src/posix.c (spawn_): Likewise.
* lib/windows/spawn.c (spawn): Likewise.
* lib/windows/ntdll.c (__ntdll_find_module, __ntdll_resolve_export):
Likewise.
* scaffold/ntdll-resolve-test.c (main): Likewise.
The head comments of lib/windows/ntdll.c and include/windows/ntdll.h told
how the layer came to be, named lib/windows/ntdll.h for
include/windows/ntdll.h, placed __ntdll in ntdll.c where it is in
ntlow.c, and said nothing compiled from C calls __ntdll, which
scaffold/ntdll-resolve-test.c does.

* lib/windows/ntdll.c: Rewrite the head comment.
* include/windows/ntdll.h: Likewise for the slot table's.
* scaffold/ntdll-resolve-test.c (main): Say why NT_CLONE is not checked.
include/mes/lib.h declares __mesabi_uldiv on unsigned long, as its name
says, while lib/mes/ntoab.c defined it, and the values ntoab hands it, on
size_t.  The two are one type only where size_t is unsigned long, which
Mes's own headers make it for every compiler but M2-Planet.  Where it is
unsigned int, as an i386 compiler's own stddef.h has it, ntoab.c fails
with conflicting types for __mesabi_uldiv.

nixpkgs makes this change with replace in
pkgs/os-specific/linux/minimal-bootstrap/mes/default.nix, from nixpkgs
commit 839e2de5d85649c01cd99f6da861747385e4feab, unchanged at
510ce503d5055ea25a461f95b793ee992a650d44.  GNU Mes does not have it as of
master 746a8209, after 0.28.

* lib/mes/ntoab.c (__mesabi_uldiv): Use unsigned long.
(ntoab): Likewise for i, u and b.
include/sys/ioctl.h declares ioctl3's command unsigned long for every
compiler but M2-Planet, which sees unsigned, and lib/stub/ioctl3.c defines
it so.  include/mes/lib.h declared it, and lib/linux/ioctl3.c and
lib/windows/ioctl3.c defined it, size_t.  Where size_t is unsigned int, a
file that includes both headers, as lib/posix/isatty.c does, fails with
conflicting types for ioctl3.

nixpkgs makes this change to include/mes/lib.h and lib/linux/ioctl3.c
with replace in pkgs/os-specific/linux/minimal-bootstrap/mes/default.nix,
from nixpkgs commit 839e2de5d85649c01cd99f6da861747385e4feab, unchanged
at 510ce503d5055ea25a461f95b793ee992a650d44.  lib/windows/ioctl3.c is
this port's and gets the same change.  GNU Mes does not have it as of
master 746a8209, after 0.28.

* include/mes/lib.h (ioctl3): Declare command unsigned long.
* lib/linux/ioctl3.c (ioctl3): Define it so.
* lib/windows/ioctl3.c (ioctl3): Likewise.
Without an l, an integer conversion's argument is an int or an unsigned
int, and vfprintf read every one as a long.  Where long is wider than int
that reads bytes the argument does not have: built against a host C
library on 64-bit Arm, %u of -1 printed 18446744073709551615 and %x of
0xdeadbeef printed ffffffffdeadbeef.  Where int and long are one width, as
on i686, the output is the same as before.  ntoab is still asked for a
signed result for %o, so there %o of a value above INT_MAX still prints
with a minus sign.

nixpkgs makes this change with replace in
pkgs/os-specific/linux/minimal-bootstrap/mes/default.nix, from nixpkgs
commit 839e2de5d85649c01cd99f6da861747385e4feab, unchanged at
510ce503d5055ea25a461f95b793ee992a650d44.  This differs from it in three
ways.  The l is noted per conversion: nixpkgs' flag outlives a conversion
that is not an integer, so after %lc its next %u reads a long.  A second l
still warns, where nixpkgs' replace of every match makes it a silent
skip.  And %e, %f and %g keep va_arg8: nixpkgs replaces it with va_arg,
which include/stdarg.h, but for RISC-V and a system C library, defines as
reading a long, converting its value to double and stepping ap by the size
of a pointer.  GNU Mes does not have the change as of master 746a8209,
after 0.28.

* lib/stdio/vfprintf.c (vfprintf): Read an integer conversion's argument
as a long only after an l.
…int.

The same fix vfprintf has: without an l, vsnprintf read an integer
conversion's int or unsigned int argument as a long, which where long is
wider than int reads bytes the argument does not have.  Built against a
host C library on 64-bit Arm, %u of -1 printed 18446744073709551615.  On
i686 the output is the same as before.

nixpkgs makes this change with replace in
pkgs/os-specific/linux/minimal-bootstrap/mes/default.nix, from nixpkgs
commit 839e2de5d85649c01cd99f6da861747385e4feab, unchanged at
510ce503d5055ea25a461f95b793ee992a650d44.  As for vfprintf, the l is noted
per conversion, a third l still warns, and va_arg8 stays.  GNU Mes does
not have the change as of master 746a8209, after 0.28.

* lib/stdio/vsnprintf.c (vsnprintf): Read an integer conversion's argument
as a long only after an l.
The fork has no CI, so no run checks the Windows port on Windows.  On
windows-latest (x64, through WOW64) and windows-11-arm (through the x86
emulator), this builds stage0-pe32's chain at the commit nova-nix pins,
mescc-tools-extra's mkdir with it, and mes-m2 with kaem.windows.run on a
PATH of those tools alone.  scaffold/spawn.scm then has mes-m2 check that
primitive-fork answers -1 and start itself through system*, and fails
unless the child's exit status comes back.  The job also checks that the
child's output reached the parent's.

* .github/workflows/windows.yml: New file.
* scaffold/spawn.scm: New file.
@aoinoikaz
aoinoikaz merged commit cb669ce into windows-pe32-ntcall Nov 3, 2026
2 checks passed
@aoinoikaz
aoinoikaz deleted the windows-spawn branch November 4, 2026 01:08
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