Skip to content

Release 2.2026.9.7 — Fix CVE-2025-6965 and the SQLitePCLRaw Packaging Gap - #27

Merged
imranakram merged 5 commits into
masterfrom
fix/cve-2025-6965-sqlite-and-dead-deps
Sep 7, 2026
Merged

imranakram merged 5 commits into
masterfrom
fix/cve-2025-6965-sqlite-and-dead-deps

Conversation

@imranakram

Copy link
Copy Markdown
Owner

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:

System.TypeLoadException: Could not load type 'SQLitePCL.utf8z' from assembly
'SQLitePCLRaw.core, Version=1.1.13.388'
   at SQLite.SQLite3.ColumnString(...)
   at Xrm.Persistent.Collections.Backend.PersistentBlobCache.CreateConnection()

SQLitePCLRaw.core 1.1.13 is being loaded, but SQLite-net 1.9.172 is compiled against SQLitePCLRaw 2.x and calls SQLitePCL.utf8z, which does not exist in 1.x. Verified directly against both assemblies:

assembly contains utf8z
SQLitePCLRaw.core 2.1.11 yes
SQLitePCLRaw.core 1.1.13 no

1.1.13 is exactly the version this library used before the 2.0.0 upgrade.

Nothing caught it because the .nuspec never declared SQLitePCLRaw.core at 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_sqlite3 2.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/net461 layout as 2.1.11, and needs no binding-redirect changes.

Consumers of the previous release were pulling an unpatched native binary. The .nuspec floored bundle_e_sqlite3 at 2.1.10, and because NuGet resolves lowest-applicable, downstream projects resolved lib.e_sqlite3 2.1.10 — older than what this library was built against. Fixing only packages.config would 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.Memory declared 4.5.5 against 4.6.3 built). They arrive via Microsoft.CrmSdk.CoreAssembliesSystem.Text.Json at versions Microsoft ships and tests together.

Removed SQLitePCLRaw.bundle_e_sqlite3, also 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.

Added 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.

Eight declared dependencies now resolve to a 20-package closure.


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:

  1. 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 one _dbSemaphore.
  2. Semaphores were acquired inside the try. If the wait threw — ObjectDisposedException after Dispose() — the finally released a permit never taken, corrupting the count. Post-Dispose() calls now surface a clean exception.
  3. The connection was published before it was initialised. CreateConnection() assigned _db, then set the journal mode and created the schema; a caller hitting the non-null fast path in between could query a CacheItem table that did not exist yet.
  4. Every chunked query ran twice. 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 re-executing every query. Bulk reads were doing double the database work.
  5. GetObjectsCreatedAt<T>() walked its keys argument twice unmaterialized, so a single-use sequence produced a partial result.

The semaphore waits now use ConfigureAwait(false), which answers the // todo that was on the write path. It matters: LocalDictionary blocks on these tasks with .Wait()/.Result throughout, which is the shape that deadlocks on a synchronization context.

Behaviour change worth reviewing: bulk reads that previously overlapped now queue behind each other and behind writes. With one shared connection there was never real read parallelism to lose — the overlap was the bug — but it is a change. Genuine read concurrency needs a connection pool or SQLiteAsyncConnection, which is a much larger piece of work.


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 every consuming application to add or update a binding redirect just to take a patch. AssemblyFileVersion carries 2.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.IdentityModel 7.0.0 — Windows Identity Foundation 3.5, a lib/net35 assembly 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.Sdk references System.IdentityModel — the BCL assembly, already satisfied by the framework reference — which is the likely origin of the confusion.
  • SQLitePCLRaw.config.e_sqlite3 3.0.2 — sat amid an otherwise-2.1.11 stack with nothing depending on it at that version, shipped a conflicting second batteries_v2.dll, and triggered a package-downgrade error under modern resolution.

CI

The workflow triggered on main, vNext and develop for pushes, and main / vNext for pull requests. None of those branches exist here — the default branch is master. CI has never run on this repository. The pull_request filter matches the target branch, so adding master there is what makes a PR into master build.

This PR should be the first CI run the repo has ever had.


Verification

  • Clean restore with every removed package physically absent from the restore folder — NuGet never requested them.
  • Rebuild succeeds in Debug and Release (x64).
  • 48/48 tests pass, stable across six consecutive runs (43 before, 5 added).
  • The assembly stamps AssemblyVersion 2.0.0.0 and FileVersion 2.2026.9.7.
  • nuget pack produces Xrm.Persistent.Collections.2.2026.9.7.nupkg.
  • End-to-end consumer test: packed to a local feed, restored into a fresh net48 project whose only PackageReference is this library, and run. It builds with 0 warnings, resolves lib.e_sqlite3 2.1.13, deploys a native e_sqlite3.dll reporting SQLite 3.53.3, pulls System.Text.Json 8.0.5 transitively, and round-trips an Entity carrying EntityReference, OptionSetValue and Money through a real database file.
  • A NuGetAudit over 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_Once fails 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 synchronous lives on bench/sqlite-synchronous-mode, kept out of the solution so CI does not build it. Summary of what it found on a developer machine: synchronous = NORMAL would be worth ~10× on single-item writes, but batching is worth ~55× and costs nothing in durability, and synchronous does not affect reads at all — so it would do nothing for the ContainsKey-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.
  • The 2.0.0 CHANGELOG entry claims xunit.runner.visualstudio 3.0.0 and Microsoft.TestPlatform.ObjectModel 17.12.0; the repo has 2.8.2 and 17.10.0, and the heading is still 2025-01-XX.

After merging

Merging does not publish. publish-nuget.yml triggers on a pushed tag only:

git checkout master && git pull
git tag v2.2026.9.7
git push origin v2.2026.9.7

The workflow rewrites the .nuspec version from the tag name, so the tag must be exactly v2.2026.9.7 or the published version will disagree with the CHANGELOG and the assembly.

imranakram and others added 5 commits September 7, 2026 09:55
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>
@imranakram imranakram self-assigned this Sep 7, 2026
@imranakram
imranakram merged commit f8e85b0 into master Sep 7, 2026
1 check passed
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