From 8bd0a2856d1a2eb36870d73ec5307ad899c5f848 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Tue, 1 Sep 2026 11:38:25 -0700 Subject: [PATCH 1/3] compat/poll: do not collect more handles than the wait supports The Windows implementation of poll() collects one wait handle per polled descriptor in HANDLE h, handle_array[FD_SETSIZE + 2]; and appends to it without a bounds check. It then writes a NULL sentinel at handle_array[nhandles]. A caller with enough live descriptors therefore writes past the end of the array and corrupts the stack. The corruption is silent, and when it reaches the stack cookie the process aborts with STATUS_STACK_BUFFER_OVERRUN. The array is not the only limit. The collected handles are passed to MsgWaitForMultipleObjects (nhandles, handle_array, FALSE, wait_timeout, QS_ALLINPUT); which waits on at most MAXIMUM_WAIT_OBJECTS objects, and QS_ALLINPUT adds the thread message queue as one more object beyond the handles. The code shows this, because it reports the message queue as WAIT_OBJECT_0 + nhandles. One further handle is poll()'s own event object. So at most MAXIMUM_WAIT_OBJECTS - 2 descriptors can be waited on, which is the tighter of the two bounds and is well inside the array. Define that limit as POLL_MAX_DESCRIPTORS next to the poll() declaration, and refuse to collect beyond it, returning EINVAL. Two preprocessor checks tie the constant to MAXIMUM_WAIT_OBJECTS and to the size of handle_array, so the two cannot drift apart. poll() is now memory-safe for every input, and a case that previously smashed the stack fails cleanly. Undo the WSAEventSelect() registrations before returning. The loop that normally does this runs after the wait, and the new error path skips it, which would otherwise leave those sockets associated with poll()'s static event object and let later socket activity disturb an unrelated poll(). Note that the limit is on the number of handles actually collected, not on nfd. Those are different: a descriptor only takes a handle when it is non-negative, is not a socket, and has no events pending yet. Sockets are all multiplexed onto the one event object. Callers routinely pass sparse arrays, for example run_processes_parallel(), which sizes its pollfd array to the configured job count and leaves the unused slots at fd = -1. Rejecting a large nfd would break such callers even though they never come close to the wait limit. For platforms with a native poll(), which has no such limit, define POLL_MAX_DESCRIPTORS to INT_MAX so that callers can clamp against it unconditionally. Signed-off-by: Tyrie Vella --- compat/poll/poll.c | 45 ++++++++++++++++++++++++++++++++++++++++++++- compat/poll/poll.h | 14 ++++++++++++++ compat/posix.h | 10 ++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/compat/poll/poll.c b/compat/poll/poll.c index ea362b4a8e2340..ab895fc91ca309 100644 --- a/compat/poll/poll.c +++ b/compat/poll/poll.c @@ -303,6 +303,40 @@ compute_revents (int fd, int sought, fd_set *rfds, fd_set *wfds, fd_set *efds) } #endif /* !MinGW */ +#ifdef WIN32_NATIVE +/* POLL_MAX_DESCRIPTORS descriptors, plus hEvent and the QS_ALLINPUT message + queue, must fit in one MsgWaitForMultipleObjects call, and the collected + handles plus the NULL sentinel must fit in handle_array. */ +#if POLL_MAX_DESCRIPTORS + 2 > MAXIMUM_WAIT_OBJECTS +#error POLL_MAX_DESCRIPTORS exceeds MAXIMUM_WAIT_OBJECTS +#endif +#if POLL_MAX_DESCRIPTORS + 2 > FD_SETSIZE + 2 +#error POLL_MAX_DESCRIPTORS does not fit in handle_array +#endif + +/* Undo the WSAEventSelect() calls made for the first NFD descriptors. */ +static void +reset_socket_events (struct pollfd *pfd, nfds_t nfd) +{ + nfds_t i; + + for (i = 0; i < nfd; i++) + { + HANDLE h; + + if (pfd[i].fd < 0) + continue; + + h = (HANDLE) _get_osfhandle (pfd[i].fd); + if (h == NULL || h == INVALID_HANDLE_VALUE) + continue; + + if (IsSocketHandle (h)) + WSAEventSelect ((SOCKET) h, NULL, 0); + } +} +#endif + int poll (struct pollfd *pfd, nfds_t nfd, int timeout) { @@ -504,7 +538,16 @@ poll (struct pollfd *pfd, nfds_t nfd, int timeout) bits for the "wrong" direction. */ pfd[i].revents = win32_compute_revents (h, &sought); if (sought) - handle_array[nhandles++] = h; + { + /* hEvent occupies handle_array[0]. See POLL_MAX_DESCRIPTORS. */ + if (nhandles > POLL_MAX_DESCRIPTORS) + { + reset_socket_events (pfd, i); + errno = EINVAL; + return -1; + } + handle_array[nhandles++] = h; + } if (pfd[i].revents) timeout = 0; } diff --git a/compat/poll/poll.h b/compat/poll/poll.h index 1e1597360f4485..d7977806c18e96 100644 --- a/compat/poll/poll.h +++ b/compat/poll/poll.h @@ -59,6 +59,20 @@ typedef unsigned long nfds_t; extern int poll (struct pollfd *pfd, nfds_t nfd, int timeout); +/* + * This poll() is emulated with MsgWaitForMultipleObjects(), which waits on at + * most MAXIMUM_WAIT_OBJECTS (64) objects. Two of those are never available for + * polled descriptors: poll() waits on its own event object, and QS_ALLINPUT + * adds the thread message queue. Sockets do not count, because they are all + * multiplexed onto that one event object; every other descriptor takes a wait + * slot of its own. + * + * Callers that poll one or more descriptors per child must keep the number of + * simultaneously live descriptors within this limit. Exceeding it fails with + * EINVAL. + */ +#define POLL_MAX_DESCRIPTORS 62 + /* Define INFTIM only if doing so conforms to POSIX. */ #if !defined (_POSIX_C_SOURCE) && !defined (_XOPEN_SOURCE) #define INFTIM (-1) diff --git a/compat/posix.h b/compat/posix.h index e2e794cad7d419..1a77b198aa5bc7 100644 --- a/compat/posix.h +++ b/compat/posix.h @@ -133,6 +133,16 @@ /* Pull the compat stuff */ #include #endif + +/* + * compat/poll defines POLL_MAX_DESCRIPTORS to the largest number of + * descriptors its poll() emulation can wait on. A native poll() has no such + * limit, so callers that fan out one descriptor per child can clamp against + * this unconditionally. + */ +#ifndef POLL_MAX_DESCRIPTORS +#define POLL_MAX_DESCRIPTORS INT_MAX +#endif #ifdef HAVE_BSD_SYSCTL #include #endif From c10c35a993e5c73f550f76b3d071e13fc156108d Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Tue, 1 Sep 2026 11:38:35 -0700 Subject: [PATCH 2/3] parallel-checkout: limit worker count to what poll() can wait on On Windows, `git checkout` and `git reset --hard` can abort with *** stack smashing detected ***: terminated and exit code 0xC0000409 (STATUS_STACK_BUFFER_OVERRUN) when checkout.workers is large, or when it is set to 0 on a machine with many logical processors. gather_results_from_workers() polls one pipe per checkout worker. Windows has no native poll(), so compat/poll emulates it with MsgWaitForMultipleObjects(), which cannot wait on more than POLL_MAX_DESCRIPTORS descriptors at once. compat/poll collects one handle per polled descriptor in a fixed stack array, so a higher worker count writes past the end of that array and corrupts the stack. run_parallel_checkout() clamped num_workers only by the number of files. Clamp it to POLL_MAX_DESCRIPTORS as well. That function is the single choke point before the workers start and the poll() loop runs. Clamp silently: fewer workers is correct behaviour, and a warning would fire on every checkout on a large machine. A single poll() loop cannot usefully drive more readers than this anyway. On platforms with a native poll() the limit is INT_MAX, so the clamp is a no-op. The problem became reachable in 2.54. Before that, online_cpus() used GetSystemInfo(), which reports only the processors in the current processor group, and a group holds at most 64. That accidental ceiling kept the array in bounds. The move to GetLogicalProcessorInformationEx() is correct and reports the true system-wide count, which exposed the latent bug. Document the cap, because checkout.workers is otherwise described as using one worker per logical core with no upper bound. Add a test that asserts the clamp. test_checkout_workers() counts the workers that were actually spawned, so the test can check the effective count rather than only that the checkout succeeded. The test is limited to Windows, because that is the only platform where the cap applies. Signed-off-by: Tyrie Vella --- Documentation/config/checkout.adoc | 5 ++++ parallel-checkout.c | 7 ++++++ t/t2080-parallel-checkout-basics.sh | 36 +++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/Documentation/config/checkout.adoc b/Documentation/config/checkout.adoc index e35d21296978fe..45951bf38a5e3c 100644 --- a/Documentation/config/checkout.adoc +++ b/Documentation/config/checkout.adoc @@ -30,6 +30,11 @@ commands or functionality in the future. all commands that perform checkout. E.g. checkout, clone, reset, sparse-checkout, etc. + +On Windows the number of workers is capped at 62, because the `poll()` +emulation cannot wait on more worker pipes than that. A higher configured +value, including the logical core count on a machine with many cores, is +silently reduced to the cap. ++ NOTE: Parallel checkout usually delivers better performance for repositories located on SSDs or over NFS. For repositories on spinning disks and/or machines with a small number of cores, the default sequential checkout often performs diff --git a/parallel-checkout.c b/parallel-checkout.c index 1eb277a0fc0a55..4595cf4e8d9250 100644 --- a/parallel-checkout.c +++ b/parallel-checkout.c @@ -671,6 +671,13 @@ int run_parallel_checkout(struct checkout *state, int num_workers, int threshold if (parallel_checkout.nr < num_workers) num_workers = parallel_checkout.nr; + /* + * gather_results_from_workers() polls one pipe per worker, so the + * worker count must stay within what poll() can wait on. + */ + if (num_workers > POLL_MAX_DESCRIPTORS) + num_workers = POLL_MAX_DESCRIPTORS; + if (num_workers <= 1 || parallel_checkout.nr < threshold) { write_items_sequentially(state); } else { diff --git a/t/t2080-parallel-checkout-basics.sh b/t/t2080-parallel-checkout-basics.sh index 7ad96cd5cd24a3..94d1f4bf1e7718 100755 --- a/t/t2080-parallel-checkout-basics.sh +++ b/t/t2080-parallel-checkout-basics.sh @@ -319,5 +319,41 @@ test_expect_success MINGW 'parallel checkout with fscache does not fail on new d test_cmp expect2 sub/deep/dir/file2.txt ) ' +# Windows has no native poll(). compat/poll emulates it with +# MsgWaitForMultipleObjects(), which cannot wait on more than +# MAXIMUM_WAIT_OBJECTS objects, so run_parallel_checkout() caps the worker +# count at MAXIMUM_WAIT_OBJECTS - 2. Without that cap, compat/poll collected +# one wait handle per polled worker pipe in a fixed-size stack array and +# smashed the stack. +# +# MAXIMUM_WAIT_OBJECTS is 64, hence the expected 62 below. The test is +# MINGW-only because the cap only exists there; on other platforms the +# requested 200 workers are used as-is. +test_expect_success MINGW 'checkout caps workers at the poll limit' ' + test_when_finished "rm -rf many-workers" && + git init many-workers && + ( + cd many-workers && + mkdir dir && + for i in $(test_seq 1 200) + do + echo "content $i" >dir/file$i || return 1 + done && + git add -A && + git commit -q -m base && + + git checkout -q -b other && + for i in $(test_seq 1 200) + do + echo "changed $i" >dir/file$i || return 1 + done && + git commit -q -a -m changed && + git checkout -q - + ) && + + set_checkout_config 200 1 && + test_checkout_workers 62 git -C many-workers checkout other && + verify_checkout many-workers +' test_done From 6ca80248b8802357854409f7de1194cb4787753e Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Tue, 1 Sep 2026 11:38:36 -0700 Subject: [PATCH 3/3] run-command: limit concurrent children to what poll() can wait on pp_buffer_io() polls one pipe for each child that is sending output, and a second one for each child that is being fed on stdin. On Windows poll() is emulated with MsgWaitForMultipleObjects(), which cannot wait on more than POLL_MAX_DESCRIPTORS descriptors at once. A job count above that limit is reachable in practice. fetch.parallel, submodule.fetchJobs and hook.jobs all accept an explicit value, and a value of 0 means "use online_cpus()", which on a machine with many cores is well above the limit. Before the previous commit such a run corrupted the stack. Now poll() returns EINVAL, and pp_buffer_io() turns that into die_errno("poll"), so the operation fails outright. Limit how many children run at the same time, so that a large job count degrades into less concurrency instead of an error. Only concurrency is limited. The configured maximum is still used for the size of the child and pollfd arrays, and is still reported by the trace, so the number of tasks that are run in total does not change. Unused pollfd slots hold -1 and are skipped by poll(), so the larger array costs nothing. Divide the limit by two, because a child can hold two descriptors: one for its output and one for its input. Callers that group output are the only ones affected; with opts.ungroup set the caller does its own I/O and poll() is not involved. On platforms with a native poll() there is no such limit, POLL_MAX_DESCRIPTORS is INT_MAX, and this is a no-op. Signed-off-by: Tyrie Vella --- run-command.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/run-command.c b/run-command.c index ceb33119655de9..e8fcaaa7194026 100644 --- a/run-command.c +++ b/run-command.c @@ -1894,6 +1894,7 @@ void run_processes_parallel(const struct run_process_parallel_opts *opts) int i, code; int timeout = 100; int spawn_cap = 4; + size_t max_live; struct parallel_processes_for_signal pp_sig; struct parallel_processes pp = { .buffered_output = STRBUF_INIT, @@ -1903,6 +1904,18 @@ void run_processes_parallel(const struct run_process_parallel_opts *opts) const char *tr2_label = opts->tr2_label; const int do_trace2 = tr2_category && tr2_label; + /* + * Unless the caller handles its own output, pp_buffer_io() polls one + * pipe for each child that is sending output and a second one for each + * child that is being fed on stdin. Limit how many children run at once + * so that the worst case stays within what poll() can wait on. Only + * concurrency is limited; the configured maximum is still honoured for + * the number of tasks that are run in total. + */ + max_live = opts->processes; + if (!opts->ungroup && max_live > POLL_MAX_DESCRIPTORS / 2) + max_live = POLL_MAX_DESCRIPTORS / 2; + if (do_trace2) trace2_region_enter_printf(tr2_category, tr2_label, NULL, "max:%"PRIuMAX, @@ -1924,7 +1937,7 @@ void run_processes_parallel(const struct run_process_parallel_opts *opts) while (1) { for (i = 0; i < spawn_cap && !pp.shutdown && - pp.nr_processes < opts->processes; + pp.nr_processes < max_live; i++) { code = pp_start_one(&pp, opts); if (!code)