libssh2 SFTP pipelining multiple files: sftp_read() internal error
Asked Answered
F

1

8

I'm trying to reproduce a problem where when some of our clients can receive a wrong file contents while downloading with SFTP protocol.

Based on our SFTP server (CrushFTP) logs they probably open multiple files in one session and then use some pipelining to download the files. I don't know what kind of library they use as they are using some SAAS provider for this.

I'm trying to reproduce the behavior with libssh2, but I'm getting sftp_read() internal error when calling libssh2_sftp_read asynchronously on the second open file after the first one returned LIBSSH2_ERROR_EAGAIN - even on localhost connection to OpenSSH.

While browsing SSH File Transfer Protocol IETF Draft I can see that the protocol allows for opening multiple files simultaneously and requesting their contents with multiple SSH_FXP_READ request without waiting for response.

Below is the code I use for testing (SSCCE, but quite long - C is quite verbose) - compiled with gcc sftp_multifile.c -lssh2 -Wall -g -o sftp_multifile and tested with:

./sftp_multifile localhost 22 testusername testpassword /usr/share/dict/words /usr/share/doc/words/readme.txt

connect try: ai_family=10 ai_socktype=1 ai_protocol=6 addr=::1 port=22
opening: /usr/share/dict/words
opening: /usr/share/doc/words/readme.txt
reading: /usr/share/dict/words from 10134400 to 140736927200896
read result: -37
reading: /usr/share/doc/words/readme.txt from 10135536 to 140736927201920
read result: -31
Bad read result: -31
Read error: /usr/share/doc/words/readme.txt: sftp_read() internal error

Did I make an error in my code or does libssh2 just not support pipelining libssh2_sftp_read for multiple open files or maybe it's just a bug in libssh2 that should be reported to its maintainer?

#include <libssh2.h>
#include <libssh2_sftp.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <poll.h>
#include <arpa/inet.h>

#define CHUNK_SIZE (1024)

int sftp_connect(char const* hostname, char const* port)
{
    struct addrinfo* addrinfo_result, *addrinfo_current;
    int sfd, s;
    struct addrinfo addrinfo_hints = {0};
    addrinfo_hints.ai_family = AF_UNSPEC;
    addrinfo_hints.ai_socktype = SOCK_STREAM;
    s = getaddrinfo(hostname, port, &addrinfo_hints, &addrinfo_result);
    if (s != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
        exit(EXIT_FAILURE);
    }
    
    for (addrinfo_current = addrinfo_result; addrinfo_current != NULL; addrinfo_current = addrinfo_current->ai_next) {
        char s[INET6_ADDRSTRLEN];
        sfd = socket(addrinfo_current->ai_family, addrinfo_current->ai_socktype, addrinfo_current->ai_protocol);
        if (sfd == -1) continue;
        {
            void* addr;
            in_port_t port;
            switch(addrinfo_current->ai_family) {
                case AF_INET: {
                    struct sockaddr_in* sockaddr = (struct sockaddr_in*)addrinfo_current->ai_addr;
                    addr = &(sockaddr->sin_addr);
                    port = ntohs(sockaddr->sin_port);
                    break;
                }
                case AF_INET6: {
                    struct sockaddr_in6* sockaddr = (struct sockaddr_in6*)addrinfo_current->ai_addr;
                    addr = &(sockaddr->sin6_addr);
                    port = ntohs(sockaddr->sin6_port);
                    break;
                }
                default:
                    fprintf(stderr, "unknown family: %d\n", addrinfo_current->ai_family);
                    exit(EXIT_FAILURE);
            }
            inet_ntop(addrinfo_current->ai_family, addr, s, INET6_ADDRSTRLEN);
            fprintf(stderr, "connect try: ai_family=%d ai_socktype=%d ai_protocol=%d addr=%s port=%d\n", addrinfo_current->ai_family, addrinfo_current->ai_socktype, addrinfo_current->ai_protocol, s, port);
        }
        if (connect(sfd, addrinfo_current->ai_addr, addrinfo_current->ai_addrlen) != 0) {
            fprintf(stderr, "connect: %s\n", strerror(errno));
            close(sfd);
            continue;
        }
        break;
    }
    if (addrinfo_current == NULL) {
        fprintf(stderr, "connection failed\n");
        exit(EXIT_FAILURE);
    }
    freeaddrinfo(addrinfo_result);
    return sfd;
}

void retrieve_files(int socket, LIBSSH2_SESSION* session, LIBSSH2_SFTP *sftp_session, int filec, char *filev[])
{
    LIBSSH2_SFTP_HANDLE* sftp_handles[filec];
    int finished[filec];
    int toread = filec;
    for(int i=0; i<filec; i++) {
        fprintf(stderr, "opening: %s\n", filev[i]);
        sftp_handles[i] = libssh2_sftp_open(sftp_session, filev[i], LIBSSH2_FXF_READ, 0);
        if (!sftp_handles[i]) {
            char* errmsg;
            libssh2_session_last_error(session, &errmsg, NULL, 0);
            fprintf(stderr, "Failure opening remote file: %s: %s\n", filev[i], errmsg);
            exit(EXIT_FAILURE);
        }
        finished[i]=0;
    }
    char buffer[CHUNK_SIZE];
    libssh2_session_set_blocking(session, 0);
    while (toread) {
        for(int i=0; i<filec; i++) {
            if (finished[i]) {
                continue;
            } else {
                int read_result;
                do {
                    fprintf(stderr, "reading: %s from %ld to %ld\n", filev[i], (long)sftp_handles[i], (long)(buffer+CHUNK_SIZE*i));
                    read_result = libssh2_sftp_read(sftp_handles[i], buffer, CHUNK_SIZE);
                    fprintf(stderr, "read result: %d\n", read_result);
                    if (read_result > 0) {
                        printf("%s: ", filev[i]);
                        fwrite(buffer, sizeof(char), read_result, stdout);
                        printf("\n");
                    } else {
                        break;
                    }
                } while (1);
                if (read_result == LIBSSH2_ERROR_EAGAIN) {
                    continue;
                } else if (read_result == 0) {
                    finished[i] = 1;
                    toread--;
                } else {
                    fprintf(stderr, "Bad read result: %d\n", read_result);
                    char* errmsg;
                    libssh2_session_last_error(session, &errmsg, NULL, 0);
                    fprintf(stderr, "Read error: %s: %s\n", filev[i], errmsg);
                    exit(EXIT_FAILURE);
                }
            }
        }
        if (toread) {
            struct pollfd fds[1] = {0};
            fds[0].fd = socket;
            fds[0].events = POLLIN;
            poll(fds, 1, 3000);
        }
    }
}

int main(int argc, char *argv[])
{
    LIBSSH2_SESSION *session;
    LIBSSH2_SFTP *sftp_session;
    int rc, sock;
    
    if (argc<6) {
        fprintf(stderr, "Usage: %s hostname port username password file1 [file2] [file3...]\n", argv[0]);
        return EXIT_FAILURE;
    }
    
    sock = sftp_connect(argv[1], argv[2]);
    session = libssh2_session_init();
    if (!session) {
        return EXIT_FAILURE;
    }
    rc = libssh2_session_handshake(session, sock);
    if (rc) {
        char* errmsg;
        libssh2_session_last_error(session, &errmsg, NULL, 0);
        fprintf(stderr, "Failure establishing SSH session: %s\n", errmsg);
        return EXIT_FAILURE;
    }
    rc = libssh2_userauth_password(session, argv[3], argv[4]);
    if (rc) {
        char* errmsg;
        libssh2_session_last_error(session, &errmsg, NULL, 0);
        fprintf(stderr, "%s\n", errmsg);
        return EXIT_FAILURE;
    }
    
    sftp_session = libssh2_sftp_init(session);
    if (sftp_session == NULL) {
        char* errmsg;
        libssh2_session_last_error(session, &errmsg, NULL, 0);
        fprintf(stderr, "Unable to init SFTP session: %s\n", errmsg);
        return EXIT_FAILURE;
    }

    retrieve_files(sock, session, sftp_session, argc-5, argv+5);

    libssh2_session_set_blocking(session, 1);
    libssh2_sftp_shutdown(sftp_session);
    libssh2_session_disconnect(session, "");
    libssh2_session_free(session);
    libssh2_exit();
    return EXIT_SUCCESS;
}
Fischer answered 17/4, 2019 at 13:41 Comment(4)
which libssh2 version are you using?Nereidanereids
I'm testing on libssh2-1.8.1 64bit against openssh 7.8p1 (both included in my Fedora 28 workstation).Fischer
Could you add some more info about how to reproduce this? When I try it I get connect: Connection refused -- perhaps some service(s) need to be running on localhost? Maybe something more, like preparing a test user with testusername and testpassword? It would be helpful if you could list all steps needed to run your test case, to arrive at the "internal error" problem.Outhaul
@Outhaul you should connect to a working ssh server, you need to customize the command line OP posted.Nereidanereids
K
2

