From e1a7b54f8b0e24e0911b2f919cb5f9aacdde1df0 Mon Sep 17 00:00:00 2001 From: Luca Bacci Date: Fri, 14 Nov 2025 16:16:58 +0100 Subject: [PATCH] Win32: Add test for safe stdio flushing Test the two-phase flushing implemented in app_profile_dep. --- glib/tests/win32.c | 159 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/glib/tests/win32.c b/glib/tests/win32.c index b1b49f1e0..760bf77d5 100644 --- a/glib/tests/win32.c +++ b/glib/tests/win32.c @@ -25,7 +25,13 @@ #include #include +#include + #include +#include +#include +#include +#include #include #include /* for RTL_OSVERSIONINFO */ @@ -249,6 +255,157 @@ test_manifest_os_compatibility (void) FreeLibrary (module_handle); } +typedef struct { + FILE *stream; + bool done; + GMutex mutex; + GCond cond; +} StreamLockData_t; + +static gpointer +thread_acquire_stdio_stream_lock (gpointer user_data) +{ + StreamLockData_t *data = (StreamLockData_t *) user_data; + FILE *stream = data->stream; + + /* For the test to be effective, this thread must be holding + * the stream lock when the subprocess "main function" (i.e. + * test_subprocess_early_flush) returns. + * + * This simulates a thread doing I/O on a stream at the time + * exit invokes ExitProcess. Note that _lock_file is exactly + * what stdio functions call internally. + */ + + _lock_file (stream); + + g_mutex_lock (&data->mutex); + data->done = true; + g_cond_signal (&data->cond); + g_mutex_unlock (&data->mutex); + data = NULL; + + /* Sleep a bit to let the main thread exit. Note: if somehow + * the main thread doesn't exit in time, this test will + * succeed but won't actually verify anything. + */ + g_usleep (1 * G_USEC_PER_SEC); + + _unlock_file (stream); + + return NULL; +} + +static void +test_subprocess_early_stdio_flush (void) +{ + int64_t fd; + FILE *stream; + int ret; + + fd = g_ascii_strtoull (g_getenv ("G_TEST_PIPE_FD"), NULL, 10); + g_assert_cmpuint (fd, >=, 0); + g_assert_cmpuint (fd, <=, INT_MAX); + + stream = _fdopen ((int) fd, "w"); + g_assert_nonnull (stream); + + ret = setvbuf (stream, NULL, _IOFBF, 1024); + g_assert_cmpint (ret, ==, 0); + + ret = fprintf (stream, "hello world"); + g_assert_cmpint (ret, ==, (int) strlen ("hello world")); + g_assert_false (ferror (stream)); + + StreamLockData_t data; + memset (&data, 0, sizeof (data)); + data.stream = stream; + data.done = false; + + GThread *thread = g_thread_new ("lock stdio stream", + thread_acquire_stdio_stream_lock, + (gpointer) &data); + g_thread_unref (thread); + + /* Wait until the worker thread has acquired the stream lock. + */ + g_mutex_lock (&data.mutex); + while (!data.done) + g_cond_wait (&data.cond, &data.mutex); + g_mutex_unlock (&data.mutex); + + /* If the early flush is not implemented, the C RunTime will attempt + * to acquire the stream's internal CRITICAL_SECTION from DllMain. + * Then the process will be terminated abruptly (with the same exit + * code passed to exit) leaving stream unflushed. + */ +} + +static gpointer +thread_read_pipe (gpointer user_data) +{ + int fd = GPOINTER_TO_INT (user_data); + const char *iter = "hello world"; + int size = (int) strlen (iter); + + while (size > 0) + { + char buffer[20]; + int ret = _read (fd, buffer, sizeof (buffer)); + + g_assert_cmpint (ret, >, 0); + g_assert_cmpint (ret, <=, size); + + g_assert_cmpmem (buffer, ret, iter, ret); + + iter += ret; + size -= ret; + } + + return NULL; +} + +static void +test_early_stdio_flush (void) +{ + int pipe_fds[2]; + int pipe_read; + int pipe_write; + char buffer[10]; + GStrv envp = NULL; + GError *error = NULL; + int ret; + + ret = _pipe (pipe_fds, 1024, _O_BINARY); + if (ret < 0) + g_error ("%s failed: %s", "_pipe", g_strerror (errno)); + + pipe_read = pipe_fds[0]; + pipe_write = pipe_fds[1]; + + g_snprintf (buffer, sizeof (buffer), "%i", pipe_write); + envp = g_get_environ (); + envp = g_environ_setenv (g_steal_pointer (&envp), "G_TEST_PIPE_FD", buffer, TRUE); + + GThread *thread = g_thread_new ("read pipe", + thread_read_pipe, + GINT_TO_POINTER (pipe_read)); + + typedef const char * const * GStrvConst_t; + g_test_trap_subprocess_with_envp ("/win32/subprocess/early-stdio-flush", + (GStrvConst_t) envp, 0, + G_TEST_SUBPROCESS_INHERIT_DESCRIPTORS); + g_test_trap_assert_passed (); + + g_close (pipe_write, &error); + g_assert_no_error (error); + + g_thread_join (thread); + + g_close (pipe_read, NULL); + g_strfreev (envp); +} + int main (int argc, char *argv[]) @@ -275,6 +432,8 @@ main (int argc, g_test_add_func ("/win32/subprocess/stderr-buffering-mode", test_subprocess_stderr_buffering_mode); g_test_add_func ("/win32/stderr-buffering-mode", test_stderr_buffering_mode); g_test_add_func ("/win32/manifest-os-compatibility", test_manifest_os_compatibility); + g_test_add_func ("/win32/subprocess/early-stdio-flush", test_subprocess_early_stdio_flush); + g_test_add_func ("/win32/early-stdio-flush", test_early_stdio_flush); return g_test_run(); }