transport: set OUTBOUND on send_existing EAGAIN exits - #1945
Open
alexanderadam wants to merge 5 commits into
Open
alexanderadam wants to merge 5 commits into
alexanderadam wants to merge 5 commits into
Conversation
alexanderadam
force-pushed
the
fix/send_existing_bd_eagain
branch
5 times, most recently
from
May 16, 2026 22:24
4730ab0 to
4f07bda
Compare
OUTBOUND on send_existing EAGAIN exits
Member
|
Sorry for the delay, could you pull up and resolve the conflicts and we'll get this integrated. |
Other EAGAIN exits from send_existing/_libssh2_transport_send already OR in OUTBOUND; this branch was missed. Without it, callers polling libssh2_session_block_directions() get back 0 and have nothing to wait on. Fixes libssh2#1672
if LIBSSH2_SEND took some bytes but not all, more are still pending on the write side. flag OUTBOUND, otherwise bd ends up 0 when the prior transport call was a read.
static test, no network. plants a pending packet in session->packet and calls _libssh2_transport_send with a different data pointer. only the address-mismatch branch for now, partial-send needs a stub send callback.
alexanderadam
force-pushed
the
fix/send_existing_bd_eagain
branch
from
July 1, 2026 12:22
4f07bda to
7ba5174
Compare
… exits - "bd" was opaque, rename it. - add the LIBSSH2_SEND -EAGAIN and partial-send cases via a stub callback (libssh2_session_callback_set2), save/restore so test order doesn't matter. - bounds-assert in arm_pending_packet. - gitignore the binary.
alexanderadam
force-pushed
the
fix/send_existing_bd_eagain
branch
from
July 1, 2026 12:40
7ba5174 to
093d2d8
Compare
_libssh2_channel_read with buflen=0 had a bug: the packet-drain loop below is gated on `bytes_read < buflen` and never enters when both are zero, so transport_read's BLOCK_INBOUND flag (set on its internal EAGAIN exit) survives into the terminal !bytes_read branch, which then returns LIBSSH2_ERROR_EAGAIN. The caller is told to wait on inbound data they never asked for, and spins. Reproduced with an HTTP client over a forwarded channel where a bounded-body reader asked for 0 bytes once the response body length was fully accounted for. Thousands of channel_read calls with `wants 0 bytes` before the caller's nonblock-iter ceiling fired. Fix: early return 0 for buflen=0. Same noop the caller asked for, no transport involvement.
alexanderadam
force-pushed
the
fix/send_existing_bd_eagain
branch
from
July 1, 2026 12:46
093d2d8 to
6bc7006
Compare
Author
|
@willco007 done 😊 |
vszakats
reviewed
Jul 19, 2026
Comment on lines
+1953
to
+1955
| if(channel->remote.eof || channel->remote.close) | ||
| return 0; | ||
| return 0; |
Member
There was a problem hiding this comment.
Suggested change
| if(channel->remote.eof || channel->remote.close) | |
| return 0; | |
| return 0; | |
| return 0; |
was there something meant to be done under the nested if? if not, let's delete it.
vszakats
reviewed
Jul 19, 2026
Comment on lines
+1
to
+8
| /* Copyright (C) The libssh2 project and its contributors. | ||
| * | ||
| * regression test for #1672: every EAGAIN exit in send_existing() must | ||
| * leave block_directions non-zero so the caller knows what to wait on. | ||
| * | ||
| * SPDX-License-Identifier: BSD-3-Clause | ||
| */ | ||
|
|
Member
There was a problem hiding this comment.
Suggested change
| /* Copyright (C) The libssh2 project and its contributors. | |
| * | |
| * regression test for #1672: every EAGAIN exit in send_existing() must | |
| * leave block_directions non-zero so the caller knows what to wait on. | |
| * | |
| * SPDX-License-Identifier: BSD-3-Clause | |
| */ | |
| /* Copyright (C) The libssh2 project and its contributors. | |
| * | |
| * SPDX-License-Identifier: BSD-3-Clause | |
| */ | |
| /* Regression test for #1672: every EAGAIN exit in send_existing() must | |
| leave block_directions non-zero so the caller knows what to wait on. */ | |
Let's keep the copyright header and description separate.
Also could we drop the reference to #1672 and make this comment
self-explanatory, yet short?
vszakats
reviewed
Jul 19, 2026
Comment on lines
+191
to
+195
| if(test_addr_mismatch(session) != 0) | ||
| failures++; | ||
| if(test_send_eagain(session) != 0) | ||
| failures++; | ||
| if(test_partial_send(session) != 0) |
Member
There was a problem hiding this comment.
Suggested change
| if(test_addr_mismatch(session) != 0) | |
| failures++; | |
| if(test_send_eagain(session) != 0) | |
| failures++; | |
| if(test_partial_send(session) != 0) | |
| if(test_addr_mismatch(session)) | |
| failures++; | |
| if(test_send_eagain(session)) | |
| failures++; | |
| if(test_partial_send(session)) |
let's omit != 0 to match rest of codebase.
vszakats
reviewed
Jul 19, 2026
Comment on lines
+12
to
+16
| #include <assert.h> | ||
| #include <errno.h> | ||
| #include <stdio.h> | ||
| #include <stdlib.h> | ||
| #include <string.h> |
Member
There was a problem hiding this comment.
Suggested change
| #include <assert.h> | |
| #include <errno.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <assert.h> |
drop headers already included via libssh2_priv.h and libssh2.h.
also drop stdlib.h unless it's required, in which case I suggest
adding a comment saying /* for funcname() */.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I also fell into this and initially I assumed other issues in my stack. 😆
send_existing()insrc/transport.chas threeEAGAINexits. Two of them correctlyORLIBSSH2_SESSION_BLOCK_OUTBOUNDintosession->socket_block_directionsbefore returning.However, the "Address is different" branch does not. When this fires, the caller gets
EAGAINwithbd == 0.The branch is fired whenever something else on the same session calls
_libssh2_transport_sendwith a differentdatapointer whilep->olen != 0from an unfinished write.The obvious cases are interleaved channel writes, but it also fires from inside libssh2 itself when
_libssh2_kex_exchangebuilds its own packet during a write, which is exactly what #1672 describes from a different angle.There is also a 'twin defect' at the partial-send return at the end of the same function: if
LIBSSH2_SENDaccepted some bytes but not all. More are still pending, butbddoesn't get updated.This is usually already set from a prior iteration, but if the prior transport call was a clean read,
bdis0and you get the same hang.Fixes #1672
Related #1397, #1431, #1454 the regression chain that introduced the
EAGAINreturn on this branch in the first place. The fix there was correct for the data-corruption symptom but leftbdunset, which is what #1672 reports and what this PR addresses.PS: Thank you so much for your work! 🙏 So many people and projects are depending on libssh2 and it's a typical standing-on-the-shoulders-of-giants project.
PPS: huh? How is the CI green now? I'm pretty sure that I got an email that it failed!? 🤔
PPPS: I'm looking for a new adventure in case anybody is looking to hire someone or in case someone would like to work with a (Ruby/Rails/Crystal) dev and I'm also open to move to a PO/PM role.