Interesting. I'm using libssh2 1.8.1-1 on Arch. I pasted your code into sftp_multifile.c, created a Makefile with

sftp_multifile: sftp_multifile.c
    gcc -g -O -Wall $< -o $@ -lssh2

and wrote the args into 55728938.<count> (not going to post that here ;-). With only one file, I get

$ ./sftp_multifile $(cat 55728938.one)
connect try: ai_family=10 ai_socktype=1 ai_protocol=6 addr=::1 port=22
opening: path/text1
reading: path/text1 from 94221524604816 to 140729241245632
read result: -37
reading: path/text1 from 94221524604816 to 140729241245632
read result: 21
reading: path/text1 from 94221524604816 to 140729241245632
read result: -37
reading: path/text1 from 94221524604816 to 140729241245632
read result: 0
path/text1: So much depends
upon

... looks good to me. But with two or more files,

$ ./sftp_multifile $(cat 55728938.two)
connect try: ai_family=10 ai_socktype=1 ai_protocol=6 addr=::1 port=22
opening: path/text1
opening: path/text2
reading: path/text1 from 94473513555856 to 140727557764112
read result: -37
reading: path/text2 from 94473513556256 to 140727557765136
read result: -31
Bad read result: -31
Read error: path/text2: sftp_read() internal error
$ ./sftp_multifile $(cat 55728938.three)
connect try: ai_family=10 ai_socktype=1 ai_protocol=6 addr=::1 port=22
opening: path/text1
opening: path/text2
opening: path/text3
reading: path/text1 from 94652164568976 to 140727538608784
read result: -37
reading: path/text2 from 94652164569376 to 140727538609808
read result: -31
Bad read result: -31
Read error: path/text2: sftp_read() internal error

This is consistent with what you saw. Since the packaged libssh2 was not built with debugging symbols, gdb doesn't help much. I'd suggest taking this up with the libssh2 mailing list.

Edit to add:

Removing the line libssh2_session_set_blocking(session, 0); appears to resolve the problem.

$ make && ./sftp_multifile $(cat 55728938.four)
gcc -g -O -Wall sftp_multifile.c -o sftp_multifile -lssh2
connect try: ai_family=10 ai_socktype=1 ai_protocol=6 addr=::1 port=22
opening: /home/pi/stackoverflow/text1
opening: /home/pi/stackoverflow/text2
opening: /home/pi/stackoverflow/text3
opening: /home/pi/stackoverflow/text4
4 of 4
reading: /home/pi/stackoverflow/text1 from 5453288 to 2129740008
read result: 21
/home/pi/stackoverflow/text1: So much depends
upon

reading: /home/pi/stackoverflow/text1 from 5453288 to 2129740008
read result: 0
3 of 4
reading: /home/pi/stackoverflow/text2 from 5453640 to 2129741032
read result: 19
/home/pi/stackoverflow/text2: a red wheel
barrow

reading: /home/pi/stackoverflow/text2 from 5453640 to 2129741032
read result: 0
2 of 4
reading: /home/pi/stackoverflow/text3 from 5455408 to 2129742056
read result: 23
/home/pi/stackoverflow/text3: glazed with rain
water

reading: /home/pi/stackoverflow/text3 from 5455408 to 2129742056
read result: 0
1 of 4
reading: /home/pi/stackoverflow/text4 from 5455760 to 2129743080
read result: 27
/home/pi/stackoverflow/text4: beside the white
chickens.

reading: /home/pi/stackoverflow/text4 from 5455760 to 2129743080
read result: 0

All four files, present and accounted for. Results are the same on Arch (libssh2-1.8) and Raspbian (libssh-1.7).

Kylynn answered 2/5, 2019 at 3:51 Comment(5)
Wait! That looks familiar... Midnight Commander - #3406 (-31: SFTP Protocol Error when transferring file via SFTP LinkPantomimist
This is not an answer. I too, can replicate the behavior, same with blocking. I don't know if the problem lies in the libssh2_sftp_read state machine, or this is expected.Nereidanereids
When you remove the libssh2_session_set_blocking(session, 0) I think you actually remove any pipelining from the protocol, so it's not actually very similar to the problem I'm originally trying to reproduce. Also - you can install debug symbols for libraries on most distributions - on my Fedora you just need to run sudo dnf install libssh2-debuginfo --enablerepo=fedora-debuginfo --enablerepo=updates-debuginfo.Fischer
Yep, @Fischer you're right -- removing the line just makes it block, so that's no solution.Kylynn
@TrevorKeller 1+ for your efforts .Diamagnetic

© 2022 - 2024 — McMap. All rights reserved.