[PATCH] winsup: console: validate duplicated handle set for master thread

飛飛飛 <[email protected]>
Newsgroups gmane.os.cygwin.patches
Message-ID <CAD8hRBMNcuh+k0btNomyhcmn=yuNyn-Hh1bkC7gBKEiT=9kT0Q@mail.gmail.com>
Hi Cygwin maintainers,

I originally came across this issue while working on LibreOffice development
on Windows.

My main development environment is:

  CPU: AMD Ryzen 7 9800X3D, 8 cores / 16 hardware threads
  OS: Windows
  Visual Studio: Visual Studio 2022
  Windows SDK / UCRT: 10.0.26100.0
  Cygwin runtime: E:\LibreOfficeDev\cygwin64
  LibreOffice source tree: E:\LibreOfficeDev\src

While developing LibreOffice, I found that parallel builds could become
unreliable in my environment, and I often had to reduce the parallelism
significantly to avoid a stuck build.

Out of curiosity, I started reading the Cygwin source to understand what was
happening under parallel workloads. I used ChatGPT as an assistant while
inspecting logs, dumps, source code and synchronization paths, but I
reproduced,
built and validated the changes locally myself.

During the investigation, I reproduced a console-related failure path:

  close_handle: CloseHandle(p->input_handle<0x0>) failed, Win32 error 6

Reviewing the console master-thread handle setup showed that
get_duplicated_handle_set() assumed that all DuplicateHandle() calls
succeeded, while cleanup could later attempt to close members of a partially
initialized handle set.

The attached patch makes handle acquisition all-or-nothing.

It:

  * initializes handle_set_t before duplication;
  * changes get_duplicated_handle_set() to return bool;
  * checks every DuplicateHandle() result;
  * cleans up already duplicated handles if a later duplication fails;
  * avoids starting the console master thread with an incomplete handle set;
  * releases duplicated console handles if thread_sync_event duplication
fails;
  * makes close_handle_set() tolerate partially initialized handle sets.

The patch does not intentionally change console synchronization semantics.

I tested this against the current official newlib-cygwin source.

Upstream base:

  81f45549e771fd4da7b88ad04bd9e6ec5c765d50

Patch commit:

  1d5fbfa8e
  winsup: console: validate duplicated handle set for master thread

The patch was also verified in both directions:

  reverse apply against the patched HEAD: PASS
  forward apply against the upstream parent: PASS

The resulting current-master Cygwin DLL was built and tested locally.

Runtime smoke tests:

  true.exe   PASS
  uname.exe  PASS

I then installed the resulting DLL into the Cygwin runtime used by my actual
LibreOffice development environment and tested real parallel LibreOffice
builds:

  make -j4   PASS, exit status 0, elapsed time 25 seconds
  make -j8   PASS, exit status 0

The -j8 run exercised the normal LibreOffice gbuild process tree, including
make, sh, touch, cp and other Cygwin utilities.

I am submitting this because I believe that when an open-source failure path
can be reproduced and hardened without changing intended behavior, it is
worth
trying to fix it rather than only working around it.

I am not a Cygwin maintainer, so I would be grateful for review and any
corrections needed to better match Cygwin conventions.

Thank you for maintaining Cygwin.

Regards,
Chris Chen
[email protected]
0001-winsup-console-validate-duplicated-handle-set-for-ma.patch (application/octet-stream, 6.3 KB)
From 1d5fbfa8e1bf2593231417e8200f354240ad4ecc Mon Sep 17 00:00:00 2001
From: Chris Chen <[email protected]>
Date: Tue, 11 Aug 2026 04:48:27 +0800
Subject: [PATCH] winsup: console: validate duplicated handle set for master
 thread

The console master thread currently assumes that all handles in the
duplicated handle set were duplicated successfully.

If one of the DuplicateHandle calls fails, however, the master thread
may be started with a partially initialized handle set.  Failure to
duplicate thread_sync_event can also leave already duplicated console
handles unreleased.

Change get_duplicated_handle_set to return bool and treat the handle set
as an all-or-nothing resource.

Initialize the handle set before duplication, clean up handles already
acquired when a subsequent duplication fails, and do not start the
master thread unless the complete handle set is available.

Also release the duplicated console handles if duplication of
thread_sync_event fails, and make close_handle_set tolerate partially
initialized handle sets.

This change does not alter console synchronization semantics.  It only
hardens handle acquisition and cleanup on failure paths.
---
 winsup/cygwin/fhandler/console.cc       | 101 ++++++++++++++++++------
 winsup/cygwin/local_includes/fhandler.h |   2 +-
 2 files changed, 80 insertions(+), 23 deletions(-)

diff --git a/winsup/cygwin/fhandler/console.cc b/winsup/cygwin/fhandler/console.cc
index ba35ca44c..b4d18d4f7 100644
--- a/winsup/cygwin/fhandler/console.cc
+++ b/winsup/cygwin/fhandler/console.cc
@@ -282,9 +282,17 @@ cons_master_thread (VOID *arg)
 {
   fhandler_console *fh = (fhandler_console *) arg;
   tty *ttyp = (tty *) fh->tc ();
-  fhandler_console::handle_set_t handle_set;
-  fh->get_duplicated_handle_set (&handle_set);
-  HANDLE thread_sync_event;
+  fhandler_console::handle_set_t handle_set = {};
+
+  /* Require a complete duplicated console handle set. */
+  if (!fh->get_duplicated_handle_set (&handle_set))
+    {
+      debug_printf ("cons_master_thread not started because console "
+		    "handle duplication failed");
+      return 0;
+    }
+
+  HANDLE thread_sync_event = NULL;
   if (DuplicateHandle (GetCurrentProcess (), fh->thread_sync_event,
 		       GetCurrentProcess (), &thread_sync_event,
 		       0, FALSE, DUPLICATE_SAME_ACCESS))
@@ -300,8 +308,13 @@ cons_master_thread (VOID *arg)
       master_thread_started = false;
     }
   else
