android: advance the alias counter before continuing in isKnownRoot - #246
Open
ChoiSeungMyung wants to merge 1 commit into
Open
android: advance the alias counter before continuing in isKnownRoot#246ChoiSeungMyung wants to merge 1 commit into
ChoiSeungMyung wants to merge 1 commit into
Conversation
`isKnownRoot` walks the collision-suffixed aliases of a system trust anchor. `i += 1` was the last statement of the loop body, and two of the three branches reach the next iteration via `continue`, jumping over it. When either is taken the alias is recomputed to the same string, the same file still exists so the loop does not break, and the keystore returns the same value, so the branch is taken again forever. The thread running the handshake never returns and keeps a core busy repeating one `File.exists()` and one `getCertificate()`. The `anchor == null` branch is reachable exactly as its own comment describes. conscrypt's TrustedCertificateStore returns null for a system alias once the certificate has been copied into $ANDROID_DATA/misc/keychain/cacerts-removed, which is what happens when a user disables a preinstalled CA; the file under $ANDROID_ROOT/etc/security/cacerts stays on disk because that partition is read-only. The Chromium code this was ported from keeps the increment in the update clause of a C-style `for`, so `continue` still advances there. Kotlin has no such form and the port moved it into the body, which changed what `continue` does. Incrementing right after the alias is built restores that invariant: progress no longer depends on how many `continue` paths the body has. `i` is not read after the loop, so incrementing before the `break` check is harmless. Fixes rustls#245
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.
Fixes #245.
isKnownRootwalks the collision-suffixed aliases of a system trust anchor.i += 1was the last statement of the loop body, and two of the three branches reach the next iteration throughcontinue, which jumps over it. When either branch is taken:iis unchanged, soaliasis recomputed to the same string,break,getCertificate("system:$alias")returns the same value, so the same branch is taken again.The thread running the handshake never returns and keeps a core busy repeating one
File.exists()and onegetCertificate()call. Theanchor !is X509Certificatebranch additionally logs on every iteration.This is an availability bug, not a security one — no verification decision is weakened, the call simply never completes.
Why the branch is reachable
Exactly as its own comment says. conscrypt's
TrustedCertificateStore.getCertificatereturnsnullfor a system alias onceisDeletedSystemCertificatematches, which happens after the certificate is copied into$ANDROID_DATA/misc/keychain/cacerts-removed— the state produced by disabling a preinstalled CA in Settings. The file under$ANDROID_ROOT/etc/security/cacertsstays on disk because that partition is read-only, soFile.exists()still returns true.The Chromium comment this was ported from spells out the same mechanism: "the certificate remains in the system directory but is also added to another file. Continue iterating as there may be further collisions after the deleted anchor."
isKnownRootis reached fromverifyCertificateChainat theocspResponse == nullrevocation-gating path (API >= 24), so any chain whose root subject hashes to the disabled anchor's alias hits this.The fix
Chromium keeps the increment in the update clause of a C-style
for, socontinuestill advances there. Kotlin has no such form and the port moved it into the body, changing whatcontinuedoes. Incrementing right after the alias is built restores that invariant — progress no longer depends on how manycontinuepaths the body has, which is the property that broke here.iis not read after the loop, so incrementing before thebreakcheck is harmless. I checked the alias sequence against the originalfor (int i = 0; true; i++)semantics over every combination of 0–4 alias files × {absent, null, non-X509, non-matching, matching} anchors (341 cases): visit order and termination match in all of them.val alias = "$hash.${i++}"is an equivalent one-liner if you prefer it; I went with the separate statement to keep the mutation out of the string template.Testing
No test is included.
isKnownRootreadssystemKeystoreandsystemCertificateDirectory, bothprivate vals with no injection point, andKeyStore.getInstance("AndroidCAStore")is unavailable off-device, so reaching this loop from a test would need either a seam in the object or an instrumented device with a disabled system CA. Happy to add one if you'd like it done a particular way.I found this by code review while vendoring the file, not from a field report, so I have no device trace — the reachability argument above is the whole of the evidence.