Release 2.2026.9.7 — Fix CVE-2025-6965 and the SQLitePCLRaw Packaging Gap - #27
Merged
Merged
Conversation
Upgrade SQLitePCLRaw.lib.e_sqlite3 2.1.11 -> 2.1.13 to resolve GHSA-2m69-gcr7-jv3q / CVE-2025-6965 (High, CVSS 7.2). The 2.1.11 package embeds SQLite 3.49.1; the flaw (aggregate terms exceeding available columns, leading to memory corruption) is fixed in SQLite 3.50.2. 2.1.13 embeds SQLite 3.53.3. 2.1.13 is a drop-in for net48: the package carries native assets only (no managed assembly), keeps the same buildTransitive/net461 layout as 2.1.11, and needs no binding-redirect changes. The .nuspec previously floored SQLitePCLRaw.bundle_e_sqlite3 at 2.1.10. Because NuGet resolves lowest-applicable, consumers of this package were pulling lib.e_sqlite3 2.1.10 - older than what this repo built against, and vulnerable. Raise that floor to 2.1.13 and add an explicit lib.e_sqlite3 floor so downstream projects cannot resolve an unpatched native binary. Also drop two packages that nothing needs: - Microsoft.IdentityModel 7.0.0 is Windows Identity Foundation 3.5, a lib/net35 assembly superseded by WIF's integration into .NET 4.5. No package in the graph depends on it, no source file uses it, and the built assembly carried zero references to it. Microsoft.Xrm.Sdk references System.IdentityModel (the BCL assembly, already satisfied by the framework reference), not microsoft.identitymodel - likely the original reason it was added. - SQLitePCLRaw.config.e_sqlite3 3.0.2 sat amid an otherwise-2.1.11 stack with nothing depending on it at that version. It shipped a second copy of SQLitePCLRaw.batteries_v2.dll conflicting with the 2.1.11 copy the projects actually reference from bundle_green, and triggered a package-downgrade error under modern resolution. Verified: clean restore, Rebuild in Debug and Release x64, all 43 tests pass, deployed e_sqlite3.dll reports SQLite 3.53.3, and a NuGet audit across all remaining packages reports no vulnerabilities. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bump the package to 2.2026.9.7 (CalVer 2.YYYY.M.D) and rewrite the release notes and changelog to describe this as what it is: a security release, not a performance one. Version metadata had drifted - the published 2.2026.3.1 package contained an assembly stamped 2.2026.3.2. Package, AssemblyVersion and AssemblyFileVersion are now all 2.2026.9.7. Also align SQLitePCLRaw.bundle_e_sqlite3 in packages.config with the 2.1.13 floor the .nuspec now publishes, so the version built against and the version declared to consumers agree. Fix two pieces of stale package metadata: the <repository> element declared branch "main" while the repository's default branch is "master", and the copyright year was still 2025. Verified: restore, Rebuild in Release x64, 43/43 tests pass, the built assembly stamps FileVersion 2.2026.9.7, and `nuget pack` produces Xrm.Persistent.Collections.2.2026.9.7.nupkg with the expected dependency floors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Concurrency ----------- PersistentBlobCache holds a single SQLiteConnection, which sqlite-net does not make thread-safe, so every operation against it has to be serialized. It was not: - Read() took no lock at all while Write() held _writeSemaphore, so a read could execute against a connection a concurrent write was mutating. Both now share a single _dbSemaphore. - CreateConnection() and Write() acquired their semaphore inside the try block. If the wait threw - ObjectDisposedException after Dispose(), for example - the finally released a permit that had never been taken, corrupting the count. The acquire now happens before the try, so a post-Dispose call surfaces a clean ObjectDisposedException. - CreateConnection() assigned _db and only then set the journal mode and created the schema, so a caller reaching the non-null fast path in between could query a CacheItem table that did not exist yet. The connection is built in a local and published only once initialised. - Get(IEnumerable, string) and GetObjectsCreatedAt<T>() built a lazy IEnumerable<Task<..>>, awaited it with Task.WhenAll, then re-enumerated it through .Result - creating a second set of tasks and running every chunked query against the database twice. Both are now materialized before being awaited. - GetObjectsCreatedAt<T>() also walked its keys argument twice without materializing it, so a single-use sequence produced a partial result. The semaphore waits use ConfigureAwait(false), which answers the todo that was on the write path. It matters here: LocalDictionary blocks on these tasks with .Wait()/.Result, which would deadlock on a context. Versioning ---------- AssemblyVersion is pinned to 2.0.0.0 and no longer tracks the CalVer release. It is the identity the CLR binds against, so bumping it every release forced consumers to add or update a binding redirect just to take a patch. AssemblyFileVersion carries the release version. Anyone upgrading from 2.2026.3.1 can drop an existing redirect for this assembly or retarget it to 2.0.0.0. Tests ----- Five tests added. GetCreatedAt_Enumerates_The_Supplied_Keys_Only_Once fails against the pre-fix code, which was confirmed by reverting the production change and re-running. The other four are stress coverage for the shared-connection paths and do not deterministically reproduce a race - they guard against regressions rather than prove the fix. Verified: Rebuild in Release x64, 48/48 tests pass across six consecutive runs, the assembly stamps AssemblyVersion 2.0.0.0 and FileVersion 2.2026.9.7, and nuget pack succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The published dependency list had drifted from what the library needs, in both directions. Removed, because nothing needs them declared directly: - Microsoft.Bcl.AsyncInterfaces, System.Buffers, System.Memory, System.Numerics.Vectors, System.Runtime.CompilerServices.Unsafe, System.Threading.Tasks.Extensions, System.ValueTuple. These were hand-maintained transitive leaves and nothing kept them in sync - four of them floored *below* the versions the library was built against (System.Memory 4.5.5 declared against 4.6.3 built). They arrive through Microsoft.CrmSdk.CoreAssemblies -> System.Text.Json at versions Microsoft ships and tests together. - SQLitePCLRaw.bundle_e_sqlite3, also dropped from both packages.config files. Nothing referenced it: no csproj HintPath or Import points into it, and the solution builds and passes its tests with the package physically absent from the restore folder. Declaring it pulled a second, conflicting copy of SQLitePCLRaw.batteries_v2.dll alongside the one bundle_green supplies - the same failure mode as the config.e_sqlite3 package removed earlier on this branch. Added, because a fresh consumer genuinely needs them: - SQLitePCLRaw.bundle_green, .core and .provider.dynamic_cdecl at 2.1.11. sqlite-net initialises its native provider through SQLitePCLRaw.batteries_v2, and sqlite-net-pcl 1.9.172 alone floors that stack at 2.1.2. These floors match what this release is built and tested against. Correcting an earlier assessment on this branch: System.Text.Json, System.Text.Encodings.Web and System.ServiceModel.Http/Primitives were never missing from a consumer's graph. Microsoft.CrmSdk.CoreAssemblies 9.0.2.60 declares them itself and floors System.Text.Json at 8.0.5, the build patched for CVE-2024-43485. Verification ------------ The eight declared dependencies resolve to a 20-package closure. The release was packed to a local feed and restored into a fresh net48 project whose only PackageReference is this library, then run. That consumer: - builds with 0 warnings and 0 errors - resolves SQLitePCLRaw.lib.e_sqlite3 2.1.13 and deploys a native e_sqlite3.dll reporting SQLite 3.53.3 - resolves System.Text.Json 8.0.5 transitively - round-trips an Entity carrying EntityReference, OptionSetValue and Money attributes through a real database file - reports no known vulnerabilities under a NuGetAudit over the whole closure at the lowest severity threshold Solution still builds and 48/48 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The workflow triggered on main, vNext and develop for pushes, and on main and vNext for pull requests. The repository's default branch is master, and none of those other branches exist here, so CI has never run: not on a push to master, and not on any pull request into it. The pull_request filter matches the target branch, so adding master there is what makes a PR into master build. This is the same main/master confusion as the .nuspec <repository> element corrected earlier on this branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Fixes a high-severity CVE in the bundled native SQLite binary, and repairs the packaging gap that has been causing
TypeLoadException: Could not load type 'SQLitePCL.utf8z'in downstream jobs.Release 2.2026.9.7. No API changes; existing database files are unaffected.
Why now
A downstream job is failing on its first cache operation:
SQLitePCLRaw.core1.1.13 is being loaded, butSQLite-net1.9.172 is compiled against SQLitePCLRaw 2.x and callsSQLitePCL.utf8z, which does not exist in 1.x. Verified directly against both assemblies:utf8zSQLitePCLRaw.core2.1.11SQLitePCLRaw.core1.1.131.1.13 is exactly the version this library used before the 2.0.0 upgrade.
Nothing caught it because the
.nuspecnever declaredSQLitePCLRaw.coreat all. NuGet had no constraint to enforce, so it could not warn — it did not know a requirement existed. This PR declares it, so restore now either resolves a compatible version or raises a visible conflict.Security — CVE-2025-6965
GHSA-2m69-gcr7-jv3q, High, CVSS 7.2.
SQLitePCLRaw.lib.e_sqlite32.1.11 → 2.1.13.The 2.1.11 package embeds SQLite 3.49.1; the flaw — aggregate terms exceeding the available column count, leading to memory corruption — is fixed in SQLite 3.50.2. 2.1.13 embeds 3.53.3.
2.1.13 over the 3.x line deliberately: it carries native assets only (no managed assembly), keeps the same
buildTransitive/net461layout as 2.1.11, and needs no binding-redirect changes.Consumers of the previous release were pulling an unpatched native binary. The
.nuspecflooredbundle_e_sqlite3at 2.1.10, and because NuGet resolves lowest-applicable, downstream projects resolvedlib.e_sqlite32.1.10 — older than what this library was built against. Fixing onlypackages.configwould have fixed the build here and left every consumer exposed.Dependency list rebuilt from the real reference graph
Removed
Microsoft.Bcl.AsyncInterfaces,System.Buffers,System.Memory,System.Numerics.Vectors,System.Runtime.CompilerServices.Unsafe,System.Threading.Tasks.Extensions,System.ValueTuple. These were hand-maintained transitive leaves and nothing kept them in sync — four floored below the versions built against (System.Memorydeclared 4.5.5 against 4.6.3 built). They arrive viaMicrosoft.CrmSdk.CoreAssemblies→System.Text.Jsonat versions Microsoft ships and tests together.Removed
SQLitePCLRaw.bundle_e_sqlite3, also from bothpackages.configfiles. Nothing referenced it — no csprojHintPathorImportpoints into it, and the solution builds and passes its tests with the package physically absent from the restore folder. Declaring it pulled a second, conflicting copy ofSQLitePCLRaw.batteries_v2.dll.Added
SQLitePCLRaw.bundle_green,.coreand.provider.dynamic_cdeclat 2.1.11.SQLite-netinitialises its native provider throughSQLitePCLRaw.batteries_v2, andsqlite-net-pcl1.9.172 alone floors that stack at 2.1.2.Eight declared dependencies now resolve to a 20-package closure.
Concurrency
PersistentBlobCacheholds a singleSQLiteConnection, which sqlite-net does not make thread-safe, so every operation against it has to be serialized. It was not:Read()took no lock at all whileWrite()held_writeSemaphore, so a read could execute against a connection a concurrent write was mutating. Both now share one_dbSemaphore.try. If the wait threw —ObjectDisposedExceptionafterDispose()— thefinallyreleased a permit never taken, corrupting the count. Post-Dispose()calls now surface a clean exception.CreateConnection()assigned_db, then set the journal mode and created the schema; a caller hitting the non-null fast path in between could query aCacheItemtable that did not exist yet.Get(IEnumerable, string)andGetObjectsCreatedAt<T>()built a lazyIEnumerable<Task<…>>, awaited it withTask.WhenAll, then re-enumerated it through.Result— creating a second set of tasks and re-executing every query. Bulk reads were doing double the database work.GetObjectsCreatedAt<T>()walked itskeysargument twice unmaterialized, so a single-use sequence produced a partial result.The semaphore waits now use
ConfigureAwait(false), which answers the// todothat was on the write path. It matters:LocalDictionaryblocks on these tasks with.Wait()/.Resultthroughout, which is the shape that deadlocks on a synchronization context.Versioning
AssemblyVersionis pinned to2.0.0.0and no longer tracks the CalVer release. It is the identity the CLR binds against, so bumping it every release forced every consuming application to add or update a binding redirect just to take a patch.AssemblyFileVersioncarries2.2026.9.7.These had also drifted: the published 2.2026.3.1 package contained an assembly stamped 2.2026.3.2.
Consumers upgrading from 2.2026.3.1 can drop any binding redirect for this assembly, or retarget it to
2.0.0.0.Removed
Microsoft.IdentityModel7.0.0 — Windows Identity Foundation 3.5, alib/net35assembly superseded by WIF's integration into .NET 4.5. No package in the graph declared it, no source file used it, and the built assembly carried zero references to it.Microsoft.Xrm.SdkreferencesSystem.IdentityModel— the BCL assembly, already satisfied by the framework reference — which is the likely origin of the confusion.SQLitePCLRaw.config.e_sqlite33.0.2 — sat amid an otherwise-2.1.11 stack with nothing depending on it at that version, shipped a conflicting secondbatteries_v2.dll, and triggered a package-downgrade error under modern resolution.CI
The workflow triggered on
main,vNextanddevelopfor pushes, andmain/vNextfor pull requests. None of those branches exist here — the default branch ismaster. CI has never run on this repository. Thepull_requestfilter matches the target branch, so addingmasterthere is what makes a PR intomasterbuild.This PR should be the first CI run the repo has ever had.
Verification
Rebuildsucceeds in Debug and Release (x64).AssemblyVersion 2.0.0.0andFileVersion 2.2026.9.7.nuget packproducesXrm.Persistent.Collections.2.2026.9.7.nupkg.PackageReferenceis this library, and run. It builds with 0 warnings, resolveslib.e_sqlite32.1.13, deploys a nativee_sqlite3.dllreporting SQLite 3.53.3, pullsSystem.Text.Json8.0.5 transitively, and round-trips anEntitycarryingEntityReference,OptionSetValueandMoneythrough a real database file.NuGetAuditover the whole consumer closure at the lowest severity threshold reports no known vulnerabilities.Caveat on the new tests, stated plainly: of the five added, only
GetCreatedAt_Enumerates_The_Supplied_Keys_Only_Oncefails against the pre-fix code — confirmed by reverting the production change and re-running. The other four are stress coverage for the shared-connection paths and do not deterministically reproduce a race. They guard against regressions; they do not prove the fix.Performance
No performance change is claimed. This is a security and correctness release.
A benchmark for
PRAGMA synchronouslives onbench/sqlite-synchronous-mode, kept out of the solution so CI does not build it. Summary of what it found on a developer machine:synchronous = NORMALwould be worth ~10× on single-item writes, but batching is worth ~55× and costs nothing in durability, andsynchronousdoes not affect reads at all — so it would do nothing for theContainsKey-per-item loop in the failing job's stack. Measure on the real server before changing anything.Not addressed here
Dispose()still races with in-flight operations. Making it block on the semaphore risks a deadlock; that deserves a decision rather than a drive-by change.xunit.runner.visualstudio3.0.0 andMicrosoft.TestPlatform.ObjectModel17.12.0; the repo has 2.8.2 and 17.10.0, and the heading is still2025-01-XX.After merging
Merging does not publish.
publish-nuget.ymltriggers on a pushed tag only:git checkout master && git pull git tag v2.2026.9.7 git push origin v2.2026.9.7The workflow rewrites the
.nuspecversion from the tag name, so the tag must be exactlyv2.2026.9.7or the published version will disagree with the CHANGELOG and the assembly.