gitoxide: *Sometimes* `gix fetch` gets stuck in negotiation with `ssh://` remotes (hosted by `gitea`)

Current behavior 😯

This happens on some repos sporadically (using v0.29.0, but it has happened long before that).

When you run gix fetch it is stuck in the negotiation phase forever(?) I tend to stop it after a few seconds, but I seem to remember it staying there for a few minutes.

Cancelling it with CTRL+C and rerunning the command causes the same behaviour.

Running a git fetch fixes the repo(?) and now gix fetch works again.

This is the error displayed after sending CTRL+C:

Error: An IO error occurred when talking to the server

Caused by:
    Broken pipe (os error 32)

Expected behavior πŸ€”

gix fetch should work or time out the negotiation after a resonable amount of time (a few seconds to a minute).

Steps to reproduce πŸ•Ή

???

I see it happenning on my selfhosted gitea repos relatively often (~once every two or so weeks) but I have no idea how to reproduce this.

If you have any idea how I could go about diagnosing the issue I’ll make sure to keep it in mind for the next time it happens. For now this is all I have.

About this issue

  • Original URL
  • State: closed
  • Created 9 months ago
  • Comments: 51 (50 by maintainers)

Commits related to this issue

Most upvoted comments

Can you try once more from this PR? It contains adjustments to the logic to work with more test-cases, and I can only hope that it also still covers your case.

Verified that main fixes the issue c: Thanks for all of this!

Can you try once more from this PR? It contains adjustments to the logic to work with more test-cases, and I can only hope that it also still covers your case.

That’s incredible! A small change with huge effect! I can now just hope that the test-coverage is as good as I think or else something else might break πŸ˜… (at least the blast radios is limited to V1).

Alright, the PR is in flight and I hope it will be smooth sailing from now on πŸ˜ƒ.

Thanks a lot for trying!

This means I am puzzled as to where the done could have gone. Rust sends it into a pipe that should connect to the git-upload-pack which has just sent ACK and NAK and would now proceed to read the next packetline. That should be done and then the pack is sent.

But that clearly doesn’t happen.

Maybe something else is happening here, somehow. What confuses me is that done is sent in the second round which would mean that it finished parsing the first response. But according to the logic here with client_expects_pack=false and saw_ready=true, we’d get false for the filter which would then try to read past the last NAK which should make it stall right there. Thus it wouldn’t get to send done at all in the second round.

In any case, the way I understand the code in git-upload-pack, a logic change seems in order:

diff --git a/gix-protocol/src/fetch/response/blocking_io.rs b/gix-protocol/src/fetch/response/blocking_io.rs
index 309f5a7c5..d36a1a45f 100644
--- a/gix-protocol/src/fetch/response/blocking_io.rs
+++ b/gix-protocol/src/fetch/response/blocking_io.rs
@@ -85,7 +85,7 @@ impl Response {
                     assert_ne!(reader.readline_str(&mut line)?, 0, "consuming a peeked line works");
                     // When the server sends ready, we know there is going to be a pack so no need to stop early.
                     saw_ready |= matches!(acks.last(), Some(Acknowledgement::Ready));
-                    if let Some(Acknowledgement::Nak) = acks.last().filter(|_| !client_expects_pack && !saw_ready) {
+                    if let Some(Acknowledgement::Nak) = acks.last().filter(|_| !client_expects_pack || !saw_ready) {
                         break 'lines false;
                     }
                 };

Can you try it with this patch? it passes the test-suite, so that’s a start (and it will hang if I butcher it too much).

Here is the log with git-upload-pack’s output:

log
13:15:09.294150 pkt-line.c:85           packet:  upload-pack> ff30028d5fb155dcd6fcc250cb256fccba528245 HEAD\0multi_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed symref=HEAD:refs/heads/main object-format=sha1 agent=git/2.42.0
13:15:09.294589 pkt-line.c:85           packet:  upload-pack> ff30028d5fb155dcd6fcc250cb256fccba528245 refs/heads/main
13:15:09.294626 pkt-line.c:85           packet:  upload-pack> 0000
13:15:09.296360 pkt-line.c:85           packet:  upload-pack< want ff30028d5fb155dcd6fcc250cb256fccba528245 thin-pack side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative multi_ack_detailed agent=git/oxide-0.55.2 include-tag
13:15:09.296523 pkt-line.c:85           packet:  upload-pack< 0000
13:15:09.296538 pkt-line.c:85           packet:  upload-pack< have 713418f2c3ef057538264c6c379d45836a86ca06
13:15:09.296678 pkt-line.c:85           packet:  upload-pack> ACK 713418f2c3ef057538264c6c379d45836a86ca06 common
13:15:09.296698 pkt-line.c:85           packet:  upload-pack< have 24ed375e274f6c90b846b5579ae01ab8ac77e631
13:15:09.296781 pkt-line.c:85           packet:  upload-pack> ACK 24ed375e274f6c90b846b5579ae01ab8ac77e631 common
13:15:09.296799 pkt-line.c:85           packet:  upload-pack< have 4896909faec56497fdd03f7c00ec51570a6c5d88
13:15:09.296883 pkt-line.c:85           packet:  upload-pack> ACK 4896909faec56497fdd03f7c00ec51570a6c5d88 common
13:15:09.296902 pkt-line.c:85           packet:  upload-pack< have ef17d82d35d901a702d561561cedd6d9ee73af14
13:15:09.296985 pkt-line.c:85           packet:  upload-pack> ACK ef17d82d35d901a702d561561cedd6d9ee73af14 common
13:15:09.297002 pkt-line.c:85           packet:  upload-pack< have 4da23d2886e815c9b3c524826bf3fdd83226a85e
13:15:09.297075 pkt-line.c:85           packet:  upload-pack> ACK 4da23d2886e815c9b3c524826bf3fdd83226a85e common
13:15:09.297092 pkt-line.c:85           packet:  upload-pack< have d29e402d3e90a3bcc352107a2ec7cc499d799479
13:15:09.297165 pkt-line.c:85           packet:  upload-pack> ACK d29e402d3e90a3bcc352107a2ec7cc499d799479 common
13:15:09.297182 pkt-line.c:85           packet:  upload-pack< have ec4b3825f8fe612a88e00ffdca41fb4b51972bd0
13:15:09.297262 pkt-line.c:85           packet:  upload-pack> ACK ec4b3825f8fe612a88e00ffdca41fb4b51972bd0 common
13:15:09.297279 pkt-line.c:85           packet:  upload-pack< have 82a264f12274ca8e1999b73b5fd850c85118f50a
13:15:09.297352 pkt-line.c:85           packet:  upload-pack> ACK 82a264f12274ca8e1999b73b5fd850c85118f50a common
13:15:09.297370 pkt-line.c:85           packet:  upload-pack< have 2204a3efc5352b1dbcc6d211aa2a53b23f74ff27
13:15:09.297445 pkt-line.c:85           packet:  upload-pack> ACK 2204a3efc5352b1dbcc6d211aa2a53b23f74ff27 common
13:15:09.297462 pkt-line.c:85           packet:  upload-pack< have 9a827eb717c1cde2a5e7f5046d5da869fcbe9be4
13:15:09.297537 pkt-line.c:85           packet:  upload-pack> ACK 9a827eb717c1cde2a5e7f5046d5da869fcbe9be4 common
13:15:09.297554 pkt-line.c:85           packet:  upload-pack< have b805cc79f48ff3b1adf0f52b5042b17108e0aaa1
13:15:09.297628 pkt-line.c:85           packet:  upload-pack> ACK b805cc79f48ff3b1adf0f52b5042b17108e0aaa1 common
13:15:09.297645 pkt-line.c:85           packet:  upload-pack< have 795e583ee2931e25b7347616e013099fcaa9789c
13:15:09.297718 pkt-line.c:85           packet:  upload-pack> ACK 795e583ee2931e25b7347616e013099fcaa9789c common
13:15:09.297734 pkt-line.c:85           packet:  upload-pack< have 552a1d3b6d81bb0d184bc93996b1dbdc20b0f2ca
13:15:09.297807 pkt-line.c:85           packet:  upload-pack> ACK 552a1d3b6d81bb0d184bc93996b1dbdc20b0f2ca common
13:15:09.297824 pkt-line.c:85           packet:  upload-pack< have 4661adc46821622bcecc94188e2e1e4382f0c2cc
13:15:09.297962 pkt-line.c:85           packet:  upload-pack> ACK 4661adc46821622bcecc94188e2e1e4382f0c2cc common
13:15:09.297981 pkt-line.c:85           packet:  upload-pack< have d131fb4dd3611b1ceed828b3c3b61266e928d031
13:15:09.298459 pkt-line.c:85           packet:  upload-pack> ACK d131fb4dd3611b1ceed828b3c3b61266e928d031 common
13:15:09.298491 pkt-line.c:85           packet:  upload-pack< have ab24553467cc323133e9e81eeeabf21f08e2d97e
13:15:09.298530 pkt-line.c:85           packet:  upload-pack> ACK ab24553467cc323133e9e81eeeabf21f08e2d97e common
13:15:09.298543 pkt-line.c:85           packet:  upload-pack< 0000
13:15:09.298604 pkt-line.c:85           packet:  upload-pack> ACK ab24553467cc323133e9e81eeeabf21f08e2d97e ready
13:15:09.298616 pkt-line.c:85           packet:  upload-pack> NAK
 13:15:58 tracing INFO     run [ 49.1s | 0.02% / 100.00% ]
 13:15:58 tracing INFO     ┝━ ThreadSafeRepository::discover() [ 7.39ms | 0.00% / 0.02% ]
 13:15:58 tracing INFO     β”‚  ┕━ open_from_paths() [ 7.02ms | 0.00% / 0.01% ]
 13:15:58 tracing INFO     β”‚     ┝━ gix_path::git::install_config_path() [ 4.97ms | 0.01% ]
 13:15:58 tracing INFO     β”‚     ┕━ gix_odb::Store::at() [ 195Β΅s | 0.00% ]
 13:15:58 tracing INFO     ┝━ remote::Connection::ref_map() [ 3.83ms | 0.00% / 0.01% ]
 13:15:58 tracing INFO     β”‚  ┕━ remote::Connection::fetch_refs() [ 3.82ms | 0.00% / 0.01% ]
 13:15:58 tracing DEBUG    β”‚     ┕━ gix_protocol::handshake() [ 3.79ms | 0.01% ] service: UploadPack | extra_parameters: []
 13:15:58 tracing DEBUG    β”‚        ┝━ πŸ› [debug]: gix_transport::SpawnProcessOnDemand | command: "git-upload-pack" "/home/jalil/**CENSORED**.git"
 13:15:58 tracing TRACE    β”‚        ┝━ πŸ“ [trace]: << ff30028d5fb155dcd6fcc250cb256fccba528245 HEADοΏ½multi_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed symref=HEAD:refs/heads/main object-format=sha1 agent=git/2.42.0
 13:15:58 tracing TRACE    β”‚        ┝━ πŸ“ [trace]: << ff30028d5fb155dcd6fcc250cb256fccba528245 refs/heads/main
 13:15:58 tracing TRACE    β”‚        ┕━ πŸ“ [trace]: << FLUSH
 13:15:58 tracing INFO     ┕━ fetch::Prepare::receive() [ 49.1s | 0.00% / 99.96% ]
 13:15:58 tracing DEBUG       ┕━ negotiate [ 49.1s | 0.00% / 99.96% ] protocol_version: 1
 13:15:58 tracing DEBUG          ┝━ mark_complete_and_common_ref [ 826Β΅s | 0.00% / 0.00% ] mappings: 1
 13:15:58 tracing INFO           β”‚  ┝━ mark_all_refs [ 401Β΅s | 0.00% ]
 13:15:58 tracing DEBUG          β”‚  ┝━ mark_alternate_refs [ 483ns | 0.00% ] num_odb: 0
 13:15:58 tracing INFO           β”‚  ┝━ mark known_common [ 746ns | 0.00% ]
 13:15:58 tracing DEBUG          β”‚  ┕━ mark tips [ 9.55Β΅s | 0.00% ] num_tips: 1
 13:15:58 tracing DEBUG          ┝━ negotiate round [ 49.1s | 99.95% ] round: 1
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> want ff30028d5fb155dcd6fcc250cb256fccba528245 thin-pack side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative multi_ack_detailed agent=git/oxide-0.55.2 include-tag
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> FLUSH
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have 713418f2c3ef057538264c6c379d45836a86ca06
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have 24ed375e274f6c90b846b5579ae01ab8ac77e631
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have 4896909faec56497fdd03f7c00ec51570a6c5d88
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have ef17d82d35d901a702d561561cedd6d9ee73af14
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have 4da23d2886e815c9b3c524826bf3fdd83226a85e
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have d29e402d3e90a3bcc352107a2ec7cc499d799479
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have ec4b3825f8fe612a88e00ffdca41fb4b51972bd0
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have 82a264f12274ca8e1999b73b5fd850c85118f50a
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have 2204a3efc5352b1dbcc6d211aa2a53b23f74ff27
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have 9a827eb717c1cde2a5e7f5046d5da869fcbe9be4
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have b805cc79f48ff3b1adf0f52b5042b17108e0aaa1
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have 795e583ee2931e25b7347616e013099fcaa9789c
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have 552a1d3b6d81bb0d184bc93996b1dbdc20b0f2ca
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have 4661adc46821622bcecc94188e2e1e4382f0c2cc
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have d131fb4dd3611b1ceed828b3c3b61266e928d031
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> have ab24553467cc323133e9e81eeeabf21f08e2d97e
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: >> FLUSH
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK 713418f2c3ef057538264c6c379d45836a86ca06 common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK 24ed375e274f6c90b846b5579ae01ab8ac77e631 common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK 4896909faec56497fdd03f7c00ec51570a6c5d88 common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK ef17d82d35d901a702d561561cedd6d9ee73af14 common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK 4da23d2886e815c9b3c524826bf3fdd83226a85e common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK d29e402d3e90a3bcc352107a2ec7cc499d799479 common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK ec4b3825f8fe612a88e00ffdca41fb4b51972bd0 common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK 82a264f12274ca8e1999b73b5fd850c85118f50a common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK 2204a3efc5352b1dbcc6d211aa2a53b23f74ff27 common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK 9a827eb717c1cde2a5e7f5046d5da869fcbe9be4 common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK b805cc79f48ff3b1adf0f52b5042b17108e0aaa1 common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK 795e583ee2931e25b7347616e013099fcaa9789c common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK 552a1d3b6d81bb0d184bc93996b1dbdc20b0f2ca common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK 4661adc46821622bcecc94188e2e1e4382f0c2cc common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK d131fb4dd3611b1ceed828b3c3b61266e928d031 common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK ab24553467cc323133e9e81eeeabf21f08e2d97e common
 13:15:58 tracing TRACE          β”‚  ┝━ πŸ“ [trace]: << ACK ab24553467cc323133e9e81eeeabf21f08e2d97e ready
 13:15:58 tracing TRACE          β”‚  ┕━ πŸ“ [trace]: << NAK
 13:15:58 tracing DEBUG          ┕━ negotiate round [ 132Β΅s | 0.00% ] round: 2
 13:15:58 tracing TRACE             ┕━ πŸ“ [trace]: >> done
Error: An IO error occurred when talking to the server

Caused by:
    Broken pipe (os error 32)

gitoxide.tracePacket=1 Seems to print binary characters (β€˜\0’) which makes it a pain to deal (grep and diff need the --text flag or they refuse to print to stdout).

Here are the logs:

git.log

gix.log

Packetline support is cooking in this PR and should be merged soon.

Then I’d need the output of GIT_TRACE_PACKET=1 git fetch and gix -c gitoxide.tracePacket=1 --trace fetch for comparison. If redaction of the output is happening, please be sure to apply the same β€˜function’ to both outputs - it’s fine to remove everything except the negotiation part - it should be distinct enough.

My expectation is that both interactions should be very similar if not the same, so there is probably some difference that explains the deadlock. My hope is that this is something obvious, like a protocol error of some sort, while the negotiation commit-walk is exactly the same.

To be sure that it’s (probably) not a bug on the server side, I have checked the code of gitea and believe that they also just run git under the hood, i.e. git-upload-pack.

Another test we can try is to locally host the repo in the state that is on the server using git daemon, and tune up gits own tracing, add it as remote to the local clone that is in a state that hangs, and see what that yields - usually that will provide additional information about the state of the git-upload-pack process.

But one step at a time πŸ˜….

PS: I also implemented auto-strict so -c x=foo will not quietly ignore obvious errors anymore.

Is there a config option for that?

You could automatically set it to the strict version of the desired mode when CLI overrides are detected - I think that’s a great idea. Should just be a couple of lines if you want to try it.

The repo is behind by just one commit, […]

That would be very strange, it shouldn’t need two negotiation rounds if just one commit is missing. It would definitely be interesting to see what happens when using HTTPS (as a state-less variant of the protocol). I guess once there is something to reproduce the issue that can also reproduce on GitHub, which could be used to compare both HTTPS and SSH.

Never mind the rambling above though, I think once packetline tracing is available, the issue will clear up quickly.

How many negotiation rounds did you get with skipping? Just one, is my prediction. (Concurrent editing)

Also, when setting an option with -c it would be nice to error if the config is wrong (I set skip instead of skipping and didn’t notice). Should I post an issue about that?

In theory, that’s a feature, and it’s intentionally lenient there. This makes it easy to change and maybe it should change. If you want to change it to strict mode please be my guest.

I am looking into adding tracing support similar to GIT_TRACE_PACKET now.

A bit more context on when it happens (I’m not 100% sure this is the pattern because it happens so infrequently).

I have two computers where I have the same git repos (the ones that get stuck from my selfhosted gitea instance). I have a preference for one PC so I leave the other alone for a while. When I return to the other computer the repos sometimes get stuck.

So how I think the issue could be reproduced is:

  1. Get a gitea repo with an ssh origin.
  2. Clone it on two different locations (maybe two computers?).
  3. Make a few commits on one location (maybe over a certain period of time).
  4. Try to fetch the updated repo on the out of date location.

I’ll see if I can reproduce this like that.

Thanks so much, I forgot that it’s possible to interrupt and then shut-down the application normally, showing the trace.

We see that it hangs in round two, which probably means it blocks while sending or… it blocks while receiving a reply maybe because the sending didn’t get flushed so that would be a local problem. Since I pretty much trust negotiation by now I’d think it might be something silly like a flush that wasn’t performed.

Using gix --trace fetch (private repo, can’t upload .git repository) (using gix 0.30.0):

$ gix --trace fetch
^C 19:21:55 tracing INFO     run [ 7.06s | 23.06% / 100.00% ]                                                                                                                                    racing
 19:21:55 tracing INFO     ┝━ ThreadSafeRepository::discover() [ 10.2ms | 0.01% / 0.14% ]
 19:21:55 tracing INFO     β”‚  ┕━ open_from_paths() [ 9.61ms | 0.03% / 0.14% ]
 19:21:55 tracing INFO     β”‚     ┝━ gix_path::git::install_config_path() [ 7.43ms | 0.11% ]
 19:21:55 tracing INFO     β”‚     ┕━ gix_odb::Store::at() [ 245Β΅s | 0.00% ]
 19:21:55 tracing DEBUG    ┝━ πŸ› [debug]: gix_transport::SpawnProcessOnDemand | command: GIT_PROTOCOL="version=2" LANG="C" LC_ALL="C" "ssh" "-o" "SendEnv=GIT_PROTOCOL" "gitea@**censored**" "git-upload-pack" "\'jalil/**censored**.git\'"
 19:21:55 tracing INFO     ┕━ fetch::Prepare::receive() [ 5.42s | 0.00% / 76.80% ]
 19:21:55 tracing INFO        ┕━ negotiate [ 5.42s | 0.01% / 76.79% ]
 19:21:55 tracing DEBUG          ┝━ mark_complete_and_common_ref [ 1.52ms | 0.01% / 0.02% ] mappings: 1
 19:21:55 tracing INFO           β”‚  ┝━ mark_all_refs [ 880Β΅s | 0.01% ]
 19:21:55 tracing DEBUG          β”‚  ┝━ mark_alternate_refs [ 1.18Β΅s | 0.00% ] num_odb: 0
 19:21:55 tracing INFO           β”‚  ┝━ mark known_common [ 2.48Β΅s | 0.00% ]
 19:21:55 tracing DEBUG          β”‚  ┕━ mark tips [ 2.77Β΅s | 0.00% ] num_tips: 1
 19:21:55 tracing DEBUG          ┝━ negotiate round [ 5.42s | 76.77% ] round: 1
 19:21:55 tracing DEBUG          ┕━ negotiate round [ 90.7Β΅s | 0.00% ] round: 2
Error: An IO error occurred when talking to the server

Caused by:
    Broken pipe (os error 32)

Killing it after 7s or 1min seems to make no difference to the trace output.

I will make a back up of this repo in case you have a fix you’d like to test.

I’ll see if I can reproduce it in a small repo then upload the full (.git included?) repo as a test case?

Yes, that would be optimal.

I’m sorry, this is such a flaky thing to reproduce. Thanks for all the help!

Thanks for helping me to make gix better!

There is also a --trace option, but right now it only prints at the end of an invocation which doesn’t happen during hangs. Having an altenrative trace-mode that is instant might alleviate this, even though I don’t think it would reveal that much.

There is a light at the end of the tunnel though, as it’s definitely planned to offer a built-in native ssh client as transport as well instead of forwarding to the ssh binary. Once that is in place, and if this fixes the issue, it’s clear that the cause of this issue is something about how gix communicates with the ssh process via pipelines, which is really easy to get wrong without noticing as many tests never reach certain thresholds that may cause these bugs to appear.