You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Likely an LMDB / lmdbjava-level issue, not BAF code — see lmdbjava/lmdbjava#253, which reports the same native crash signature (SIGSEGV in mdb_txn_renew0+0x13e) under heavy multithreaded read access (env.txnRead() / Dbi.get()). There the reporter tried several synchronization variants without fixing it and suspects a thread-safety/JNI issue inside lmdbjava itself; the upstream issue is still open with no resolution.
Added an opt-in concurrency stress harness to help reproduce/validate this: src/test/java/net/ladenthin/bitcoinaddressfinder/persistence/lmdb/LmdbConcurrentReadStressTest.java.
What it does: spins N threads doing concurrent read-only containsAddress() lookups against a small test LMDB — each thread with its own direct key buffer and its own env.txnRead() per lookup (the same pattern ConsumerJava uses) — to provoke the sporadic native SIGSEGV in mdb_txn_renew0.
It is off by default (a normal mvn test skips it). A real reproduction is a native crash that kills the JVM and is probabilistic, so a passing run proves nothing — only a crash is meaningful; that's why it's gated and not in the default suite.
Run it:
mvn test -Dtest=LmdbConcurrentReadStressTest \
-Dnet.ladenthin.bitcoinaddressfinder.lmdbStressTest=true \
-Dnet.ladenthin.bitcoinaddressfinder.lmdbStressTest.threads=64 \
-Dnet.ladenthin.bitcoinaddressfinder.lmdbStressTest.seconds=60
(threads / seconds tunable; also honours -Dnet.ladenthin.bitcoinaddressfinder.disableLMDBTest.)
Intended use: run it repeatedly before and after the env-flag change suggested upstream in lmdbjava/lmdbjava#253 — i.e. open the read env with EnvFlags.MDB_NOTLS and set maxReaders >= thread count (and reconsider MDB_NOLOCK) — to gauge whether that removes the crash. The read path already uses one short-lived txnRead() per call per thread (not a shared Txn); the current read env uses MDB_RDONLY_ENV | MDB_NOLOCKwithoutMDB_NOTLS, which is the prime suspect for the mdb_txn_renew0 reader-slot crash.
Sanity check in CI-like env: enabled, 8 threads / 3s → ~1.5M reads, 0 errors, no crash (expected for a short low-thread run). Keeping this issue open to track the upstream fix + the flag change.
The sporadic mdb_txn_renew0 SIGSEGV is a close-during-read use-after-unmap, not the reader-slot / pthread-TLS race we long suspected.
What actually happens
ConsumerJava.interrupt() proceeds to persistence.close()even when awaitTermination times out with a consumer thread still live:
awaitTermination(awaitQueueEmptySeconds) // times out -> only a WARN
...
localPersistence.close() // LMDBPersistence.close() -> Env.close() unmaps the mmap
If a straggler consumer is inside containsAddress → mdb_txn_begin at that instant, Env.close() unmaps the mmap out from under it and mdb_txn_renew0 dereferences freed memory:
Linux: SIGSEGV (SEGV_MAPERR) — address not mapped (matches the attached hs_err: a ConsumerJava thread in mdb_txn_renew0, with a Surefire shutdown thread present → the JVM was tearing down)
Windows: EXCEPTION_ACCESS_VIOLATION (0xC0000005) — same fault
This also explains every earlier clue:
synchronized containsAddress didn't fix it — synchronizing reads against each other does nothing about a close() racing a read.
Disabling JaCoCo / forkCount=0 helped — they only shift shutdown timing, narrowing the window.
Evidence: two scenarios in the reproduction harness
scenario
what it does
result
churn (original hypothesis)
many short-lived reader threads, env kept open the whole run
0/48 on Linux (even with JaCoCo) — never overlaps a close with a live read
closerace (root cause)
close the env while reads are in flight
5/5 crashes on Windows — deterministic use-after-unmap
The churn negative result is what redirected the diagnosis from "steady-state read race" to "shutdown lifecycle".
In LMDBPersistence — the resource owner, where the missing coordination lived:
a ReentrantReadWriteLock + volatile closed;
containsAddress / getAmount take the read lock across their whole native transaction;
close() takes the write lock, so it waits for every in-flight native read to finish and blocks new ones before unmapping, and is now idempotent;
a lookup that arrives after close observes closed under the read lock and returns absent instead of touching the torn-down mmap.
Scoped to the two point-lookups (the only ops that race with close on the scan path); the long full-DB iterations run during population/export, are never concurrent with a close from another thread, and must not hold the read lock for a whole-database scan.
After the fix the closerace harness reports 0/5 (was 5/5). Locked in by a new default-running regression test LMDBPersistenceCloseDuringReadTest; LMDBPersistenceTest (49) and ConsumerJavaTest (32) unaffected.
Activity
bernardladenthin commentedon Apr 8, 2025
Disabling JaCoCo and setting forkCount to 0 in the matrix CI and assembly builds should avoid the problem. See:
bernardladenthin commentedon Apr 9, 2025
b2c0517
I'm sure the method is not thread safe from a native point of view, synchronized works.
bernardladenthin commentedon Apr 11, 2025
I got an exception here also:
surefire-reports-ubuntu-latest-microsoft-java21.zip
docs: mark #13, #10, #5 answered/closed in issue-status triage
docs: mark #39, #36, #24 closed (assume-fixed) in issue-status triage
docs: remove github-issue-status.md triage worktable
bernardladenthin commentedon Jun 10, 2026
Likely an LMDB / lmdbjava-level issue, not BAF code — see lmdbjava/lmdbjava#253, which reports the same native crash signature (
SIGSEGVinmdb_txn_renew0+0x13e) under heavy multithreaded read access (env.txnRead()/Dbi.get()). There the reporter tried several synchronization variants without fixing it and suspects a thread-safety/JNI issue inside lmdbjava itself; the upstream issue is still open with no resolution.Keeping this open and tracking upstream.
Generated by Claude Code
test: add opt-in LMDB concurrent-read stress harness (for #50 / lmdbj…
bernardladenthin commentedon Jun 10, 2026
Added an opt-in concurrency stress harness to help reproduce/validate this:
src/test/java/net/ladenthin/bitcoinaddressfinder/persistence/lmdb/LmdbConcurrentReadStressTest.java.What it does: spins N threads doing concurrent read-only
containsAddress()lookups against a small test LMDB — each thread with its own direct key buffer and its ownenv.txnRead()per lookup (the same patternConsumerJavauses) — to provoke the sporadic nativeSIGSEGVinmdb_txn_renew0.It is off by default (a normal
mvn testskips it). A real reproduction is a native crash that kills the JVM and is probabilistic, so a passing run proves nothing — only a crash is meaningful; that's why it's gated and not in the default suite.Run it:
(
threads/secondstunable; also honours-Dnet.ladenthin.bitcoinaddressfinder.disableLMDBTest.)Intended use: run it repeatedly before and after the env-flag change suggested upstream in lmdbjava/lmdbjava#253 — i.e. open the read env with
EnvFlags.MDB_NOTLSand setmaxReaders >= thread count(and reconsiderMDB_NOLOCK) — to gauge whether that removes the crash. The read path already uses one short-livedtxnRead()per call per thread (not a sharedTxn); the current read env usesMDB_RDONLY_ENV | MDB_NOLOCKwithoutMDB_NOTLS, which is the prime suspect for themdb_txn_renew0reader-slot crash.Sanity check in CI-like env: enabled, 8 threads / 3s → ~1.5M reads, 0 errors, no crash (expected for a short low-thread run). Keeping this issue open to track the upstream fix + the flag change.
Generated by Claude Code
Merge pull request #313 from bernardladenthin/claude/20260724_issue50…
bernardladenthin commentedon Jul 24, 2026
Root cause found and fixed — PR #314
The sporadic
mdb_txn_renew0SIGSEGV is a close-during-read use-after-unmap, not the reader-slot / pthread-TLS race we long suspected.What actually happens
ConsumerJava.interrupt()proceeds topersistence.close()even whenawaitTerminationtimes out with a consumer thread still live:If a straggler consumer is inside
containsAddress → mdb_txn_beginat that instant,Env.close()unmaps the mmap out from under it andmdb_txn_renew0dereferences freed memory:SIGSEGV (SEGV_MAPERR)— address not mapped (matches the attachedhs_err: aConsumerJavathread inmdb_txn_renew0, with a Surefire shutdown thread present → the JVM was tearing down)EXCEPTION_ACCESS_VIOLATION (0xC0000005)— same faultThis also explains every earlier clue:
synchronized containsAddressdidn't fix it — synchronizing reads against each other does nothing about aclose()racing a read.forkCount=0helped — they only shift shutdown timing, narrowing the window.Evidence: two scenarios in the reproduction harness
churn(original hypothesis)closerace(root cause)The
churnnegative result is what redirected the diagnosis from "steady-state read race" to "shutdown lifecycle".The fix (PR #314)
In
LMDBPersistence— the resource owner, where the missing coordination lived:ReentrantReadWriteLock+volatile closed;containsAddress/getAmounttake the read lock across their whole native transaction;close()takes the write lock, so it waits for every in-flight native read to finish and blocks new ones before unmapping, and is now idempotent;closedunder the read lock and returns absent instead of touching the torn-down mmap.Scoped to the two point-lookups (the only ops that race with close on the scan path); the long full-DB iterations run during population/export, are never concurrent with a close from another thread, and must not hold the read lock for a whole-database scan.
After the fix the
closeraceharness reports 0/5 (was 5/5). Locked in by a new default-running regression testLMDBPersistenceCloseDuringReadTest;LMDBPersistenceTest(49) andConsumerJavaTest(32) unaffected.Merge pull request #314 from bernardladenthin/claude/20260724_issue50…
Remove the issue #50 repro workflow: the bug is fixed and guarded
Stop the crash-log step asserting an abort it cannot observe; superse…
Merge pull request #350 from bernardladenthin/claude/java-llama-cpp-b…