Fix panics on malformed transaction data in legacy instruction decoding#223
Merged
prasanna-anchorage merged 2 commits intoMar 25, 2026
Merged
Conversation
6 tasks
Copilot started reviewing on behalf of
shahan-khatchadourian-anchorage
March 25, 2026 20:09
View session
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens the legacy Solana transaction decoding path to avoid panics on malformed input, aligning behavior more closely with the existing v0 transaction handling strategy.
Changes:
- Add bounds checks when expanding legacy compiled instructions to prevent index-out-of-bounds panics.
- Prevent arithmetic underflow in account writable/readonly classification by switching to
saturating_subin both legacy and v0 account decoding.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
src/chain_parsers/visualsign-solana/src/core/instructions.rs |
Adds guard for empty account keys and bounds-checks compiled instruction indices to avoid panics during instruction expansion. |
src/chain_parsers/visualsign-solana/src/core/accounts/decode.rs |
Replaces potentially-underflowing header arithmetic with saturating_sub for safer account classification on malformed headers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Two panic sites in the legacy transaction path: 1. instructions.rs: unchecked indexing into account_keys with program_id_index and account indices from compiled instructions. Malformed transactions with out-of-bounds indices cause index-out-of-bounds panics. 2. accounts/decode.rs: arithmetic underflow when message header values (num_readonly_signed_accounts, num_readonly_unsigned_accounts) exceed the actual account keys array length. Fix: apply the same defensive patterns already used in the v0 transaction path — filter_map with bounds checks for instruction indices, saturating_sub for header arithmetic, and an explicit error for empty account keys. Verified with cargo-fuzz: ~930,000 runs across both fuzz targets (fuzz_transaction_string, fuzz_versioned_transaction) with zero crashes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Clarify comment to distinguish skipped instructions (OOB program_id) from omitted accounts (OOB account index) - Use "Legacy transaction" in error message for consistency with v0 path - Add unit tests for decode_accounts and decode_v0_accounts with inconsistent header values to lock in saturating_sub behavior Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
b5e52cb to
e1286e9
Compare
prasanna-anchorage
approved these changes
Mar 25, 2026
| let readonly_signer_start = message.header.num_required_signatures as usize | ||
| - message.header.num_readonly_signed_accounts as usize; | ||
| let readonly_signer_start = (message.header.num_required_signatures as usize) | ||
| .saturating_sub(message.header.num_readonly_signed_accounts as usize); |
Contributor
There was a problem hiding this comment.
nice, I always forget about saturating_sub
febf931
into
shahankhatch/add-fuzz-targets
7 checks passed
prasanna-anchorage
pushed a commit
that referenced
this pull request
Mar 27, 2026
* Add cargo fuzz targets for visualsign-solana Two libFuzzer targets covering the full visualsign-solana stack: - fuzz_transaction_string: arbitrary bytes into transaction_string_to_visual_sign - fuzz_versioned_transaction: arbitrary bytes deserialized as VersionedTransaction then passed to versioned_transaction_to_visual_sign Run with: cargo +nightly fuzz run <target> (from src/chain_parsers/visualsign-solana/fuzz/) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add proptest and fuzz label-triggered CI jobs - proptest label: runs cargo test -p visualsign-solana - fuzz label: installs nightly + cargo-fuzz, runs each fuzz target for 30s - ubuntu job: restricted to main push/PR to avoid triggering on label events Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Post fuzz crash summary as PR comment on failure On crash, extracts the libFuzzer summary (everything after the ─── line) and posts it as a PR comment via gh. No artifacts or separate jobs needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Split proptest and fuzz workflows into separate files Move proptest and fuzz jobs out of main.yml into dedicated workflow files (proptest.yml, fuzz.yml) so they appear as distinct named checks. Add pull-requests: write permission to fuzz job to allow posting crash comments via gh pr comment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add reusable post-failure-comment action tagging @copilot Shared composite action posts crash/failure output as a PR comment and tags @copilot to fix the issue. Fuzz and proptest workflows use it via extract steps that write output to GITHUB_OUTPUT. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Extract shared pipeline helpers into common module Move build_transaction, options_with_idl, instruction_fields, find_text and related helpers from pipeline_integration.rs into tests/common/mod.rs so they can be reused by other test files without duplication. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add semantic pipeline tests for 8 embedded IDLs Test the full visualization pipeline with realistic Borsh-serialized instruction data for Drift deposit, Lifinity swap, Raydium swapBaseInput, Orca swap (including u128 sqrtPriceLimit), Meteora swap, Kamino deposit, Stabble swap (Option<u64> arg), and OpenBook deposit. Each test uses assert_semantic() to verify decoded instruction name and arg values match what was serialized. Lives in its own file to keep pipeline_integration.rs focused on proptest-based property tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Allow triggering CI on any PR via 'ci' label Add 'labeled' to pull_request trigger types and allow the ubuntu job to run when the 'ci' label is present, not just for PRs targeting main. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix fuzz build and simplify CI workflows - Pin fuzz Cargo.lock to avoid spl-token-2022 breakage on nightly (spl-token-group-interface 0.7.2 pulled in solana-nullable which is incompatible with spl-token-2022 10.0.0) - Remove stale Cargo.lock gitignore rule from visualsign-solana - Remove post-failure-comment action and simplify fuzz/proptest workflows - Use continue-on-error on fuzz steps so crashes show as warnings (known pre-existing panics in instructions.rs bounds checking) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add failure labels to fuzz and proptest workflows On crash/failure, add fuzz-failure or proptest-failure label to the PR. On clean run, remove the label. This gives visible signal without blocking the check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Harden label steps in fuzz and proptest workflows Add || true to --add-label calls so the step doesn't fail if the label operation itself has issues (e.g. label not yet created in the repo). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add cargo fuzz, semantic tests, and CI labels to testing guide Extend the property-based testing section with cargo fuzz targets, semantic pipeline tests, CI workflow labels (proptest, fuzz, ci), and failure label behavior (fuzz-failure, proptest-failure). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address Copilot review: pin nightly, lock cargo-fuzz, fix cache key - Pin nightly toolchain to 2026-03-13 (known-good) in both rust-toolchain.toml and fuzz.yml - Use --locked for cargo install cargo-fuzz in CI and docs - Cache fuzz/target/ and key off fuzz/Cargo.lock instead of src/Cargo.lock Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address code review: Default::default(), env var for PR number - Use ..VisualSignOptions::default() in test helpers to prevent breakage when new fields are added to the struct - Pass github.event.pull_request.number through env var instead of direct context interpolation in shell blocks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix panics on malformed transaction data in legacy instruction decoding (#223) * fix: guard against panics on malformed transaction data Two panic sites in the legacy transaction path: 1. instructions.rs: unchecked indexing into account_keys with program_id_index and account indices from compiled instructions. Malformed transactions with out-of-bounds indices cause index-out-of-bounds panics. 2. accounts/decode.rs: arithmetic underflow when message header values (num_readonly_signed_accounts, num_readonly_unsigned_accounts) exceed the actual account keys array length. Fix: apply the same defensive patterns already used in the v0 transaction path — filter_map with bounds checks for instruction indices, saturating_sub for header arithmetic, and an explicit error for empty account keys. Verified with cargo-fuzz: ~930,000 runs across both fuzz targets (fuzz_transaction_string, fuzz_versioned_transaction) with zero crashes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address Copilot review: improve comments, error message, add tests - Clarify comment to distinguish skipped instructions (OOB program_id) from omitted accounts (OOB account index) - Use "Legacy transaction" in error message for consistency with v0 path - Add unit tests for decode_accounts and decode_v0_accounts with inconsistent header values to lock in saturating_sub behavior Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
prasanna-anchorage
pushed a commit
that referenced
this pull request
Apr 8, 2026
* Add cargo fuzz targets for visualsign-solana Two libFuzzer targets covering the full visualsign-solana stack: - fuzz_transaction_string: arbitrary bytes into transaction_string_to_visual_sign - fuzz_versioned_transaction: arbitrary bytes deserialized as VersionedTransaction then passed to versioned_transaction_to_visual_sign Run with: cargo +nightly fuzz run <target> (from src/chain_parsers/visualsign-solana/fuzz/) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add proptest and fuzz label-triggered CI jobs - proptest label: runs cargo test -p visualsign-solana - fuzz label: installs nightly + cargo-fuzz, runs each fuzz target for 30s - ubuntu job: restricted to main push/PR to avoid triggering on label events Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Post fuzz crash summary as PR comment on failure On crash, extracts the libFuzzer summary (everything after the ─── line) and posts it as a PR comment via gh. No artifacts or separate jobs needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Split proptest and fuzz workflows into separate files Move proptest and fuzz jobs out of main.yml into dedicated workflow files (proptest.yml, fuzz.yml) so they appear as distinct named checks. Add pull-requests: write permission to fuzz job to allow posting crash comments via gh pr comment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add reusable post-failure-comment action tagging @copilot Shared composite action posts crash/failure output as a PR comment and tags @copilot to fix the issue. Fuzz and proptest workflows use it via extract steps that write output to GITHUB_OUTPUT. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Extract shared pipeline helpers into common module Move build_transaction, options_with_idl, instruction_fields, find_text and related helpers from pipeline_integration.rs into tests/common/mod.rs so they can be reused by other test files without duplication. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add semantic pipeline tests for 8 embedded IDLs Test the full visualization pipeline with realistic Borsh-serialized instruction data for Drift deposit, Lifinity swap, Raydium swapBaseInput, Orca swap (including u128 sqrtPriceLimit), Meteora swap, Kamino deposit, Stabble swap (Option<u64> arg), and OpenBook deposit. Each test uses assert_semantic() to verify decoded instruction name and arg values match what was serialized. Lives in its own file to keep pipeline_integration.rs focused on proptest-based property tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Allow triggering CI on any PR via 'ci' label Add 'labeled' to pull_request trigger types and allow the ubuntu job to run when the 'ci' label is present, not just for PRs targeting main. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix fuzz build and simplify CI workflows - Pin fuzz Cargo.lock to avoid spl-token-2022 breakage on nightly (spl-token-group-interface 0.7.2 pulled in solana-nullable which is incompatible with spl-token-2022 10.0.0) - Remove stale Cargo.lock gitignore rule from visualsign-solana - Remove post-failure-comment action and simplify fuzz/proptest workflows - Use continue-on-error on fuzz steps so crashes show as warnings (known pre-existing panics in instructions.rs bounds checking) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add failure labels to fuzz and proptest workflows On crash/failure, add fuzz-failure or proptest-failure label to the PR. On clean run, remove the label. This gives visible signal without blocking the check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Harden label steps in fuzz and proptest workflows Add || true to --add-label calls so the step doesn't fail if the label operation itself has issues (e.g. label not yet created in the repo). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add cargo fuzz, semantic tests, and CI labels to testing guide Extend the property-based testing section with cargo fuzz targets, semantic pipeline tests, CI workflow labels (proptest, fuzz, ci), and failure label behavior (fuzz-failure, proptest-failure). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address Copilot review: pin nightly, lock cargo-fuzz, fix cache key - Pin nightly toolchain to 2026-03-13 (known-good) in both rust-toolchain.toml and fuzz.yml - Use --locked for cargo install cargo-fuzz in CI and docs - Cache fuzz/target/ and key off fuzz/Cargo.lock instead of src/Cargo.lock Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address code review: Default::default(), env var for PR number - Use ..VisualSignOptions::default() in test helpers to prevent breakage when new fields are added to the struct - Pass github.event.pull_request.number through env var instead of direct context interpolation in shell blocks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix panics on malformed transaction data in legacy instruction decoding (#223) * fix: guard against panics on malformed transaction data Two panic sites in the legacy transaction path: 1. instructions.rs: unchecked indexing into account_keys with program_id_index and account indices from compiled instructions. Malformed transactions with out-of-bounds indices cause index-out-of-bounds panics. 2. accounts/decode.rs: arithmetic underflow when message header values (num_readonly_signed_accounts, num_readonly_unsigned_accounts) exceed the actual account keys array length. Fix: apply the same defensive patterns already used in the v0 transaction path — filter_map with bounds checks for instruction indices, saturating_sub for header arithmetic, and an explicit error for empty account keys. Verified with cargo-fuzz: ~930,000 runs across both fuzz targets (fuzz_transaction_string, fuzz_versioned_transaction) with zero crashes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address Copilot review: improve comments, error message, add tests - Clarify comment to distinguish skipped instructions (OOB program_id) from omitted accounts (OOB account index) - Use "Legacy transaction" in error message for consistency with v0 path - Add unit tests for decode_accounts and decode_v0_accounts with inconsistent header values to lock in saturating_sub behavior Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add real-IDL structural validation tests Extract load_idl_from_env into common/mod.rs and add real_idl_validation.rs with deterministic structural invariant tests for production IDLs: discriminator presence/uniqueness, instruction name uniqueness, and IDL hash stability. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add proptest-based real-IDL coverage tests Add 4 new property tests against real production IDLs: - arg name completeness in parsed output - overlong data rejection (trailing byte) - truncated data rejection (missing byte) - discriminator isolation (cross-instruction swap) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes two panic sites in the legacy transaction path found by cargo-fuzz targets introduced in PR #204.
Bug 1: Unchecked array indexing in
instructions.rsdecode_instructionsindexes intoaccount_keysusingprogram_id_indexand account indices from compiled instructions without bounds checks. Malformed transactions where these indices exceed the account keys array cause index-out-of-bounds panics.Fix: Use
filter_mapwith bounds checks, matching the pattern already used inv0.rs(lines 137-165). Instructions with out-of-boundsprogram_id_indexare skipped; accounts with out-of-bounds indices are dropped from the instruction. An explicitErris returned for empty account keys.Bug 2: Arithmetic underflow in
accounts/decode.rsdecode_accountsanddecode_v0_accountssubtract header values (num_readonly_signed_accounts,num_readonly_unsigned_accounts) from array lengths without checking that the header values are consistent. Malformed transactions where header counts exceed the actual account keys length cause underflow panics in debug builds and wrapping in release builds.Fix: Replace bare subtraction with
saturating_sub. On inconsistent headers, the writable/readonly classification degrades gracefully (accounts may be misclassified as writable or readonly) rather than panicking.Silent failure trade-offs
Both fixes align with the existing v0 transaction handling strategy: silently skip or degrade on malformed data rather than returning errors. This is the pragmatic choice for a visualization tool — showing partial results is better than crashing — but it has risks:
Future option: These could be surfaced as
VisualSignErrorvariants or as warning fields in theSignablePayloadoutput, so callers can distinguish "clean parse" from "degraded parse". This would require a new error variant or a warnings field in the output type.Test plan
cargo test -p visualsign-solana)cargo +nightly fuzz run fuzz_transaction_string -- -max_total_time=30— ~420,000 runs, zero crashescargo +nightly fuzz run fuzz_versioned_transaction -- -max_total_time=30— ~510,000 runs, zero crashescargo clippy -p visualsign-solana --tests -- -D warnings— cleancargo fmt -- --check— clean🤖 Generated with Claude Code