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

fix(http2): check content-length, fix sending RST #53160

Open
wants to merge 37 commits into
base: main
Choose a base branch
from

Conversation

szmarczak
Copy link
Member

@szmarczak szmarczak commented May 26, 2024

Fixes #35209
Closes #35378

Time spent: 68h

Caution

These bug fixes are potentially semver-major!

  1. Fixed stream.close(NGHTTP2_CANCEL) closing with 0 aka NGHTTP2_NO_ERROR.
  2. serverStream.destroy() closed with 0, now NGHTTP2_INTERNAL_ERROR.
  3. clientStream.destroy() closed with 0, now NGHTTP2_CANCEL.
  4. Mismatched content-length now throws NGHTTP2_PROTOCOL_ERROR as according to the spec.
  5. Fixed GOAWAY (server -> client) being greeted with GOAWAY (client -> server) as it's against the spec.
  6. Fixed streams being always closed with NGHTTP2_CANCEL on socket close, now respects goawayCode and destroyCode.
    For client, the default remains NGHTTP2_CANCEL.
    For server, the default now is NGHTTP2_INTERNAL_ERROR.
  7. Fixed stream.rstCode being 0 while active - docs say it should be undefined.
  8. Fixed preferring sessionCode over rstCode when destroying a stream with an error.
  9. Fixed streams being closed with NO_ERROR if session received GOAWAY with NO_ERROR and remote closed connection.
  10. Fixed streams being closed with NO_ERROR if session sent GOAWAY with NO_ERROR and destroyed.
  11. Fixed GOAWAY preventing RST_STREAM from being sent before GOAWAY.
    nghttp2 correctly assumes that it should prevent RST_STREAM from being sent but incorretly applies it to all frames in a packet instead of frames defined after GOAWAY. This is not the only thing that nghttp2 does wrong:
  12. Fixed benchmark/http2/headers.js calling client.destroy() (resulting in dropped requests).
    Now calls client.close() which closes gracefully.
  13. connectStreamSocket.destroy() now closes with NGHTTP2_CONNECT_ERROR.
  14. Fixed double GOAWAY on session.close().
  15. Fixed queued RST_STREAM and GOAWAY being dropped if Http2Session::Close() and previous writes weren't finished yet.
  16. Fixed stream frame errors closing the entire session instead of just the stream.
  17. Fixed stream frame errors sending RST_STREAM on idle streams (to reproduce 16. needs to be fixed).

Sorry so many bugs are fixed in a single PR but it's impossible to fix one without fixing them all!

Best if nghttp2/nghttp2#1508 got merged before this, but it has been open for years 😢
Hence the patch for nghttp2_session.c


/cc @jasnell

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/http
  • @nodejs/http2
  • @nodejs/net
  • @nodejs/security-wg

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels May 26, 2024
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.

Wow. congrats! It would really be better if the patch was landed, yes.

@mcollina
Copy link
Member

The linter and commit validator are failing, could you check?

@szmarczak

This comment was marked as resolved.

@mcollina mcollina added request-ci Add this label to start a Jenkins CI on a PR. baking-for-lts PRs that need to wait before landing in a LTS release. dont-land-on-v18.x PRs that should not land on the v18.x-staging branch and should not be released in v18.x. labels May 29, 2024
@mcollina mcollina requested a review from jasnell May 29, 2024 12:45
@mcollina
Copy link
Member

@jasnell could you reach out to the nghttp2 maintainers and ask about the status of that PR? I would sincerely hope we wouldn't have to maintain a floating patch.

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.

Therefore the best change - although breaking - is to make it emit an error by default.

The reason why .destroy() does not emit an error by default is that it's a Stream API. .destroy() without error is supposed to be "safe" to call.

@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label May 29, 2024
@nodejs-github-bot
Copy link
Collaborator

@szmarczak

This comment was marked as resolved.

@szmarczak

This comment was marked as outdated.

@mcollina
Copy link
Member

Have you tried pinging nghttp2?

@szmarczak

This comment was marked as resolved.

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

@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label May 30, 2024
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label May 30, 2024
@nodejs-github-bot
Copy link
Collaborator

@szmarczak

This comment was marked as resolved.

@szmarczak

This comment was marked as off-topic.

@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label May 31, 2024
@szmarczak

This comment was marked as resolved.

@szmarczak

This comment was marked as resolved.

@szmarczak
Copy link
Member Author

I just found a race after enabling RST_STREAM and GOAWAY in the same packet. When the server sends both RST_STREAM and a terminating GOAWAY in a single frame, the client might send its GOAWAY just after parsing RST_STREAM because it no longer needs the session, but before acknowledging that there's still a GOAWAY to parse.

Currently the client sends the GOAWAY, but the server doesn't read it, resulting in stall by the client side as it waits for the server to acknowledge the client's GOAWAY packet, which will never happen.

I am yet to figure out a solution to this problem.

Copy link

codecov bot commented Oct 4, 2024

Codecov Report

Attention: Patch coverage is 89.38053% with 12 lines in your changes missing coverage. Please review.

Project coverage is 88.40%. Comparing base (103b843) to head (d15855f).
Report is 40 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/http2/core.js 90.00% 9 Missing and 1 partial ⚠️
src/node_http2.cc 84.61% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #53160      +/-   ##
==========================================
+ Coverage   88.23%   88.40%   +0.17%     
==========================================
  Files         651      652       +1     
  Lines      183863   186639    +2776     
  Branches    35824    36075     +251     
==========================================
+ Hits       162235   165002    +2767     
+ Misses      14932    14909      -23     
- Partials     6696     6728      +32     
Files with missing lines Coverage Δ
src/node_http2.cc 84.43% <84.61%> (-0.72%) ⬇️
lib/internal/http2/core.js 95.36% <90.00%> (-0.18%) ⬇️

... and 73 files with indirect coverage changes

@szmarczak
Copy link
Member Author

szmarczak commented Oct 4, 2024

If the above is true then I believe it should send TCP RST when it sends GOAWAY and receives a GOAWAY afterwards to prevent stalling.

@szmarczak
Copy link
Member Author

szmarczak commented Oct 4, 2024

Hmm... it seems that once the session is closed nghttp2 won't resume parsing the GOAWAY, so we won't know that there was a GOAWAY from the server. In that case there are two options:

  1. patch nghttp2 to continue parsing only GOAWAYs and send TCP RST only if there was a GOAWAY from the server - I'm -1 on this since there may be erroneous frames in the queue,
  2. always send an RST after sending a terminating GOAWAY - I'm +1 on this one.

The downside with RST is that it won't be retransmitted if it isn't delivered. With FIN instead it would be delivered, but the connection would be stalled as the client's GOAWAY would be never ACKed. So the first could be better...

@szmarczak
Copy link
Member Author

Also the drawback with the first one is that the GOAWAY does not have to be necessarily in the same packet where the client decides to end the connection. So, to properly do this it would need to still parse frames and ignore all except GOAWAY.

I'll go with always sending an RST after sending a terminating GOAWAY.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
baking-for-lts PRs that need to wait before landing in a LTS release. c++ Issues and PRs that require attention from people who are familiar with C++. dont-land-on-v18.x PRs that should not land on the v18.x-staging branch and should not be released in v18.x. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

http2: Client can return partial response (instead of error emit)
3 participants