Visitar URL original
libssh2_channel_exec may return -1 when using non-blocking sockets · Issue #671 · libssh2/libssh2 · GitHub
Skip to content

libssh2_channel_exec may return -1 when using non-blocking sockets #671

Open

Description

@awaeag

When using non-blocking sockets, libssh2_channel_exec may return -1, and the channel becomes dead.
I fixed the issue by modifying the function _libssh2_packet_requirev in packet.c in libssh2 sources:
line 1393:
old code:

        if(strchr((char *) packet_types, ret)) {
            /* Be lazy, let packet_ask pull it out of the brigade */
            int ret = _libssh2_packet_askv(session, packet_types, data,
                                        data_len, match_ofs, match_buf,
                                        match_len);
            state->start = 0;
            return ret;
        }
    }

new code:

        if(strchr((char *) packet_types, ret)) {
            /* Be lazy, let packet_ask pull it out of the brigade */
            if (_libssh2_packet_askv(session, packet_types, data,
                                        data_len, match_ofs, match_buf,
                                        match_len)) {
                return LIBSSH2_ERROR_EAGAIN;
            }
            else {
                state->start = 0;
                return 0;
            }
        }
    }

I'm no libssh2 developer, so maybe I do not have the whole picture. But this fixed my problem.

Activity

  1. stale commented on Jun 13, 2022

    @stale

    This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

  2. rmsh1216 commented on Aug 3, 2023

    @rmsh1216
    Contributor

    The _libssh2_packet_askv is used to check whether there are expected messages among the messages receive from the peer.
    Before that, the local has successfully receive messages from the peer by calling _libssh2_transport_read. If the local does not receive messages from the peer due to some unkown reasons, the local will try again.
    When the expected message is not received, this means that the peer do not send message according to the SSH protocol (please see rfc4250 to rfc4254). In this case, an error should be reported instead of trying to receive the message from the peer again.
    In your case, restarting a new ssh connection may be a better alternative.

  3. awaeag commented on Aug 4, 2023

    @awaeag
    Author

    Thanks for this answer. But since this is quite old, I do not really remember all the researches I did at the time I opened the issue.
    About your answer: I doubt this. What I understand here is the a message has been partially read, which is perfectly possible and does not break the ssh protocol as long as the rest of the message follows.
    The problem may as well come from the _libssh2_transport_read you're talking about, that should only "accept" messages when they are completed.
    I was just hoping a libssh2 developer would review this code, modify it or completely changes it, but at least fix the problem.

  4. mchlbowyer commented on Nov 12, 2026

    @mchlbowyer

    We hit this too (1.11.1, and the same code on current master), and I think I can explain why it isn't a protocol problem: it happens whenever two channels on one session have a request waiting for a reply at the same time, in non-blocking mode.

    Why

    _libssh2_packet_requirev() reads a packet with _libssh2_transport_read(). If the packet's type is one of the wanted types (SSH_MSG_CHANNEL_SUCCESS/FAILURE), it assumes the packet is the caller's and returns whatever _libssh2_packet_askv() finds. askv also matches on the recipient channel:

    if(strchr((char *) packet_types, ret)) {
        /* Be lazy, let packet_ask pull it out of the brigade */
        ret = _libssh2_packet_askv(session, packet_types, data, data_len,
                                   match_ofs, match_buf, match_len);
        state->start = 0;
        return ret;
    }

    When the reply that just arrived belongs to another channel's pending request, askv finds nothing for this channel and returns -1, and the caller treats that as a failure of its own request. The peer did nothing wrong: its reply to this channel simply arrives later, and the other channel's reply stays in the brigade for its owner. _libssh2_packet_require() has the same shortcut.

    What we saw

    Six channels opened concurrently on one session against OpenSSH 10.3p1, each requesting agent forwarding and then running a command, failed on every attempt:

    • libssh2_channel_request_auth_agent() returned LIBSSH2_ERROR_PROTO ("Failed to request auth-agent"), then retried with the legacy auth-agent-req, which OpenSSH refuses (LIBSSH2_ERROR_CHANNEL_REQUEST_DENIED);
    • libssh2_channel_process_startup() (so libssh2_channel_exec(), as reported here) and libssh2_channel_request_pty_ex() failed the same way.

    Reproduction outline

    1. Connect and authenticate one session; libssh2_session_set_blocking(session, 0).
    2. Open two session channels.
    3. Call libssh2_channel_request_auth_agent() once on each, so both requests are sent and both calls return LIBSSH2_ERROR_EAGAIN.
    4. Keep calling both, alternately, while they return LIBSSH2_ERROR_EAGAIN. Whichever call reads the other channel's reply first returns LIBSSH2_ERROR_PROTO.

    Suggested fix (close to the one in the report, without returning EAGAIN after having read data): return only when the matching packet was found; otherwise keep reading, as the loop already does for other packet types:

    if(strchr((char *) packet_types, ret)) {
        /* It may be the reply to another channel's request: stop only if it is ours. */
        if(_libssh2_packet_askv(session, packet_types, data, data_len,
                                match_ofs, match_buf, match_len) == 0) {
            state->start = 0;
            return 0;
        }
    }

    and the same in _libssh2_packet_require(). The other channel's packet stays in the brigade for its owner.

    Workaround until then: allow only one request that waits for a reply per session at a time.

    Happy to turn this into a pull request with a test if that would help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions