Skip to content

Websockets: a bug in curl_ws_recv #10438

Closed
@mikeduglas

Description

@mikeduglas

Description

Current curl_ws_recv implementation (ws.c) works properly if a message (frame) size is less than 16K and buffer size is equal or greater than a message size, so curl_ws_recv reads entire message in one call. In other cases it returns incomplete data or hangs.

To make it working, wsp->frame.bytesleft needs to be initialized not with oleft ("bytes yet to come (for this frame)"), but with datalen + oleft ("total payload size"):

    if(!wsp->frame.bytesleft) {
      size_t headlen;
      curl_off_t oleft;
      /* detect new frame */
      result = ws_decode(data, (unsigned char *)wsp->stillb, wsp->stillblen,
                         &headlen, &datalen, &oleft, &recvflags);
      if(result == CURLE_AGAIN)
        /* a packet fragment only */
        break;
      else if(result)
        return result;
      wsp->stillb += headlen;
      wsp->stillblen -= headlen;
      wsp->frame.offset = 0;
      // wsp->frame.bytesleft = oleft;  <-- this is wrong, next line is a fix
      wsp->frame.bytesleft = datalen + oleft; /* total payload size */
      wsp->frame.flags = recvflags;
    }

I tested modified code, it works as expected.

I expected the following

curl/libcurl version

curl 7.87.1-DEV (i386-pc-win32) libcurl/7.87.1-DEV Schannel WinIDN

operating system

Windows 11

Activity

dfandrich

dfandrich commented on Feb 7, 2023

@dfandrich
Contributor

That would explain my confused comment in #10347. That failing assert may have been pointing to this.

bagder

bagder commented on Feb 8, 2023

@bagder
Member

I don't think that is correct. The frame struct contains metadata about this frame, not about pending data in general as we provide a pointer to that struct with curl_ws_meta(). frame.bytesleft should therefor not have a larger value than number of bytes left that belongs to this frame.

dfandrich

dfandrich commented on Feb 8, 2023

@dfandrich
Contributor

Clearly, I haven't puzzled my way through the websockets code sufficiently yet...

bagder

bagder commented on Feb 8, 2023

@bagder
Member

BTW, this really shows we should improve the test suite for WebSocket. It's too "bare bones" right now.

mikeduglas

mikeduglas commented on Feb 8, 2023

@mikeduglas
ContributorAuthor

@bagder What is a frame?
Suppose the server sent 20K message as a single frame, with FIN bit set. libcurl receives first 16K, processes it, then receives final 4K, processes it. But both 16K and 4K fragments are the same frame from my point of view. So it is naturally that bytesLeft has the value of 20K (not 4K which it is in current code).

bagder

bagder commented on Feb 8, 2023

@bagder
Member

I mean frame as in a WebSocket frame as defined in RFC 6455.

both 16K and 4K fragments are the same frame from my point of view

If they are indeed a single 20K WebSocket frame, then the two data pieces are indeed just two parts of the same frame. The libcurl API can provide the frame to the application in two parts though.

it is naturally that bytesLeft has the value of 20K (not 4K which it is in current code).

Bytesleft should then be (20K - size of the current fragment), yes. Number of bytes left of this frame after this piece has been delivered. If this is 16K and the entire frame is 20K, then there is 4K left.

mikeduglas

mikeduglas commented on Feb 8, 2023

@mikeduglas
ContributorAuthor

Suppose I pass 1K buffer to curl_ws_recv. If Bytesleft is initally 4K, then I will get 4K data in 4 curl_ws_recv calls, bytesLeft will decreased to 0 (4-1-1-1-1) and game over. If I call curl_ws_recv 5th time for some reason then new 4K fragment will be read, accidently decoded and bytesLeft will have random value.

bagder

bagder commented on Feb 8, 2023

@bagder
Member

Sorry, I don't follow. I understand that there is an issue here, I just think that the fix needs to be different than what you proposed above.

If I call curl_ws_recv 5th time for some reason then new 4K fragment will be read, accidently decoded and bytesLeft will have random value.

Why? When curl_ws_recv is called with bytesleft == 0, then does it not reach this code block?

curl/lib/ws.c

Lines 431 to 446 in ead2b2d

if(!wsp->frame.bytesleft) {
size_t headlen;
curl_off_t oleft;
/* detect new frame */
result = ws_decode(data, (unsigned char *)wsp->stillb, wsp->stillblen,
&headlen, &datalen, &oleft, &recvflags);
if(result == CURLE_AGAIN)
/* a packet fragment only */
break;
else if(result)
return result;
wsp->stillb += headlen;
wsp->stillblen -= headlen;
wsp->frame.offset = 0;
wsp->frame.bytesleft = oleft;
wsp->frame.flags = recvflags;

mikeduglas

mikeduglas commented on Feb 8, 2023

@mikeduglas
ContributorAuthor

Why? When curl_ws_recv is called with bytesleft == 0, then does it not reach this code block?

Oops sorry you're right, in the case of bytesleft == 0 ws_decode block is not called, datalen is assigned to 0 and we get an endless loop.

I just think that the fix needs to be different than what you proposed above.

I believe that my bad English does not allow me to convince you. :-)

mikeduglas

mikeduglas commented on Feb 8, 2023

@mikeduglas
ContributorAuthor

then the two data pieces are indeed just two parts of the same frame.

Hence wsp->frame.bytesleft should contain bytes left in the frame to be delivered to a caller. Initially (right after ws_decode call) it is 20K (full frame size), 1K of these 20K are memcopied to the 1K user's buffer, at the same time bytesleft is reduced by 1K, so the user gets back 1K in *nread and 19K in meta.bytesleft after 1st curl_ws_recv call. Next call gets a user 1K in *nread and 18K in bytesleft and so on.

mikeduglas

mikeduglas commented on Feb 8, 2023

@mikeduglas
ContributorAuthor

badger: Why? When curl_ws_recv is called with bytesleft == 0, then does it not reach this code block?

mikeduglas: Oops sorry you're right

Actually you aren't right, the condition is "!wsp->frame.bytesleft" so when bytesleft == 0 then the condition is true and the block is reached. But it makes little difference, in any case when bytesleft is initially set to 4K, the code fails.

By the way, in struct websocket (ws.h) there is a member oleft:

  curl_off_t oleft; /* outstanding number of payload bytes left from the
                       server */

which is never used. In my example that 4K should be assigned to this wsp->oleft.

bagder

bagder commented on Feb 8, 2023

@bagder
Member

The fix is bigger than what has been discussed here. I am working on it.

self-assigned this
on Feb 8, 2023

8 remaining items

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

    Development

    Participants

    @bagder@dfandrich@mikeduglas

    Issue actions

      Websockets: a bug in curl_ws_recv · Issue #10438 · curl/curl