Skip to content

feat(frost): fail closed on an incompatible libfrost_tbtc FFI contract version#4105

Merged
mswilkison merged 4 commits into
feat/frost-schnorr-migration-scaffoldfrom
feat/frost-abi-version-assertion
Jun 22, 2026
Merged

feat(frost): fail closed on an incompatible libfrost_tbtc FFI contract version#4105
mswilkison merged 4 commits into
feat/frost-schnorr-migration-scaffoldfrom
feat/frost-abi-version-assertion

Conversation

@mswilkison

Copy link
Copy Markdown
Contributor

What

The Go-side companion to the mirror's frost_tbtc_abi_version export (#4104). The bridge and libfrost_tbtc are linked at runtime via dlsym; a symbol that resolves but changed meaning is silent corruption. This adds a fail-closed preflight that asserts the linked lib's FFI contract version before any contract-sensitive call.

Depends on #4104 (the mirror PR adding frost_tbtc_abi_version). This PR bumps ci/frost-signer-pin.env to that mirror commit (25e4f277a), so the CI gate builds a lib whose ABI the preflight accepts. Merge #4104 first (or together).

Design (Codex-validated)

  • Rule (major.minor): lib abi_major must equal the bridge's required major (a higher major broke something newer; a lower major is too old), and lib abi_minor must be >= the required minimum minor (additive is backward-compatible; higher minor's extras are ignored). Required = 1.0.
  • Missing symbol (old/absent lib) → keeps ErrNativeCryptographyUnavailable in the chain (explicit message): skips in dev, fatal under the require-cgo gate — same as any stale lib.
  • Present-but-wrong (malformed / wrong major / too-old minor) → ErrTBTCSignerABIIncompatible, fails loudly always. A node must not sign through a lib whose contract diverges.
  • Once-per-process preflight (sync.Once; the lib is process-global, not hot-swapped), gated at callBuildTaggedTBTCSignerOperation — the single chokepoint every request-taking engine op funnels through. The no-arg version calls bypass it (no recursion).

Tests

  • Untagged unit tests cover every branch of the rule via a parameterized helper (default build, no cgo needed).
  • cgo preflight test asserts the linked lib is compatible (skips without it).
  • Validated: builds across tag combos; require-cgo full run green against the ABI lib; no-lib skips; default suite unchanged; vet + gofmt clean.

🤖 Generated with Claude Code

…t version

The Go-side companion to the mirror's frost_tbtc_abi_version export. The bridge and the
lib are linked at runtime via dlsym; a symbol that resolves but changed MEANING is
silent corruption. This adds a fail-closed preflight that asserts the linked lib's FFI
contract version before any contract-sensitive call.

- Pure, fully-unit-tested rule (native_tbtc_signer_abi_version.go, untagged): lib
  abi_major must EQUAL requiredTBTCSignerABIMajor (1) and abi_minor must be >=
  requiredTBTCSignerABIMinMinor (0). Default-build tests cover every branch (wrong major
  either direction, too-old minor, higher-minor-additive-ok) via a parameterized helper.
- cgo preflight (native_tbtc_signer_abi_version_frost_native.go): fetches the version via
  the new frost_tbtc_abi_version FFI, runs ONCE per process (sync.Once - the lib is
  process-global, not hot-swapped) before the first engine op. MISSING symbol keeps
  ErrNativeCryptographyUnavailable in the chain (explicit message; lib predates ABI
  versioning) so it SKIPS in dev and is FATAL under the require-cgo gate like any stale
  lib; PRESENT-but-wrong (malformed/wrong-major/too-old-minor) is ErrTBTCSignerABIIncompatible
  and fails loudly ALWAYS.
- Gate: callBuildTaggedTBTCSignerOperation (the single chokepoint every request-taking
  engine op funnels through) runs the preflight first. The no-arg version/abi-version
  calls bypass it, so no recursion.
- Bumps ci/frost-signer-pin.env to the mirror commit that adds frost_tbtc_abi_version
  (25e4f27), and adds that symbol to the CI gate's verified set, so the gate builds a
  lib whose ABI the preflight accepts.

Design via Codex consult (major.minor; exact major + min minor; init-time fail-closed;
explicit missing-symbol error; no upper bound; frozen {abi_major,abi_minor} surface).
Builds across tag combos; untagged rule tests + cgo preflight test pass; require-cgo full
run green against the ABI lib; no-lib skips; default suite unchanged; vet + gofmt clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7645b131-a29f-4fd5-ae7d-795f71f7e79e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/frost-abi-version-assertion

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

mswilkison and others added 3 commits June 21, 2026 22:05
Codex #4105 P2: the ABI preflight failed closed at the FFI op level, but the coarse
engine path (signWithTBTCSignerCoarseEngine) treats engine-op errors as ordinary
tbtc-signer failures and calls fallbackTBTCSignerLegacySigning - which, if the material
carries a legacy private key share, signs via legacy tECDSA anyway. So an incompatible
lib was NOT actually failing closed end to end: the ABI error got swallowed into a
legacy signature.

Fix at the single chokepoint - the top of signWithTBTCSignerCoarseEngine, before any
fallback: a guard that fails closed on ErrTBTCSignerABIIncompatible (a PRESENT-but-wrong
lib). A MISSING/absent lib is ErrNativeCryptographyUnavailable, NOT this sentinel, and
still falls back to legacy as the transitional design intends - only a responding-but-
incompatible lib is refused. The check is a build-split helper (tbtcSignerABIIncompatibility:
real cgo preflight when linked, no-op otherwise), indirected through a var
(tbtcSignerABIIncompatibilityCheck) so the no-fallback behavior is testable in the
frost_native build.

New test (mirrors _InvalidAttemptPolicy_DoesNotFallback): injects an incompatible ABI
verdict + a valid attempt + a legacy-key-share payload, asserts Sign returns
ErrTBTCSignerABIIncompatible, no signature, and ZERO fallback events. Bumps the signer
pin to the mirror tip (6e3718b, which also adds the header decl).

Builds across all tag combos; new test passes; existing fallback tests unchanged; cgo
vet + gofmt clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The P2-2 fail-closed test (a present-but-incompatible lib must not fall back to legacy
signing) is frost_native-tagged and uses the test seam, so it does not need an
actually-incompatible lib - but it was matched by neither standard CI (no frost_native
tags) nor the cgo gate's TestRealCgoInteractiveSigning -run filter, so its proof ran
only locally. This gate is the only CI that builds these tags; broaden -run to include
it so the no-fallback guarantee is enforced in CI alongside the real-crypto suite.

Verified locally: the broadened -run matches both groups and all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex #4105 re-review P2: Go's json.Unmarshal silently zero-fills a missing field, so a
present-but-partial frost_tbtc_abi_version response like {"abi_major":1} left abi_minor
at 0 - and since the required minimum minor is also 0, the compatibility check ACCEPTED
it. A broken/partial lib could thus bypass the fail-closed guard.

Extract the decode into a pure parseTBTCSignerABIVersion (untagged, unit-testable in the
default build) that uses pointer fields to distinguish "absent" from a legitimate zero
and rejects a payload missing abi_major and/or abi_minor as ErrTBTCSignerABIIncompatible.
Malformed JSON is rejected too; extra/unknown fields are still tolerated (an additive
minor bump may add fields old bridges ignore). The cgo assertTBTCSignerABICompatible now
calls it instead of a lenient inline Unmarshal.

Tests: parser accepts valid (incl. present zero minor) + extra fields; rejects missing
abi_minor, missing abi_major, empty object, malformed json, non-object. Builds across tag
combos; cgo preflight still passes against the real lib; gofmt + vet clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mswilkison mswilkison merged commit 402911a into feat/frost-schnorr-migration-scaffold Jun 22, 2026
16 of 17 checks passed
@mswilkison mswilkison deleted the feat/frost-abi-version-assertion branch June 22, 2026 02:36
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