Bounds-check pzTail in prepare_v2/v3 span overloads (fix AOOR variant of #430)#663
Open
jamescater2 wants to merge 1 commit intoericsink:mainfrom
Open
Bounds-check pzTail in prepare_v2/v3 span overloads (fix AOOR variant of #430)#663jamescater2 wants to merge 1 commit intoericsink:mainfrom
jamescater2 wants to merge 1 commit intoericsink:mainfrom
Conversation
On native error paths where sqlite3_prepare_v2/v3 returns without writing *pzTail (e.g. sqlite3LockAndPrepare returning SQLITE_MISUSE_BKPT when the db handle fails sqlite3SafetyCheckOk or zSql is null), the wrapper's `out byte* p_tail` stays at zero. The subsequent `(int)(p_tail - p_sql)` can truncate to a negative int depending on the managed-heap address, which makes sql.Slice(len_consumed, len_remain) throw ArgumentOutOfRangeException instead of letting the error rc propagate. Guard the tail-span construction behind an explicit bounds check: only compute the slice when p_tail falls within [p_sql, p_sql + sql.Length]; otherwise return an empty tail. Normal happy paths are unchanged. This is the ArgumentOutOfRangeException variant of the bug family documented in ericsink#108, ericsink#321, ericsink#430, ericsink#479, ericsink#588; those reports surface as AccessViolationException when the stale pointer lands in unmapped memory. Both variants share the same call site (raw.cs:815, already annotated "// ericsink#430 happens here"). Also adds regression tests in src/common/tests_xunit.cs that deterministically fire the AOOR on unpatched builds by closing the db with manual_close_v2 and growing the heap into the bit-31-clear address range. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner
|
Interesting PR. Thanks. I'll take a closer look ASAP. |
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.
Summary
Adds a bounds check on
p_tailbefore the tail-span arithmetic in theReadOnlySpan<byte>overloads ofsqlite3_prepare_v2andsqlite3_prepare_v3. Without the check, native error paths that leave*pzTailunwritten cause the wrapper to throwArgumentOutOfRangeExceptionfromsql.Sliceinstead of letting the errorrcpropagate to the caller.Scope is deliberately narrow — this is the smallest defensive fix for the AOOR symptom. I haven't touched the AV side of the same bug family (#108, #321, #430, #479, #588) because that needs a larger, more invasive change; the AOOR variant closes cleanly here.
Root cause
In
src/providers/provider.tt(and the four regeneratedprovider_e_sqlite3_*.csfiles):Some native paths return without writing
*pzTail— notablysqlite3LockAndPreparereturningSQLITE_MISUSE_BKPTwhensqlite3SafetyCheckOk(db)fails orzSqlis null. On those paths the caller'sout byte* p_tailstays at its.locals initdefault of 0. Then:(p_tail - p_sql)is a 64-bit signed difference. Cast toint, its sign depends on bit 31 of the low 32 bits ofp_sql.len_consumed.sql.Length - negativeoverflows positive, solen_remain > 0is true andsql.Slice(negative, positive)throwsArgumentOutOfRangeExceptionthrough the stack instead of the caller seeingrc == SQLITE_MISUSE.Relationship to existing issues
src/SQLitePCLRaw.core/raw.cs:815already carries the comment// #430 happens here— the call site is known. #108 / #321 / #430 / #479 / #588 are all the same root cause but surface asAccessViolationExceptionwhen the stale/bad pointer lands in unmapped memory rather than valid memory at a bad offset. TheArgumentOutOfRangeExceptionvariant of the same bug seems to be under-reported; every public report I could find is the AV flavour. This PR closes the AOOR variant specifically.Fix
Six added lines per overload (for prepare_v2 and prepare_v3):
On a valid
p_tailwithin the SQL buffer, behaviour is identical. On ap_tailoutside[p_sql, p_sql + sql.Length], the wrapper returns an empty tail and letsrcpropagate — the caller now sees the realSQLITE_MISUSE(or whatever native returned) instead of an AOOR from inside the wrapper.Applied to:
src/providers/provider.tt(generator source of truth)src/SQLitePCLRaw.provider.e_sqlite3/Generated/(funcptrs × win/notwin, prenet5 × win/notwin)Other providers (
sqlite3,sqlcipher,winsqlite3,dynamic_cdecl,dynamic_stdcall) share the same template — regenerating them will pick up the same fix on the nextgen_providersrun.Regression tests
Added in
src/common/tests_xunit.cs:test_prepare_v2_span_tolerates_uninitialised_pzTailtest_prepare_v3_span_tolerates_uninitialised_pzTailEach:
manual_close_v2()to zero the handle.rc == SQLITE_MISUSEandtail.IsEmptyon every iteration.Verified locally against both the patched and unpatched provider packages via a scratch project:
Same test, same address range, opposite outcomes.
Test project modernisation
While I was here,
src/tests/tests.csprojon currentmaindidn't build on modern .NET SDKs — it targetednetcoreapp3.1, project-referenced a deletedSQLitePCLRaw.nativelibrary, and called a 3-argNativeLibrary.Loadoverload that no longer exists. I bumped it to the minimum needed to run:TargetFramework→$(tfm_net)(net8.0)SQLitePCLRaw.nativelibraryand unusedSQLitePCLRaw.provider.dynamic_cdeclproject referencesProjectReferencetoSQLitePCLRaw.provider.e_sqlite3andPackageReferencetoSourceGear.sqlite3for the native librarysrc/tests/my_batteries_v2.csto init againstSQLite3Provider_e_sqlite3directly instead of the deleted dynamic-load pathDotNetCliToolReferencefordotnet-reportgenerator-cli(deprecated syntax)Full suite now runs green:
dotnet test src/tests/tests.csproj -c Release— 124 / 124 pass in ~300 ms on my box. Happy to split this into a separate prep-PR if you'd prefer it that way.Verification on a downstream consumer
A WPF / .NET 10 Windows application that was reliably tripping this AOOR at ~40 % per full-suite run (6 / 15 runs of a 32-way concurrent upsert test, hitting
ExecuteScalar("PRAGMA journal_mode=WAL;")throughMicrosoft.Data.Sqlite10.0.7) ran cleanly for 30 / 30 full-suite runs against a locally-builtSQLitePCLRaw.bundle_e_sqlite3with this patch applied — zero AOOR, zero AV. All environmental mitigations were peeled off for the A/B (test-collection serialization removed, contention tune-down reverted to 32 × 100). A fresh stack trace from one of the failing unpatched runs is available if useful.