Skip to content

Encode HKDF labels in temporary storage - #199

Open
rnro wants to merge 2 commits into
apple:mainfrom
rnro:alloc/hkdf-label
Open

rnro wants to merge 2 commits into
apple:mainfrom
rnro:alloc/hkdf-label

Conversation

@rnro

@rnro rnro commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Protector derives every packet protection key, IV and header protection key
with an HKDF expansion whose label it encodes first. The encoding went into a
fresh array each time, though it only has to last for the one expansion, and a
connection derives around two dozen of them across its initial, handshake and
application keys. withEncodedLabel builds it in temporary storage instead.

Measured on my Mac against main with the package's QUIC benchmark tools.
Allocation counts come from full malloc stack logging, which records every
allocation:

QUICTransfer -size 1200, per message      24.37 -> 24.34
QUICTransfer, per 500 KB transfer       6,187.6 -> 6,182.4
QUICHandshake, per connection           1,947.4 -> 1,892.0
QUICStreamLoad, per stream                111.8 -> 112.0

The expansions themselves still allocate inside CryptoKit.

Wall-clock time comes from running main, this change and the other changes
measured alongside it in a rotating order for 9 rounds, and comparing each run
with main's in the same round. Changes moved paths they do not touch by up to
about 1.3%, so differences that size count as noise. Nothing changed beyond
noise.

`Protector` derives every packet protection key, IV and header protection key
with an HKDF expansion whose label it encodes first. The encoding went into a
fresh array each time, though it only has to last for the one expansion, and a
connection derives around two dozen of them across its initial, handshake and
application keys. `withEncodedLabel` builds it in temporary storage instead.

Measured on my Mac against `main` with the package's QUIC benchmark tools.
Allocation counts come from full malloc stack logging, which records every
allocation:

  QUICTransfer -size 1200, per message      24.37 -> 24.34
  QUICTransfer, per 500 KB transfer       6,187.6 -> 6,182.4
  QUICHandshake, per connection           1,947.4 -> 1,892.0
  QUICStreamLoad, per stream                111.8 -> 112.0

The expansions themselves still allocate inside CryptoKit.

Wall-clock time comes from running `main`, this change and the other changes
measured alongside it in a rotating order for 9 rounds, and comparing each run
with `main`'s in the same round. Changes moved paths they do not touch by up to
about 1.3%, so differences that size count as noise. Nothing changed beyond
noise.
result[index] = 0

return result
return withUnsafeTemporaryAllocation(byteCount: totalLength, alignment: 1) { result in

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.

Why not use InlineArray?

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.

To clarify: there's a maximum size of the label, so I thought we could use InlineArray.

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.

On the other hand, withUnsafeTemporaryAllocation() may already be allocating on the stack, so 🤷🏼

@rnro rnro Oct 2, 2026 •

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.

withUnsafeTemporaryAllocation does allocate on the stack, but it is 'unsafe'. Let me investigate the InlineArray

@agnosticdev agnosticdev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you!

TLS caps an HKDF label at 255 bytes, so the encoding fits a fixed 259-byte
inline array, and building it no longer writes through an unsafe buffer: every
store is bounds-checked. Only handing the finished label to `HKDF.expand`,
which takes `DataProtocol`, still reads it through a scoped unsafe buffer. The
allocation count is unchanged.
@rnro rnro added the 🔨 semver/patch No public API change. label Oct 2, 2026
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.

3 participants