Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Dependency audit — 2026-08

Scope: every one of the 121 third-party entries in the root Cargo.toml [workspace.dependencies], plus the literal-version declarations in member manifests that the root table does not control. It answers two questions: what can move right now without breaking anything, and what is blocked, on what.

Unlike the 2026-07 audit, which this supersedes, the safe tier was not only identified but landed and verified — see What was landed.

Method (and why it differs from the 2026-07 audit)

  • “Latest” was read live from the crates.io sparse index (https://index.crates.io/...) — the same source Cargo itself resolves against. The 2026-07 audit read the local index cache (~/.cargo/registry/index), so its “latest” meant “latest that cache had seen at last fetch”, and it flagged rows (offline?) where it could not confirm. Several of those rows had drifted. (The crates.io JSON API is not usable here: it refuses these requests under its data-access policy. The sparse index has no such restriction.)
  • Resolution was proven with cargo update --dry-run, and then the result was actually built. That distinction earned its keep — see the wincode split, which resolves cleanly and fails to compile.
  • MSRV was read per version from each index entry’s rust_version field. This is the dimension the 2026-07 audit had no coverage of at all.
  • cargo-outdated is still unusable in this tree. The 2026-07 audit blamed the azure_core_legacy package alias plus a yanked transitive; the yanked transitive is the real cause and it now has a name — see The ipfs-cid wildcard.

Two toolchains, nine Rust versions apart

This workspace is built by two different Rust toolchains:

BuildToolchainrustc
Host — cargo build, scripts/gate.shsystem1.98.0
On-chain — cargo build-sbf --tools-version v1.53~/.cache/solana/v1.53/platform-tools1.89.0-dev

A dependency that raises its MSRV past 1.89 breaks cargo build-sbf while the entire workspace gate stays green. scripts/gate.sh excludes the build-sbf legs deliberately (they apply only when a program crate changed, and live in the onchain-program-build skill instead), so nothing in the routine gate can catch an SBF-only MSRV break. Any dependency work touching the on-chain closure has to run the three build-sbf legs as its own positive control.

What limits the blast radius is that the on-chain closure is small by construction. Each of mail_program, alias_program and domain_program declares exactly five production dependencies:

mail-model  pinocchio  pinocchio-log  pinocchio-system  program-common

build-sbf builds only the lib/cdylib target, never the test harness — so rsa, rand 0.8, bs58, sha2, p256, ed25519-dalek and the whole agave/solana-program-test cluster are dev-only and never reach the bytecode. program_common additionally puts sha2, solana-big-mod-exp and solana-address’s curve25519 feature behind cfg(not(any(target_os = "solana", target_arch = "bpf"))), so those are host-only too.

Result for this audit: MSRV blocked nothing. Across every package a full cargo update would move, exactly 14 declare a rust_version above 1.89 — all of them the AWS SDK / smithy cluster at 1.94.1:

aws-config  aws-runtime  aws-sdk-appconfigdata  aws-sdk-dynamodb
aws-sdk-secretsmanager  aws-sdk-sqs  aws-sdk-sts  aws-smithy-http-client
aws-smithy-query  aws-smithy-runtime  aws-smithy-runtime-api
aws-smithy-types  aws-smithy-xml  aws-types

None is in the on-chain closure — they reach only mail_store’s aws feature, key_source’s asm, and app_config’s AWS AppConfig tier. Nothing anywhere exceeds the host’s 1.98. What did block the update was something else entirely.

The wincode split — why the solana stack cannot move piecemeal

A blanket cargo update resolves cleanly and then fails to build. This is the finding worth carrying forward, and no amount of dry-running would have surfaced it.

The solana crates are mid-migration between two majors of wincode, the serialization-schema crate whose derive macros generate SchemaRead/ SchemaWrite impls. As of this audit the split runs straight through the dependency graph:

SideCratesRequires
movedsolana-address 2.7.0, solana-hash 4.6.0, solana-message 4.5.0, solana-reward-info 6.3.0, solana-short-vec 3.3.0, solana-signature 3.5.2, solana-transaction 4.3.0wincode ^0.6
not movedsolana-transaction-status-client-types — at 4.1.2 and at its newest 4.2.1 alikewincode ^0.5

Cargo is happy to put both in one graph, because two semver-incompatible majors of a crate are allowed to coexist. But wincode::SchemaWrite is a trait, and the two copies are different types. solana-transaction-status-client-types derives SchemaWrite for its own structs against wincode 0.5, while CompiledInstruction — which comes from the moved side — implements wincode 0.6’s trait. The build dies with ten instances of:

error[E0277]: the trait bound `CompiledInstruction: wincode::SchemaWrite<__WincodeConfig>`
              is not satisfied
note: there are multiple different versions of crate `wincode` in the dependency graph

Consequence: the solana stack must move as one coordinated set, and today it cannot move at all — the standalone crates have crossed to wincode 0.6 and the status-types crate has not followed. Until it does, cargo update must leave every solana-* package alone.

That is the shape of the tier below: it is not “everything semver-compatible”, it is “everything semver-compatible except the solana stack”.

Verdict legend

Carried over from the 2026-07 audit so the two read alike:

  • free — semver-compatible, lockfile-only, no manifest edit. (new tier)
  • ready — a real bump is available and the adaptation is understood and small.
  • decision — available, but taking it changes behaviour someone must sign off on.
  • blocked — cannot move until an upstream event; no action possible now.
  • reject — available but must not be taken.
  • hold — already at latest, or intentionally pinned.

Tier 1 — free (landed)

43 packages, all non-solana, lockfile-only. Direct workspace entries advanced:

EntryChange
aes-gcm0.11.0 → 0.11.1
async-trait0.1.91 → 0.1.92
aws-config and the AWS SDK / smithy cluster12 packages
blake31.8.5 → 1.8.7
futures (+ its 8 sub-crates)0.3.33 → 0.3.34
google-cloud-secretmanager-v1 (+ auth/gax/wkt)1.11.0 → 1.12.0
mail-auth0.11.1 → 0.11.2
mail-parser0.11.5 → 0.11.8
psl2.1.219 → 2.1.226
rcgen0.14.8 → 0.14.9
rusty-s30.10.1 → 0.10.2
thiserror (+ -impl)2.0.19 → 2.0.20
toml (+ toml_parser)1.1.3 → 1.1.4
uuid1.24.0 → 1.25.0

One addition: base64 0.23.1 enters as a transitive, because rusty-s3 0.10.2 requires ^0.23. The workspace’s own base64 = "0.22" is untouched, so the two majors now coexist — which is an argument for the Tier 3 base64 bump, since that would collapse them back to one.

Only two of the 43 reach the on-chain bytecodeblake3 and thiserror, both patch bumps, both MSRV well under 1.89. Everything else is host-side. pinocchio 0.11.2, pinocchio-log 0.5.1, pinocchio-system 0.6.1 and borsh 1.8.0 were already at latest.

Two entries that a naive reading would expect here and which are absent:

  • wasm-bindgen 0.2.126 → 0.2.127 does not move. It is only reachable by dragging the solana graph’s js-sys/web-sys churn along with it, which this tier refuses to do. cargo update -p wasm-bindgen on its own is a no-op.
  • Every solana-* package, for the wincode reason above.

mail-auth 0.11.1 → 0.11.2 — the hop that needed scrutiny

The 2026-07 audit left a standing rule: mail-auth has already shipped one SemVer-illegal API-breaking patch (0.11.1 removed with_domain_suffix_fn and snapped three call sites), so “treat any cargo update touching mail-auth as a change that needs the DMARC suites re-run, not as routine maintenance.” This tier touches it, so the two versions were diffed directly from their cached .crate archives rather than trusted to a changelog:

  • No public API changed. Not one pub fn / pub struct / pub enum / pub trait / impl line differs between the two source trees. Report::parse_rfc5322 keeps its single-argument signature — the max_size parameter the changelog describes lands in 0.12, not here.
  • Ten files differ, and nearly all of it is lint-driven refactoring with byte-identical output: [b' ', b'\t'].contains(&c) becomes b" \t".contains(&c) in dkim/canonicalize.rs and common/headers.rs; &self.p becomes self.p inside writeln! in report/dmarc/generate.rs. DKIM1 canonicalization bytes and DMARC report XML bytes are unchanged.
  • The three real behaviour changes are all in code this workspace neither compiles nor calls: DKIM2 header classification (common/message.rs, dkim2/*), ARC sealing (arc/seal.rs, arc/verify.rs), and common/crypto/rust_crypto.rs — the last behind the rust-crypto feature, while the manifest takes default-features = false, features = ["ring", "report"]. There is no reference to mail_auth::arc or mail_auth::dkim2 anywhere in the workspace.

The DMARC/DKIM/SPF suites were re-run anyway, per the standing rule.

mail-parser 0.11.7 introduced an rkyv layout regression that 0.11.8 fixes; this tier lands on 0.11.8, and nothing in the workspace uses rkyv directly — it is purely a transitive of smtp-proto and mail-auth. Non-issue.

Tier 2 — the solana stack

solana-program-test, solana-rpc-client, solana-rpc-client-api, solana-cli-config, solana-system-program and solana-transaction-status-client-types hold at 4.1.2 with 4.2.1 available, alongside the standalone solana-* crates listed in the wincode table above.

Taken on its own the 4.2.1 move looks cheap: ProgramTest and BanksClient’s public API and the .so search order are unchanged between the two tags, and none of the spl/zk churn that Agave 4.2 carries internally (spl-token-interface 2→3, solana-zk-sdk dropped for solana-zk-sdk-pod) reaches solana-program-test.

But it cannot be assessed independently of the wincode split, because solana-transaction-status-client-types 4.2.1 still requires wincode ^0.5.5 — the same side of the divide as 4.1.2. Whether a coordinated all-solana move resolves to a single wincode major, or reproduces the same E0277, is the question that gates this tier. Verdict: blocked pending that determination, and in any case its own item with its own gate run, never a ride-along on a lockfile refresh. Note also that solana-sbpf moves to =0.21.1 at 4.2.1, which means every .so must be rebuilt.

Tier 3 — needs a manifest edit

EntryManifestAvailableVerdict
base640.220.23.1LANDED 2026-08-23
mail-auth0.110.12.1LANDED 2026-08-23
jsonwebtoken1011.0.0LANDED 2026-08-23
smtp-proto=0.2.30.2.3LANDED 2026-08-25
mail-builder0.40.5.0blocked
sha20.100.11.0blocked
bincode13.0.0reject

All three “ready” rows landed on 2026-08-23, each as its own gated commit, and taking them corrected two claims this audit had stated as fact. Both corrections are recorded in the workspace manifest beside the entries they concern, and neither changes a verdict:

  1. The base64 bump collapses no duplicate. 0.22 stays in the tree regardless — sqlx-core and azure_core both require it — while 0.23 was already resolved via rusty-s3. Four base64 majors (0.13 / 0.21 / 0.22 / 0.23) are present either way; the bump only moves first-party code onto the copy already being compiled.
  2. The mail-auth bump drops no duplicate ed25519-dalek. The claim below that “0.12 moves to ^3” mis-reads the manifest: 0.11.2 already required ^3, and the dependency is optional, reached only through mail-auth’s rust-crypto feature — which this workspace never enables, since it builds default-features = false, features = ["ring", "report"]. ed25519-dalek 2.x is required independently by five other packages (ed25519-dalek-bip32, libp2p-identity, solana-keypair, solana-signature, and jsonwebtoken 11), so nothing about it moved.

A third fact the audit missed is not a correction but a trap worth carrying: since 11, jsonwebtoken’s crypto backend is a process-wide CryptoProvider resolved from cargo features, and it resolves correctly only while exactly one of rust_crypto / aws_lc_rs is enabled anywhere in the graph. Because cargo unifies features across a workspace, a second consumer enabling aws_lc_rs would select a stub provider whose every entry point panics at run time, with no compile error. account_api is the only consumer today.

base64 0.22 → 0.23 — ready, and now more attractive than it was. The whole workspace uses only Engine::encode/decode with BASE64_STANDARD; there is no custom alphabet, no GeneralPurposeConfig, and no use of the deprecated free functions. The one breaking change (DecodeError::InvalidLastSymbol gained a payload) is not matched on anywhere here. MSRV rises to 1.71 — comfortably under both toolchains. Since Tier 1 pulled base64 0.23.1 in as a rusty-s3 transitive, bumping the workspace entry would collapse a duplicate rather than create one. Note 0.23 adds a default-on simd-unsafe feature.

mail-auth 0.11 → 0.12 — ready, and smaller than it looks. The verifiers (verify_dkim, verify_spf, verify_dmarc, verify_arc) and the MessageAuthenticator type are unchanged. The only signature break is Report::parse_rfc5322 and TlsReport::parse_rfc5322 gaining a max_size argument that bounds decompressed report size — a hardening fix. Behavioural changes are DMARC identifiers folded to A-label form and alignment becoming case-insensitive. It would also drop the duplicate ed25519-dalek 2.x from the tree, since 0.12 moves to ^3 — matching what the workspace already declares. Struck 2026-08-23: wrong on both halves — see correction 2 above. The landed bump moved exactly one package in the lockfile.

What max_size became. The bound is dmarc_rua::MAX_REPORT_SIZE = 25 MiB, matching [smtp] max_message_size rather than introducing a second size vocabulary. Only one production call site exists (mail_spooler/src/adapters/dmarc_rua.rs); TLS-RPT reports are emit-only here, so TlsReport::parse_rfc5322 is reached from tests alone.

jsonwebtoken 10 → 11 — ready, moderate. One file, account_api/src/jwt.rs, HS256 only, about six API items. The rust_crypto feature name survives. The exposure is Header.extras becoming a struct and the removal of EncodingKey::inner / DecodingKey::as_bytes; neither is used today, so this is close to a version-number change.

smtp-proto =0.2.1=0.2.3 — LANDED 2026-08-25, by user decision. This audit framed the change correctly; the workspace manifest and HANDOFF had compressed it into “0.2.2 appends a trailing CRLF”, which reads as though 0.2.2 added something spurious. It is the other way round. 0.2.2 changed request/receiver.rs from buf.truncate(len - 3) to buf.truncate(len - 1), and at the moment that line runs the buffer always holds the body’s own final CRLF plus the terminator’s stray CR — so 0.2.1 was deleting a byte pair that belonged to the message, and every stored body lost its last line break. The CRLF before the terminating dot is content, not terminator (RFC 5321 §4.5.2), so 0.2.2 fixed a real defect and 0.2.3 rightly did not revert it; 0.2.3 only reworked FUTURERELEASE to parse RFC 3339 datetimes, which is inert here because unsupported_mail_param already refuses HOLDFOR/HOLDUNTIL with 555 5.5.4.

What the bump moves, and what it does not. Only the stored copy changes — the bytes sealed to IPFS and served over IMAP/POP now keep the sender’s final CRLF. Relayed mail is byte-identical either way, because dot_stuff (smtp_session/src/client.rs) already re-adds a trailing CRLF only when one is absent. The DKIM exposure this audit flagged turned out benign and is now measured rather than reasoned about: under relaxed body canonicalization (RFC 6376 §3.4.4) both shapes produce the same bh=, fenced by a_trailing_crlf_does_not_move_the_dkim_body_hash in mail_submit/src/dkim.rs.

The pin stays exact, because this crate decides delivered-mail bytes with no compiler error when it changes. mx_transaction_delivers_with_trace_headers was silently pinning the truncation — its ends_with("line one\r\n.leading dot") assertion encoded the missing CRLF — and now asserts the CRLF is kept. A dedicated fence, data_body_keeps_its_final_crlf, pins a trailing blank line end-to-end; it was positive-controlled by re-pinning 0.2.1, where it fails.

mail-builder 0.4 → 0.5 — blocked. mail-auth 0.12.1 still requires ^0.4, so taking 0.5 puts two copies of the crate in the tree. Unblocks when mail-auth 0.12.2 publishes (it is in the changelog, not yet released).

sha2 0.10 → 0.11 — blocked. 0.11 rides digest 0.11 and const-oid 0.10, while rsa 0.9.10 requires digest ^0.10.5 and const-oid ^0.9. The AssociatedOid trait therefore differs between them, so Pkcs1v15Sign::new::<Sha256>() will not accept a 0.11 Sha256 — and that pairing is exactly why the oid feature is enabled in program_common, domain_program and mail_program. Unblocks when rsa 0.10 leaves rc.

bincode 1 → 3 — reject; 3.0.0 is a tombstone, not a release. The project was archived in 2025 and 3.0.0 ships a lib.rs containing only a compile_error!, published solely to signal abandonment because crates.io has no way to mark a crate archived. The index metadata corroborates it: 3.0.0 declares zero dependencies and zero features, where 1.3.3 declares serde ^1.0.63 — there is no serialization library in it. Stay on bincode = "1". Should it ever have to move, the successors are bincode-next or wincode, and the real break was 1.x → 2.0 (default config stopped being byte-compatible), which makes it a persisted-format migration gated by mail_wasm’s parity suite rather than a version bump.

Tier 4 — hold

  • imap-codec =2.0.0-alpha.9, imap-types =2.0.0-alpha.7, imap-next =0.3.4 — all three are the newest published version on their line. The exact pins remain correct.
  • azure_core_legacy / azure_data_tables / azure_storage 0.21 — still the last published releases on the legacy line; still blocked on the Cosmos/Tables GA crate.
  • azure_storage_blob / azure_storage_queue 1.0 — only a 1.1.0-beta exists.
  • libsql 0.9.30 — only a 0.10.0-pre exists.
  • libp2p 0.56.0 and libp2p-stream 0.4.0-alpha — at latest. This settles the (offline?) flag the 2026-07 audit left on the libp2p row: there is no 0.57.

Correcting a count. HANDOFF.md carried the estimate “~18 direct deps behind latest-incompatible await a semver-major pass.” Measured against a live index, the real figure is 7 entries that are actionable someday and 6 that are blocked or exist only as pre-releases.

The ipfs-cid wildcard

mail_client/Cargo.toml declared ipfs-cid = "*" in [dependencies] — the only wildcard in a production dependency table anywhere in the workspace. The 2026-07 audit’s item 2 removed every * from the root manifest but did not scan member manifests, so this one survived it.

It resolved to 1.0.0 purely by accident: ipfs-cid 2.0.0 requires cid ^0.10.1, which requires core2 ^0.4, whose 0.4.0 is yanked. That unresolvable chain was the only thing standing between the workspace and a silent major bump on the next cargo update. It is also the specific reason cargo-outdated cannot run here — the 2026-07 audit observed the symptom without naming the cause.

Only one API is used, generate_cid() followed by .to_string(), at six call sites under mail_client/src/commands/. Fixed: hoisted into [workspace.dependencies] as ipfs-cid = "1", with mail_client inheriting it. Notation only — the resolved version did not move.

Note also that ipfs-cid drags in a second, older CID stack (cid 0.10) alongside the workspace’s own cid = "0.11" used by bitswap_proto and ipfs_repo. Consolidating the two is worth its own item.

Follow-up: the remaining wildcards

mail_client’s [dev-dependencies] carried assert_cmd = "*", predicates = "*", regex = "*" and tempfile = "*" — the last of which shadowed the root’s tempfile = "3". They are dev-only, so they cannot reach a shipped artifact, but they carried the same silent-major hazard. LANDED 2026-08-23: all four pinned — assert_cmd = "2", predicates = "3", regex = "1", and tempfile.workspace = true (inheriting the root’s tempfile = "3"). Cargo.lock byte-identical.

What was landed

Two commits:

  1. ipfs-cid pinned — root Cargo.toml gains ipfs-cid = "1"; mail_client/Cargo.toml switches to ipfs-cid.workspace = true. Cargo.lock byte-identical.
  2. Tier 1 lockfile update — 43 non-solana packages, no manifest edit, Cargo.lock the only changed file.

How to reproduce the tier

cargo update has no --exclude, so the tier is expressed positively: every non-solana, non-path entry in [workspace.dependencies], passed as explicit -p specs. Specs must be version-qualified (aes-gcm@0.11.0) wherever the lock holds more than one version of a name, or cargo rejects the spec as ambiguous.

Verification

The .so files come first, because the program suites assert against the bytecode on disk and a stale .so is the classic misdiagnosis here:

cargo build-sbf --arch v3 --tools-version v1.53 --manifest-path mail_program/Cargo.toml
cargo build-sbf --arch v3 --tools-version v1.53 --manifest-path alias_program/Cargo.toml
cargo build-sbf --arch v3 --tools-version v1.53 --manifest-path domain_program/Cargo.toml
bash scripts/gate.sh --mail-store

Those three legs are the only positive control that exists for the MSRV finding, since the workspace gate structurally cannot fail on an SBF-only MSRV break. --mail-store is required because the AWS SDK cluster moved and mail_store’s cloud backends are feature-gated out of the default test leg.

The program_common/src/modexp.rs endianness shim was re-read, as HANDOFF.md requires on any SDK bump. It is untouched by this tier: solana-big-mod-exp stays at 4.0.0 and solana-define-syscall stays at 5.1.0, because no solana-* package moves.