Skip to content

馃悰 fix(fork): preserve parent ownership - #653

Merged
gaborbernat merged 1 commit into
fix/640-async-cancellationfrom
fix/634-fork-ownership
Jul 14, 2026
Merged

gaborbernat merged 1 commit into
fix/640-async-cancellationfrom
fix/634-fork-ownership

Conversation

@gaborbernat

Copy link
Copy Markdown
Member

A forked child inherits a parent's descriptors and Python lock state. Releasing an inherited native lock can unlock the
shared open-file description, while releasing a soft lock can unlink the parent's marker. Same-path non-singleton soft
read/write locks could also disappear from the fork registry before child cleanup. This caused the ownership violation
reported in #634.

Track held and provisional descriptors by object identity and creator PID. The child checks the descriptor identity
before closing it, without issuing an OS unlock or removing a marker, then clears the inherited in-memory lock state. An
unavailable identity leaves the descriptor untouched because an earlier child callback may have reused its number.

Coordinate descriptor transitions with fork callbacks across threads and concurrent coroutine tasks. Registration and
rollback failures remain observable, including cancellation paths inherited from
#652. Reentrant singleton construction during a parent callback reports
a RuntimeError. Child cache insertion rejects an instance whose construction crossed the fork boundary.

This branch depends on #652 so fork tracking can wrap its cancellation-safe async transitions. Rebase it onto main
after merging #652.

Fixes #634.

@gaborbernat
gaborbernat force-pushed the fix/634-fork-ownership branch from 3134f0a to ff61d31 Compare July 14, 2026 07:35
@gaborbernat
gaborbernat marked this pull request as ready for review July 14, 2026 07:44
@gaborbernat
gaborbernat force-pushed the fix/640-async-cancellation branch from e9782a8 to 63a038a Compare July 14, 2026 07:46
Track held and provisional descriptors across process forks so child cleanup
cannot change ownership that belongs to its parent. Reinitialize inherited lock
state before child code runs.

Keep acquisition and rollback failures observable when descriptor registration
also fails, and avoid closing descriptor numbers that cannot be identity-checked
at the fork boundary.

Fixes #634
@gaborbernat
gaborbernat force-pushed the fix/634-fork-ownership branch from ff61d31 to 9cfc800 Compare July 14, 2026 07:46
@gaborbernat
gaborbernat merged commit 04546bf into fix/640-async-cancellation Jul 14, 2026
33 checks passed
@gaborbernat
gaborbernat deleted the fix/634-fork-ownership branch July 14, 2026 13:43
gaborbernat added a commit that referenced this pull request Jul 14, 2026
SQLite forbids a process from using or closing a database connection
inherited through `fork()`. Read-write locks kept one connection for the
object's lifetime, so a child could close its parent's handle during
interpreter shutdown and corrupt the database. Each outer acquisition
now owns its connection, and a forked child abandons inherited handles
according to [SQLite's fork
guidance](https://sqlite.org/howtocorrupt.html#_carrying_an_open_database_connection_across_a_fork_).
Closes #635.

The child rejects an active database by canonical path or inode until
`exec()`. CPython 3.10 and 3.11 keep inherited base connections alive
through a raw-reference escrow; later CPython versions use the guarded
subclass finalizer. The implementation follows the [CPython 3.10
`_sqlite`
lifecycle](https://github.com/python/cpython/blob/v3.10.20/Modules/_sqlite/connection.c)
and [current CPython
source](https://github.com/python/cpython/blob/main/Modules/_sqlite/connection.c).
A child forked from a SQLite callback waits for the call to leave
SQLite, or exits with status 70 if an earlier audit hook prevents
coordination. Fork registration uses a reentrant gate because connection
finalizers can register another lock while the current thread owns that
gate.

PyPy can retain unsafe process-wide SQLite state after tracked
connections close. A PyPy child rejects read-write locks when the parent
used SQLite after importing filelock; importing filelock in the child
cannot detect earlier parent use. The public acquisition interfaces stay
unchanged. Final release closes the per-acquisition connection. A local
2,000-acquisition benchmark measured 5.7 microseconds for the former
persistent connection and 45.6 microseconds for the per-acquisition
design.

This replaces #654, which GitHub closed after merging #653 deleted its
base branch `fix/634-fork-ownership`. The branch now sits on `main` and
carries the same work.

It also carries the `close_failure` fixture change from #656, without
which the `pypy3.11` job fails here. Landing #656 first drops that
commit from this branch on the next rebase.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant