* [PATCH] sub-process: use gentle handshake to avoid die() on startup failure
@ 2026-06-01 4:15 Michael Montalbo via GitGitGadget
2026-06-01 6:43 ` Junio C Hamano
2026-06-01 21:20 ` [PATCH v2] " Michael Montalbo via GitGitGadget
0 siblings, 2 replies; 4+ messages in thread
From: Michael Montalbo via GitGitGadget @ 2026-06-01 4:15 UTC (permalink / raw)
To: git; +Cc: Michael Montalbo, Michael Montalbo
From: Michael Montalbo <mmontalbo@gmail.com>
When the configured subprocess command contains shell metacharacters
(such as a space), prepare_shell_cmd() wraps it in "sh -c <cmd>".
The shell itself always starts successfully, so start_command()
returns zero even if the tool inside does not exist. The subsequent
handshake then reads from a dead pipe and calls die() via the
non-gentle packet_read_line(), killing the parent process instead of
letting it handle the error.
Before this change, a missing filter process at a path containing
spaces produces a confusing error:
$ git -c filter.lfs.process="/path with space/tool" \
-c filter.lfs.required=true add file.txt
fatal: the remote end hung up unexpectedly
After this change, the proper error is reported:
$ git ... add file.txt
error: initialization for subprocess '/path with space/tool' failed
fatal: file.txt: clean filter 'lfs' failed
Switch the subprocess handshake from the dying packet_read_line()
to packet_read_line_gently() so that a process that exits during
startup produces an error return instead of killing the caller.
This affects any subprocess consumer whose command path contains
spaces. On Windows this routinely happens because programs live
under "C:/Program Files/...", and MSYS2 path conversion can rewrite
absolute paths to include that prefix. On POSIX it triggers
whenever the configured path naturally contains a space or other
metacharacter. convert.c (filter.<driver>.process, used by git-lfs
and custom clean/smudge filters) is the primary affected consumer.
Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
---
sub-process: use gentle handshake to avoid die() on startup failure
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2133%2Fmmontalbo%2Fmm%2Fsubprocess-handshake-fix-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2133/mmontalbo/mm/subprocess-handshake-fix-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2133
sub-process.c | 11 ++++++-----
t/t0021-conversion.sh | 17 +++++++++++++++++
2 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/sub-process.c b/sub-process.c
index 83bf0a0e82..22c68bd10d 100644
--- a/sub-process.c
+++ b/sub-process.c
@@ -132,18 +132,19 @@ static int handshake_version(struct child_process *process,
if (packet_flush_gently(process->in))
return error("Could not write flush packet");
- if (!(line = packet_read_line(process->out, NULL)) ||
+ if (packet_read_line_gently(process->out, NULL, &line) <= 0 ||
!skip_prefix(line, welcome_prefix, &p) ||
strcmp(p, "-server"))
return error("Unexpected line '%s', expected %s-server",
line ? line : "<flush packet>", welcome_prefix);
- if (!(line = packet_read_line(process->out, NULL)) ||
+ if (packet_read_line_gently(process->out, NULL, &line) <= 0 ||
!skip_prefix(line, "version=", &p) ||
strtol_i(p, 10, chosen_version))
return error("Unexpected line '%s', expected version",
line ? line : "<flush packet>");
- if ((line = packet_read_line(process->out, NULL)))
- return error("Unexpected line '%s', expected flush", line);
+ if (packet_read_line_gently(process->out, NULL, &line) < 0 || line)
+ return error("Unexpected line '%s', expected flush",
+ line ? line : "<read error>");
/* Check to make sure that the version received is supported */
for (i = 0; versions[i]; i++) {
@@ -171,7 +172,7 @@ static int handshake_capabilities(struct child_process *process,
if (packet_flush_gently(process->in))
return error("Could not write flush packet");
- while ((line = packet_read_line(process->out, NULL))) {
+ while (packet_read_line_gently(process->out, NULL, &line) > 0) {
const char *p;
if (!skip_prefix(line, "capability=", &p))
continue;
diff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh
index f0d50d769e..033b00a364 100755
--- a/t/t0021-conversion.sh
+++ b/t/t0021-conversion.sh
@@ -857,6 +857,23 @@ test_expect_success 'invalid process filter must fail (and not hang!)' '
)
'
+test_expect_success 'missing process filter with space in path does not die' '
+ test_config_global filter.protocol.process "/non existent/tool" &&
+ test_config_global filter.protocol.required true &&
+ rm -rf repo &&
+ mkdir repo &&
+ (
+ cd repo &&
+ git init &&
+
+ echo "*.r filter=protocol" >.gitattributes &&
+
+ cp "$TEST_ROOT/test.o" test.r &&
+ test_must_fail git add . 2>git-stderr.log &&
+ test_grep "clean filter.*protocol.*failed" git-stderr.log
+ )
+'
+
test_expect_success 'delayed checkout in process filter' '
test_config_global filter.a.process "test-tool rot13-filter --log=a.log clean smudge delay" &&
test_config_global filter.a.required true &&
base-commit: 29bd7ed5127255713c1ac2f43b7c6f257d7b4594
--
gitgitgadget
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH] sub-process: use gentle handshake to avoid die() on startup failure
2026-06-01 4:15 [PATCH] sub-process: use gentle handshake to avoid die() on startup failure Michael Montalbo via GitGitGadget
@ 2026-06-01 6:43 ` Junio C Hamano
2026-06-01 15:51 ` Michael Montalbo
2026-06-01 21:20 ` [PATCH v2] " Michael Montalbo via GitGitGadget
1 sibling, 1 reply; 4+ messages in thread
From: Junio C Hamano @ 2026-06-01 6:43 UTC (permalink / raw)
To: Michael Montalbo via GitGitGadget; +Cc: git, Michael Montalbo
"Michael Montalbo via GitGitGadget" <gitgitgadget@gmail.com> writes:
> diff --git a/sub-process.c b/sub-process.c
> index 83bf0a0e82..22c68bd10d 100644
> --- a/sub-process.c
> +++ b/sub-process.c
> @@ -132,18 +132,19 @@ static int handshake_version(struct child_process *process,
> if (packet_flush_gently(process->in))
> return error("Could not write flush packet");
>
> - if (!(line = packet_read_line(process->out, NULL)) ||
> + if (packet_read_line_gently(process->out, NULL, &line) <= 0 ||
> !skip_prefix(line, welcome_prefix, &p) ||
> strcmp(p, "-server"))
> return error("Unexpected line '%s', expected %s-server",
> line ? line : "<flush packet>", welcome_prefix);
If `packet_read_line_gently()` returns `< 0` (due to an EOF or read
error), `line` will be `NULL`. The error message printed will be:
`Unexpected line '<flush packet>', expected filter-server`
This is misleading when the remote process didn't send a flush
packet; it hung up or crashed.
> - if (!(line = packet_read_line(process->out, NULL)) ||
> + if (packet_read_line_gently(process->out, NULL, &line) <= 0 ||
> !skip_prefix(line, "version=", &p) ||
> strtol_i(p, 10, chosen_version))
> return error("Unexpected line '%s', expected version",
> line ? line : "<flush packet>");
Ditto.
> - if ((line = packet_read_line(process->out, NULL)))
> - return error("Unexpected line '%s', expected flush", line);
> + if (packet_read_line_gently(process->out, NULL, &line) < 0 || line)
> + return error("Unexpected line '%s', expected flush",
> + line ? line : "<read error>");
We catch error return (< 0) or a line with payload (!!line) and
report an error here, because we want to see <flush> here. OK.
> @@ -171,7 +172,7 @@ static int handshake_capabilities(struct child_process *process,
> if (packet_flush_gently(process->in))
> return error("Could not write flush packet");
>
> - while ((line = packet_read_line(process->out, NULL))) {
> + while (packet_read_line_gently(process->out, NULL, &line) > 0) {
> const char *p;
> if (!skip_prefix(line, "capability=", &p))
> continue;
While this correctly stops the loop if packet_read_line_gently()
returns a non-positive value, doesn't it introduce a subtle bug?
`packet_read_line_gently()` returns:
- `> 0` for a normal line (which keeps the loop running).
- `0` for a flush packet (which we expect as the normal terminator
of the capabilities list, stopping the loop).
- `< 0` for an EOF or read error (which also stops the loop).
In the original code, an EOF or read error would have caused
`packet_read_line()` to call `die()`, aborting the process.
With the new code, if the child process dies or closes its pipe
during the capabilities handshake, the loop will terminate, and the
function will return `0` (success). The parent process will proceed
as if the capabilities were successfully negotiated. Any further
communication with the child process would fail so the damage may
not be huge, but somebody must check if the loop terminated because
of a flush packet, or an error.
while (1) {
const char *p;
int len = packet_read_line_gently(process->out, NULL, &line);
if (len < 0)
return error(_("subprocess `%s` failed to give capabilities"),
process->args.v[0]);
if (!skip_prefix(line, "capability=", &p))
continue;
...
or something, perhaps?
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH] sub-process: use gentle handshake to avoid die() on startup failure
2026-06-01 6:43 ` Junio C Hamano
@ 2026-06-01 15:51 ` Michael Montalbo
0 siblings, 0 replies; 4+ messages in thread
From: Michael Montalbo @ 2026-06-01 15:51 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Michael Montalbo via GitGitGadget, git
On Sun, May 31, 2026 at 11:43 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> "Michael Montalbo via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
> > diff --git a/sub-process.c b/sub-process.c
> > index 83bf0a0e82..22c68bd10d 100644
> > --- a/sub-process.c
> > +++ b/sub-process.c
> > @@ -132,18 +132,19 @@ static int handshake_version(struct child_process *process,
> > if (packet_flush_gently(process->in))
> > return error("Could not write flush packet");
> >
> > - if (!(line = packet_read_line(process->out, NULL)) ||
> > + if (packet_read_line_gently(process->out, NULL, &line) <= 0 ||
> > !skip_prefix(line, welcome_prefix, &p) ||
> > strcmp(p, "-server"))
> > return error("Unexpected line '%s', expected %s-server",
> > line ? line : "<flush packet>", welcome_prefix);
>
> If `packet_read_line_gently()` returns `< 0` (due to an EOF or read
> error), `line` will be `NULL`. The error message printed will be:
>
> `Unexpected line '<flush packet>', expected filter-server`
>
> This is misleading when the remote process didn't send a flush
> packet; it hung up or crashed.
>
Makes sense. Will fix.
>
>
> > - if (!(line = packet_read_line(process->out, NULL)) ||
> > + if (packet_read_line_gently(process->out, NULL, &line) <= 0 ||
> > !skip_prefix(line, "version=", &p) ||
> > strtol_i(p, 10, chosen_version))
> > return error("Unexpected line '%s', expected version",
> > line ? line : "<flush packet>");
>
> Ditto.
>
Will fix.
> > - if ((line = packet_read_line(process->out, NULL)))
> > - return error("Unexpected line '%s', expected flush", line);
> > + if (packet_read_line_gently(process->out, NULL, &line) < 0 || line)
> > + return error("Unexpected line '%s', expected flush",
> > + line ? line : "<read error>");
>
> We catch error return (< 0) or a line with payload (!!line) and
> report an error here, because we want to see <flush> here. OK.
>
>
> > @@ -171,7 +172,7 @@ static int handshake_capabilities(struct child_process *process,
> > if (packet_flush_gently(process->in))
> > return error("Could not write flush packet");
> >
> > - while ((line = packet_read_line(process->out, NULL))) {
> > + while (packet_read_line_gently(process->out, NULL, &line) > 0) {
> > const char *p;
> > if (!skip_prefix(line, "capability=", &p))
> > continue;
>
> While this correctly stops the loop if packet_read_line_gently()
> returns a non-positive value, doesn't it introduce a subtle bug?
>
> `packet_read_line_gently()` returns:
>
> - `> 0` for a normal line (which keeps the loop running).
>
> - `0` for a flush packet (which we expect as the normal terminator
> of the capabilities list, stopping the loop).
>
> - `< 0` for an EOF or read error (which also stops the loop).
>
> In the original code, an EOF or read error would have caused
> `packet_read_line()` to call `die()`, aborting the process.
>
> With the new code, if the child process dies or closes its pipe
> during the capabilities handshake, the loop will terminate, and the
> function will return `0` (success). The parent process will proceed
> as if the capabilities were successfully negotiated. Any further
> communication with the child process would fail so the damage may
> not be huge, but somebody must check if the loop terminated because
> of a flush packet, or an error.
>
> while (1) {
> const char *p;
> int len = packet_read_line_gently(process->out, NULL, &line);
>
> if (len < 0)
> return error(_("subprocess `%s` failed to give capabilities"),
> process->args.v[0]);
> if (!skip_prefix(line, "capability=", &p))
> continue;
> ...
>
> or something, perhaps?
Good catch, thank you. I will update the logic so a failure during
capabilities handshake
is correctly marked an error instead of silently succeeding.
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v2] sub-process: use gentle handshake to avoid die() on startup failure
2026-06-01 4:15 [PATCH] sub-process: use gentle handshake to avoid die() on startup failure Michael Montalbo via GitGitGadget
2026-06-01 6:43 ` Junio C Hamano
@ 2026-06-01 21:20 ` Michael Montalbo via GitGitGadget
1 sibling, 0 replies; 4+ messages in thread
From: Michael Montalbo via GitGitGadget @ 2026-06-01 21:20 UTC (permalink / raw)
To: git; +Cc: Michael Montalbo, Michael Montalbo
From: Michael Montalbo <mmontalbo@gmail.com>
When the configured subprocess command contains shell metacharacters
(such as a space), prepare_shell_cmd() wraps it in "sh -c <cmd>".
The shell itself always starts successfully, so start_command()
returns zero even if the tool inside does not exist. The subsequent
handshake then reads from a dead pipe and calls die() via the
non-gentle packet_read_line(), killing the parent process instead of
letting it handle the error.
Before this change, a missing filter process at a path containing
spaces produces a confusing error:
$ git -c filter.myfilter.process="/path with space/tool" \
-c filter.myfilter.required=true add file.txt
/path with space/tool: line 1: /path: No such file or directory
fatal: the remote end hung up unexpectedly
After this change, the proper error is reported:
$ git ... add file.txt
/path with space/tool: line 1: /path: No such file or directory
error: could not read greeting from subprocess '/path with space/tool'
error: initialization for subprocess '/path with space/tool' failed
fatal: file.txt: clean filter 'myfilter' failed
Switch the subprocess handshake from the dying packet_read_line()
to packet_read_line_gently() so that a process that exits during
startup produces an error return instead of killing the caller.
This affects any subprocess consumer whose command path contains
spaces. On Windows this routinely happens because programs live
under "C:/Program Files/...", and MSYS2 path conversion can rewrite
absolute paths to include that prefix. On POSIX it triggers
whenever the configured path naturally contains a space or other
metacharacter. convert.c (filter.<driver>.process, used by git-lfs
and custom clean/smudge filters) is the primary affected consumer.
Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
---
sub-process: use gentle handshake to avoid die() on startup failure
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2133%2Fmmontalbo%2Fmm%2Fsubprocess-handshake-fix-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2133/mmontalbo/mm/subprocess-handshake-fix-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2133
Range-diff vs v1:
1: c11b01c156 ! 1: cb7bd777dc sub-process: use gentle handshake to avoid die() on startup failure
@@ Commit message
Before this change, a missing filter process at a path containing
spaces produces a confusing error:
- $ git -c filter.lfs.process="/path with space/tool" \
- -c filter.lfs.required=true add file.txt
+ $ git -c filter.myfilter.process="/path with space/tool" \
+ -c filter.myfilter.required=true add file.txt
+ /path with space/tool: line 1: /path: No such file or directory
fatal: the remote end hung up unexpectedly
After this change, the proper error is reported:
$ git ... add file.txt
+ /path with space/tool: line 1: /path: No such file or directory
+ error: could not read greeting from subprocess '/path with space/tool'
error: initialization for subprocess '/path with space/tool' failed
- fatal: file.txt: clean filter 'lfs' failed
+ fatal: file.txt: clean filter 'myfilter' failed
Switch the subprocess handshake from the dying packet_read_line()
to packet_read_line_gently() so that a process that exits during
@@ sub-process.c: static int handshake_version(struct child_process *process,
return error("Could not write flush packet");
- if (!(line = packet_read_line(process->out, NULL)) ||
-+ if (packet_read_line_gently(process->out, NULL, &line) <= 0 ||
- !skip_prefix(line, welcome_prefix, &p) ||
+- !skip_prefix(line, welcome_prefix, &p) ||
++ if (packet_read_line_gently(process->out, NULL, &line) < 0)
++ return error("could not read greeting from subprocess '%s'",
++ process->args.v[0]);
++ if (!line || !skip_prefix(line, welcome_prefix, &p) ||
strcmp(p, "-server"))
return error("Unexpected line '%s', expected %s-server",
line ? line : "<flush packet>", welcome_prefix);
- if (!(line = packet_read_line(process->out, NULL)) ||
-+ if (packet_read_line_gently(process->out, NULL, &line) <= 0 ||
- !skip_prefix(line, "version=", &p) ||
+- !skip_prefix(line, "version=", &p) ||
++ if (packet_read_line_gently(process->out, NULL, &line) < 0)
++ return error("could not read version from subprocess '%s'",
++ process->args.v[0]);
++ if (!line || !skip_prefix(line, "version=", &p) ||
strtol_i(p, 10, chosen_version))
return error("Unexpected line '%s', expected version",
line ? line : "<flush packet>");
- if ((line = packet_read_line(process->out, NULL)))
-- return error("Unexpected line '%s', expected flush", line);
-+ if (packet_read_line_gently(process->out, NULL, &line) < 0 || line)
-+ return error("Unexpected line '%s', expected flush",
-+ line ? line : "<read error>");
++ if (packet_read_line_gently(process->out, NULL, &line) < 0)
++ return error("could not read version flush from subprocess '%s'",
++ process->args.v[0]);
++ if (line)
+ return error("Unexpected line '%s', expected flush", line);
/* Check to make sure that the version received is supported */
- for (i = 0; versions[i]; i++) {
@@ sub-process.c: static int handshake_capabilities(struct child_process *process,
if (packet_flush_gently(process->in))
return error("Could not write flush packet");
- while ((line = packet_read_line(process->out, NULL))) {
-+ while (packet_read_line_gently(process->out, NULL, &line) > 0) {
++ for (;;) {
const char *p;
++ int len = packet_read_line_gently(process->out, NULL, &line);
++
++ if (len < 0)
++ return error("could not read capabilities from subprocess '%s'",
++ process->args.v[0]);
++ if (!line)
++ break;
if (!skip_prefix(line, "capability=", &p))
continue;
+
## t/t0021-conversion.sh ##
@@ t/t0021-conversion.sh: test_expect_success 'invalid process filter must fail (and not hang!)' '
sub-process.c | 26 ++++++++++++++++++++------
t/t0021-conversion.sh | 17 +++++++++++++++++
2 files changed, 37 insertions(+), 6 deletions(-)
diff --git a/sub-process.c b/sub-process.c
index 83bf0a0e82..2d5c965169 100644
--- a/sub-process.c
+++ b/sub-process.c
@@ -132,17 +132,24 @@ static int handshake_version(struct child_process *process,
if (packet_flush_gently(process->in))
return error("Could not write flush packet");
- if (!(line = packet_read_line(process->out, NULL)) ||
- !skip_prefix(line, welcome_prefix, &p) ||
+ if (packet_read_line_gently(process->out, NULL, &line) < 0)
+ return error("could not read greeting from subprocess '%s'",
+ process->args.v[0]);
+ if (!line || !skip_prefix(line, welcome_prefix, &p) ||
strcmp(p, "-server"))
return error("Unexpected line '%s', expected %s-server",
line ? line : "<flush packet>", welcome_prefix);
- if (!(line = packet_read_line(process->out, NULL)) ||
- !skip_prefix(line, "version=", &p) ||
+ if (packet_read_line_gently(process->out, NULL, &line) < 0)
+ return error("could not read version from subprocess '%s'",
+ process->args.v[0]);
+ if (!line || !skip_prefix(line, "version=", &p) ||
strtol_i(p, 10, chosen_version))
return error("Unexpected line '%s', expected version",
line ? line : "<flush packet>");
- if ((line = packet_read_line(process->out, NULL)))
+ if (packet_read_line_gently(process->out, NULL, &line) < 0)
+ return error("could not read version flush from subprocess '%s'",
+ process->args.v[0]);
+ if (line)
return error("Unexpected line '%s', expected flush", line);
/* Check to make sure that the version received is supported */
@@ -171,8 +178,15 @@ static int handshake_capabilities(struct child_process *process,
if (packet_flush_gently(process->in))
return error("Could not write flush packet");
- while ((line = packet_read_line(process->out, NULL))) {
+ for (;;) {
const char *p;
+ int len = packet_read_line_gently(process->out, NULL, &line);
+
+ if (len < 0)
+ return error("could not read capabilities from subprocess '%s'",
+ process->args.v[0]);
+ if (!line)
+ break;
if (!skip_prefix(line, "capability=", &p))
continue;
diff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh
index f0d50d769e..033b00a364 100755
--- a/t/t0021-conversion.sh
+++ b/t/t0021-conversion.sh
@@ -857,6 +857,23 @@ test_expect_success 'invalid process filter must fail (and not hang!)' '
)
'
+test_expect_success 'missing process filter with space in path does not die' '
+ test_config_global filter.protocol.process "/non existent/tool" &&
+ test_config_global filter.protocol.required true &&
+ rm -rf repo &&
+ mkdir repo &&
+ (
+ cd repo &&
+ git init &&
+
+ echo "*.r filter=protocol" >.gitattributes &&
+
+ cp "$TEST_ROOT/test.o" test.r &&
+ test_must_fail git add . 2>git-stderr.log &&
+ test_grep "clean filter.*protocol.*failed" git-stderr.log
+ )
+'
+
test_expect_success 'delayed checkout in process filter' '
test_config_global filter.a.process "test-tool rot13-filter --log=a.log clean smudge delay" &&
test_config_global filter.a.required true &&
base-commit: 29bd7ed5127255713c1ac2f43b7c6f257d7b4594
--
gitgitgadget
^ permalink raw reply related [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-06-01 21:20 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-01 4:15 [PATCH] sub-process: use gentle handshake to avoid die() on startup failure Michael Montalbo via GitGitGadget
2026-06-01 6:43 ` Junio C Hamano
2026-06-01 15:51 ` Michael Montalbo
2026-06-01 21:20 ` [PATCH v2] " Michael Montalbo via GitGitGadget
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox