What
OauthPlan::access_token (crates/registry-evidence/src/source.rs) holds the
per-source token-cache mutex across the token round trip, and builds the token
request while holding it. Building that request does two things that block a
Tokio worker thread:
- a synchronous filesystem read of the client secret or the client assertion
private JWK, through SecretResolver;
- for the
private_key_jwt form, synchronous ECDSA P-384 or RSA-2048 signing of
the client assertion.
Neither runs under spawn_blocking.
Why it is not simply a bug
Holding the lock across the refresh is the intended single-flight behaviour: it
is what stops N concurrent evidence requests for one source from each firing
their own token request at the authorization server. That part should stay.
The cost is that the blocking work sits inside that critical section, so it is
serialized behind the same lock and lands on a runtime worker rather than the
blocking pool. Every concurrent caller for that source waits through it, bounded
only by the configured source timeout via admission_timeout. Adding assertion
signing made the section measurably more expensive than when it only read a
secret file.
Impact
Availability and tail latency under concurrent refresh for a single source. Not
a correctness, disclosure, or authentication problem: the token is still
correct, the credential still never leaves the process, and a failure is still
fail-closed. Filing publicly for that reason, rather than through SECURITY.md,
whose scope is authentication bypass, credential disclosure, audit redaction or
integrity failure, signing-key handling, connector data leakage, and privacy
regressions.
Sketch of a fix
Move the secret read and the signature onto the blocking pool, and prepare the
request before taking the lock so the critical section covers only the round trip
and the cache write. Either change alone helps; the second is the one that
restructures the cache, which is why this is its own change rather than part of
the assertion work.
Notes
Pre-existing in shape. The secret read was already there; assertion signing was
added alongside the private_key_jwt client authentication form and made the
section heavier. Deliberately left out of scope of that change.
What
OauthPlan::access_token(crates/registry-evidence/src/source.rs) holds theper-source token-cache mutex across the token round trip, and builds the token
request while holding it. Building that request does two things that block a
Tokio worker thread:
private JWK, through
SecretResolver;private_key_jwtform, synchronous ECDSA P-384 or RSA-2048 signing ofthe client assertion.
Neither runs under
spawn_blocking.Why it is not simply a bug
Holding the lock across the refresh is the intended single-flight behaviour: it
is what stops N concurrent evidence requests for one source from each firing
their own token request at the authorization server. That part should stay.
The cost is that the blocking work sits inside that critical section, so it is
serialized behind the same lock and lands on a runtime worker rather than the
blocking pool. Every concurrent caller for that source waits through it, bounded
only by the configured source timeout via
admission_timeout. Adding assertionsigning made the section measurably more expensive than when it only read a
secret file.
Impact
Availability and tail latency under concurrent refresh for a single source. Not
a correctness, disclosure, or authentication problem: the token is still
correct, the credential still never leaves the process, and a failure is still
fail-closed. Filing publicly for that reason, rather than through
SECURITY.md,whose scope is authentication bypass, credential disclosure, audit redaction or
integrity failure, signing-key handling, connector data leakage, and privacy
regressions.
Sketch of a fix
Move the secret read and the signature onto the blocking pool, and prepare the
request before taking the lock so the critical section covers only the round trip
and the cache write. Either change alone helps; the second is the one that
restructures the cache, which is why this is its own change rather than part of
the assertion work.
Notes
Pre-existing in shape. The secret read was already there; assertion signing was
added alongside the
private_key_jwtclient authentication form and made thesection heavier. Deliberately left out of scope of that change.