Windows: APE exit status is shifted left by 8 (exit code does not round-trip; failures read as success in POSIX shells)
Summary
On Windows, a program built with cosmocc exits with its status shifted left by 8 — i.e. the raw wait()-style status rather than the exit code. return 1 becomes a Windows process exit code of 256, return 42 becomes 10752.
The practical consequence is that in a POSIX-style shell on Windows (MSYS2, Cygwin, Git Bash) every failure looks like success, because those shells keep only the low byte and code << 8 has a low byte of 0 for any code < 256. cmd.exe and PowerShell see the shifted value instead. Either way, the exit status does not round-trip.
I hit this in CI for a project that ships an APE: a build tool correctly called exit(1), printed its error, wrote no output file, and the shell still saw success. Anything of the form mytool && next-step silently proceeds after a failure.
Reproduction
/* exitcode.c */
#include <stdlib.h>
int main(int argc, char **argv) {
return argc > 1 ? atoi(argv[1]) : 1;
}cosmocc -O0 -o exitcode.com exitcode.cRun it, asking for a specific exit code, and compare what the shell reports:
# MSYS2 bash
for c in 0 1 2 3 42 127 255; do ./exitcode.com $c; echo "$c -> $?"; done# PowerShell
foreach ($c in 0,1,2,42,255) { & .\exitcode.exe $c; "$c -> $LASTEXITCODE" }:: cmd.exe (delayed expansion, otherwise %ERRORLEVEL% expands at parse time)
cmd /v:on /c "exitcode.exe 42 & echo !ERRORLEVEL!"Observed
| requested | MSYS2 bash $? |
PowerShell $LASTEXITCODE |
cmd !ERRORLEVEL! |
code << 8 |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 256 | 256 | 256 |
| 2 | 0 | 512 | 512 | 512 |
| 3 | 0 | 768 | – | 768 |
| 42 | 0 | 10752 | 10752 | 10752 |
| 127 | 0 | 32512 | – | 32512 |
| 255 | 0 | 65280 | 65280 | 65280 |
The observed value equals code << 8 exactly, for every value tested.
Expected
The exit code should round-trip: return 42 should be observable as 42.
Control — the same source built with a native Windows compiler
Same .c, same shells, built with MSYS2's UCRT64 GCC, so this isolates the behaviour to the APE rather than to the shells:
| requested | native gcc (bash) | native gcc (PowerShell) | cosmocc APE (PowerShell) |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 256 |
| 2 | 2 | 2 | 512 |
| 42 | 42 | 42 | 10752 |
| 255 | 255 | 255 | 65280 |
exit(n) and return n from main behave identically; both are affected.
Only the 0 case is correct, which is why this is easy to miss — a passing build behaves exactly as expected, and only failures are silently swallowed.
Environment
- Windows 11 Pro, 10.0.26200 build 26200, AMD64
cosmocc4.0.2 fromhttps://cosmo.zip/pub/cosmocc/cosmocc-4.0.2.zip(sha25685b8c37a406d862e656ad4ec14be9f6ce474c1b436b9615e91a55208aced3f44)cosmocc (GCC) 14.1.0- Shells: MSYS2 (MSYS) bash, PowerShell 5.1.26100.9444, cmd.exe
Not verified
I did not have a Linux machine available to run the same .com as a control, so I have not confirmed first-hand that the same binary round-trips its exit code on Linux — only that a native Windows binary does on Windows. If it would help, I am happy to run any additional probe.
Source: jart/cosmopolitan