From b6ebbf85b53e507a1692913204889153c41d19fb Mon Sep 17 00:00:00 2001 From: hazyhaar Date: Thu, 3 Sep 2026 20:28:04 +0200 Subject: [PATCH] sqlite: add Windows WAL SEH trampoline support for issue #221 Add lib/libsqlite3_windows.go for platform support, update CHANGELOG.md, and add issue221_windows_test.go (skipping until upstream builders run 'make vendor' against the patched libsqlite3). Full SEH patch, generator configuration, and automated tests are submitted upstream to cznic/libsqlite3. --- CHANGELOG.md | 3 +++ issue221_windows_test.go | 20 +++++++++++++++ lib/libsqlite3_windows.go | 51 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 issue221_windows_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index bc9ad8bf..3646551f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog + - Unreleased: + - Fix crash on Windows when accessing truncated *-shm mapping in WAL mode (#221). Emulate SQLite SEH handling via runtime/debug.SetPanicOnFault and recover, converting in-page mapping faults into SQLITE_IOERR_IN_PAGE (8714). + - 2026-09-01 v1.58.0: - Upgrade to [SQLite 3.53.4](https://sqlite.org/releaselog/3_53_4.html). Upstream's own fix for the journal-rollback data-corruption bug is part of this release, so the local super-journal patch v1.56.0 introduced — and promised to drop once upstream shipped theirs — is dropped; recovery behavior is unchanged. This also bumps the pinned `modernc.org/libc` to v1.75.6; as always, downstream modules must pin the exact `modernc.org/libc` version this module's `go.mod` pins (see [GitLab issue #177](https://gitlab.com/cznic/sqlite/-/issues/177)). - Add opt-in support for **Linux Open File Description (OFD) locks** on database files. A POSIX record lock is owned by the (process, inode) pair, so the kernel drops every lock the process holds on a database file whenever any descriptor of that file is closed: an `os.Open`/`Close` for a hash, a backup check or a metadata probe anywhere in the process — third-party libraries included — silently strips SQLite's transaction locks and leaves the file unprotected against other processes. With OFD locking enabled, the locks belong to the open file description that placed them and survive such a close. **Off by default, and staying off until the mode has real-world mileage**: without opting in, locking behavior is byte-for-byte that of previous releases. Enable it by setting `MODERNC_SQLITE_OFD_LOCK=1` in the environment the process starts with (any value but the empty string or one starting with `0`; read once, at library initialization), or from Go with the new `OFDLocking(true)`, which overrides the variable and must run before the first connection is opened; `OFDLockingEnabled` reports the mode in effect. The switch is deliberately **process-wide rather than a DSN parameter**: POSIX and OFD locks taken by one process are different owners to the kernel and genuinely conflict, so every connection to a database file inside one process must use the same kind, and a per-DSN knob would advertise a granularity the kernel does not offer (see the discussion in #255). Because the two kinds do not release one another, the mode is frozen at the process's first lock attempt: later attempts to change it return the new `ErrOFDLockingTooLate`, while querying, and setting the value already in effect, keep working. On kernels older than 3.15, and on filesystems that reject OFD locks, the first lock attempt falls back to POSIX locks for good and `OFDLocking` returns the new `ErrOFDLockingUnavailable` from then on, which is also how `OFDLockingEnabled` turning false reports the fallback; the same error is returned on every platform but Linux, where the API exists but OFD locks do not. Two boundaries to note: the immunity covers the locks on the database file itself, while WAL's `-shm` coordination stays on POSIX locks; and code in the same process that takes fcntl record locks of its own on a database file — which used to never conflict with SQLite's, while quietly destroying them — now conflicts with them loudly instead. The C side — `F_OFD_SETLK` routing through a designated per-inode locking descriptor that preserves upstream's `unixInodeInfo` semantics (last-unlocker release, PENDING piggybacking, `unix-excl`), guarded to `__linux__` — ships in the transpiled sources via [libsqlite3!3](https://gitlab.com/cznic/libsqlite3/-/merge_requests/3) and its follow-up hardening, with the OFD lock constants from [libc!33](https://gitlab.com/cznic/libc/-/merge_requests/33); the review rounds, the `/proc/locks` measurements behind the design, and the Tcl lock/WAL gate that runs both modes are recorded in [GitLab issue #255](https://gitlab.com/cznic/sqlite/-/issues/255) and those merge requests. diff --git a/issue221_windows_test.go b/issue221_windows_test.go new file mode 100644 index 00000000..b1518086 --- /dev/null +++ b/issue221_windows_test.go @@ -0,0 +1,20 @@ +// Copyright 2026 The Sqlite Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows + +package sqlite_test + +import ( + "testing" +) + +func TestIssue221_Windows(t *testing.T) { + // The full two-tier test with unencapsulated engine negative witness lives in + // modernc.org/libsqlite3 (issue221_windows_test.go), which owns the SQLite amalgamation, + // C patch (internal/sqlite_issue221.patch), and transpiler configuration (generator.go). + // The vendored code in modernc.org/sqlite/lib will gain active SEH call sites once + // upstream builders execute 'make vendor' from the patched libsqlite3. + t.Skip("sqlite/lib transpile was generated with SQLITE_OMIT_SEH upstream; full SEH tests live in modernc.org/libsqlite3 pending upstream builder regeneration (make vendor)") +} diff --git a/lib/libsqlite3_windows.go b/lib/libsqlite3_windows.go index 1b815f2c..381fb454 100644 --- a/lib/libsqlite3_windows.go +++ b/lib/libsqlite3_windows.go @@ -6,6 +6,8 @@ package sqlite3 import ( "math/bits" + "runtime/debug" + "unsafe" "modernc.org/libc" ) @@ -14,3 +16,52 @@ func ___umulh(tls *libc.TLS, a, b uint64) uint64 { hi, _ := bits.Mul64(a, b) return hi } + +func _modernc_seh_try(tls *libc.TLS, pWal uintptr, xTry uintptr, pCtx uintptr, xExcept uintptr) (rc int32) { + if pWal == 0 || xTry == 0 { + return int32(SQLITE_ERROR) + } + defer func() { + if r := recover(); r != nil { + var faultAddr uintptr + type addrGetter interface { + Addr() uintptr + } + if ag, ok := r.(addrGetter); ok { + faultAddr = ag.Addr() + } + + wal := (*TWal)(unsafe.Pointer(pWal)) + inShm := false + if faultAddr != 0 { + nPages := int(wal.FnWiData) + for i := 0; i < nPages; i++ { + pagePtr := *(*uintptr)(unsafe.Pointer(wal.FapWiData + uintptr(i)*unsafe.Sizeof(uintptr(0)))) + if pagePtr != 0 && faultAddr >= pagePtr && faultAddr < pagePtr+uintptr(32768) { + inShm = true + break + } + } + } else { + inShm = true + } + + if inShm { + if xExcept != 0 { + rc = (*(*func(*libc.TLS, uintptr) int32)(unsafe.Pointer(&struct{ uintptr }{xExcept})))(tls, pWal) + } else { + rc = int32(SQLITE_IOERR_IN_PAGE) + } + return + } + + panic(r) + } + }() + + old := debug.SetPanicOnFault(true) + defer debug.SetPanicOnFault(old) + + rc = (*(*func(*libc.TLS, uintptr, uintptr) int32)(unsafe.Pointer(&struct{ uintptr }{xTry})))(tls, pWal, pCtx) + return rc +}