Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

websocket: fix ByteParser.run and parseCloseBody #3080

Closed

Conversation

Uzlopak
Copy link
Contributor

@Uzlopak Uzlopak commented Apr 10, 2024

ParseCloseBody can return null. In that case this.#info.closeInfo would be null and the following line would result in a crash.

if (this.#info.closeInfo.code) {

Also reduces the amount of Buffers created.

@Uzlopak Uzlopak requested a review from KhafraDev April 10, 2024 03:04
@codecov-commenter
Copy link

codecov-commenter commented Apr 10, 2024

Codecov Report

Attention: Patch coverage is 87.50000% with 2 lines in your changes are missing coverage. Please review.

Project coverage is 94.19%. Comparing base (48af032) to head (3def0c3).
Report is 76 commits behind head on main.

Files Patch % Lines
lib/web/websocket/receiver.js 87.50% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3080      +/-   ##
==========================================
+ Coverage   94.00%   94.19%   +0.18%     
==========================================
  Files          89       90       +1     
  Lines       24314    24436     +122     
==========================================
+ Hits        22856    23017     +161     
+ Misses       1458     1419      -39     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@KhafraDev
Copy link
Member

tests are rightfully failing, the PR assumes that having no closing code is invalid when it's optional

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@Uzlopak Uzlopak force-pushed the improve-websocket-receiver-parseCloseBody branch from 52df42c to 2df1bb3 Compare April 10, 2024 12:55
@Uzlopak
Copy link
Contributor Author

Uzlopak commented Apr 10, 2024

@KhafraDev
PTAL

@Uzlopak
Copy link
Contributor Author

Uzlopak commented Apr 10, 2024

I think there is a bug in the original logic.
parseCloseBody is returning in two places null. This will actually result in code breaking.

@Uzlopak Uzlopak marked this pull request as draft April 10, 2024 18:09
@Uzlopak Uzlopak marked this pull request as ready for review April 10, 2024 19:51
@Uzlopak
Copy link
Contributor Author

Uzlopak commented Apr 10, 2024

@KhafraDev

What do you think now about this?

Copy link
Member

@KhafraDev KhafraDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some major issues with this PR

return null
}
} else {
return null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A close frame doesn't need to contain a reason or code, data (the body) can be empty here. You could assert the data length isn't 1 here, but we do check for its length elsewhere so it'd just be for safety.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is:
https://app.codecov.io/gh/nodejs/undici/pull/3080/blob/lib/web/websocket/receiver.js

This line is covered by test. So it is possible that the size of the data is 0.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, I said that. The buffer here is not the entire frame (which has to have a minimum length of 2), but the body of a frame. If there is no code and no reason, it is still a valid close frame, and will be empty here. Returning null means that the frame body is invalid, but that's not correct.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, the body can be empty or 2+ bytes, but it cannot be 1 byte, which is why I mentioned earlier that for safety we could assert that the body isn't a single byte. We already check elsewhere, but as I mentioned earlier, would be for safety.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well but it is happening:

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what that has to do with what I said?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means, that your claim, that we already handled all cases with data.length < 2 is invalid. Either data.length === 0 or data.length === 1 or both are not covered by the calling function.

lib/web/websocket/receiver.js Outdated Show resolved Hide resolved
@Uzlopak Uzlopak force-pushed the improve-websocket-receiver-parseCloseBody branch from 53cbe14 to e5a9114 Compare April 15, 2024 04:52
}

let reason = ''
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a misunderstanding here. Returning null means that the close frame's body is invalid. If we can't parse the reason, due to invalid utf-8, we need to return null here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be a false conclusion. We got a valid code, so we wont need to set the code 1005 or 1006. The message is somewhat corrupt. So. We need to prefer the valid code and discard the message.

@KhafraDev
Copy link
Member

I'm going to go ahead and close this. The main change is creating a singular less Buffer in a very cold case. As lpinca said in my original PR implementing websocket (paraphrasing): "the performance of the opening handshake doesn't matter because it rarely happens". It's the same for closing the connection.

@KhafraDev KhafraDev closed this May 4, 2024
@Uzlopak
Copy link
Contributor Author

Uzlopak commented May 5, 2024

I disagree. At first it was about a performance improvement. Then I found a bug when I implemented this PR, which could lead to a crash.

@Uzlopak Uzlopak reopened this May 5, 2024
@Uzlopak Uzlopak changed the title websocket: improve parseCloseBody websocket: fix parseCloseBody May 5, 2024
@Uzlopak Uzlopak changed the title websocket: fix parseCloseBody websocket: fix ByteParser.run May 5, 2024
@Uzlopak Uzlopak changed the title websocket: fix ByteParser.run websocket: fix ByteParser.run and parseCloseBody May 5, 2024
@KhafraDev
Copy link
Member

There isn't a bug being fixed without a test to cover it. The test that you added passes in main. The logic is mostly flawed as I pointed out.

@Uzlopak
Copy link
Contributor Author

Uzlopak commented May 5, 2024

well, lets see if i can write a rogue websocket server.

@Uzlopak
Copy link
Contributor Author

Uzlopak commented May 5, 2024

@KhafraDev
added unit test ;)

@KhafraDev
Copy link
Member

KhafraDev commented May 5, 2024

This diff makes that test pass. There is quite a lot of extraneous stuff here.

diff --git a/lib/web/websocket/receiver.js b/lib/web/websocket/receiver.js
index 4b35ceb5..b1ef4720 100644
--- a/lib/web/websocket/receiver.js
+++ b/lib/web/websocket/receiver.js
@@ -316,7 +316,7 @@ class ByteParser extends Writable {
     try {
       reason = utf8Decode(reason)
     } catch {
-      return null
+      reason = undefined
     }

     return { code, reason }

@KhafraDev
Copy link
Member

KhafraDev commented May 5, 2024

I'm not sure if ignoring the reason is correct though. Maybe failing the connection (basically, force closing it) is a better idea. In that case, nothing would need to be changed in parseCloseBody.

edit: Yeah, I don't think the test case makes sense. We're receiving an invalid frame, why are we handling it gracefully?

@Uzlopak
Copy link
Contributor Author

Uzlopak commented May 5, 2024

same test against main:
image

@KhafraDev
Copy link
Member

I understand, but the way this PR is fixing this is wrong.

@KhafraDev
Copy link
Member

1007

      1007 indicates that an endpoint is terminating the connection
      because it has received data within a message that was not
      consistent with the type of the message (e.g., non-UTF-8 [[RFC3629]]
      data within a text message).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants