Skip to content

WriteAhead#advanceTxId crashes with TypeError: Cannot read properties of null (reading 'id') when #readTx() returns null — unlike #readAllTx(), it has no guard #345

Description

@darshanp40

Summary

WriteAhead.js#advanceTxId() can crash with TypeError: Cannot read properties of null (reading 'id') when the local WAL-file read fallback (#readTx()) returns null. The identical call is correctly guarded in #readAllTx() but not in #advanceTxId(). The crash happens inside jUnlockrejoin(), gets caught by OPFSWriteAheadVFS's blanket per-method catch, and is reported to the application as SQLITE_IOERR_UNLOCK — which SQLite renders as the generic, undiagnostic string "disk I/O error".

Confirmed present in 2.0.3 (currently pinned) and still present, unchanged, in the latest published 2.0.4.

The bug

// #readAllTx() — correctly guarded
*#readAllTx() {
  while (true) {
    const tx = this.#readTx();
    if (!tx) break;         // null is treated as "nothing more yet"
    yield tx;
  }
}

// #advanceTxId() — same call, no guard
#advanceTxId(options = {}) {
  let didAdvance = false;
  while (this.#mapIdToPendingTx.size) {
    const nextTxId = this.#txId + 1;
    let tx;
    if (this.#mapIdToPendingTx.has(nextTxId)) {
      tx = this.#mapIdToPendingTx.get(nextTxId);
      this.#mapIdToPendingTx.delete(tx.id);
      this.#skipTx(tx);
    } else {
      tx = this.#readTx();   // can return null (see #readTx's own @returns {Transaction?})
    }
    this.#activateTx(tx);    // <-- crashes here if tx is null
    didAdvance = true;
  }
  ...

#activateTx:

#activateTx(tx) {
  this.#mapIdToTx.set(tx.id, tx);   // TypeError: Cannot read properties of null (reading 'id')
  ...

How we believe this triggers

#advanceTxId is entered from #handleMessage's 'tx' branch (a peer connection broadcast a transaction) and from rejoin(). The while (this.#mapIdToPendingTx.size) guard only confirms some pending transaction exists — not that the specific next txId this connection needs is available. When it isn't in #mapIdToPendingTx, the code falls back to reading straight from the WAL file via #readTx().

#readFrame()'s own comments note two of its null-return paths are "not necessarily an error, could be from a restart without truncation" — specifically a salt/generation mismatch against #activeHeader. That's exactly what happens when another connection has called #swapActiveFile() (rotated the active WAL file) and this connection hasn't yet caught up via #followFileChange: it receives a broadcast for a transaction after the swap, tries to read the transaction at the swap boundary with its stale salt, #readFrame sees the mismatch and returns null, and #advanceTxId doesn't handle that — #readAllTx would have.

Observed impact

Once this fires, the connection's state isn't repaired (the pending-tx gap is never resolved), so every subsequent rejoin() (i.e. every transaction end) hits the identical crash. In one captured production session this repeated 46 times in the visible log plus 16,014 more collapsed by the browser console — i.e. it never recovers for the rest of the session, and each occurrence surfaces to the application as an opaque "disk I/O error" with no distinguishing detail (SQLITE_IOERR_UNLOCK, code 2058).

Suggested fix

Mirror #readAllTx()'s handling in #advanceTxId():

} else {
  tx = this.#readTx();
  if (!tx) break;   // or: wait/retry, if a peer catch-up mechanism exists
}
this.#activateTx(tx);

Happy to open a PR if that's the preferred direction — wanted to raise it for input first since I'm not certain whether break (stop advancing, try again later) or an active wait/retry against the salt mismatch is the intended recovery here.

Environment

  • @journeyapps/wa-sqlite 2.0.3 (also confirmed present in 2.0.4)
  • OPFSWriteAheadVFS, enableMultiTabs: true, WAL mode
  • Chrome on macOS, multi-tab usage

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions