Skip to content

QUIC: enforce active_connection_id_limit on received NEW_CONNECTION_ID - #216

Open
deadcaf3 wants to merge 5 commits into
apple:mainfrom
deadcaf3:worktree-fix-enforce-connid-limit
Open

deadcaf3 wants to merge 5 commits into
apple:mainfrom
deadcaf3:worktree-fix-enforce-connid-limit

Conversation

@deadcaf3

@deadcaf3 deadcaf3 commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

RFC 9000 section 5.1.1:

After processing a NEW_CONNECTION_ID frame and adding and retiring active connection IDs, if the number of active connection IDs exceeds the value advertised in its active_connection_id_limit transport parameter, an endpoint MUST close the connection with an error of type CONNECTION_ID_LIMIT_ERROR.

processNewConnectionIDFrame logged and dropped a connection ID that went over the limit, and .connectionIDLimitError had no raise site.

Change

  • Close with CONNECTION_ID_LIMIT_ERROR when a NEW_CONNECTION_ID frame would take the number of active remote connection IDs past the limit we advertised, and return false like the other close sites in the function.
  • Ignore a NEW_CONNECTION_ID frame whose sequence number the remote list has held before. A connection ID that we retire on our own (retireOutboundCID(forPathGoingAway:), sendRetireConnectionIDFrame) is removed from the list, so a late retransmission of its frame was taken for a new connection ID: added back when there was room, and counted against the limit when there was not. Section 19.15 says receiving the same frame more than once must not be a connection error.
  • QUICConnectionIDList keeps the sequence numbers it has held in a RangeSet, which is a single range when every frame arrives before a later Retire Prior To passes it. A frame that arrives after that is retired without being held, so it leaves a gap. Gaps are capped at twice the limit, oldest closed first.

Testing

  • testNewConnectionIDOverLimitClosesConnection: reaching the limit is accepted, a repeated frame is ignored, and one connection ID over the limit closes the connection.
  • testRepeatedNewConnectionIDForLocallyRetiredCIDIsIgnored: a repeated frame for a connection ID we retired is not added back, and does not close the connection once the peer has replaced it.
  • testHasHeldAfterRetire and testHasHeldClosesOldestGap cover the list.

swift test passes locally on macOS (1283 test cases, 0 failures). Both connection tests fail without their change.

A NEW_CONNECTION_ID frame that took the number of active connection IDs
past our advertised active_connection_id_limit was logged and dropped.
RFC 9000 section 5.1.1 requires closing the connection with
CONNECTION_ID_LIMIT_ERROR, which had no raise site.

- Close with CONNECTION_ID_LIMIT_ERROR when the limit is exceeded
- Add a test covering at-limit, repeated and over-limit frames
@rpaulo rpaulo added the 🔨 semver/patch No public API change. label Oct 6, 2026
@rpaulo

rpaulo commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for working on this. Your known limitation is indeed correct.

@rpaulo

rpaulo commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

@deadcaf3 would you be able to work on the known limitation? I'm worried that by merging this PR we may start closing connections just due to packet loss or reordering.

A connection ID that we retire ourselves is removed from the remote
list, so a late retransmission of its NEW_CONNECTION_ID frame was taken
for a new connection ID. It was added back when there was room, and
with a full list it closed the connection with
CONNECTION_ID_LIMIT_ERROR. RFC 9000 section 19.15 says receiving the
same frame more than once must not be a connection error.

- Remember the sequence numbers a connection ID list has held
- Ignore a NEW_CONNECTION_ID frame whose sequence number was held before
- Add tests for a repeated frame after local retirement
@deadcaf3

deadcaf3 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

@deadcaf3 would you be able to work on the known limitation? I'm worried that by merging this PR we may start closing connections just due to packet loss or reordering.

fixed in 260ac14, pushed the commit here as a separate PR might leave 216 unsafe to merge on its own. nw we remember the sequence nums we have held, so a late NEW_CONNECTION_ID retransmit for a connid we retired is ignored instead of counting against the limit, and there's a test here too..

Edited and updated the PR body


// Sequence numbers this list has held, remembered after they are retired so that a repeated
// NEW_CONNECTION_ID frame for a retired connection ID is not taken for a new one.
private var heldSequenceNumbers = RangeSet<UInt64>()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a way to avoid the cost of this approach for localCIDs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

agreed, fixed in a87c427

}

private mutating func recordHeld(sequenceNumber: UInt64) {
heldSequenceNumbers.insert(contentsOf: sequenceNumber..<sequenceNumber + 1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We could be more explicit about the fact that they are just 62 bit only (with a comment). and avoid the overflow check:

Suggested change
heldSequenceNumbers.insert(contentsOf: sequenceNumber..<sequenceNumber + 1)
heldSequenceNumbers.insert(contentsOf: sequenceNumber..<sequenceNumber &+ 1)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

nice to make it explicit, avoiding crashes, fixed in a87c427

…e list

Only the list of the peer's connection IDs receives NEW_CONNECTION_ID
frames, so it is the only list that needs to remember the sequence
numbers it has held. The local list now records nothing.

Sequence numbers are variable-length integers, at most 2^62 - 1, so the
increment uses a wrapping add instead of an overflow-checked one.
@rpaulo

rpaulo commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

When heldSequenceNumbers is {0, 2} because seq=1 was lost and when we receive seq=3 retirePriorTo=3, heldSequenceNumbers will be {0, 2, 3}. The the retransmission of seq=1 will not close the gap on heldSequenceNumbers because we return here: frame.sequence < retiredRemoteCIDSequenceNumberThreshold

A NEW_CONNECTION_ID frame whose sequence number is below the Retire
Prior To threshold is retired on arrival and never reaches the list, so
a frame that was lost and later retransmitted left a permanent gap in
the held sequence numbers. Fill in everything below the threshold when
it rises, so a gap can only be a frame above it that is lost or in
flight. The cap on gaps stays as a backstop against a peer that ignores
the limit.
…imit is checked

RFC 9000 section 5.1.1 lets a peer exceed active_connection_id_limit
when the same NEW_CONNECTION_ID frame retires the excess. Cover that
path so the limit check cannot be moved ahead of retirement.
@deadcaf3

deadcaf3 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

When heldSequenceNumbers is {0, 2} because seq=1 was lost and when we receive seq=3 retirePriorTo=3, heldSequenceNumbers will be {0, 2, 3}. The the retransmission of seq=1 will not close the gap on heldSequenceNumbers because we return here: frame.sequence < retiredRemoteCIDSequenceNumberThreshold

Edit: you're correct, and 253257d plugs that gap;. the context of your comment got me looking at the RETIRE_CONNECTION_ID paths, looks like main is correct in that the early return --> frame below Retire Prior To threshold gets the RETIRE_CONNECTION_ID and returns, yet a retransmit of such a frame sends a 2nd RETIRE_CONNECTION_ID which is probably not acc to the rfc, and needs to be skipped, can be a separate PR (although some overlap w #218), so just noting this down here

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

Labels

🔨 semver/patch No public API change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants