Skip to content

Flaky: 'Unit tests: lmdb' SIGSEGV (exit 139) in subscriptionReplay edge cases, around '!omitCurrent: empty primaryStore delivers nothing initially' — Node 22, 24 and 26 #3349

Open

Description

@Ethan-Arrowood

Summary

The Unit tests: lmdb step (test:unit:lmdb, in its first stage, HARPER_STORAGE_ENGINE=lmdb npm run test:unit:resources) intermittently dies with Segmentation fault (core dumped), exit 139, inside unitTests/resources/subscriptionReplay.test.js › Subscription replay › edge cases. It always lands in the same three-test window, on all three Node lines, across seven different PR branches (dependency bumps, a restart test, fulltext, auth), which points at something on main rather than any one branch. Six runs in the last two days hit it (seven jobs), plus one on 2026-10-14. The re-run of the most recent one passed.

Date Run / job Node PR (head) Last test that passed Last ✔ to SIGSEGV
2026-10-14 run 38645062397, job 116129758444 v26 #3201 (d20cf8a14) startTime: startTime = 0 with no records 9.8 s
2026-10-19 run 39262381337, job 118093112483 v26 #3162 (098efac98) startTime: startTime = 0 with no records 9.8 s
2026-10-19 run 39265280289, job 118103121781 v24 #2901 (7c253b96a) !omitCurrent: empty primaryStore delivers nothing initially 11.1 s
2026-10-19 run 39280957232, job 118194344436 (attempt 3) v26 #3121 (70bc7fbb1) startTime: startTime = 0 with no records 10.2 s
2026-10-19 run 39331415523, job 118330261285 v24 #3301 (a7359c48e) !omitCurrent: deleted keys are not delivered as current state 11.5 s
2026-10-20 run 39367103563, job 118446326036 v24 #3313 (88874034b) !omitCurrent: empty primaryStore delivers nothing initially 11.9 s
2026-10-20 run 39367103563, job 118446326002 v22 #3313 (88874034b) !omitCurrent: deleted keys are not delivered as current state 8.8 s
2026-10-20 run 39462572144, job 118765545982 (attempt 1) v22 #3336 (89150ccea) !omitCurrent: empty primaryStore delivers nothing initially 9.0 s; attempt 2 passed

From the last one (timestamps trimmed):

17:19:47.647       ✔ startTime: startTime = 0 with no records (empty audit log) (144ms)
17:19:47.682 The one-to-many/many-to-many relationship property "badRelationship" in table "RelatedTable" must have an array type referencing a table as the elements
17:19:47.797       ✔ !omitCurrent: empty primaryStore delivers nothing initially (149ms)
17:19:47.834 The one-to-many/many-to-many relationship property "badRelationship" in table "RelatedTable" must have an array type referencing a table as the elements
17:19:56.752 Segmentation fault (core dumped)
##[error]Process completed with exit code 139.

What the timing says

The badRelationship warning is printed each time a test in this file calls table() in the test database, which also holds query.test.js's RelatedTable. In all eight sightings it appears 35–91 ms after the last ✔, so the next test had started and created its table. Then nothing for 9–12 s, then the crash. So the fault is inside one of three consecutive tests: !omitCurrent: empty primaryStore delivers nothing initially, !omitCurrent: deleted keys are not delivered as current state, or non-collection: subscribe to non-existent record delivers nothing initially.