-    debug_printf ("cons_master_thread not started because thread_sync_event "
-		  "could not be duplicated %08x", GetLastError ());
+    {
+      DWORD err = GetLastError ();
+      /* Release duplicated console handles if sync-event duplication fails. */
+      fhandler_console::close_handle_set (&handle_set);
+      debug_printf ("cons_master_thread not started because thread_sync_event "
+		    "could not be duplicated %08x", err);
+    }
   return 0;
 }
 
@@ -4803,22 +4816,58 @@ fhandler_console::__release_output_mutex (const char *fn, int ln)
 #endif
 }
 
-void
+bool
 fhandler_console::get_duplicated_handle_set (handle_set_t *p)
 {
-  DuplicateHandle (GetCurrentProcess (), get_handle (),
-		   GetCurrentProcess (), &p->input_handle,
-		   0, FALSE, DUPLICATE_SAME_ACCESS);
-  DuplicateHandle (GetCurrentProcess (), get_output_handle (),
-		   GetCurrentProcess (), &p->output_handle,
-		   0, FALSE, DUPLICATE_SAME_ACCESS);
-  DuplicateHandle (GetCurrentProcess (), input_mutex,
-		   GetCurrentProcess (), &p->input_mutex,
-		   0, FALSE, DUPLICATE_SAME_ACCESS);
-  DuplicateHandle (GetCurrentProcess (), output_mutex,
-		   GetCurrentProcess (), &p->output_mutex,
-		   0, FALSE, DUPLICATE_SAME_ACCESS);
+  /* Validate duplicated console handles before starting the master thread.
+     A master thread must never start with a partial handle set. */
+  p->input_handle = NULL;
+  p->output_handle = NULL;
+  p->input_mutex = NULL;
+  p->output_mutex = NULL;
   p->unit = unit;
+
+  if (!DuplicateHandle (GetCurrentProcess (), get_handle (),
+			GetCurrentProcess (), &p->input_handle,
+			0, FALSE, DUPLICATE_SAME_ACCESS))
+    {
+      debug_printf ("DuplicateHandle(input_handle %p) failed %08x",
+		    get_handle (), GetLastError ());
+      goto fail;
+    }
+
+  if (!DuplicateHandle (GetCurrentProcess (), get_output_handle (),
+			GetCurrentProcess (), &p->output_handle,
+			0, FALSE, DUPLICATE_SAME_ACCESS))
+    {
+      debug_printf ("DuplicateHandle(output_handle %p) failed %08x",
+		    get_output_handle (), GetLastError ());
+      goto fail;
+    }
+
+  if (!DuplicateHandle (GetCurrentProcess (), input_mutex,
+			GetCurrentProcess (), &p->input_mutex,
+			0, FALSE, DUPLICATE_SAME_ACCESS))
+    {
+      debug_printf ("DuplicateHandle(input_mutex %p) failed %08x",
+		    input_mutex, GetLastError ());
+      goto fail;
+    }
+
+  if (!DuplicateHandle (GetCurrentProcess (), output_mutex,
+			GetCurrentProcess (), &p->output_mutex,
+			0, FALSE, DUPLICATE_SAME_ACCESS))
+    {
+      debug_printf ("DuplicateHandle(output_mutex %p) failed %08x",
+		    output_mutex, GetLastError ());
+      goto fail;
+    }
+
+  return true;
+
+fail:
+  close_handle_set (p);
+  return false;
 }
 
 /* The function close_handle_set() should be static so that they can
@@ -4826,13 +4875,21 @@ fhandler_console::get_duplicated_handle_set (handle_set_t *p)
 void
 fhandler_console::close_handle_set (handle_set_t *p)
 {
-  CloseHandle (p->input_handle);
+  /* Close only console handles that were successfully duplicated. */
+  if (p->input_handle)
+    CloseHandle (p->input_handle);
   p->input_handle = NULL;
-  CloseHandle (p->output_handle);
+
+  if (p->output_handle)
+    CloseHandle (p->output_handle);
   p->output_handle = NULL;
-  CloseHandle (p->input_mutex);
+
+  if (p->input_mutex)
+    CloseHandle (p->input_mutex);
   p->input_mutex = NULL;
-  CloseHandle (p->output_mutex);
+
+  if (p->output_mutex)
+    CloseHandle (p->output_mutex);
   p->output_mutex = NULL;
 }
 
diff --git a/winsup/cygwin/local_includes/fhandler.h b/winsup/cygwin/local_includes/fhandler.h
index d11b3ec4f..a9683637d 100644
--- a/winsup/cygwin/local_includes/fhandler.h
+++ b/winsup/cygwin/local_includes/fhandler.h
@@ -2350,7 +2350,7 @@ private:
   size_t &raixput ();
   size_t &rabuflen ();
 
-  void get_duplicated_handle_set (handle_set_t *p);
+  bool get_duplicated_handle_set (handle_set_t *p);
   static void close_handle_set (handle_set_t *p);
 
   static void cons_master_thread (handle_set_t *p, tty *ttyp);
-- 
2.55.0.windows.3
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.