handles.cs: Interlocked CAS for sqlite3 extra slot#666
Merged
ericsink merged 2 commits intoMay 6, 2026
Conversation
Two related fixes around the sqlite3.extra slot that holds hook-container state (commit hooks, update hooks, etc.): Audit ericsink#7 — GetOrCreateExtra used to do a plain check-and-set: if (extra != null) return (T)extra; else { var q = f(); extra = q; return q; } Two threads racing the first hook registration can both observe extra == null, both create T, and both write into extra. One write wins; the other container holds GCHandles for delegates the db no longer tracks. When the db closes, only the winning container is disposed — the loser's GCHandles leak, and if SQLite still holds the loser's native function pointer, it outlives managed tracking and can be invoked on a freed handle. Replace with Volatile.Read + Interlocked. CompareExchange; dispose the losing candidate on a CAS miss. Audit ericsink#8 — dispose_extra used a non-atomic null-and-dispose: if (extra != null) { extra.Dispose(); extra = null; } Pair it with Interlocked.Exchange so disposal and a concurrent creation path can't race on a half-swapped field. Adds test_get_or_create_extra_returns_same_instance_under_contention which spawns N threads racing GetOrCreateExtra and asserts every caller gets the same instance back. Note: this branch is independent of the PR currently open for the span-overload pzTail fix (fix/prepare-v2-pztail-bounds-check). The new test compiles fine but main's tests.csproj currently fails to build due to pre-existing test-infrastructure drift that the other PR's test refactor also fixes; the two will need to land together for CI to go green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner
|
What's the context for these pull requests? Are they AI-generated? |
Contributor
Author
|
I am building an in memory caching architecture and trying to run unit tests in parallel.
We kept getting crashes which hinted at an issue with sqlite. We tracked down and fixed the first one but could not find the cause of the other three.
I then asked Claude to audit sqlite and it came up with 8 critical errors. We fixed those errors and retested. That resolved two of the three errors.
I spend most of today matching the 8 modifications to the crashes. After a lot of testing I identified the changes that actually fixed our issues.
You saw the first PR last night. The second PR is just an update to the first one. The third PR is a fix for the threading issue.
Many thanks,
James.
|
Owner
|
Thanks for the background. I'll be looking more closely at these soon. |
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.
handles.cs: Interlocked CAS for sqlite3 extra slot
Summary
Make
sqlite3.GetOrCreateExtraanddispose_extrauseInterlockedso concurrent hook registration and disposal can't race the backingextrafield.Context
sqlite3.extrais a lazily-created container that holds managed state for hooks — commit / rollback / update hooks, authoriser, progress handler, trace, profile, and thehook_handlesthat pin user delegates viaGCHandleso SQLite's native function pointers remain valid.#7—GetOrCreateExtraTOCTOU raceCurrent code:
Two threads racing first registration:
extra == nullf()(each builds its own container)extra = q; one store wins, the other'sqis orphanedThe orphaned
qholdsGCHandles for the user's delegates, but the db'sextraslot now points at a different container. On close, onlyextra's container is disposed — the orphanedGCHandlesleak. Worse, if both threads registered their callback via the native API, the later native registration overwrites the earlier one; SQLite still holds the overwritten function pointer, but theGCHandleprotecting that delegate is no longer reachable fromextra→ use-after-free when SQLite next invokes it.Fix:
Volatile.Readfor the fast path,Interlocked.CompareExchangefor the slow path, dispose the losing candidate on CAS miss.#8—dispose_extranon-atomic read-and-nullCurrent code:
Narrow but real: a finalizer-thread
dispose_extraconcurrent with a user-threadGetOrCreateExtracan reorder around the non-atomic read/write. Pairingdispose_extrawithInterlocked.Exchangemakes the read-and-null a single atomic step and composes cleanly with the CAS in#7.Changes
src/SQLitePCLRaw.core/handles.csusing System.Threading; rewriteGetOrCreateExtrawith CAS; rewritedispose_extrawithInterlocked.Exchangesrc/common/tests_xunit.cstest_get_or_create_extra_returns_same_instance_under_contentionTests
test_get_or_create_extra_returns_same_instance_under_contentionspawnsmax(4, ProcessorCount)threads that raceGetOrCreateExtra, all unblocked by aManualResetEventSlim, and asserts every caller gets the same instance back viaAssert.Same. On unpatched code under contention it eventually observes the race and returns distinct instances; the assertion fails. Patched code passes deterministically.Heads-up on CI
The lib project (
SQLitePCLRaw.core.csproj) builds clean onmain. The test project (tests.csproj) currently does not —src/tests/my_batteries_v2.csonmainreferencesNativeLibrary.Load(name, assy, flags)andNativeLibrary.WHERE_PLAIN, neither of which exist in the current core surface. This is pre-existing test-infrastructure drift unrelated to this PR; #663's test-harness refactor resolves it as a side effect. CI on this PR will be red until that lands; the handles.cs changes themselves are correct onmain.Independence
This PR is independent of #663 at the library level —
handles.csis untouched by #663. The only overlap is the shared test-harness breakage noted above. The fix stands on its own merits.