[HIGH] Bound peer-created stream state - #201
Open
OskarEichler wants to merge 3 commits into
Open
Conversation
Comment on lines
+321
to
+328
| priority = @idle_stream_priorities.delete(stream_id) || {} | ||
| if frame[:flags].anybits?(PRIORITY) | ||
| priority = { | ||
| weight: frame[:weight], | ||
| dependency: frame[:dependency], | ||
| exclusive: frame[:exclusive] | ||
| } | ||
| end |
Collaborator
There was a problem hiding this comment.
Suggested change
| priority = @idle_stream_priorities.delete(stream_id) || {} | |
| if frame[:flags].anybits?(PRIORITY) | |
| priority = { | |
| weight: frame[:weight], | |
| dependency: frame[:dependency], | |
| exclusive: frame[:exclusive] | |
| } | |
| end | |
| if frame[:flags].anybits?(PRIORITY) | |
| priority = { | |
| weight: frame[:weight], | |
| dependency: frame[:dependency], | |
| exclusive: frame[:exclusive] | |
| } | |
| else | |
| priority = @idle_stream_priorities.delete(stream_id) | |
| end |
Comment on lines
+331
to
+333
| weight: priority.fetch(:weight, DEFAULT_WEIGHT), | ||
| dependency: priority.fetch(:dependency, 0), | ||
| exclusive: priority.fetch(:exclusive, false) |
Collaborator
There was a problem hiding this comment.
Suggested change
| weight: priority.fetch(:weight, DEFAULT_WEIGHT), | |
| dependency: priority.fetch(:dependency, 0), | |
| exclusive: priority.fetch(:exclusive, false) | |
| weight: priority&[:weight] || DEFAULT_WEIGHT, | |
| dependency: priority&[:dependency] || 0, | |
| exclusive: priority&[:exclusive] || false |
| _verify_pseudo_headers(frame, REQUEST_MANDATORY_HEADERS) | ||
| verify_stream_order(pid) | ||
|
|
||
| promised_stream_count = @streams.each_value.count(&:parent) |
Collaborator
There was a problem hiding this comment.
instead of counting on the fly and in order to avoid a O(n), bookkeep this in an ivar instead.
| end | ||
|
|
||
| def validate_push_promise(stream_id) | ||
| connection_error(:protocol_error, msg: "clients cannot send PUSH_PROMISE") if @local_role == :server |
Collaborator
There was a problem hiding this comment.
this clause is easier to read with if @remote_role == :client
| def validate_push_promise(stream_id) | ||
| connection_error(:protocol_error, msg: "clients cannot send PUSH_PROMISE") if @local_role == :server | ||
| if @local_settings[:settings_enable_push].zero? | ||
| connection_error(:protocol_error, msg: "received PUSH_PROMISE while push is disabled") |
Collaborator
There was a problem hiding this comment.
Suggested change
| connection_error(:protocol_error, msg: "received PUSH_PROMISE while push is disabled") | |
| connection_error(:protocol_error, msg: "push promises are disabled") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Security impact
A peer could send a PRIORITY frame for an idle stream, causing a Stream object to be retained without counting against the concurrent-stream limit. Repeating that with distinct IDs grew the connection registry linearly: 1,000 small frames retained 1,000 streams while active_stream_count stayed at zero.
The retained idle object also changed later HEADERS handling. A request missing :authority was rejected on a fresh stream but accepted when a PRIORITY frame pre-created that stream, because new-stream pseudo-header validation was skipped.
Servers also accepted client-sent PUSH_PROMISE frames even though clients cannot push. A focused reproduction retained 1,001 streams from one active request after 1,000 promises. Clients did not enforce SETTINGS_ENABLE_PUSH=0 or bound reserved promises.
After this change, the priority reproduction retains no live stream objects, limits compatibility IDs to 100, and rejects the missing-authority request. Servers reject client push, disabled clients reject push, and the 101st concurrently retained promise is refused when the configured limit is 100.
Verification
rbenv exec bundle exec rake: 448 examples, 0 failures; 41 RuboCop files, no offensesrbenv exec bundle exec rake buildgit diff --checkAdded focused regression coverage for repeated idle PRIORITY updates and the priority retained when that stream later opens.
Limitations
The repository's HPACK fixture task cannot currently run because its fixtures are absent (tracked separately in issue #200). Live sockets, alternate Ruby engines, and a long-running production server were not exercised.
Breaking changes
Illegal client PUSH_PROMISE frames and pushes received while disabled now close the connection with PROTOCOL_ERROR. Excess valid promises are refused. Untracked PRIORITY frames no longer enter the live stream registry; the existing stream callback remains available for the first bounded set of distinct idle priority IDs.