Repository navigation
Detect a coroutine re-entering a kotlinx Mutex it already holds (#1007) - #1222
Merged
Merged
Conversation
kotlinx.coroutines.sync.Mutex is not reentrant. A coroutine that calls lock() or withLock() on a mutex it already owns suspends forever: no exception, no stack trace, just a test that hangs until its timeout. Nothing in the library saw it, because the Mutex is a library object whose calls never pass through code the agent wove. KotlinMutexWeaver replaces the Mutex lock, tryLock, unlock and withLock call sites in woven classes with static hooks in KotlinMutexes, which record the holder and report KotlinMutexReentrancyDetector's finding when the same holder asks again. Each hook calls ControlledSchedule.point() first, like runBlockingEntered, because every call the agent replaces must also be a point where a controlled schedule can switch threads (#1023). Still open, and documented in docs/JVM_LANGUAGES.md: a holder that had to wait or that took the mutex with tryLock is never judged, and races across suspensions stay with #1007. Verified: library fast tier 3,931 tests and agent 117 tests green; the Kotlin fixture against real kotlinx and the consumer fixture (287 tests, new Phase26KotlinMutexFixtureTest) green; example 152 passes under Maven and Gradle and shows the real hang under a timeout plus the fixed twin passing. Static gate chain (install -DskipTests) and tools/guardrail-drift.sh re-run on this exact tree, both green. Class diagram regenerated: the only new nodes are KotlinMutexes and KotlinMutexReentrancyDetector. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
) agent_kotlinMutex_lockedAgainByItsHolder locks a fresh kotlinx Mutex twice from one coroutine; agent_kotlinMutex_unlockedBetweenLocks unlocks in between. Both call the real kotlinx 1.11 Mutex from the test body, so they sit in BODY_CALL_SITE_ROWS and LibraryReach records why no corpus library reaches the detector. The detector is held at PROMPT on its model (PairEvidence.HELD_ON_MODEL and a "# held:" line in verdict-evidence-corpus): an unlock outside includes= leaves a stale holder that can draw a false reentry (#1221). Doc counts move with the corpus: 149 of 149 paired, 51 in the agent-pair lane, ten corpus libraries.
…ntrancy class-diagram.svg conflicted (generated): regenerated with tools/generate-architecture-diagrams.sh, which keeps main's AgentHandOffHooks (#1218) and adds this branch's KotlinMutexReentrancyDetector and KotlinMutexes.
7 of 8 tasks
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.
Part of #1007 (the Mutex reentrancy third of it; runBlocking shipped earlier, races across suspensions stay open there).
Why
kotlinx.coroutines.sync.Mutexis not reentrant. A coroutine that callslock()orwithLock()on a mutex it already holds suspends forever: no exception, no stack trace, a test that hangs until its timeout. The library had no view of it, because a Mutex call never passed through a woven site.What
KotlinMutexWeaver(agent) replaces Mutexlock,tryLock,unlockandwithLockcall sites, and their$defaultbridges, in woven classes with static hooks.KotlinMutexes(lib) reads the caller'sJobfrom the continuation, records the holder, and hands the finding to the newKotlinMutexReentrancyDetector(detector 149) when the holder, or a scope it is suspended in, waits on the mutex again.ControlledSchedule.point()first, likerunBlockingEntered, because every call the agent replaces must also be a scheduling point (controlledSchedule: woven atomic and collection hooks are not scheduling points #1023).DetectorType,AsyncTestConfig,DetectorRegistry,DetectorFeeds,DetectorTrust,AsyncTestContext.Known limits, filed
unlockin an unwoven class leaves a stale holder, and a later waitinglockfrom that coroutine is reported.includes=clears it.tryLockorselect, is not known, so a reentry under it is missed.Verified
KotlinMutexReentrancyIsReportedTest,KotlinMutexWithoutReentryIsNotReportedTest: both directions, invariant 10) and consumer fixture 287 tests incl. newPhase26KotlinMutexFixtureTest: green.examples/152-kotlin-mutex-reentrancy: 3 tests green under Maven and Gradle; shows the real hang under a timeout, the recorded finding, and the fixed twin passing.mvn install -DskipTests -Djacoco.skip=true) andtools/guardrail-drift.shre-run on this exact tree: green.tools/generate-architecture-diagrams.sh); the only new nodes areKotlinMutexesandKotlinMutexReentrancyDetector.Expected red
Locked Files Guard fails:
DetectorTypeis@AILockedand gains a constant, with its config flag, registry field, factory row andifIssuecall in the same commit (invariant 3). Needslock-overridefrom whoever merges (invariant 14).Docs
docs/JVM_LANGUAGES.md: the three coroutine rules and what is still open.docs/agent/attaching.md: how the Mutex weaving works.docs/CHANGELOG.md:### Addedentry.DetectorCatalogCoverageTestreads; agent-fed 48 to 49 (docs/DETECTOR_CATALOG.md,docs/analysis/corpus-eval.md); README "code you did not modify" 52 to 53; examples 150 to 151 in eight places (two workflow comments, two skill files). The CI example sample stays 38 (every 4th of 151).Guardrails considered
AsyncTestAgent's@AICorenote now describesKotlinMutexWeaver's stack shape, so the frames-stay-valid argument covers the new weaver.KotlinMutexReentrancyDetectorcarries@AIThreadSafe(what makes its per-mutex state safe: weak-keyed map, volatile weak holder, description written before count) and@AITestDriven, as every detector does.KotlinMutexesand the hooks hold no PII, licence state or security boundary, so no safety annotation applies.🤖 Generated with Claude Code