vfs: fix Windows WAL corruption by mapping the wal-index into wasm memory#405
Open
franchb wants to merge 2 commits into
Open
vfs: fix Windows WAL corruption by mapping the wal-index into wasm memory#405franchb wants to merge 2 commits into
franchb wants to merge 2 commits into
Conversation
franchb
added a commit
to franchb/go-sqlite3
that referenced
this pull request
Jul 6, 2026
Owner
|
I've made this a bit harder to merge, because I committed my non fixes (because they cleaned up some deprecations, mostly). Sorry about that. If it's not too much trouble to fix the conflict, as you drop the old Windows fallback, I'd appreciate it. Otherwise, if it's too much of a bother, I can always merge this to a branch and take over. |
…mory Fixes ncruces#404. On Windows the WAL-index was kept in a per-connection private copy and synchronized with the shared -shm file only at xShmLock boundaries. But wal.c reads and writes the WAL-index *between* lock calls and relies on seeing other connections' updates live: dirty-header re-reads, aReadMark probes when a checkpointer's exclusive slot probe loses to a live reader, and post-lock header re-checks. The copy-on-lock-boundary scheme served stale views on those paths, so under concurrent WAL writes a checkpointer could backfill stale page versions into the database and truncate frames that were never backfilled, corrupting the file (and producing SQLITE_PROTOCOL error storms). On Windows 10 1803+ / Server 2019+, map the -shm regions directly into the wasm linear memory using address-space placeholders (VirtualAlloc2 + MapViewOfFile3), the analogue of the MAP_FIXED mapping the unix build already uses. SQLite then works on genuinely shared, always-current memory; xShmBarrier becomes a plain fence and the private/shadow copies and their sync points disappear. The heap is committed at 64K granularity so a wal-index view (64K aligned, 64K) never straddles two allocations, which a placeholder split cannot cross. Below Windows 10 1803 the placeholder APIs are unavailable. Rather than keep a copy-on-lock-boundary fallback that cannot make wal.c's between-lock reads coherent, shmMap refuses shared memory there (_IOERR_SHMMAP): such a build must use WAL with EXCLUSIVE locking mode, like platforms without shared-memory support. This drops the old Windows copy path entirely and the now-unused mmap_windows.go. The copy scheme (shm_copy.go) is retained for the sqlite3_dotlk build, which has no file mapping and genuinely needs it; it keeps the per-file copy serialization and exact per-page comparison that make it correct within a single process.
A repeat-until-budget stress test that drives many concurrent WAL writers through one *sql.DB, aggressively checkpoints (TRUNCATE), then cold-reopens the file and runs PRAGMA integrity_check. It repeats that cycle until it either observes the defect or a time budget elapses. On the pre-fix copy-on-lock-boundary Windows scheme this reliably corrupts the database within the first round (database disk image malformed / file is not a database / SQLITE_PROTOCOL). On the fix — and on every platform whose VFS maps the -shm directly (unix, native) — every round stays clean. The test therefore goes red only on an unfixed Windows build and green everywhere else, so it can be watched to flip red→green. It is opt-in: several minutes of heavy I/O, gated behind SQLITE3_TEST_WAL_STRESS so it never taxes the normal test matrix. The wal-repro workflow (workflow_dispatch) flips the gate and runs it on Windows and Linux on demand. BUSY, IOERR and FULL are tolerated during the storm: heavy concurrency and a full or slow scratch filesystem are environment limits, not the defect, and the defect surfaces only as SQLITE_PROTOCOL / CORRUPT / NOTADB. The cold-reopen integrity_check is the definitive corruption gate regardless. The workload (64 writers, 2000 iters, 8K rows, checkpoint every 25) is sized to open the race window even on the slow temp disk of hosted CI runners; lighter workloads under-expose it and can false-green.
43a7fcd to
8645d97
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #404.
Problem
On Windows (and in the
sqlite3_dotlkbuild), concurrent WAL writes across pooled connections — a single*sql.DBwithSetMaxOpenConns(N>1)and ordinaryBEGIN DEFERREDwriters — corrupt the database, and produceSQLITE_PROTOCOL/database is lockederror storms under load.The root cause is the WAL-index copy scheme (
vfs/shm_copy.go). The WAL-index is kept in a per-connection private copy and synchronized with the shared-shmfile only atxShmLockboundaries. Butwal.creads and writes the WAL-index between lock calls and relies on seeing other connections' updates live:walIndexTryHdr);walTryBeginReadre-checksaReadMark[]and the header after taking a read lock and retries on any change;mxSafeFrameon theaReadMarkvalue it just read (walCheckpoint).Copy-at-lock-boundary is too coarse for those patterns, so a connection acts on a stale view. Under concurrent writers this lets a checkpointer backfill stale page versions into the database and truncate frames that were never backfilled — corrupting the file. I confirmed this with word-level instrumentation of the private/shared/shadow copies and a content trace of the data plane (the checkpointer demonstrably writes older committed page versions to the main DB).
This is Windows-only among the VFS shapes (the
unixbuild maps-shminto wasm memory directly; native C shares one mapping), and it reproduces on every released version back to when the Windows shared memory was introduced.Fix
Stop copying. On Windows 10 1803+ / Server 2019+, map the
-shmregions directly into the wasm linear memory using address-space placeholders (VirtualAlloc2(MEM_RESERVE_PLACEHOLDERS)+MapViewOfFile3(MEM_REPLACE_PLACEHOLDER)) — the analogue of theMAP_FIXEDmapping theunixbuild already uses. SQLite then works on genuinely shared, always-current memory;xShmBarrierbecomes a plain fence, and the private/shadow copies and their synchronization points disappear. This removes the entire staleness class by construction — both the corruption and theSQLITE_PROTOCOLstorms.The wasm heap is committed at 64K (allocation-granularity) so a 64K-aligned WAL-index view never straddles two allocations, which a placeholder split cannot cross.
On older Windows the copy scheme remains as a fallback, now with process-wide copy serialization and exact per-page comparison (instead of the header-equality fast path, which is unsound: SQLite changes hash-table words while leaving the 136-byte header byte-identical, e.g.
walCleanupHashafter a rollback). This makes the fallback less bad, though it cannot be made fully correct — see the caveat below.What changed
internal/sqlite3_wrap/placeholder_windows.go(new): thin bindings forVirtualAlloc2/MapViewOfFile3/UnmapViewOfFile2and a capability probe.internal/sqlite3_wrap/mem_windows.go: reserve the wasm memory with placeholders;MapFileRegion/UnmapFileRegionmap file views into carved holes.vfs/shm_windows.go: dual path — direct-map on 1803+, copy scheme as fallback.vfs/shm_copy.go,vfs/shm_dotlk.go: copy-scheme hardening for the fallback path.vfs/tests/wal_stress_test.go(new): a heavy concurrent-WAL-writers regression test (short-mode skipped; excluded fromsqlite3_dotlk).Validation
sqlite3.exe(stock 3.53.2) reading the same WAL database concurrently —PRAGMA integrity_check= ok,foreign_key_checkclean, 128000 rows consistent, no read errors. Lock offsets and the on-disk format are unchanged, so native tools stay compatible.-raceis clean with the fix.Caveats / open questions
Memoryplaceholder logic is factored, or the capability gate) to your taste.