Repository navigation
Update dependency filelock to v4 [SECURITY] - autoclosed - #262
Closed
renovate[bot] wants to merge 1 commit into
Closed
renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
auto-merge was automatically disabled
October 24, 2026 21:06
Pull request was closed
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.
Note
This PR body was truncated due to platform limits.
This PR contains the following updates:
==3.25.2→==4.2.1filelock: On Windows an oplock on a soft lock's marker holds SoftFileLock.acquire() past its timeout and blocks the stale break
CVE-2026-112907 / GHSA-567q-5qvw-pf92
More information
Details
On Windows the soft locks (
SoftFileLock,MarkerSoftFileLock,SoftFileLease, their async forms, and aFileLockthat fell back to a soft lock) read their marker by path. The marker is the lock file whose presence says a process holds the lock. Eachacquire()attempt that finds a marker opens it withos.opento read the holder's process id, and the lock renames and deletes a marker it judges stale, by path. On Windows each of these calls waits for an oplock. An oplock (opportunistic lock) is a request by a process that holds a handle on a file to be told before another process opens, renames or deletes that file; the other process waits until the holder answers the break or closes its handle.Another local account that can open the marker for reading can take a Level 1 oplock on it while no process holds it open, and leave the break unanswered. Every
acquire()by the user running the lock (below: the lock user) then waits inside the open, past itstimeout,blocking=Falseandcancel_check, and a stale marker under that oplock is not broken. Mutual exclusion holds and the lock touches no other file. Affected versions are filelock 3.22.0 through 4.2.0 on Windows. The maintainers found this while reviewing the fix for the report Anthropic's OSS Scanner sent on 15 October 2026 (review round 4, finding F4); it is not among the scanner's findings.Severity
CVSS 3.1
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:Lscores 2.5 (Low).AV:L(local): the other account needs a logon on the same host.AC:H(high attack complexity): Windows grants a Level 1 or batch oplock only to the sole open handle on a file, so the marker must have no holder handle when the other account asks. That account cannot bring this about; it waits for a holder that died, closed its descriptor, or left a marker past itslifetime.PR:L(low privileges): an ordinary account with read access to the marker through the directory's inherited ACL (access control list).UI:N(no user interaction).S:U(scope unchanged).C:NandI:N: that account can read the marker's content (process id and host name) anyway, and mutual exclusion holds.A:L(partial availability impact): one thread, or the event loop withrun_in_executor=False, waits until the holder answers; 10 s per attempt in the two-account runs, no crash. Metrics in doubt take the lower reading.Classification from the review: security, conditional on an unheld marker that another account can open.
Scope and preconditions
SoftFileLock,MarkerSoftFileLock,SoftFileLease,AsyncSoftFileLockand the other async soft locks, andFileLockafter itsENOSYSfallback to a soft lock.WindowsFileLockopens its lock file on a separate code path that this advisory does not cover.os.open, which on Windows applies no more ofmodethan the read-only attribute (GHSA-8mx5-7m5m-r77g). The marker carries the lock directory's inheritable entries. Another account that those entries grant read access to new files can open the marker with a read handle that shares read, write and delete.lifetimehas passed. While the holder keeps its handle, the request fails.DeviceIoControland never acknowledges the break. Windows cancels a thread's pending request when the thread exits, so the holder thread stays alive. A same-account process that answers slowly, such as an indexer, has the same effect for its answer time.SoftFileLock.acquire(timeout=1)returnedTimeoutafter 10.02 s with the second account's handle. In the same-account probeos.openwithO_RDONLYwas still running after 15 s and returned once the holder closed. The retry loop consultstimeout,blocking=Falseandcancel_checkbetween attempts only. WithAsyncSoftFileLock(run_in_executor=False)the event loop waits.break_lock_filerenames and unlinks by path. Both calls wait in the same way, so a stale marker under such an oplock stays, and no process acquires the lock until the holder closes.Affected code
Scanned commit
b831496b0273: theos.openin_read_lock_file(function), called from_try_break_stale_lockon every attempt that finds a marker, and from_unprobeable_holder,pidandis_lock_held_by_us.break_lock_filerenames at L116 and unlinks at L118 and L122, by path._poll_until_acquiredand_check_give_uptest the deadline,blockingandcancel_checkonly between attempts.Introduced
e35c3af "feat(soft): detect and break stale locks" (#476, 2026-02-13), first tag 3.22.0 (verified with
git tag --contains). Before it a soft lock created its marker withO_CREAT | O_EXCLand never opened an existing one. #476 read the marker on Windows throughCreateFileWand broke a stale one withPath.renameandPath.unlink. These wait for an oplock asos.opendoes, because none of them passesFILE_COMPLETE_IF_OPLOCKED, anNtCreateFileoption that returns instead of waiting. The read became theos.openin_read_lock_filewith bfbfa76 (#548, 3.29.1) and the break moved intobreak_lock_filewith d244c6d (#551, 3.29.2); the by-path calls stayed.Mechanism
Each attempt of
_acquirefirst callsos.open(path, O_WRONLY | O_CREAT | O_EXCL). On an existing marker that fails withEEXIST(orEACCESon Windows) at once; the same-account probe shows the failed exclusive create returning at once under an oplock too._acquirethen calls_try_break_stale_lock, which calls_read_lock_file:os.lstat, thenos.open(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK). On WindowsO_NOFOLLOWandO_NONBLOCKdo not exist (the code reads them withgetattr(os, ..., 0)), andos.opencallsCreateFileW. When another handle holds a Level 1 or batch oplock on the file, that call sends the holder a break request and returns only when the holder acknowledges it or closes its handle._check_give_uptests the deadline after the attempt returns, so the wait overrunstimeout,blocking=Falseandcancel_checkby its length. Once the open returns, the attempt ends and_check_give_upraisesTimeout, because the deadline has passed.A marker whose holder is gone takes the second path.
break_lock_filerenames it to a break name and unlinks it, by path. On Windows the rename and the delete open the file with delete access, which breaks a Level 1 or batch oplock the same way; the same-account probe showsos.renameandos.unlinkstill running after 15 s and returning once the holder closed. Thelifetimebreak at L123 runs the same function. A stale marker under such an oplock is therefore not removed, and every attempt by every process waits inside the read. The other account can request the oplock again as soon as the lock user's handle closes, between two attempts or twoacquire()calls, as in the Linux lease case of GHSA-7hmp-gf99-57hx. Nothing in the retry loop bounds this wait.Reproduction
The review's reproducers, quoted as received.
oplock_vm.pysection[2]is this finding; sections[1a]and[1b]belong to therelease()advisory.oplock_semantics_vm.pyis the same-account probe of which path operations wait. They ran on a Windows 11 VM (NTFS, CPython 3.14.8) against the head of PR #835 at3e4c5ef, where_read_lock_fileandbreak_lock_fileare the same code as atb831496b0273apart from import changes. The second accountfltesteris a member ofUsers; the directory grants it(OI)(CI)(M).oplock_vm.pyRecorded output of section
[2]:oplock_semantics_vm.pyThe review kept this probe's result as a sentence rather than its printed lines: under a same-account Level 1 or batch oplock,
os.open(path, os.O_RDONLY),os.renameandos.unlinkwere still running after 15 s and returned only once the holder closed its handle;os.utimeby path and a failedO_CREAT | O_EXCLcreate returned at once.Failure sequence
sequenceDiagram participant A as other local account participant M as soft.lock marker, no handle open participant L as lock user: SoftFileLock(path).acquire(timeout=1) participant W as Windows (NTFS) A->>W: CreateFileW(soft.lock, GENERIC_READ, share read/write/delete, FILE_FLAG_OVERLAPPED) A->>W: DeviceIoControl(FSCTL_REQUEST_OPLOCK_LEVEL_1), granted, break never acknowledged L->>W: os.open(soft.lock, O_CREAT|O_EXCL) fails with EEXIST at once L->>W: os.lstat(soft.lock) regular file rect rgb(255, 214, 214) L->>W: os.open(soft.lock, O_RDONLY) inside _read_lock_file Note over L,W: CreateFileW asks A to break the oplock and waits#59; the 1 s deadline, blocking=False and cancel_check are not consulted W-->>L: handle after 10.02 s (two-account run), or when A closes (same-account probe) end L->>L: _check_give_up: past the deadline, raise Timeout after 10.02 s Note over A,M: a stale marker takes the same wait in Path.rename and Path.unlink, so it is not brokenEvidence
Section
[2]ofoplock_vm.pyacquiredsoft.lockas the test account, closed the holder's descriptor so the marker stayed with a live process id and no open handle, took a Level 1 oplock through the second account's read handle (oplock granted: True), and ranSoftFileLock(marker, is_singleton=False).acquire(timeout=1)on a thread. The call returnedTimeoutafter 10.02 s, before the holder closed. The review did not establish what ends the wait at 10 s in the two-account runs; section[1a]of the same script, with the second account's handle, shows the same 10 s. In the same-account probe (oplock_semantics_vm.py) the open returned only when the holder closed, still running at 15 s. The probe also shows that the failed exclusive create andos.utimeby path return at once, so the wait is in the read and the break, not in the create attempt.Impact
Each
acquire()of a soft lock that finds a marker another account holds an oplock on runs past itstimeout,blocking=Falseandcancel_checkuntil the holder answers or closes, 10 s per attempt in the recorded two-account runs. A stale marker under that oplock is not broken, so no process acquires the lock while the holder keeps its handle. WithAsyncSoftFileLock(run_in_executor=False)the wait is on the event loop thread and stops every task in the process; with the default executor one worker thread waits. The documentation names a soft lock as the option for network filesystems, after verification on the mount, andFileLockis a soft lock on builds withoutfcntland after itsENOSYSfallback. Mutual exclusion is not affected and nothing other than the marker is read, written or deleted.Workarounds
SYSTEMandAdministratorsgranted (icacls <dir> /inheritance:r /grant:r "<owner>:(OI)(CI)F" "SYSTEM:(OI)(CI)F" "Administrators:(OI)(CI)F").modedoes not do this on Windows.AsyncSoftFileLockwith the default executor rather thanrun_in_executor=False, so that only a worker thread waits.timeout,blockingorcancel_checkvalue bounds the wait.Patched version
4.2.1.
Fix: https://github.com/tox-dev/filelock/pull/835
Credits
Found by the maintainers while reviewing the fix for the report Anthropic's OSS Scanner sent on 15 October 2026 (review round 4, finding F4).
Severity
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:LReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
filelock: ReadWriteLock on Windows acts on SQLite side files another local user places beside its database
CVE-2026-112913 / GHSA-9vhv-jxww-xfm5
More information
Details
On Windows,
ReadWriteLockandAsyncReadWriteLock(the SQLite-backed reader/writer lock) connect SQLite to the lock database by its path, in every directory. SQLite derives three side files from that path:<database>-journal, the rollback journal (pages saved to undo an interrupted write);<database>-wal, the write-ahead log (changes kept outside the main file); and<database>-shm, the shared-memory index of the WAL. SQLite acts on whichever of these it finds beside the database before the lock runs any statement. Another local account that can create files in the database's directory, but not delete files of other accounts there, places such a side file beside the database and denies everyoneDELETEon it, which a file's creator may do.C:\Windows\Temp(the temporary directory of services, grantingBUILTIN\Userscreate but not delete) andC:\Users\Publicare such directories.With a
-walin place, everyread_lock()andwrite_lock()of the user running the lock (below: the lock user) raisessqlite3.OperationalError: disk I/O errorat once, whatevertimeoutsays, until that account removes its file; the lock user cannot. With a-journalbeside a lock database that holds a table, SQLite replays the journal and deletes the file the journal names, a file of the lock user, and the acquire reports success. Affected versions are filelock 3.20.4 through 4.2.0 on Windows. The maintainers found this while reviewing the fix for the POSIX form of the defect, GHSA-v4x8-3wr5-3wq8, whose text says "Windows does not take this path" (review round 4, finding F2 of set D). It is not among the findings of the report Anthropic's OSS Scanner sent on 15 October 2026.Severity
CVSS 3.1
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:Lscores 3.3 (Low).AV:L(local): the other account needs a logon on the same host.AC:L(low attack complexity): create one file beside the database and set its ACL (access control list); no race, and the other account can place the file days earlier.PR:L(low privileges): an ordinary account that can create files in the directory is enough.UI:N(no user interaction).S:U(scope unchanged).C:N(no confidentiality impact): the other account learns nothing.I:N(no integrity impact in the scored variant): see the scoring note.A:L(partial availability impact): every acquire fails with an exception thatTimeouthandlers do not catch, until the other account removes its file; no crash, and the lock's own data is not changed.Scoring note. The deletion variant reads
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:L(3.6):I:Lbecause the lock user's process deletes one file the journal names,AC:Hbecause it needs a lock database that holds a table, which filelock does not create, and a journal the lock user can open for writing. The lower-reading rule of this advisory set scores the variant that reaches everyReadWriteLockon Windows in such a directory, 3.3, and describes the deletion variant under Mechanism. Classification from the review: security (another account), availability for the scored variant.Scope and preconditions
ReadWriteLockandAsyncReadWriteLock, both of which connect through_connect. On Linux and macOS the lock takes other routes, covered by GHSA-v4x8-3wr5-3wq8.C:\Windows\TempcarriesBUILTIN\Users:(CI)(S,WD,AD,X)(create file and create subdirectory, no delete child);C:\Users\Publicand any directory with such an entry qualify. A directory only the lock user can write, such as%LOCALAPPDATA%or the per-user temporary directory, is not affected.<database>-wal, grants everyone modify and denies everyoneDELETEon it. A file's creator may set both. The lock user can open and read the file but not delete it.<database>-journalgrants the lock account modify rights. The journal's tail names a file the lock user can delete.write_lock(timeout=1)raisedsqlite3.OperationalError: disk I/O errorwith the undeletable-walin place; with the writable journal the acquire succeeded and the named file was gone; with an undeletable journal every acquire raisedsqlite3.OperationalError: unable to open database file.WindowsFileLock, in its symlink paragraph; theReadWriteLockdocumentation names no directory requirement on Windows.Affected code
Scanned commit
b831496b0273: the Windows branch of_connectchecks the database through a handle and then callssqlite3.connect(database, ...)with the path._open_lock_databaseand_raise_if_walinspect that handle, not the side files. The first statement isPRAGMA busy_timeout=...; PRAGMA journal_mode=MEMORY;at L816._do_acquire_innerat L763 turns only"database is locked"intoTimeout; any othersqlite3.OperationalErrorleaves the acquire.Introduced
cfd4446 "feat(lock): add SQLite-based ReadWriteLock" (#399, 2026-02-12), first tag 3.20.4 (verified with
git tag --contains), passed the lock path tosqlite3.connecton every platform. The Linux descriptor route (4.0.9) and the macOS hard-link route (4.1.1) did not change Windows, where the connect has been by name in every release since. The reproducer ran against the head of PR #834 at1f9e415, whose Windows branch of_connectis the same code as atb831496b0273.Mechanism
_connecton Windows opens the database with_open_non_reparse_fd, checks through that handle that the file is a regular file, not a reparse point, and that its header does not declare WAL mode, and keeps the handle open whilesqlite3.connect(database)opens the same name. The check covers the database, not the names beside it. On the first statement,PRAGMA journal_mode=MEMORY, SQLite takes its first lock on the database and looks for<database>-wal. A non-empty-walbeside a database that is not in WAL mode is a leftover SQLite deletes. The other account's file deniesDELETE, so the pragma raisesSQLITE_IOERR_DELETE, which reaches the caller assqlite3.OperationalError: disk I/O error._do_acquire_innerconverts only "database is locked" intoTimeout, soacquire_read(),acquire_write()and the async forms raise at once, on every call, until the file is gone. The lock user has no right to delete it.The journal variant needs a database with content. Beside a non-empty database SQLite treats a
<database>-journalwith a valid header as hot (left by an interrupted write) and replays it before the first statement runs. The review's journal is 46 bytes: a file path, its 4-byte length, the 4-byte sum of its bytes and SQLite's journal magic. That tail names a super-journal, and recovery ends by deleting the file it names. The lock user'swrite_lock()therefore deletes one file the other account chose, provided the lock user can delete it, and returns without error. SQLite opens a hot journal for writing, so the other account's journal must grant the lock account modify rights; a read-only journal makes every acquire raisesqlite3.OperationalError: unable to open database fileinstead. GHSA-v4x8-3wr5-3wq8 describes the same side-file behavior on POSIX, where the sticky bit plays the part the directory ACL plays here.Reproduction
The review's reproducer, quoted as received except for the second account's password on line 2, replaced by
<password>. It ran on a Windows 11 VM (NTFS, CPython 3.14, SQLite 3.53.1) against the head of PR #834 at1f9e415, as the administrator accountbernat, which stands in for the other account that creates the side files; the lock account isfltester, a member ofUsers, whose database lives inC:\Windows\Temp.win_two_accounts.pyRecorded output on the VM:
Failure sequence
flowchart TD P["other account: create lock.db-wal (or lock.db-journal) beside lock.db in C:/Windows/Temp, grant everyone modify, deny everyone DELETE"] --> C["lock user: ReadWriteLock(lock.db).write_lock(timeout=1)"] C --> O["_open_lock_database checks the handle: regular file, no reparse point, header not WAL"] O --> S["sqlite3.connect(lock.db) by path, then PRAGMA journal_mode=MEMORY"] S -- "empty database, undeletable -wal" --> E1["SQLite cannot delete the leftover -wal: OperationalError disk I/O error on every acquire, timeout not consulted"] S -- "database with a table, writable -journal" --> E2["SQLite treats the journal as hot, replays it and deletes the file its tail names, acquire succeeds"] S -- "database with a table, read-only -journal" --> E3["OperationalError unable to open database file on every acquire"] classDef ok fill:#d8f3dc,stroke:#​2d6a4f,color:#​1b4332; classDef bad fill:#ffd6d6,stroke:#c1121f,color:#​5c0000; classDef info fill:#cfe4ff,stroke:#​1f6feb,color:#​0b1f3a; class P,C,O,S info; class E1,E2,E3 bad;Evidence
The reproducer printed the three lines above on the VM: the undeletable
-walbeside an empty database madewrite_lock(timeout=1)fail withOperationalError: disk I/O errorand left the file in place; the writable journal beside a database with one table let the acquire succeed while the file the journal named, created by the lock account with the contentkeep, was gone along with the journal; the read-only journal made the acquire fail withOperationalError: unable to open database fileand left both files. The review notes that the Windows branch of_connectis unchanged since #399, so the behavior is the same atb831496b0273and in 4.2.0. The scanner and the maintainers measured the same side-file effects on Linux for GHSA-v4x8-3wr5-3wq8.Impact
In a directory such as
C:\Windows\Temp, another local account turns everyread_lock()andwrite_lock()on aReadWriteLockdatabase into an immediatesqlite3.OperationalError, for as long as its file stays. Callers that handleTimeoutorOSErrordo not catch it, and the lock user cannot remove the file. With a lock database that holds a table and a journal the lock user can write, the lock user's process deletes one file of the other account's choosing, among the files the lock user may delete, and the acquire reports success. The lock's own database is not changed, nothing is read, and mutual exclusion between processes that acquire the lock holds.Workarounds
ReadWriteLockdatabase in a directory other accounts cannot create files in:%LOCALAPPDATA%, the per-user temporary directory (%TEMP%of a user session), or a directory with inheritance removed and only the owner,SYSTEMandAdministratorsgranted.C:\Windows\TempandC:\Users\Publicdo not qualify.sqlite3.OperationalError: disk I/O erroron every acquire as a foreign<database>-walbeside the database, which only its creator or an administrator can remove.Patched version
4.2.1.
Fix: https://github.com/tox-dev/filelock/pull/854
Credits
Found by the maintainers while reviewing the fix for the POSIX form of this defect, GHSA-v4x8-3wr5-3wq8, reported by Anthropic's OSS Scanner on 15 October 2026 (review round 4, set D, finding F2).
Severity
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:LReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
filelock ReadWriteLock: SQLite reopens the lock database by name, so a planted symlink or journal file makes the lock user's process overwrite or unlink its own files
CVE-2026-112940 / GHSA-v4x8-3wr5-3wq8
More information
Details
ReadWriteLockandAsyncReadWriteLock(the SQLite-backed reader/writer lock) hand SQLite a name they do not control. On Linux the lock validates its database through a descriptor (the number the kernel gives a process for an open file). It then connects SQLite to/proc/self/fd/<n>, a Linux path that names the process's own open filen. SQLite turns that path back into a plain path and reopens it by name, following symlinks (a symlink is a file that points at another path). On every POSIX route SQLite also acts on-journal,-waland-shmfiles it finds beside that name. Those are SQLite's side files: the rollback journal (pages saved to undo an interrupted write), the write-ahead log or WAL (changes kept outside the main file), and the shared-memory index for the WAL.Another local user who can create or rename entries in the directory that holds the lock database can turn this against the user running the lock (below: the lock user). Such a directory is a
0777or group-writable cache, build or project directory without the sticky bit (a directory flag that lets only a file's owner remove or rename it). That other user can make the lock user's ordinaryread_lock()orwrite_lock()write bytes of the other user's choosing into the lock user's own files. The same position lets the other user make that call create or unlink files of the lock user, admit a reader beside a writer, or kill the holder. All of this runs with the lock user's privileges. In a sticky directory such as/tmpa planted-walfile makes every acquire fail. Affected versions are filelock 3.20.4 through 4.2.0 (the/proc/self/fdredirect since 4.0.9, the side-file variants since 3.20.4).Anthropic's OSS Scanner reported this in private as
ANT-2026-KMJ2575J[high, CWE-363]: ReadWriteLock: SQLite still follows planted symlinks, overwriting files.Severity
CVSS 3.1
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:Hscores 7.1 (High).AV:L(local): the other user needs an account on the same host.PR:L(low privileges): an ordinary account is enough.UI:N(no user interaction).AC:L(low attack complexity): the planted-journal variant needs no race, and the redirect won 77 of 77 attempts.I:H(high integrity impact): the other user writes bytes of their choosing into files of the lock user and unlinks files of their choosing.A:H(high availability impact): the other user makes every acquire fail or kills the lock holder.C:N(no confidentiality impact): the other user learns nothing.S:U(scope unchanged): the effect stays within the lock user's own privileges.Classification from the external review: conditional security, a cross-user effect that exists only when another user can change names in the lock database's directory. The review notes that the 77/77 inotify result (inotify is a Linux interface that reports file events) is stronger evidence than a theoretical TOCTOU (a time-of-check to time-of-use race). The scanner rated it high. The review accepts the mechanism and narrows it to that precondition.
Scope and preconditions
0777directory or a group-writable one (including a setgid2775project directory shared with that account), in each case without the sticky bit. The lock user's lock must be able to open the database read-write, so a database the other user creates is0666. filelock itself creates the database0600./tmp,/var/tmp, any1777directory) narrow the finding rather than exclude it. The redirect and the journal replay do not reach a database the lock user owns there (tested withfs.protected_regular=2). What remains is a planted non-empty<db>-wal(any mode; a directory or FIFO works too) that the lock user cannot unlink. It makes every acquire raisesqlite3.OperationalError: disk I/O error. A hard link (a second name for the same file) to a file of the lock user at the lock path also remains, iffs.protected_hardlinks=0.fs.protected_regular(1 or 2 is the systemd default on most distributions; the kernel's own default is 0) keeps a user from opening a file another user owns in a sticky world-writable directory. Where it is 0, the other user can own the database in/tmp, and SQLite follows a planted symlink itself (not tested).fs.protected_hardlinks=1(most distributions; kernel default 0) blocks a hard link of a lock user's file onto the lock path. Neither setting affects a non-sticky shared directory./proc/self/fdredirect is Linux with/procmounted, 4.0.9 and later. The side-file variants (-journal,-wal,-shm) exist on every POSIX route. On the hard-link route (macOS since 4.1.1 for every connection, Linux without/proc) they need the link-directory swap described under "Mechanism", so a non-sticky parent. Neither the scanner nor the review executed on macOS; the statement rests on the platform branch and the identical code after it. Windows does not take this path.Affected code
At the scanned commit
b831496b0273:_connect_through_descriptorbuilds/proc/self/fd/<n>and hands it tosqlite3.connect. The assumption is the comment at line 63, "Descriptor paths reopen the validated inode" (an inode is the kernel's identity for a file, independent of its name). SQLite first reads the file it opened atPRAGMA journal_mode=MEMORY, line 816. The check in_raise_if_database_replacedat line 836 runs only afterBEGIN. Both_open_lock_databaseand_raise_if_walinspect the descriptor, not the file SQLite is on. The one-level hard link of the macOS route is_link_beside.Introduced
The
/proc/self/fdredirect came with4d45e83("Merge commit from fork", the fix for GHSA-j8f7-rjxc-mr56), first released in 4.0.9 (verified:git tag --contains 4d45e83gives 4.0.9); 4.0.8 does not have it. The side-file variants are older. They reproduce oncfd4446("feat(lock): add SQLite-based ReadWriteLock (#399)"), first released in 3.20.4 (verified), which passed the lock path straight tosqlite3.connect.Mechanism
On Linux the lock opens the database with
O_RDWR|O_CREAT|O_NOFOLLOW|O_NONBLOCK. It checks through the descriptor that the file is a regular file and not a WAL database. It then callssqlite3.connect("/proc/self/fd/<n>")and expects SQLite to reopen the validated inode. SQLite does not open that name. It runsreadlink()on it, which yields the path string the kernel reports for the descriptor at that moment, and opens that string by path, following every symlink. Without interference the trace showsreadlink("/proc/534/fd/3") = "/tmp/benign/app.lock.db"and then a secondopenatof that path. The descriptor contributes only a string. Once the directory entry behind the descriptor is gone, the string is<database> (deleted). A symlink with that exact name sends SQLite wherever its creator chose.The other user prepares the directory before the lock user runs. They create a database
cache.lock.db(0666, so the lock user's lock accepts it) with a second hard linkcache.lock.db.keep, and the symlinkcache.lock.db (deleted) -> /tmp/victim-notes.txt. The lock user'sos.openraises an inotifyIN_OPENevent. The other user unlinkscache.lock.dband linkscache.lock.db.keepback under the same name. The lock user's descriptor still refers to the unlinked entry, so/proc/self/fd/3now reads/tmp/shared-cache/cache.lock.db (deleted).sqlite3.connectresolves it, follows the symlink and opens the targetO_RDWR|O_CREAT. None of filelock's checks look at that file._open_lock_databaseand_raise_if_walinspect descriptor 3._raise_if_database_replacedcompares anlstatof the lock path with the descriptor's(st_dev, st_ino), which passes because the other user linked the same inode back, and it runs only afterBEGIN.The damage comes from the first statement,
PRAGMA journal_mode=MEMORY. SQLite finds<target>-journal, treats it as the rollback journal of an interrupted transaction, and copies its page records into the target before it checks whether the target is a database at all. The journal header supplies the page size and the page count SQLite resizes the file to. Whoever wrote the journal computed the per-page checksum. The content from offset 0 and the final length are therefore the other user's. Strace of the lock user's process (SQLite 3.40.1):