Fix: in the WebSocket Client add-on, fix the base64 encoding - #396
Conversation
|
Thank you for the report @Yves57 , and especially for including the Go reference and the digest you tested with — that made this quick to check properly. Unfortunately, I cannot reproduce the behaviour the fix is based on, and I think the actual bug is somewhere else. I extracted Your own vector confirms it. With Reading the implementation confirms why. Only steps 1 and 2 of the encode loop look at So Where the random failure probably comes fromThis is the part worth pursuing, and I would rather redirect the PR than close it.
So as soon as the 101 response arrives as a packet chain, Note that If you can capture a failing handshake, the thing to check is whether While looking at that function I also noticed a separate latent bug in the two scan loops at Process noteThe PR is based on Latent bug found nearby (separate from this PR)You could bundle this fix in a refreshed PR or create a new PR to address it. Anchor: Both header scan loops have an inverted compound condition:
at With well-formed CRLF line endings the two forms happen to agree, which is why this has not shown up. But against a server or proxy that emits bare LF line endings, the |
fdesbiens
left a comment
There was a problem hiding this comment.
The misconception behind thid PR is not yours — it is written down in the tree, at
test/regression/mqtt_test/netx_mqtt_websocket_block_test.c:462-463:/* Set the last extra byte of the digest to be zero, since the function _nx_utility_base64_encode will use this byte for calculation */ digest[20] = 0;That claim is wrong for the same reasons as above, and since it is the thing that sent you down this path it should be removed so it stops misleading people.
_nx_utility_base64_encode()would also benefit from an explicit note in its header block stating that it reads exactlyname_sizebytes.Would you like to include that cleanup in this PR once the rest is reverted? It would turn the PR into a net improvement rather than a revert.
| /* Set the last extra byte of the digest to be zero, since the function _nx_utility_base64_encode will use this byte for calculation */ | ||
| digest[NX_WEBSOCKET_ACCEPT_DIGEST_SIZE] = 0; | ||
|
|
There was a problem hiding this comment.
This store has no effect on the result.
_nx_utility_base64_encode()never readsname[name_size].In the encode loop,
name[i + 1]is only read inside step 1 (nx_utility.c:397) and step 2 (:414), and each is guarded byif ((i + 1) < input_name_size)with a zero-padded fallback in theelse. Steps 0 (:390) and 3 (:431) read onlyname[i].For the 20-byte digest here,
name_sizeis adjusted to 27 output characters with 1 pad, and the loop terminates withi == 20after step 2 takes itselsebranch — so index 20 is never dereferenced.I verified this rather than relying on the reading. The function was extracted verbatim into a harness and given a 20-byte exact-size heap allocation under AddressSanitizer, using the digest from your PR description:
status = 0 bytes_copied = 28 nx output = esgBQXYr0So6/Oj99PJCQ5SHuWk= go expected = esgBQXYr0So6/Oj99PJCQ5SHuWk= match = YESNo ASan report, and the output already matches your Go reference without this store. Encoding the same input with
name[20]set to0xFFinstead of0gives byte-identical output, so the byte genuinely does not participate.Please drop this store and the
+ 1on thedigestdeclaration.
| /* Set the last extra byte of the buffer to be zero, since the function _nx_utility_base64_encode will use this extra-byte for calculation */ | ||
| client_ptr -> nx_websocket_client_guid[NX_WEBSOCKET_CLIENT_GUID_SIZE] = 0; | ||
|
|
There was a problem hiding this comment.
Same as the comment on
:1110. The GUID is 16 bytes, which is a multiple of neither 3 nor 4 but still does not cause an extra read: the encode loop ends withi == 16after step 1 takes its guardedelsebranch, soguid[16]is never read.I ran every length from 1 to 40 through the harness against an independent encoder, each on an exact-size heap buffer under ASan. Zero over-reads, zero output mismatches, and zero cases where the output changed when the byte at
name[name_size]was flipped from0x00to0xFF. Size 16 is included in that sweep.So this store is dead and can be removed along with the
+ 1in the header.
|
|
||
| /* Globally Unique Identifier. */ | ||
| UCHAR nx_websocket_client_guid[NX_WEBSOCKET_CLIENT_GUID_SIZE]; | ||
| UCHAR nx_websocket_client_guid[NX_WEBSOCKET_CLIENT_GUID_SIZE + 1]; |
There was a problem hiding this comment.
Since the extra byte is never read, this enlarges
NX_WEBSOCKET_CLIENT_STRUCTfor nothing. In practice it will cost four bytes per client instance rather than one, because of the alignment of the pointer field that follows at:179.
NX_WEBSOCKET_CLIENT_STRUCTis a public type that applications allocate, so its size is part of the ABI — worth avoiding a change to it unless it buys something. Given ThreadX's footprint goals I would rather not pay this.
| UCHAR *field_value; | ||
| UINT field_value_length; | ||
| UCHAR digest[NX_WEBSOCKET_ACCEPT_DIGEST_SIZE]; | ||
| UCHAR digest[NX_WEBSOCKET_ACCEPT_DIGEST_SIZE + 1]; |
There was a problem hiding this comment.
Consistent with the store at
:1111, so it should be reverted together with it.NX_WEBSOCKET_ACCEPT_DIGEST_SIZEis exactly the SHA-1 output length and_nx_sha1_digest_calculate()writes exactly that many bytes, so 20 is the correct size.To be clear, the PR is internally consistent — it enlarges the array and writes to the new slot, so there is no out-of-bounds access here. The point is only that neither is needed.
|
I don't reproduce the problem anymore. The proposition was not necessary. Closing the PR. |
When the number of bytes to encode is not a multiple of 3, the implementation of
_nx_utility_base64_encode()uses an extra byte in the input buffernamethat must equal to 0 (but this extra-byte must not be counted inname_size).You can see a comment about that in this source code for example.
The bug causes the WebSocket handshake to fail randomly.
The fix has been double-checked with the following Go code as Base64 encoding reference: