Commit 722282f3 authored by cznic's avatar cznic
Browse files

CHANGELOG.md: document the OFD locking opt-in and the SQLite 3.53.4 upgrade

parent 5dcac5fb
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
# Changelog

 - 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.
     - Add the accompanying regression tests. The four OFD scenarios contributed in merge request #136 — lock survival across a rogue `os.Close`, the interleaved-readers lock lifecycle, the read-only-first designated-descriptor migration, and the failed-first-lock cleanup — run in a re-executed child process with `MODERNC_SQLITE_OFD_LOCK=1`, since the mode is process-wide and frozen at the first lock so the suite's own process cannot switch; a positive control asserts the variable really switched the mode on before each scenario runs, and the two scenarios whose invariants hold under both modes also run in the parent process's inherited mode. `TestOFDLockingSetter` exercises the Go call path end to end in a clean child — off by default, enabled before the first connection, the kernel-visible `OFDLCK` record in `/proc/locks` as the positive control, frozen after the first lock — and on every other platform the switch is asserted to report itself unavailable.
     - Resolves [GitLab issue #255](https://gitlab.com/cznic/sqlite/-/issues/255). See [GitLab merge request #136](https://gitlab.com/cznic/sqlite/-/merge_requests/136), thanks @technosloth, and thanks Gani Georgiev (@ganigeorgiev) for pressing the opt-in default!

 - 2026-08-19 v1.57.0:
     - Add an opt-in `_defensive` DSN query parameter that turns on SQLite's defensive mode for the connection, disabling the SQL-level features that let ordinary statements deliberately corrupt the database file. When `_defensive=1` (or any `strconv.ParseBool` true value) is supplied, the driver calls `sqlite3_db_config` with `SQLITE_DBCONFIG_DEFENSIVE` immediately after `sqlite3_open_v2` and before every other parameter is applied, so the PRAGMAs the driver itself runs, the `_pragma` list, and every statement the caller prepares are all subject to it. On such a connection `PRAGMA writable_schema=ON`, `PRAGMA journal_mode=OFF` and `PRAGMA schema_version=N` become silent no-ops, and writes to a virtual table's shadow tables (fts5's `_data`, `_idx` and so on) and to `sqlite_dbpage` fail with "table ... may not be modified"; reading those tables, ordinary use of the virtual tables that own them, and `VACUUM` are unaffected. The flag has no PRAGMA equivalent, so `sqlite3_db_config` — and therefore a DSN parameter — is the only way to reach it short of dropping to `modernc.org/sqlite/lib`. The value is parsed before `sqlite3_open_v2`, so an invalid one fails the connection without creating the database file, and the parameter must appear at most once: a repeated `_defensive` is an error rather than letting the first value silently win. Absence of the parameter, or `_defensive=0`, leaves SQLite's default behavior unchanged; existing DSNs continue to work byte-for-byte. Two limits are worth stating plainly, since the name invites more confidence than the flag earns. Defensive mode is a hardening measure, not a sandbox for hostile database files: it is one of several steps [SQLite recommends](https://www.sqlite.org/security.html) for that purpose, and this build compiles with neither `SQLITE_TRUSTED_SCHEMA=0` nor `SQLITE_DQS=0` and exposes no authorizer. And it is a property of the connection, not of the database file — a second handle opened on the same file without the parameter is unrestricted.
     - Reject the one DSN combination defensive mode would otherwise swallow in silence. `_defensive=1` together with `_journal_mode=OFF` (or `_journal=OFF`) now fails the connection instead of opening one in which neither parameter was honoured: SQLite turns `PRAGMA journal_mode=OFF` into a no-op that still reports success, so the driver would have accepted the mode, executed it, and left the journal untouched without telling anyone. The check runs in the validation phase introduced in v1.55.0, before any statement executes, so a rejected DSN cannot leave the database half-configured. `_pragma` remains the exception it has always been: `_pragma=journal_mode(OFF)` alongside `_defensive=1` still runs and is still silently ignored by SQLite. Only DSNs using `_defensive` can be affected, and that parameter is new, so no DSN that opened before changes behavior.