Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
20 changes: 20 additions & 0 deletions issue221_windows_test.go
Original file line number Diff line number Diff line change
@@ -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)")
}
51 changes: 51 additions & 0 deletions lib/libsqlite3_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package sqlite3

import (
"math/bits"
"runtime/debug"
"unsafe"

"modernc.org/libc"
)
Expand All @@ -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
}