Skip to content

JVM Crash in LMDB Native Code via lmdbjava #50

Closed

Description

@bernardladenthin

https://github.com/bernardladenthin/BitcoinAddressFinder/actions/runs/14339219251/job/40194071370

Issue:
A segmentation fault (SIGSEGV) occurs during test execution when using lmdbjava, crashing the JVM in native code (mdb_txn_renew0).

Native lib: lmdbjava-native-library

SIGSEGV at mdb_txn_renew0+0x13e

I assume the issue is sporadic and may be related to the Maven Surefire plugin's forked JVMs.
jacoco/jacoco#917

hs_err_pid2053.log
job-logs.txt
2025-04-08T15-58-46_111-jvmRun1.txt

Activity

  1. bernardladenthin commented on Apr 8, 2025

    @bernardladenthin
    OwnerAuthor

    Disabling JaCoCo and setting forkCount to 0 in the matrix CI and assembly builds should avoid the problem. See:

  2. bernardladenthin commented on Apr 9, 2025

    @bernardladenthin
    OwnerAuthor

    b2c0517

    I'm sure the method is not thread safe from a native point of view, synchronized works.

    @Override
    public synchronized boolean containsAddress(ByteBuffer hash160) {
        try (Txn<ByteBuffer> txn = env.txnRead()) {
            ByteBuffer byteBuffer = lmdb_h160ToAmount.get(txn, hash160);
            return byteBuffer != null;
        }
    }
    
  3. bernardladenthin commented on Apr 11, 2025

    @bernardladenthin
    OwnerAuthor

    I got an exception here also:

    @Override
    public boolean containsAddress(ByteBuffer hash160) {
        Txn<ByteBuffer> txn;
        synchronized (env) {
            txn = env.txnRead();
            try (txn) {
                ByteBuffer byteBuffer = lmdb_h160ToAmount.get(txn, hash160);
                return byteBuffer != null;
            }
        }
    }

    surefire-reports-ubuntu-latest-microsoft-java21.zip

  4. added 3 commits that reference this issue on Jun 10, 2026
  5. bernardladenthin commented on Jun 10, 2026

    @bernardladenthin
    OwnerAuthor

    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.

    Keeping this open and tracking upstream.


    Generated by Claude Code

  6. bernardladenthin commented on Jun 10, 2026

    @bernardladenthin
    OwnerAuthor

    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_NOLOCK without MDB_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.


    Generated by Claude Code

  7. added a commit that references this issue on Jul 24, 2026
  8. bernardladenthin commented on Jul 24, 2026

    @bernardladenthin
    OwnerAuthor

    Root cause found and fixed — PR #314

    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".

    The fix (PR #314)

    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.

  9. added a commit that references this issue on Jul 24, 2026
  10. added 3 commits that reference this issue on Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions