Make Env.closed volatile and document the close() lifecycle contract - #288
Open
bernardladenthin wants to merge 1 commit into
Open
Make Env.closed volatile and document the close() lifecycle contract#288bernardladenthin wants to merge 1 commit into
bernardladenthin wants to merge 1 commit into
Conversation
Two no-cost hardening changes for the close-during-read hazard that surfaces as a native SIGSEGV in mdb_txn_renew0 (see lmdbjava#253): C) Env.closed is now volatile. close() may run on a different thread than the readers calling checkNotClosed(); as a plain field there was no happens-before between the write and those reads, so a reader could indefinitely observe a stale false (the JIT may even hoist the check out of a hot loop) and proceed into a native call on a freed env. volatile does not make close() atomic w.r.t. an in-flight txnRead()/txnWrite() -- the check-then-mdb_txn_begin window remains -- but it removes the pure visibility bug and turns more of those races into a clean AlreadyClosedException instead of a JVM crash. Cost is a single volatile read on paths that already read the field. D) Env.close() now documents the lifecycle contract that LMDB's C API imposes but lmdbjava never surfaced: all txns/cursors/dbis must be closed and no other thread may be touching the env (or a handle derived from it) during close(), and violating this is undefined behaviour that crashes the JVM (SIGSEGV / EXCEPTION_ACCESS_VIOLATION 0xC0000005), not a Java exception. txnRead()/ txnWrite() gain a cross-reference and an @throws AlreadyClosedException. No behavioural change to the default hot path; consistent with the package policy that LmdbJava provides no concurrency guarantees. A follow-up branch proposes an opt-in safe close (drain in-flight readers) on top of this. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01GdiZ3ABYsVHXBRNCkEizpE
This was referenced Jul 24, 2026
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.
Summary
Two no-cost, no-behaviour-change hardening changes for the close-during-read hazard that surfaces as a native
SIGSEGVinmdb_txn_renew0under concurrent access. Motivated by #253 (whose crash stack ismdb_txn_renew0 ← mdb_txn_begin ← containsAddress), which I traced to closing anEnvwhile a reader is still insidetxnRead()/mdb_txn_beginon another thread.Neither change adds locking to the hot path, consistent with the package policy that "LmdbJava DO NOT provide any concurrency guarantees". They make the failure less likely and clearly documented without changing the zero-overhead contract. (A separate, opt-in follow-up PR proposes an actual drain-on-close guard for callers who want it — deliberately kept out of this PR.)
C —
Env.closedis nowvolatileclose()may run on a different thread than the readers callingcheckNotClosed(). As a plain field there was no happens-before between the write and those reads, so a reader could indefinitely observe a stalefalse(the JIT may even hoist the check out of a hot loop) and proceed into a native call on a freed env.volatiledoes not makeclose()atomic w.r.t. an in-flighttxnRead()/txnWrite()— the check-then-mdb_txn_beginwindow remains — but it removes the pure visibility bug and turns more of those races into a cleanAlreadyClosedExceptioninstead of a JVM crash. Cost is a single volatile read on paths that already read the field.D — document the lifecycle contract
Env.close()previously documented only "silently return if already closed". It now spells out the contract LMDB's C API imposes but lmdbjava never surfaced: all txns/cursors/dbis must be closed and no other thread may be touching the env (or a derived handle) duringclose(), and violating this is undefined behaviour that crashes the JVM (SIGSEGV/EXCEPTION_ACCESS_VIOLATION 0xC0000005), not a Java exception.txnRead()/txnWrite()gain a cross-reference and@throws AlreadyClosedException.Test plan
EnvTest38/38 (covers idempotentclose()and the existingAlreadyClosedExceptionpaths).com.spotify.fmt:fmt-maven-plugin:checkclean (0/96 non-complying).Refs #253.