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_versionfield. This is the dimension the 2026-07 audit had no coverage of at all. cargo-outdatedis still unusable in this tree. The 2026-07 audit blamed theazure_core_legacypackage alias plus a yanked transitive; the yanked transitive is the real cause and it now has a name — see Theipfs-cidwildcard.
Two toolchains, nine Rust versions apart
This workspace is built by two different Rust toolchains:
| Build | Toolchain | rustc |
|---|---|---|
Host — cargo build, scripts/gate.sh | system | 1.98.0 |
On-chain — cargo build-sbf --tools-version v1.53 | ~/.cache/solana/v1.53/platform-tools | 1.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:
| Side | Crates | Requires |
|---|---|---|
| moved | solana-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.0 | wincode ^0.6 |
| not moved | solana-transaction-status-client-types — at 4.1.2 and at its newest 4.2.1 alike | wincode ^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:
| Entry | Change |
|---|---|
aes-gcm | 0.11.0 → 0.11.1 |
async-trait | 0.1.91 → 0.1.92 |
aws-config and the AWS SDK / smithy cluster | 12 packages |
blake3 | 1.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-auth | 0.11.1 → 0.11.2 |
mail-parser | 0.11.5 → 0.11.8 |
psl | 2.1.219 → 2.1.226 |
rcgen | 0.14.8 → 0.14.9 |
rusty-s3 | 0.10.1 → 0.10.2 |
thiserror (+ -impl) | 2.0.19 → 2.0.20 |
toml (+ toml_parser) | 1.1.3 → 1.1.4 |
uuid | 1.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 bytecode — blake3 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-bindgen0.2.126 → 0.2.127 does not move. It is only reachable by dragging the solana graph’sjs-sys/web-syschurn along with it, which this tier refuses to do.cargo update -p wasm-bindgenon 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/implline differs between the two source trees.Report::parse_rfc5322keeps its single-argument signature — themax_sizeparameter 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)becomesb" \t".contains(&c)indkim/canonicalize.rsandcommon/headers.rs;&self.pbecomesself.pinsidewriteln!inreport/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), andcommon/crypto/rust_crypto.rs— the last behind therust-cryptofeature, while the manifest takesdefault-features = false, features = ["ring", "report"]. There is no reference tomail_auth::arcormail_auth::dkim2anywhere 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
| Entry | Manifest | Available | Verdict |
|---|---|---|---|
base64 | 0.22 | 0.23.1 | LANDED 2026-08-23 |
mail-auth | 0.11 | 0.12.1 | LANDED 2026-08-23 |
jsonwebtoken | 10 | 11.0.0 | LANDED 2026-08-23 |
smtp-proto | =0.2.3 | 0.2.3 | LANDED 2026-08-25 |
mail-builder | 0.4 | 0.5.0 | blocked |
sha2 | 0.10 | 0.11.0 | blocked |
bincode | 1 | 3.0.0 | reject |
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:
- The
base64bump collapses no duplicate.0.22stays in the tree regardless —sqlx-coreandazure_coreboth require it — while0.23was already resolved viarusty-s3. Fourbase64majors (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. - The
mail-authbump drops no duplicateed25519-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’srust-cryptofeature — which this workspace never enables, since it buildsdefault-features = false, features = ["ring", "report"].ed25519-dalek2.x is required independently by five other packages (ed25519-dalek-bip32,libp2p-identity,solana-keypair,solana-signature, andjsonwebtoken11), 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 Struck 2026-08-23: wrong on both halves — see correction 2
above. The landed bump moved exactly one package in the lockfile.ed25519-dalek 2.x from
the tree, since 0.12 moves to ^3 — matching what the workspace already
declares.
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_storage0.21 — still the last published releases on the legacy line; still blocked on the Cosmos/Tables GA crate.azure_storage_blob/azure_storage_queue1.0 — only a 1.1.0-beta exists.libsql0.9.30 — only a 0.10.0-pre exists.libp2p0.56.0 andlibp2p-stream0.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:
ipfs-cidpinned — rootCargo.tomlgainsipfs-cid = "1";mail_client/Cargo.tomlswitches toipfs-cid.workspace = true.Cargo.lockbyte-identical.- Tier 1 lockfile update — 43 non-solana packages, no manifest edit,
Cargo.lockthe 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.