Skip to content

🐛 fix(read-write): link private databases on Windows - #854

Merged
gaborbernat merged 6 commits into
fix/rw-sqlite-filesfrom
fix/rw-windows-private-link
Oct 17, 2026
Merged

gaborbernat merged 6 commits into
fix/rw-sqlite-filesfrom
fix/rw-windows-private-link

Conversation

@gaborbernat

@gaborbernat gaborbernat commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

On Windows, ReadWriteLock handed SQLite the database path in every directory, so SQLite acted on -journal and -wal files found beside it. It now chooses the route from the directory's access control list (ACL, the list of accounts allowed to act on it) and links the database privately where other accounts can add names, as the POSIX code from #834 does. This PR is based on fix/rw-sqlite-files (#834) and also carries one commit from #835, the owner-only security descriptor helper, until #835 merges.

SQLite opened side files that another account could place beside the database

SQLite derives the -journal, -wal and -shm names from the path it is given. Before the lock runs any statement, SQLite deletes a -wal beside an empty database and replays a rollback journal (its record of a transaction to undo) beside a non-empty one. Replaying a journal also deletes the super-journal file the journal names. In a directory where another account can create files, such as C:\Windows\Temp or C:\Users\Public, that account can place such a file. A -wal the lock account cannot delete made every acquire fail with sqlite3.OperationalError: disk I/O error. A journal made SQLite write into the database and delete a file of the lock account. A read-only journal made every acquire fail with unable to open database file. The Windows branch of _connect has connected by name since #399, and #834 left it unchanged.

_connect now asks whether the directory is private. _directory_is_private reads the owner and the ACL with GetNamedSecurityInfoW. The directory is private when its owner, and every access-allowed entry that grants FILE_ADD_FILE, FILE_ADD_SUBDIRECTORY, FILE_DELETE_CHILD, WRITE_DAC, WRITE_OWNER, GENERIC_WRITE or GENERIC_ALL, names the current account, SYSTEM, Administrators or CREATOR OWNER. Entries that only children inherit count, and an ACL the account cannot read counts as not private. In a private directory SQLite gets the database path. Otherwise the lock makes a .filelock-* directory with the owner-only descriptor from #835 and a random subdirectory inside it, hard links the database there, and gives SQLite the link. Nobody else can create a side file in a directory with that ACL.

While the link is made, an open handle without delete sharing pins the directory's name against a rename, and the ACL is read again once the handle is open. The link's file index must match the database's. On release the lock opens the directory again, compares its file index with the one recorded at creation, removes the link, its side files and the subdirectory by full path while the handle pins the name, then removes the directory. The sweep of directories left by dead holders applies the same checks and parses a name only in a private directory. A filesystem without hard links raises OSError, as on POSIX. The how-to keeps the advice to use a directory other accounts cannot write for such filesystems.

flowchart TD
    A[acquire] --> B{only the account,<br/>SYSTEM, Administrators<br/>can add names?}
    B -- yes --> C[SQLite opens<br/>the database path]
    B -- "no, or ACL unreadable" --> D[owner-only .filelock-* directory<br/>pinned by an open handle]
    D --> E[hard link to the database<br/>file index checked]
    E --> F[SQLite opens the link]
    E -- "no hard links" --> G[OSError]
    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 A,B info;
    class C,F ok;
    class G bad;
Loading

Behavior changes

On Windows, a database in a directory that other accounts can write now opens through a link in a .filelock-* directory beside it, and the filesystem must support hard links. A directory only the locking account, SYSTEM and Administrators can write is unchanged.

This resolves GHSA-9vhv-jxww-xfm5.

WindowsFileLock reduced mode to a read-only attribute and created the lock file
without a DACL of its own, so mode=0o600 gave the file whatever ACL its
directory handed down, the same as no mode at all. The ReadWriteLock database,
which filelock itself asks to be 0o600, had the same gap.

A mode without group and other bits now creates the file with a protected DACL
granting the owner, SYSTEM and Administrators full access, the ACL CPython
applies for os.mkdir(mode=0o700). Other modes have no Windows counterpart and
keep inheriting. As on Unix, a file that already exists keeps what it has.

(cherry picked from commit 61a13d1)
On Windows ReadWriteLock handed SQLite the database path in every
directory. SQLite also uses the -journal and -wal names beside it, so a
file placed under one of those names by another account could fail every
acquire or make SQLite replay a journal and delete the file it names.

The lock now reads the directory's owner and ACL. When anyone besides
the locking account, SYSTEM and Administrators may add or remove names
there, SQLite opens a hard link inside a directory created with an
owner-only ACL, as on POSIX. The link directory is pinned by an open
handle while it is made, checked by file index, and removed with the
same handle open. The sweep of leftover link directories uses the same
checks. A filesystem without hard links raises OSError.
The concepts page and the how-to said Windows connects by name in every
directory. Describe the ACL check and the link route, and keep the
advice about directories for filesystems without hard links.
@gaborbernat gaborbernat added bug security Fixes a published or draft security advisory labels Oct 16, 2026
A directory whose ACL lets another account rewrite the ACL or take
ownership was classified as private, though that account can then grant
itself the right to add names. The check now counts WRITE_DAC and
WRITE_OWNER with the name-creation rights.
@gaborbernat
gaborbernat merged commit 0954ae0 into fix/rw-sqlite-files Oct 17, 2026
44 checks passed
@gaborbernat
gaborbernat deleted the fix/rw-windows-private-link branch October 17, 2026 10:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug security Fixes a published or draft security advisory

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant