Skip to content

verifier-service: sqlx 0.9 → rusqlite 0.42, clearing RUSTSEC-2026-0459 (stacked on #3535) - #3537

Merged
brandon-coproduct merged 1 commit into
deps/rustsec-20261113from
deps/verifier-rusqlite-0459
Nov 14, 2026
Merged

brandon-coproduct merged 1 commit into
deps/rustsec-20261113from
deps/verifier-rusqlite-0459

Conversation

@brandon-coproduct

Copy link
Copy Markdown
Contributor

Stacked on #3535. Base: deps/rustsec-20261113. Neither PR clears audit and deny on its own:

They are separate so the storage change is reviewed on its own. Once it is approved, merging it into #3535's branch makes #3535 pass deny and audit with every advisory cleared and no ignore. Its diff against that base is the storage change and nothing else.

Why a storage change, not a version bump

RUSTSEC-2026-0459 is libsqlite3-sys 0.30.1, which nucleus-verifier-service reaches through sqlx-sqlite 0.9.0. Every bundled SQLite before 3.54.0 can return uninitialized page-cache memory when a database is opened beside a -wal file with a different page size.

No sqlx release, pre-releases included, allows the patched libsqlite3-sys (>= 0.39):

sqlx-sqlite requires libsqlite3-sys
0.9.0 >=0.30.1, <0.38.0
0.9.0-alpha.1 <0.36.0
every 0.8.x ^0.30.1

Upstream main allows <0.40.0 and transact-rs/sqlx#4525 proposes <0.41, but neither is released. So this moves the service to rusqlite 0.42, which uses libsqlite3-sys 0.40 (SQLite 3.54+).

Semantics first

docs/verifier-service-storage.md states what the database and the transparency log guarantee (G1–G8):

  • first write wins per bundle;
  • persistence never changes a verdict;
  • only successful verifications are logged;
  • the hash-chain definition;
  • reading the tip and appending happen in one transaction;
  • seq is never reused;
  • foreign keys are enforced;
  • migrations run once, and an edited migration is refused.

It also states what is not guaranteed, what the driver change keeps and changes, and how each was checked.

Compatibility with existing databases

  • tests/fixtures/sqlx-0.9-created.db was written by this crate at 7f511159f (sqlx 0.9.0, libsqlite3-sys 0.30.1), through its public functions. It holds three verifications and two log entries. src/db_sqlx_compat_tests.rs checks four things against it:
    • sqlx's stored checksums and descriptions equal this build's;
    • opening the file twice leaves _sqlx_migrations unchanged, including installed_on and execution_time;
    • every row reads back, and the stored tip equals the recomputed chain;
    • the next append continues from sqlx's tip as seq 3.
  • With the checksum computed over a trimmed script, all four tests fail with checksum mismatch. Restored, they pass.
  • The migration SQL files are unchanged.
  • Both binaries on one file. The base branch's binary (sqlx) and this branch's binary (rusqlite) were started in turn against the same --db file.
    • On a copy of the fixture, in the order old, new, old, new, each start served GET /v1/log/size as {"tree_size":2} and rebuilt merkle_leaves=2, with no errors.
    • On a fresh file, in the order new, old, new, each start served {"tree_size":0}.

Behaviour that changes

  • One connection instead of a pool of 8. It sits behind a mutex, calls run on tokio's blocking pool, and reads no longer run in parallel. The pragmas are unchanged: WAL, synchronous=NORMAL, foreign_keys=ON, and a 5 s busy timeout.
  • --db query parameters (?mode=…) are refused. sqlx accepted them.
  • The migration list comes from build.rs. It is derived from migrations/ instead of sqlx::migrate!.

Evidence

Run on macOS arm64 at this head (stacked on 023828bed).

  • cargo deny check advisories bans licenses sources prints advisories ok, bans ok, licenses ok, sources ok.
  • cargo audit --deny warnings exits 0.
  • nucleus-verifier-service: cargo test --locked passes 116 tests (64 lib, 1, 51 integration).
  • cargo clippy --all-targets -D warnings passes for nucleus-verifier-service, ctf-mcp and ctf-server.
  • Cargo.lock is cargo's own re-resolution of deps: rmcp 3 and lockfile bumps clear five of the six open RustSec advisories #3535's lockfile with this manifest. sqlx and its transitive crates leave; rusqlite brings 8.
  • Linux x86_64, on an earlier revision of this code (before the compatibility tests and the semantics note): clippy -D warnings and the crate's tests pass.

Known, out of scope

Both are documented in docs/verifier-service-storage.md. Both behave the same under sqlx on main.

  • The retention sweep never deletes a logged verification. retention::sweep_once runs DELETE FROM verifications WHERE submitted_at < ?. With foreign_keys=ON, that fails with FOREIGN KEY constraint failed whenever a row it would delete is referenced by log_entries, which is every successful verification. The statement is atomic, so the sweep deletes nothing and logs a warning. I confirmed this with a throwaway test. Fixing it means deciding whether the foreign key or the sweep's scope gives way.
  • The in-memory Merkle tree can take leaves out of seq order under concurrent appends. routes.rs pushes the leaf after append_entry returns, in a separate step. Two concurrent verify requests can push in the opposite order to their seq, so proofs served before a restart can be over a different leaf order than after it. This comes from reading the code; I did not measure it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JpH4L6YWcVu9DFsq8PZypJ

RUSTSEC-2026-0459: bundled SQLite before 3.54.0 can return uninitialized
page-cache memory when a database is opened beside a -wal file with a
different page size. sqlx-sqlite 0.9.0, the newest sqlx release with
pre-releases included, requires libsqlite3-sys >=0.30.1, <0.38.0. The
patched line starts at 0.39, so no sqlx bump clears it. rusqlite 0.42 uses
libsqlite3-sys 0.40 (SQLite 3.54+).

Semantics first: docs/verifier-service-storage.md states what the database
and the transparency log guarantee (G1-G8), what they do not, and how the
driver change was checked against them.

- One rusqlite connection behind a mutex, run on tokio's blocking pool.
  It sets the same pragmas sqlx did: WAL, synchronous=NORMAL,
  foreign_keys=ON, 5 s busy timeout.
- Migrations keep sqlx's `_sqlx_migrations` table, SHA-384 checksums and
  descriptions. build.rs derives the list from migrations/.
- tests/fixtures/sqlx-0.9-created.db was written by this crate at
  7f51115 (sqlx). src/db_sqlx_compat_tests.rs checks four things against
  it: sqlx's checksums equal this build's, opening it re-runs nothing,
  every row reads back with the chain intact, and the log continues from
  sqlx's tip. Computing the checksum over a trimmed script fails all four.
- `--db` query parameters, which sqlx accepted, are refused.

Found, not changed (recorded in the doc): the retention sweep fails with
FOREIGN KEY constraint failed on any logged row, so it deletes nothing; and
the in-memory Merkle tree can take leaves out of seq order under concurrent
appends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpH4L6YWcVu9DFsq8PZypJ
@brandon-coproduct
brandon-coproduct merged commit 23d1f45 into deps/rustsec-20261113 Nov 14, 2026
94 of 95 checks passed
@brandon-coproduct
brandon-coproduct deleted the deps/verifier-rusqlite-0459 branch November 14, 2026 03:46
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