diff --git a/compat/poll/poll.c b/compat/poll/poll.c index ea362b4a8e2340..cc61b2704c7bd4 100644 --- a/compat/poll/poll.c +++ b/compat/poll/poll.c @@ -504,7 +504,21 @@ 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; + { + /* + * MsgWaitForMultipleObjects() below waits on the handles + * collected here plus the message queue implied by + * QS_ALLINPUT, so at most MAXIMUM_WAIT_OBJECTS - 1 handles + * fit. Refuse to collect more instead of writing past the + * end of handle_array. + */ + if (nhandles >= MAXIMUM_WAIT_OBJECTS - 1) + { + errno = EINVAL; + return -1; + } + handle_array[nhandles++] = h; + } if (pfd[i].revents) timeout = 0; } diff --git a/parallel-checkout.c b/parallel-checkout.c index 1eb277a0fc0a55..5f27143d6cf532 100644 --- a/parallel-checkout.c +++ b/parallel-checkout.c @@ -41,6 +41,18 @@ enum pc_status parallel_checkout_status(void) static const int DEFAULT_THRESHOLD_FOR_PARALLELISM = 100; static const int DEFAULT_NUM_WORKERS = 1; +#ifdef GIT_WINDOWS_NATIVE +/* + * Windows has no native poll(). compat/poll emulates it with + * MsgWaitForMultipleObjects(), which waits on at most MAXIMUM_WAIT_OBJECTS + * objects. One of those is the message queue implied by QS_ALLINPUT, and + * compat/poll reserves another for its own event object, leaving room for + * MAXIMUM_WAIT_OBJECTS - 2 descriptors. gather_results_from_workers() polls + * one pipe per worker, so the worker count must stay within that limit. + */ +#define MAX_PARALLEL_CHECKOUT_WORKERS (MAXIMUM_WAIT_OBJECTS - 2) +#endif + void get_parallel_checkout_configs(int *num_workers, int *threshold) { char *env_workers = getenv("GIT_TEST_CHECKOUT_WORKERS"); @@ -671,6 +683,11 @@ int run_parallel_checkout(struct checkout *state, int num_workers, int threshold if (parallel_checkout.nr < num_workers) num_workers = parallel_checkout.nr; +#ifdef GIT_WINDOWS_NATIVE + if (num_workers > MAX_PARALLEL_CHECKOUT_WORKERS) + num_workers = MAX_PARALLEL_CHECKOUT_WORKERS; +#endif + 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..cf0d822fd708de 100755 --- a/t/t2080-parallel-checkout-basics.sh +++ b/t/t2080-parallel-checkout-basics.sh @@ -320,4 +320,41 @@ test_expect_success MINGW 'parallel checkout with fscache does not fail on new d ) ' +# A worker count far above any platform limit must still produce a correct +# checkout. On Windows this used to smash the stack, because compat/poll +# collected one wait handle per polled worker pipe in a fixed-size array. +# +# This test does not verify that clamping happens, and it cannot easily do so: +# the clamp is not reported anywhere, and the number of workers that are +# actually pending inside poll() depends on timing and I/O state, so no +# specific worker count reliably demonstrates the old failure either. All the +# test asserts is that a high worker count succeeds. +test_expect_success 'checkout with more workers than the platform can poll' ' + test_when_finished "rm -rf many-workers-repo" && + git init many-workers-repo && + ( + cd many-workers-repo && + mkdir -p 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 config checkout.workers 200 && + git config checkout.thresholdForParallelism 1 && + + git checkout -q - && + git checkout -q other && + test "$(cat dir/file1)" = "changed 1" + ) +' + test_done