eduardsui / tlse

Single C file TLS 1.2/1.3 implementation, using tomcrypt as crypto library

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

EAGAIN & EINTR should be expected

vbickov opened this issue · comments

EAGAIN & EINTR ( WSAEWOULDBLOCK & WSAEINTR on windows) errors are conditional and not necessarily mean that something wrong with the socket. Truth is these codes appear with both blocked and non-blocked sockets. So, when you are attempting to send/read directly from socket you should expect these codes and process them accordingly. Otherwise you may terminate a legit connection. The simplest example from method _tls_ssl_private_send_pending updated by me (I need linux/macOS only, so no windows code here and usleep() is not mandatory of course):

int _tls_ssl_private_send_pending(struct TLSContext *context){

....
    
    while ((out_buffer) && (out_buffer_len > 0)) {

        int res;
        
        if(ssl_data->fd < 0) return TLS_GENERIC_ERROR;
        
        if (write_cb){
            res = write_cb(ssl_data->fd, (char *)&out_buffer[out_buffer_index], out_buffer_len, 0);
        }else{
            res = send(ssl_data->fd, (char *)&out_buffer[out_buffer_index], out_buffer_len, 0);
            if(res < 0){
            	  int err = errno();
            	  //NRD: EAGAIN & EINTR are expected.
            	  if (err == EAGAIN || err == EINTR){
            	  	  usleep(1000); continue;
            	  }
            }    
        }
        
        if (res <= 0) {
            send_res = res;
            break;
        }
        
        out_buffer_len -= res;
        out_buffer_index += res;
        send_res += res;
    }

Multipurpose implementation should be more complex of course.

Hello. TLSe wants to be transport layer agnostic. The SSL_* functions are just for a basic level of support/testing. You should really use the tls_* interface, which enables you to write your own write function. See tlsclienthello.c:

int send_pending(int client_sock, struct TLSContext *context) {
    unsigned int out_buffer_len = 0;
    const unsigned char *out_buffer = tls_get_write_buffer(context, &out_buffer_len);
    unsigned int out_buffer_index = 0;
    int send_res = 0;
    while ((out_buffer) && (out_buffer_len > 0)) {
        int res = send(client_sock, (char *)&out_buffer[out_buffer_index], out_buffer_len, 0);
        if (res <= 0) {
            send_res = res;
            break;
        }
        out_buffer_len -= res;
        out_buffer_index += res;
    }
    tls_buffer_clear(context);
    return send_res;
}

In my applications, I'm always using epoll/poll/select to check the write-status of the socket.

This issue is not related to a transport level only. You are clearly using system functions "send" and "recv" in _tls_ssl_private_send_pending (and in your example send_pending), therefore you should expect and process some error codes. Imagine the following situation:

_tls_ssl_private_send_pending (or send_pending) was able to send a part of the buffer, then received EINTR or EAGAIN. In reality - nothing happened. For some reason the system just wants you to retry your attempt. But the method will return error code instead. From user point of view, the only legit reaction is to close the connection, which is wrong.

See more here:

https://unix.stackexchange.com/questions/253349/eintr-is-there-a-rationale-behind-it

I understand the idea, but as I said, EAGAIN and EINTR are transport-layer specific. EAGAIN is set by send(transport layer). Assuming that a the TCP send buffer is full, you may get EAGAIN for a long time. TLSe deals only with the TLS layer. I wanted to be extremely portable and lightweight. The socket layer should be managed by you. As you posted, it is extremely simple to alter send_pending to suit your needs. send_pending isn't part of TLSe, is just an example. You may opt to manage EAGAIN by simply cheching errno or WSAGetLastError ("Error codes set by Windows Sockets are not made available through the errno variable"). I strongly advise you to use tls_* interface instead of SSL_*. When using tls_* you explicitly manage the I/O, see: https://github.com/eduardsui/tlse/blob/master/examples/tlshelloworld.c. In the send_pending function you may check for EAGAIN.

You may replace standard send/recv functions by using SSL_set_io. You may simply rewrite send_cb at your convenience.

I think the correct implementation should look something like this (not tested):

int _tls_ssl_private_send_pending(int client_sock, struct TLSContext *context) {
    unsigned int out_buffer_len = 0;
    const unsigned char *out_buffer = tls_get_write_buffer(context, &out_buffer_len);
    unsigned int out_buffer_index = 0;
    int send_res = 0;
    SOCKET_SEND_CALLBACK write_cb = NULL;
    SSLUserData *ssl_data = (SSLUserData *)context->user_data;
    if (ssl_data)
        write_cb = (SOCKET_SEND_CALLBACK)ssl_data->send;
    while ((out_buffer) && (out_buffer_len > 0)) {
        int res;
        if (write_cb)
            res = write_cb(client_sock, (char *)&out_buffer[out_buffer_index], out_buffer_len, 0);
        else
            res = send(client_sock, (char *)&out_buffer[out_buffer_index], out_buffer_len, 0);
        if (res <= 0) {
            if ((!write_cb) && (res < 0)) {
#ifdef _WIN32
                if (WSAGetLastError() == EAGAIN) {
                    context->tls_buffer_len = out_buffer_len;
                    memmove(context->tls_buffer, out_buffer + out_buffer_index, out_buffer_len);
                    return res;
                }
#else
                if ((errno == EAGAIN) || (errno == EINTR)) {
                    context->tls_buffer_len = out_buffer_len;
                    memmove(context->tls_buffer, out_buffer + out_buffer_index, out_buffer_len);
                    return res;
                }
#endif
            }
            send_res = res;
            break;
        }
        out_buffer_len -= res;
        out_buffer_index += res;
        send_res += res;
    }
    tls_buffer_clear(context);
    return send_res;
}

This will transparently pass errno to the application code to be managed there.

Also, as a side note, using poll to manage I/O provides huge advantages, especially in asynchronous implementations.

If you want to keep external interface portable, you can return your own error code. OpenSSL returns SSL_ERROR_SYSCALL from SSL_get_error in such case.

Trying to compile samples in latest master I get this (under WSL)

wqw@wqw-pc:~/tlse/examples$ gcc tlshelloworld.c -o tlshelloworld -DTLS_AMALGAMATION
In file included from tlshelloworld.c:1:0:
../tlse.c: In function ‘_tls_ssl_private_send_pending’:
../tlse.c:10100:22: error: ‘errno’ undeclared (first use in this function)
                 if ((errno == EAGAIN) || (errno == EINTR)) {
                      ^
../tlse.c:10100:22: note: each undeclared identifier is reported only once for each function it appears in
../tlse.c:10100:31: error: ‘EAGAIN’ undeclared (first use in this function)
                 if ((errno == EAGAIN) || (errno == EINTR)) {
                               ^
../tlse.c:10100:52: error: ‘EINTR’ undeclared (first use in this function)
                 if ((errno == EAGAIN) || (errno == EINTR)) {
                                                    ^
tlshelloworld.c: In function ‘main’:
tlshelloworld.c:192:45: warning: format ‘%x’ expects argument of type ‘unsigned int’, but argument 4 has type ‘struct TLSContext *’ [-Wformat=]
                             fprintf(stderr, "Imported context (size: %i): %x\n", size, imported_context);

I forgot to include errno.h. Thanks.

E.