From 3468da5cd72c54d5024b2c9c453b2f9589d28831 Mon Sep 17 00:00:00 2001 From: Chris Speciale Date: Thu, 20 Aug 2026 11:20:38 -0400 Subject: [PATCH] [windows] file_write: require a console, not just a character device _isatty is true for any character device, so a write to NUL took the WriteConsoleW path. WriteConsoleW fails on a non-console handle and the file_error after it throws from inside a GC-free zone, so the process dies instead of reporting anything: sys.io.File.write("NUL").writeString("x") prog.exe >NUL GetConsoleMode only succeeds on a real console, and __hxcpp_print already uses it. _isatty stays as the first check so files and pipes take the same fwrite path they did before. Came in with 1544ff5e (#1307), which fixed utf console output. That part is unchanged here. --- Changes.md | 1 + src/hx/libs/std/File.cpp | 5 ++++- test/regression/NulDeviceWrite/Main.hx | 13 +++++++++++++ test/regression/NulDeviceWrite/build.hxml | 2 ++ test/regression/NulDeviceWrite/stdout.txt | 1 + 5 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 test/regression/NulDeviceWrite/Main.hx create mode 100644 test/regression/NulDeviceWrite/build.hxml create mode 100644 test/regression/NulDeviceWrite/stdout.txt diff --git a/Changes.md b/Changes.md index 816b4be7c..2e6d75d30 100644 --- a/Changes.md +++ b/Changes.md @@ -16,6 +16,7 @@ * Updated sqlite to 3.40.1 * Updated zlib to 1.2.13 +* Fixed crash writing to the null device on Windows * Fixed SSL socket non blocking handshake throwing an exception on 64bit Windows * Fixed Windows 64bit architecture detection * Fixed critial error handler returning the wrong callstack diff --git a/src/hx/libs/std/File.cpp b/src/hx/libs/std/File.cpp index a88318746..1a2435334 100644 --- a/src/hx/libs/std/File.cpp +++ b/src/hx/libs/std/File.cpp @@ -149,7 +149,10 @@ int _hx_std_file_write( Dynamic handle, Array s, int p, int n ) hx::AutoGCFreeZone zone; #ifdef HX_WINDOWS - if (_isatty(_fileno(f->io))) { + // _isatty is true for ANY character device - NUL, a serial port, a printer - and none of + // those accept WriteConsoleW. Only a real console has a console mode. + DWORD console_mode; + if (_isatty(_fileno(f->io)) && GetConsoleMode((HANDLE)_get_osfhandle(_fileno(f->io)), &console_mode)) { fflush(f->io); HANDLE win_handle = (HANDLE)_get_osfhandle(_fileno(f->io)); static const int MAX_BUFFER_SIZE = 8192; diff --git a/test/regression/NulDeviceWrite/Main.hx b/test/regression/NulDeviceWrite/Main.hx new file mode 100644 index 000000000..5d3388d81 --- /dev/null +++ b/test/regression/NulDeviceWrite/Main.hx @@ -0,0 +1,13 @@ +// The null device is a character device, so _isatty reports it as a tty. file_write used that as +// its test for a console and called WriteConsoleW, which only accepts a console handle; the failure +// threw from inside a GC-free zone and took the process down instead of reporting an error. +function main() { + final nul = Sys.systemName() == "Windows" ? "NUL" : "/dev/null"; + + final out = sys.io.File.write(nul); + out.writeString("written to the null device\n"); + out.flush(); + out.close(); + + Sys.println("ok"); +} diff --git a/test/regression/NulDeviceWrite/build.hxml b/test/regression/NulDeviceWrite/build.hxml new file mode 100644 index 000000000..f35632dbc --- /dev/null +++ b/test/regression/NulDeviceWrite/build.hxml @@ -0,0 +1,2 @@ +-cpp bin +-m Main diff --git a/test/regression/NulDeviceWrite/stdout.txt b/test/regression/NulDeviceWrite/stdout.txt new file mode 100644 index 000000000..9766475a4 --- /dev/null +++ b/test/regression/NulDeviceWrite/stdout.txt @@ -0,0 +1 @@ +ok