Skip to content

Fix fetch cursor wedging permanently after a seek/decode error - #20

Merged
mlin merged 4 commits into
mlin:mainfrom
Wid-L-Hacker:conrad/f3-fetch-cursor-wedge
Oct 25, 2026
Merged

mlin merged 4 commits into
mlin:mainfrom
Wid-L-Hacker:conrad/f3-fetch-cursor-wedge

Conversation

@Wid-L-Hacker

Copy link
Copy Markdown
Contributor

A seek or decode error can leave a fetch job's cursor stepped while its bookkeeping says "unpositioned". Later reads assigned to that job then fail internally with SQLITE_MISUSE until the database file is closed and reopened.

What happens today

Execute() catches the exception and sets cursor_pageno = 0, but it does not reset the underlying statement. ResetCursor() also skips its reset when cursor_pageno is already 0. That combination breaks an invariant: a "missing page" seek can leave the cursor stepped onto a neighbouring page while cursor_pageno is still 0. So cursor_pageno == 0 no longer guarantees "unpositioned and safe to bind", and the next attempt to bind that statement fails with SQLITE_MISUSE.

The fix

  • Execute()'s error path now calls ResetCursor() instead of only zeroing cursor_pageno.
  • ResetCursor() resets unconditionally, via tryReset(). sqlite3_reset() resets the statement even when it returns the preceding execution's error code; tryReset() discards that already-handled status, which is what the destructor and the noexcept callers of ResetCursor() need.
  • The sequential-stepping fast path (cursor_pageno + 1 == pageno) does not call ResetCursor(), is unchanged, and gains no resets.

How it was verified

  • Reproduced end-to-end before the fix: the corrupt-page and missing-page repros left a fetch job wedged, causing later reads of untouched tables to fail with internal SQLITE_MISUSE. After the fix, the original reads still fail, but the untouched tables read successfully.
  • The full Catch2 suite passes on the patched tree: 36,338 assertions, identical to the pristine tree.

Points for review

This PR is a draft because the fix touches cursor state handling, and three things deserve a maintainer's eye:

  1. The stable-state claim after a failed job: statement reset, cursor_pageno == 0, and any borrowed page data discarded before the job publishes its result.
  2. The fast path above: it must not gain a reset.
  3. The unconditional reset also covers the missing-page shape, where the cursor is stepped but cursor_pageno was never set. That case is the reason the old > 0 gate had to go.

Related: the ThreadPool races found alongside this (C1/C2 in the audit) live in sqlite_web_vfs and reach this repo through its web pin, not through this PR.

Found in the 2026-10-23 correctness audit of the GenomicSQLite family.

Execute() now resets the cursor on error and ResetCursor() resets unconditionally (via tryReset()); previously cursor_pageno was zeroed without resetting the stepped statement, so the job's next bind failed with SQLITE_MISUSE until reopen.
Second-opinion review cycle on the F3 diff (see PR body): the fix
itself was confirmed, but it shipped with no committed regression
coverage. test/test.cc now carries both wedge shapes (corrupt page;
missing root page with cursor_pageno == 0), mutation-verified three
ways; test_exe links zstd and compiles zstd_vfs.h so the tests drive
the real ZstdVFS. The ResetCursor rationale now lives in one place.
@mlin
mlin marked this pull request as ready for review October 25, 2026 06:49
@mlin
mlin merged commit 02bc923 into mlin:main Oct 25, 2026
5 checks passed
@mlin
mlin deleted the conrad/f3-fetch-cursor-wedge branch October 25, 2026 08:01
Wid-L-Hacker added a commit to Wid-L-Hacker/sqlite_zstd_vfs that referenced this pull request Oct 25, 2026
Brings the web fixes merged since 16072b1 into this build: F5 +
ThreadPool C1/C2 (mlin#17), F6 Close self-delete (mlin#18), F7 Content-Range
validation (mlin#19), C6 last_error_ synchronization (mlin#20).
mlin pushed a commit that referenced this pull request Oct 25, 2026
Brings the web fixes merged since 16072b1 into this build: F5 +
ThreadPool C1/C2 (#17), F6 Close self-delete (#18), F7 Content-Range
validation (#19), C6 last_error_ synchronization (#20).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants