Skip to content

Detect a coroutine re-entering a kotlinx Mutex it already holds (#1007) - #1222

Merged
PIsberg merged 4 commits into
mainfrom
feat/kotlin-mutex-reentrancy
Nov 8, 2026
Merged

PIsberg merged 4 commits into
mainfrom
feat/kotlin-mutex-reentrancy

Conversation

@PIsberg

@PIsberg PIsberg commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

Part of #1007 (the Mutex reentrancy third of it; runBlocking shipped earlier, races across suspensions stay open there).

Why

kotlinx.coroutines.sync.Mutex is not reentrant. A coroutine that calls lock() or withLock() 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 Mutex lock, tryLock, unlock and withLock call sites, and their $default bridges, in woven classes with static hooks.
  • KotlinMutexes (lib) reads the caller's Job from the continuation, records the holder, and hands the finding to the new KotlinMutexReentrancyDetector (detector 149) when the holder, or a scope it is suspended in, waits on the mutex again.
  • Each of the six hooks calls ControlledSchedule.point() first, like runBlockingEntered, because every call the agent replaces must also be a scheduling point (controlledSchedule: woven atomic and collection hooks are not scheduling points #1023).
  • Wiring: DetectorType, AsyncTestConfig, DetectorRegistry, DetectorFeeds, DetectorTrust, AsyncTestContext.

Known limits, filed

Verified

  • Library fast tier 3,931 tests, agent 117 tests: green.
  • Kotlin fixture against real kotlinx (KotlinMutexReentrancyIsReportedTest, KotlinMutexWithoutReentryIsNotReportedTest: both directions, invariant 10) and consumer fixture 287 tests incl. new Phase26KotlinMutexFixtureTest: 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.
  • Static gate chain (mvn install -DskipTests -Djacoco.skip=true) and tools/guardrail-drift.sh re-run on this exact tree: green.
  • Class diagram regenerated (tools/generate-architecture-diagrams.sh); the only new nodes are KotlinMutexes and KotlinMutexReentrancyDetector.

Expected red

Locked Files Guard fails: DetectorType is @AILocked and gains a constant, with its config flag, registry field, factory row and ifIssue call in the same commit (invariant 3). Needs lock-override from 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: ### Added entry.
  • Counts: detectors 148 to 149 everywhere DetectorCatalogCoverageTest reads; 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).
  • Generated guardrail files regenerated from the new annotations.

Guardrails considered

  • AsyncTestAgent's @AICore note now describes KotlinMutexWeaver's stack shape, so the frames-stay-valid argument covers the new weaver.
  • KotlinMutexReentrancyDetector carries @AIThreadSafe (what makes its per-mutex state safe: weak-keyed map, volatile weak holder, description written before count) and @AITestDriven, as every detector does.
  • Nothing else: KotlinMutexes and the hooks hold no PII, licence state or security boundary, so no safety annotation applies.

🤖 Generated with Claude Code

PIsberg and others added 2 commits November 8, 2026 07:38
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

codecov Bot commented Nov 8, 2026

Copy link
Copy Markdown

)

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.
@PIsberg
PIsberg merged commit 31b00f4 into main Nov 8, 2026
105 of 114 checks passed
@PIsberg
PIsberg deleted the feat/kotlin-mutex-reentrancy branch November 8, 2026 11:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant