Feature Request: Support Open File Description (OFD) Locking on Linux to Prevent Lock Stripping on os.Close()
Summary
In Go applications using modernc.org/sqlite on Linux, standard POSIX advisory locking (F_SETLK / F_SETLKW / F_GETLK) presents a subtle data-safety hazard: POSIX locks are process-scoped and tied to (PID, inode).
If any goroutine or third-party library opens and subsequently closes an independent os.File descriptor pointing to the same database inode (e.g., for backup checks, hashing, or metadata inspection), calling os.Close() unconditionally instructs the Linux kernel to drop all POSIX locks held by the process on that inode. This strips the active SQLite transaction lock out from under a running connection, risking database corruption.
We propose adding support for Linux Open File Description (OFD) locks (F_OFD_SETLK = 37, F_OFD_SETLKW = 38, F_OFD_GETLK = 36) with an automatic one-time fallback to standard POSIX locks if the kernel or filesystem returns EINVAL.
Technical Design & Findings
1. The C Wrapper (osFcntlOfd)
In libsqlite3, we can intercept locking calls in os_unix.c / sqlite3.c by wrapping osFcntl:
#if defined(F_OFD_SETLK)
static volatile int ofdSupported = 1;
static int osFcntlOfd(int fd, int cmd, void *arg){
if( ofdSupported ){
int ofdCmd = cmd;
if( cmd==F_SETLK ) ofdCmd = F_OFD_SETLK;
else if( cmd==F_SETLKW ) ofdCmd = F_OFD_SETLKW;
else if( cmd==F_GETLK ) ofdCmd = F_OFD_GETLK;
if( ofdCmd!=cmd ){
struct flock *pLock = (struct flock *)arg;
pLock->l_pid = 0; /* Required by Linux kernel for F_OFD_* commands */
int rc = osFcntl(fd, ofdCmd, arg);
if( rc!=(-1) || errno!=EINVAL ){
return rc;
}
ofdSupported = 0;
}
}
return osFcntl(fd, cmd, arg);
}
#undef osFcntl
#define osFcntl osFcntlOfd
#endif2. Critical Kernel Requirement: flock.l_pid == 0
During testing, we discovered why naive OFD locking fails in SQLite: the Linux kernel strictly requires flock.l_pid == 0 when setting OFD locks (F_OFD_SETLK / F_OFD_SETLKW). Because SQLite C code leaves lock.l_pid uninitialized on stack allocations when preparing advisory locks, the kernel rejects the call with EINVAL. Explicitly setting pLock->l_pid = 0 before invoking F_OFD_* resolves this.
Note that zeroing pLock->l_pid = 0 without saving or restoring its prior value is completely safe across both OFD and standard POSIX locks:
- For
F_OFD_SETLK/F_OFD_SETLKW, the Linux kernel requiresl_pid == 0. - For POSIX
F_SETLK/F_SETLKW(if falling back), POSIX definesl_pidas ignored on input when acquiring or releasing locks. - For
F_GETLK/F_OFD_GETLK,l_pidis an output field populated by the kernel if a conflicting lock is found. - Additionally,
struct flockin SQLite is always allocated as a function-local stack variable (e.g. inunixFileLock), so there are no cross-thread concurrency hazards.
3. Automatic One-Time Fallback
By declaring ofdSupported as static volatile int (ccgo translates this to atomic memory loads/stores), if an older kernel or network filesystem rejects F_OFD_* with EINVAL, ofdSupported permanently flips to 0 and all subsequent locking calls execute standard POSIX F_SETLK with zero overhead.
Verification
We validated this implementation across libc, libsqlite3, and sqlite with a unit test (TestOFDLockSurvivesOSClose):
- Open a SQLite connection and begin an exclusive write transaction.
- Open and close an independent
os.Filedescriptor to the exact same database file path. - Attempt to acquire an OS lock from a probe descriptor:
- Under legacy POSIX locks (
F_SETLK),os.Close()strips the lock and the probe lock succeeds (unsafe). - Under OFD locks (
F_OFD_SETLK), probing the lock returnsEAGAIN/EACCES, confirming that the database lock persists acrossos.Close().
- Under legacy POSIX locks (
Open Design Question for Maintainers: Default vs. Opt-In DSN Parameter
There is an important behavioral difference between POSIX locks and OFD locks when using connection pools:
- POSIX locks (
F_SETLK): Because locks are owned by PID, multiple file descriptors opened bydatabase/sqlin the same process do not conflict at the kernel level. - OFD locks (
F_OFD_SETLK): Locks are owned per open file description. Consequently, concurrent queries across a connection pool without a configured busy timeout will experience OS-level contention (SQLITE_BUSY), which causes tests likeTestIssue20andTestIssue65to fail unless a busy timeout is set.
We would appreciate your guidance on which approach you prefer:
- Make OFD locking the default on Linux (with automatic fallback to POSIX on
EINVAL), updating pool/test expectations where appropriate; OR - Gate OFD locking behind an opt-in DSN parameter (e.g.,
_ofd_lock=1) so existing users relying on intra-process POSIX lock sharing retain the current default behavior.
Associated Merge Requests (Source-Only Diffs)
To respect repository separation and keep diffs easy to review, we have submitted source-only Draft Merge Requests (excluding generated ccgo_*.go and vendored lib/sqlite_*.go files) in bottom-up dependency order:
Core OFD Locking Implementation
cznic/libc!33: Export Linux OFD lock constants (F_OFD_GETLK,F_OFD_SETLK,F_OFD_SETLKW) acrossfcntl_linux_*.goand add OFD switch routing tointernal/overlay/musl/src/fcntl/fcntl.c.cznic/libsqlite3!3(requires libc release bump): Addinternal/sqlite_issue255.patch(amalgamationsqlite3.c) andinternal/sqlite_issue255.patch2(full source treesrc/os_unix.c) and patch invocation steps ingenerator.go.cznic/sqlite!136(requires libsqlite3 release bump): Addofd_linux_test.go(TestOFDLockSurvivesOSClose) verifying lock survival acrossos.Close().
Companion Toolchain & Build Fixes
During local verification and cross-compilation (make generate / make build_all_targets), we also prepared companion fixes needed to generate and link all targets cleanly on modern Linux and Windows toolchains:
cznic/libc!32: Implement and export Windows CRT stat variants (_stat32i64,__stat32i64,__stat64i32) referenced by SQLite's Windows VFS (os_win.c).cznic/libsqlite3!2: Add C23 preprocessor macro stubs (-D_GCC_NULLPTR_T,-D_Float16=short,-D__bf16=short) toccgoinvocations ingenerator.goso modern glibc/GCC headers parse without AST errors.