Each of those creates a fresh audited table (the second also puts two records and deletes one), subscribes, collects for 100 ms (collect()'s deadline is 5 s), and calls subscription.return(), which is synchronous. Each normally passes in 145–200 ms, and .mocharc.json has timeout: 0. Nothing in the JS flow accounts for 9–12 s of silence, so the main thread was either blocked in native code or idle while another thread (the lmdb-js writer, a finalizer, a worker) faulted. The gap is a fairly steady ~10 s across Node lines. No native stack is captured: the step runs plain mocha without --report-on-fatalerror, and the runner keeps no core.

Every sighting is in the LMDB step. I didn't check whether the RocksDB run of the same file (Unit tests: resources (core)) ever crashes here.

Not reproduced locally

macOS arm64, Node v24.19.0, lmdb 3.5.7, 89150ccea: 5 runs of subscriptionReplay.test.js alone with HARPER_STORAGE_ENGINE=lmdb (43 passing each), and one full HARPER_STORAGE_ENGINE=lmdb npm run test:unit:resources (3324 passing, 738 pending). No crash.

How I found the sightings

The latest attempt of every failed Unit Test workflow run from 2026-10-07 to 2026-10-20 (253 runs; 118 jobs failed an lmdb step), checked for Segmentation fault. Earlier attempts weren't scanned, except the two named above. The only other lmdb-step segfaults in that window are two at #2669's copy-db point, both on v5.3.

Suggested next steps

  1. Capture a native stack: --report-on-fatalerror, or a SIGSEGV handler, on the Unit tests: lmdb step. Flaky: Unit Test (Node.js v24) SIGSEGV (exit 139) in schema-change / index-rebuild test group — v24-only #1380 and Flaky: 'Unit tests: lmdb' SIGSEGV (exit 139) right after copy-db integrity 'refuses a copy target that already exists' — Node 22 and 24 #2669 ask for the same, so one change covers all three.
  2. Loop subscriptionReplay.test.js under LMDB on a Linux runner, alone and after the resources files that precede it, to see whether it needs the earlier suites' state.
  3. If the stack matches Flaky: 'Unit tests: lmdb' SIGSEGV (exit 139) right after copy-db integrity 'refuses a copy target that already exists' — Node 22 and 24 #2669's, fold one into the other.

Related: #2669 (lmdb-step SIGSEGV at copy-db), #1380 (Node 24 SIGSEGV in the schema-change / index-rebuild group).

sent with Claude Opus 5.5

Activity

  1. added
    bugSomething isn't working
    flaky-testTest known to fail intermittently, unrelated to the PR's own diff
    on Oct 20, 2026
  2. kylebernhardy commented on Oct 31, 2026

    @kylebernhardy
    Member

    Observed another Node 26 occurrence in Adopt fulltext 0.8 with host query policy and fuzzy controls, at 8bf60a4: failed LMDB job.

    • The repaired encoder worker test passed in 995 ms. The later failure was a native segmentation fault, exit 139; no Mocha assertion or epilogue reported.
    • At 01:13:56.224 UTC, native LMDB printed:
      cursor_close: mc->mc_txn or mc->mc_txn->mt_env is NULL, ensure that cursors are closed before the transaction is closed
      
    • The deleted-key and nonexistent-record subscription cases then passed. The last pass was at 01:13:56.710; the process crashed at 01:14:08.627, approximately 12 seconds later. The next test was non-collection: record with single version (no version chain) delivers just current.
    • Verified at the failed head: the subscription replay fixture and transaction broadcaster are unchanged against both the PR base and current main. This occurrence extends the previously recorded edge-case window by one test.

    The location and timing resemble the existing reports. The warning adds evidence of an invalid cursor/transaction lifetime, but neither it nor matching timing establishes that every occurrence has the same native fault.

    A separate fixture ownership defect is verified in the cleanup patch: 35 calls used subscription.return?.(), but the subscription queue exposes end()/close(); return() belongs to its iterator. collect() attaches data listeners, so it does not implicitly close an iterator. A real-table check asserting closed/unregistered state failed before and passed after correcting cleanup. The fixture now tracks subscriptions from all 43 creation sites and ends them after each test, including assertion-failure paths. The actual iterator-return case remains, with stronger lifecycle/no-late-delivery assertions.

    Cleanup verification: full LMDB fixture passed 43 cases each on Node 24/26; RocksDB passed 34 with 9 existing skips. Final focused iterator/idle checks also passed. These checks prove fixture cleanup, not the native SIGSEGV cause or resolution. This issue remains open.

    Next diagnostic: capture an all-thread native backtrace at SIGSEGV while preserving the existing LMDB workload and assertions, for example by running the affected Mocha process under a native debugger. A Node fatal-error report alone is insufficient evidence that an operating-system SIGSEGV will be captured. Keep raw cores private; do not add speculative cursor fixes or treat a passing rerun as resolution. No CI job was rerun.

    Comment generated by kAIle (GPT-6)

  3. self-assigned this
    on Nov 2, 2026
  4. kriszyp commented on Nov 3, 2026

    @kriszyp
    Member

    Root cause found. The defect is in lmdb-js 3.5.9, the newest release; a fix is ready but not yet pushed.

    Native stack. Print a native backtrace when a unit-test step segfaults in CI keeps cores in the unit-test job. Its own lmdb leg then crashed in outOfOrderWriteExpiry.test.js and printed the stack (log):

    Thread 1 (main), SIGSEGV
    #0 mdb_cursor_close (mc=...) at mdb.c:10432      MDB_CURSOR_UNREF reads mc->mc_txn->mt_env->me_flags
    #1 CursorWrap::close (...) at src/cursor.cpp:60   JS cursor.close()
    

    The faulting load is the me_flags read through a freed MDB_txn. When the freed memory happens to be zero, the same call prints cursor_close: mc->mc_txn or mc->mc_txn->mt_env is NULL instead, the warning seen on 10-31.

    The 9–14 s silence before each crash came after the fault, not before it. It is the time spent writing the core: about 1 GB, piped to apport on the runner. With cores written to disk, the gap is 4 s.

    Violated invariant. finishCursor (lmdb-js read.js) parks a finished read cursor in db.availableCursor, bound to the current read txn. A later iterator renews it only when it first positions. Two paths free that txn while the cursor is still parked:

    • resetReadTxn orphans it, and Txn.done or releaseToRenewingCursors then aborts it.
    • _commitReadTxn, which runs on every openDB (every Harper table creation), commits it.

    An iterator that takes the parked cursor and is closed before it positions then calls mdb_cursor_close on freed memory. releaseToRenewingCursors does exactly that to a snapshot: false iterator suspended across a tick.

    Deterministic reproduction with plain lmdb 3.5.9, under valgrind:

    Invalid read of size 8
       at mdb_cursor_close (mdb.c:10432)
       by CursorWrap::close (cursor.cpp:60)
     Address … is 72 bytes inside a block of size 870 free'd
       by mdb_txn_end (mdb.c:4092)
    

    Fix (lmdb-js, local branch, not pushed): keep a parked cursor only while its txn is current. Close the parked cursors before the read txn stops being current (both resetReadTxn orphan branches and _commitReadTxn) and before close() aborts it.

    Check Base 3.5.9 Fixed
    valgrind on the reproduction 2 invalid reads 0 errors
    3 new lmdb-js tests asserting no cursor stays parked on a finished txn fail pass
    lmdb-js test/index.test.js — 375 pass; 2 cross-env deadlock tests fail the same way on base
    Harper HARPER_STORAGE_ENGINE=lmdb full unitTests/resources — 4872 pass, 0 fail

    Next: an lmdb-js PR and release, then a Harper bump of lmdb.

    Comment by KrAIs (Claude Opus 5.5)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingflaky-testTest known to fail intermittently, unrelated to the PR's own diff

Type

Fields

Priority

P2

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions