-
Type:
Bug
-
Resolution: Fixed
-
Priority:
Critical - P2
-
Affects Version/s: None
-
Component/s: Reactive Streams
Summary
Cancelling an encrypted operation in the reactive driver can crash the application process.
The driver holds a native libmongocrypt context for the duration of a client-side encryption operation. On
cancellation — a client disconnect or an operation timeout — Crypt frees that context from doFinally,
on the cancelling thread, with no check that another thread is still inside a state-machine step. That step
then writes into freed native memory. Result: hard JVM crash, or silent corruption in the component holding
encryption keys.
Confirmed, not theoretical: a Spring Boot WebFlux service on the reactive driver was killed by one aborted
HTTP request, crashing in mongocrypt_ctx_provide_kms_providers. Reproduced 2/2.
Scope
Affected:
- driver-reactive-streams — Crypt closes the context from doFinally while callbacks may be running.
- mongodb-crypt — MongoCryptContextImpl.close() frees with no synchronisation, and sets its closed
flag only after the free. - driver-kotlin-coroutine and driver-scala, transitively.
- Apps using on-demand KMS credentials: an empty provider document plus a kmsProviderPropertySupplier.
Includes local, not just cloud providers.
Not affected:
- driver-sync — single-threaded loop, closes on the same thread.
- Apps supplying KMS credentials explicitly, for the widest window only. Narrower windows remain.
Wider than the SECBUG-4142 states: it names one method, but all seven siblings share the defect, as do the
collinfo / markings / keys / finish callbacks in Crypt. A fix scoped to
provideKmsProviderCredentials closes the report without closing the bug.
Requirements
- No native call may run against a freed context. Calls after close must throw IllegalStateException.
- Cover every method on MongoCryptContextImpl, not just the one named.
- close() idempotent and safe to call concurrently.
- Regression test that cancels mid-operation and fails against current main.
- Any lock covers the native call only — must not block the canceller for a network round trip.
- No public API change; no behaviour change on the non-cancelled path.
- Human review required (mongodb-crypt is security-critical per AGENTS.md).
- The one-line isTrue("open", !closed) guard must not be accepted as the resolution — see Disputed.
Disputed
- Exposure frequency — libmongocrypt only requests credentials on a key-cache miss, so the window shuts
once the cache is warm and reopens on expiry (~60s, keyExpirationMS). Measured: 25 concurrent aborted
requests, no crash, 2 credential fetches. Roughly one vulnerable request per key per TTL, not one per request. - Root cause as stated — framed as a missing guard, implying a one-line fix. It is check-then-act against a
volatile boolean, and close() frees before publishing the flag. A PoC with the guard applied still
reproduces 20/20. - Cloud-only framing — local reaches the same state. Only aws is covered by our tests, which is
likely why this was missed.
Resolution Ideas
- Guard every native call and close() with one lock via Locks.withLock(). Smallest correct fix; the
canceller blocks only for a native call, since the credential fetch happens in Crypt beforehand. Preferred. - Set closed = true before mongocrypt_ctx_destroy. Needed regardless, not sufficient alone.
- Reference-count the context — last in-flight caller destroys. Avoids blocking the canceller, more state.
- Move lifetime ownership into Crypt: defer close until any in-flight step completes. Fixes it at the
causing layer; larger change. - Add the missing guard for consistency with its siblings — alongside a real fix, never as the fix.
- Follow-up ticket: the blocking HttpURLConnection credential fetch does not belong on a reactive path.
Separate defect; would shrink this window as a side effect.