aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeff King <peff@peff.net>2026-04-08 13:20:55 -0400
committerJunio C Hamano <gitster@pobox.com>2026-04-08 10:54:32 -0700
commit2226ffaacd93d3fe5554687a70d9190d72596f96 (patch)
tree04123d837b4c011876d420c0c6588426466b8238
parent26c27dd3db0e1386fe541ebd7aae001c722e5c10 (diff)
downloadgit-2226ffaacd93d3fe5554687a70d9190d72596f96.tar.xz
run_processes_parallel(): fix order of sigpipe handling
In commit ec0becacc9 (run-command: add stdin callback for parallelization, 2026-01-28), we taught run_processes_parallel() to ignore SIGPIPE, since we wouldn't want a write() to a broken pipe of one of the children to take down the whole process. But there's a subtle ordering issue. After we ignore SIGPIPE, we call pp_init(), which installs its own cleanup handler for multiple signals using sigchain_push_common(), which includes SIGPIPE. So if we receive SIGPIPE while writing to a child, we'll trigger that handler first, pop it off the stack, and then re-raise (which is then ignored because of the SIG_IGN we pushed first). But what does that handler do? It tries to clean up all of the child processes, under the assumption that when we re-raise the signal we'll be exiting the process! So a hook that exits without reading all of its input will cause us to get SIGPIPE, which will put us in a signal handler that then tries to kill() that same child. This seems to be mostly harmless on Linux. The process has already exited by this point, and though kill() does not complain (since the process has not been reaped with a wait() call), it does not affect the exit status of the process. However, this seems not to be true on all platforms. This case is triggered by t5401.13, "pre-receive hook that forgets to read its input". This test fails on NonStop since that hook was converted to the run_processes_parallel() API. We can fix it by reordering the code a bit. We should run pp_init() first, and then push our SIG_IGN onto the stack afterwards, so that it is truly ignored while feeding the sub-processes. Note that we also reorder the popping at the end of the function, too. This is not technically necessary, as we are doing two pops either way, but now the pops will correctly match their pushes. This also fixes a related case that we can't test yet. If we did have more than one process to run, then one child causing SIGPIPE would cause us to kill() all of the children (which might still actually be running). But the hook API is the only user of the new feed_pipe feature, and it does not yet support parallel hook execution. So for now we'll always execute the processes sequentially. Once parallel hook execution exists, we'll be able to add a test which covers this. Reported-by: Randall S. Becker <rsbecker@nexbridge.com> Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>
-rw-r--r--run-command.c11
1 files changed, 8 insertions, 3 deletions
diff --git a/run-command.c b/run-command.c
index 32c290ee6a..574d5c40f0 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1895,14 +1895,19 @@ void run_processes_parallel(const struct run_process_parallel_opts *opts)
"max:%"PRIuMAX,
(uintmax_t)opts->processes);
+ pp_init(&pp, opts, &pp_sig);
+
/*
* Child tasks might receive input via stdin, terminating early (or not), so
* ignore the default SIGPIPE which gets handled by each feed_pipe_fn which
* actually writes the data to children stdin fds.
+ *
+ * This _must_ come after pp_init(), because it installs its own
+ * SIGPIPE handler (to cleanup children), and we want to supersede
+ * that.
*/
sigchain_push(SIGPIPE, SIG_IGN);
- pp_init(&pp, opts, &pp_sig);
while (1) {
for (i = 0;
i < spawn_cap && !pp.shutdown &&
@@ -1928,10 +1933,10 @@ void run_processes_parallel(const struct run_process_parallel_opts *opts)
}
}
- pp_cleanup(&pp, opts);
-
sigchain_pop(SIGPIPE);
+ pp_cleanup(&pp, opts);
+
if (do_trace2)
trace2_region_leave(tr2_category, tr2_label, NULL);
}