From 8bd0a2856d1a2eb36870d73ec5307ad899c5f848 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Tue, 1 Sep 2026 11:38:25 -0700 Subject: [PATCH 1/7] 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/7] 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/7] 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) From 06f393508fd06b61af84b216e2a3f852de670d42 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 3 Sep 2026 11:56:17 +0200 Subject: [PATCH 4/7] fixup! compat/poll: do not collect more handles than the wait supports The bundled poll header is also used by non-Windows NO_POLL builds, including NonStop, where the fallback uses select() and has no Windows wait-object limit. Defining the limit unconditionally therefore capped parallel checkout at 62 workers and run_processes_parallel() at 31 on those platforms. Keep the cap specific to native Windows so other fallback builds retain their existing concurrency. Cherry-picked-from: 318303df6e43 (fixup! compat/poll: do not collect more handles than the wait supports, 2026-09-03) Assisted-by: GPT-5.6 Sol Assisted-by: GPT-6 Astra Signed-off-by: Johannes Schindelin --- compat/poll/poll.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compat/poll/poll.h b/compat/poll/poll.h index d7977806c18e96..5c1169acc2c003 100644 --- a/compat/poll/poll.h +++ b/compat/poll/poll.h @@ -59,6 +59,7 @@ typedef unsigned long nfds_t; extern int poll (struct pollfd *pfd, nfds_t nfd, int timeout); +#if (defined _WIN32 || defined __WIN32__) && ! defined __CYGWIN__ /* * This poll() is emulated with MsgWaitForMultipleObjects(), which waits on at * most MAXIMUM_WAIT_OBJECTS (64) objects. Two of those are never available for @@ -72,6 +73,7 @@ extern int poll (struct pollfd *pfd, nfds_t nfd, int timeout); * EINVAL. */ #define POLL_MAX_DESCRIPTORS 62 +#endif /* Define INFTIM only if doing so conforms to POSIX. */ #if !defined (_POSIX_C_SOURCE) && !defined (_XOPEN_SOURCE) From 0d1770385508879d2b9cb30a37594b1132bd7f24 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 3 Sep 2026 12:03:45 +0200 Subject: [PATCH 5/7] fixup! compat/poll: do not collect more handles than the wait supports The overflow path resets every earlier nonnegative socket, including entries whose events field is zero. Such entries are valid poll() input, and the classification loop never passes them to WSAEventSelect(). Calling WSAEventSelect() with a NULL event handle and a zero event mask is not harmless: it cancels a pre-existing asynchronous or event association and changes the socket mode. Returning EINVAL can therefore mutate socket state unrelated to this poll() invocation. Match the classification loop's event predicate so cleanup only undoes registrations that this poll() invocation could have installed. Cherry-picked-from: ff0271410042 (fixup! compat/poll: do not collect more handles than the wait supports, 2026-09-03) Assisted-by: GPT-5.6 Sol Assisted-by: GPT-6 Astra Signed-off-by: Johannes Schindelin --- compat/poll/poll.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compat/poll/poll.c b/compat/poll/poll.c index ab895fc91ca309..1205f09ce9fbc8 100644 --- a/compat/poll/poll.c +++ b/compat/poll/poll.c @@ -326,6 +326,9 @@ reset_socket_events (struct pollfd *pfd, nfds_t nfd) if (pfd[i].fd < 0) continue; + if (!(pfd[i].events & (POLLIN | POLLRDNORM | POLLOUT | POLLWRNORM | + POLLWRBAND | POLLPRI | POLLRDBAND))) + continue; h = (HANDLE) _get_osfhandle (pfd[i].fd); if (h == NULL || h == INVALID_HANDLE_VALUE) From 035ac5bf69220352bbf75be01d18cf704921e1fd Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 3 Sep 2026 15:03:26 +0200 Subject: [PATCH 6/7] fixup! run-command: limit concurrent children to what poll() can wait on The target commit budgets two poll descriptors for every grouped child: one for output and one for standard input. Most callers, including parallel fetches, submodule fetches and updates, and the test-suite runner, never request the latter. On Windows, this unnecessarily caps safe output-only workloads of 32 through 62 children at 31. Let callers that guarantee no standard-input pipe use one descriptor per child. Keep the conservative two-descriptor default, and BUG if a caller claims the invariant but requests such a pipe. Tests cover both budgets and verify that all queued tasks complete when the lower limit requires slot reuse. Cherry-picked-from: a819110c05dd (fixup! run-command: limit concurrent children to what poll() can wait on, 2026-09-03) Assisted-by: GPT-5.6 Sol Assisted-by: GPT-6 Astra Signed-off-by: Johannes Schindelin --- builtin/fetch.c | 1 + builtin/submodule--helper.c | 1 + hook.c | 1 + run-command.c | 19 ++++++---- run-command.h | 6 ++++ submodule.c | 1 + t/helper/test-run-command.c | 25 ++++++++++++- t/t0061-run-command.sh | 71 +++++++++++++++++++++++++++++++++++++ 8 files changed, 117 insertions(+), 8 deletions(-) diff --git a/builtin/fetch.c b/builtin/fetch.c index fc7a6e314626a5..b2e3d9bfe7ea50 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -2324,6 +2324,7 @@ static int fetch_multiple(struct string_list *list, int max_children, .tr2_label = "parallel/fetch", .processes = max_children, + .no_stdin_pipe = 1, .get_next_task = &fetch_next_remote, .start_failure = &fetch_failed_to_start, diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index ed066bb443e118..a57acd80397a80 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -2914,6 +2914,7 @@ static int update_submodules(struct update_data *update_data) .tr2_label = "parallel/update", .processes = update_data->max_jobs, + .no_stdin_pipe = 1, .get_next_task = update_clone_get_next_task, .start_failure = update_clone_start_failure, diff --git a/hook.c b/hook.c index e1d44227343807..4b1b21839d265c 100644 --- a/hook.c +++ b/hook.c @@ -982,6 +982,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name, .processes = jobs, .ungroup = jobs == 1, + .no_stdin_pipe = !options->feed_pipe, .get_next_task = pick_next_hook, .start_failure = notify_start_failure, diff --git a/run-command.c b/run-command.c index e8fcaaa7194026..50e5a653cc7cc6 100644 --- a/run-command.c +++ b/run-command.c @@ -1659,6 +1659,9 @@ static int pp_start_one(struct parallel_processes *pp, } return 1; } + if (opts->no_stdin_pipe && pp->children[i].process.in < 0) + BUG("get_next_task requested a stdin pipe despite " + "no_stdin_pipe"); if (!opts->ungroup) { pp->children[i].process.err = -1; pp->children[i].process.stdout_to_stderr = 1; @@ -1906,15 +1909,17 @@ void run_processes_parallel(const struct run_process_parallel_opts *opts) /* * 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. + * output pipe per child and, unless excluded by no_stdin_pipe, may also + * poll an input pipe. Limit the number of live children so that all of + * their descriptors fit in one poll() call. */ max_live = opts->processes; - if (!opts->ungroup && max_live > POLL_MAX_DESCRIPTORS / 2) - max_live = POLL_MAX_DESCRIPTORS / 2; + if (!opts->ungroup) { + size_t fds_per_process = opts->no_stdin_pipe ? 1 : 2; + + if (max_live > POLL_MAX_DESCRIPTORS / fds_per_process) + max_live = POLL_MAX_DESCRIPTORS / fds_per_process; + } if (do_trace2) trace2_region_enter_printf(tr2_category, tr2_label, NULL, diff --git a/run-command.h b/run-command.h index c2fad4f0f8380c..331c9391e8e2d5 100644 --- a/run-command.h +++ b/run-command.h @@ -488,6 +488,12 @@ struct run_process_parallel_opts */ unsigned int ungroup:1; + /** + * no_stdin_pipe: set if get_next_task will never request a pipe by + * setting child_process.in to -1. + */ + unsigned int no_stdin_pipe:1; + /** * get_next_task: See get_next_task_fn() above. This must be * specified. diff --git a/submodule.c b/submodule.c index c8d0587b6ba085..f5077920416b24 100644 --- a/submodule.c +++ b/submodule.c @@ -1835,6 +1835,7 @@ int fetch_submodules(struct repository *r, .tr2_label = "parallel/fetch", .processes = max_parallel_jobs, + .no_stdin_pipe = 1, .get_next_task = get_next_submodule, .start_failure = fetch_start_failure, diff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c index 4a56456894ccff..f943c8214d4dec 100644 --- a/t/helper/test-run-command.c +++ b/t/helper/test-run-command.c @@ -20,13 +20,14 @@ #include "wildmatch.h" static int number_callbacks; +static int max_callbacks = 4; static int parallel_next(struct child_process *cp, struct strbuf *err, void *cb, void **task_cb) { struct child_process *d = cb; - if (number_callbacks >= 4) + if (number_callbacks >= max_callbacks) return 0; strvec_pushv(&cp->args, d->args.v); @@ -195,6 +196,7 @@ static int testsuite(int argc, const char **argv) OPT_END() }; struct run_process_parallel_opts opts = { + .no_stdin_pipe = 1, .get_next_task = next_test, .start_failure = test_failed, .feed_pipe = test_stdin_pipe_feed, @@ -442,6 +444,16 @@ static int inherit_handle_child(void) int cmd__run_command(int argc, const char **argv) { struct child_process proc = CHILD_PROCESS_INIT; + const char * const parallel_usage[] = { + "test-tool run-command [] " + " [...]", + NULL + }; + struct option parallel_options[] = { + OPT_INTEGER_F(0, "tasks", &max_callbacks, + "number of tasks to generate", PARSE_OPT_NONEG), + OPT_END() + }; int jobs; int ret; struct run_process_parallel_opts opts = { @@ -495,17 +507,28 @@ int cmd__run_command(int argc, const char **argv) opts.ungroup = 1; } + argc = parse_options(argc - 1, argv + 1, NULL, parallel_options, + parallel_usage, PARSE_OPT_STOP_AT_NON_OPTION | + PARSE_OPT_KEEP_ARGV0); + if (argc < 3) + usage_with_options(parallel_usage, parallel_options); + if (max_callbacks < 0) + die("--tasks cannot be negative"); + jobs = atoi(argv[2]); strvec_clear(&proc.args); strvec_pushv(&proc.args, (const char **)argv + 3); if (!strcmp(argv[1], "run-command-parallel")) { + opts.no_stdin_pipe = 1; opts.get_next_task = parallel_next; opts.task_finished = task_finished_quiet; } else if (!strcmp(argv[1], "run-command-abort")) { + opts.no_stdin_pipe = 1; opts.get_next_task = parallel_next; opts.task_finished = task_finished; } else if (!strcmp(argv[1], "run-command-no-jobs")) { + opts.no_stdin_pipe = 1; opts.get_next_task = no_job; opts.task_finished = task_finished; } else if (!strcmp(argv[1], "run-command-stdin")) { diff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh index 905e90e1f72541..a9958b66dc9fa9 100755 --- a/t/t0061-run-command.sh +++ b/t/t0061-run-command.sh @@ -164,6 +164,77 @@ test_expect_success 'run_command runs ungrouped in parallel with more tasks than test_line_count = 4 err ' +wait_for_line_count () { + expected=$1 && + file=$2 && + + for i in $(test_seq 1 100) + do + if test "$(wc -l <"$file")" -eq "$expected" + then + return 0 + fi && + sleep 0.1 + done && + return 1 +} + +cleanup_parallel () { + touch release + if test -n "$parallel_pid" + then + wait "$parallel_pid" + fi +} + +test_expect_success MINGW 'setup poll descriptor limit test' ' + write_script wait-for-release <<-\EOF + echo started >>"$1" + if test "$3" = stdin + then + while read line + do + : + done + fi + while ! test -e "$2" + do + sleep 0.1 + done + EOF +' + +test_expect_success MINGW 'run_command uses full poll limit without stdin' ' + : >started && + rm -f release && + test-tool run-command run-command-parallel --tasks=40 40 \ + ./wait-for-release "$PWD/started" "$PWD/release" \ + >out 2>err & + parallel_pid=$! && + test_when_finished cleanup_parallel && + wait_for_line_count 40 started && + touch release && + wait "$parallel_pid" && + parallel_pid= +' + +test_expect_success MINGW 'run_command limits children with stdin pipes' ' + : >started && + rm -f release && + test-tool run-command run-command-stdin --tasks=40 40 \ + ./wait-for-release "$PWD/started" "$PWD/release" stdin \ + >out 2>err & + parallel_pid=$! && + test_when_finished cleanup_parallel && + wait_for_line_count 31 started && + sleep 1 && + test_line_count = 31 started && + touch release && + wait "$parallel_pid" && + parallel_pid= && + test_line_count = 40 started +' + test_expect_success 'run_command listens to stdin' ' cat >expect <<-\EOF && preloaded output of a child From f77637eed2b628fdbc73dbbad8df3731ed9b6380 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 3 Sep 2026 16:43:02 +0200 Subject: [PATCH 7/7] fixup! compat/poll: do not collect more handles than the wait supports The parallel-checkout test caps the worker count at 62, but does not exercise the poll() handle limit directly. It still passes if the root guard and reset_socket_events() are removed, leaving both the memory-safety boundary and overflow cleanup without regression coverage. Exercise poll() directly on Windows. Verify that 62 waitable handles succeed while a 63rd returns EINVAL, that sparse entries and sockets do not consume wait-handle slots, and that overflow cleanup preserves an event association on a socket this poll() invocation did not register. Cherry-picked-from: db14c17bfa5a (fixup! compat/poll: do not collect more handles than the wait supports, 2026-09-03) Assisted-by: GPT-5.6 Sol Assisted-by: GPT-6 Astra Signed-off-by: Johannes Schindelin --- Makefile | 1 + t/meson.build | 1 + t/unit-tests/u-poll.c | 163 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 t/unit-tests/u-poll.c diff --git a/Makefile b/Makefile index 0e13c2e18bcbb3..f12de78ad10a4e 100644 --- a/Makefile +++ b/Makefile @@ -1547,6 +1547,7 @@ CLAR_TEST_SUITES += u-odb-inmemory CLAR_TEST_SUITES += u-oid-array CLAR_TEST_SUITES += u-oidmap CLAR_TEST_SUITES += u-oidtree +CLAR_TEST_SUITES += u-poll CLAR_TEST_SUITES += u-prio-queue CLAR_TEST_SUITES += u-reftable-basics CLAR_TEST_SUITES += u-reftable-block diff --git a/t/meson.build b/t/meson.build index 723232a7981ac3..d18f878c63579d 100644 --- a/t/meson.build +++ b/t/meson.build @@ -11,6 +11,7 @@ clar_test_suites = [ 'unit-tests/u-oid-array.c', 'unit-tests/u-oidmap.c', 'unit-tests/u-oidtree.c', + 'unit-tests/u-poll.c', 'unit-tests/u-prio-queue.c', 'unit-tests/u-reftable-basics.c', 'unit-tests/u-reftable-block.c', diff --git a/t/unit-tests/u-poll.c b/t/unit-tests/u-poll.c new file mode 100644 index 00000000000000..bc72135da28451 --- /dev/null +++ b/t/unit-tests/u-poll.c @@ -0,0 +1,163 @@ +#include "unit-test.h" + +#ifdef GIT_WINDOWS_NATIVE +static struct { + int pipes[POLL_MAX_DESCRIPTORS + 1][2]; + size_t nr_pipes; + int listener; + int sockets[2]; + WSAEVENT event; + int event_selected; +} poll_test; + +static void open_pipes(struct pollfd *fds, size_t nr) +{ + size_t i; + + cl_assert(nr <= ARRAY_SIZE(poll_test.pipes)); + for (i = 0; i < nr; i++) { + int *pipefd = poll_test.pipes[poll_test.nr_pipes]; + + cl_assert_equal_i(pipe(pipefd), 0); + poll_test.nr_pipes++; + fds[i].fd = pipefd[0]; + fds[i].events = POLLIN; + } +} + +static void create_socket_pair(void) +{ + struct sockaddr_in address = { + .sin_family = AF_INET, + .sin_addr.s_addr = htonl(INADDR_LOOPBACK), + }; + socklen_t address_length = sizeof(address); + + poll_test.listener = socket(AF_INET, SOCK_STREAM, 0); + cl_assert(poll_test.listener >= 0); + cl_assert_equal_i(bind(poll_test.listener, + (struct sockaddr *)&address, + sizeof(address)), 0); + cl_assert_equal_i(getsockname( + (SOCKET)_get_osfhandle(poll_test.listener), + (struct sockaddr *)&address, + &address_length), 0); + cl_assert_equal_i(listen(poll_test.listener, 1), 0); + + poll_test.sockets[0] = socket(AF_INET, SOCK_STREAM, 0); + cl_assert(poll_test.sockets[0] >= 0); + cl_assert_equal_i(connect(poll_test.sockets[0], + (struct sockaddr *)&address, + sizeof(address)), 0); + + poll_test.sockets[1] = accept(poll_test.listener, NULL, NULL); + cl_assert(poll_test.sockets[1] >= 0); + close(poll_test.listener); + poll_test.listener = -1; +} +#endif + +void test_poll__initialize(void) +{ +#ifdef GIT_WINDOWS_NATIVE + memset(&poll_test, 0, sizeof(poll_test)); + poll_test.listener = -1; + poll_test.sockets[0] = -1; + poll_test.sockets[1] = -1; + poll_test.event = WSA_INVALID_EVENT; +#endif +} + +void test_poll__cleanup(void) +{ +#ifdef GIT_WINDOWS_NATIVE + size_t i; + + if (poll_test.event_selected && poll_test.sockets[0] >= 0) + WSAEventSelect( + (SOCKET)_get_osfhandle(poll_test.sockets[0]), NULL, 0); + if (poll_test.event != WSA_INVALID_EVENT) + WSACloseEvent(poll_test.event); + if (poll_test.listener >= 0) + close(poll_test.listener); + for (i = 0; i < ARRAY_SIZE(poll_test.sockets); i++) + if (poll_test.sockets[i] >= 0) + close(poll_test.sockets[i]); + for (i = 0; i < poll_test.nr_pipes; i++) { + close(poll_test.pipes[i][0]); + close(poll_test.pipes[i][1]); + } +#endif +} + +void test_poll__limit(void) +{ +#ifdef GIT_WINDOWS_NATIVE + struct pollfd fds[POLL_MAX_DESCRIPTORS + 1] = { 0 }; + + open_pipes(fds, ARRAY_SIZE(fds)); + cl_assert_equal_i(poll(fds, POLL_MAX_DESCRIPTORS, 0), 0); + + errno = 0; + cl_assert_equal_i(poll(fds, ARRAY_SIZE(fds), 0), -1); + cl_assert_equal_i(errno, EINVAL); +#else + cl_skip(); +#endif +} + +void test_poll__sparse(void) +{ +#ifdef GIT_WINDOWS_NATIVE + struct pollfd fds[2 * POLL_MAX_DESCRIPTORS + 1] = { 0 }; + size_t i; + + open_pipes(fds, POLL_MAX_DESCRIPTORS); + create_socket_pair(); + + for (i = POLL_MAX_DESCRIPTORS; i > 0; i--) { + fds[2 * i - 1] = fds[i - 1]; + fds[2 * i - 2].fd = -1; + } + fds[2 * POLL_MAX_DESCRIPTORS].fd = poll_test.sockets[0]; + fds[2 * POLL_MAX_DESCRIPTORS].events = POLLIN; + + cl_assert_equal_i(poll(fds, ARRAY_SIZE(fds), 0), 0); +#else + cl_skip(); +#endif +} + +void test_poll__socket_cleanup(void) +{ +#ifdef GIT_WINDOWS_NATIVE + struct pollfd fds[POLL_MAX_DESCRIPTORS + 2] = { 0 }; + WSANETWORKEVENTS events; + SOCKET socket_handle; + + create_socket_pair(); + socket_handle = (SOCKET)_get_osfhandle(poll_test.sockets[0]); + poll_test.event = WSACreateEvent(); + cl_assert(poll_test.event != WSA_INVALID_EVENT); + cl_assert_equal_i(WSAEventSelect(socket_handle, poll_test.event, + FD_READ), 0); + poll_test.event_selected = 1; + + fds[0].fd = poll_test.sockets[0]; + open_pipes(fds + 1, POLL_MAX_DESCRIPTORS + 1); + + errno = 0; + cl_assert_equal_i(poll(fds, ARRAY_SIZE(fds), 0), -1); + cl_assert_equal_i(errno, EINVAL); + + cl_assert_equal_i(send( + (SOCKET)_get_osfhandle(poll_test.sockets[1]), + "x", 1, 0), 1); + cl_assert_equal_i(WaitForSingleObject(poll_test.event, 1000), + WAIT_OBJECT_0); + cl_assert_equal_i(WSAEnumNetworkEvents(socket_handle, poll_test.event, + &events), 0); +#else + cl_skip(); +#endif +}