Skip to content

fix(wormhole): harden VAA and governance deserialization against malformed input#2051

Merged
troian merged 1 commit intoakash-network:mainfrom
chalabi2:vuln-fixes
Mar 20, 2026
Merged

fix(wormhole): harden VAA and governance deserialization against malformed input#2051
troian merged 1 commit intoakash-network:mainfrom
chalabi2:vuln-fixes

Conversation

@chalabi2
Copy link
Copy Markdown
Contributor

Description

ParsedVAA::deserialize panicked on inputs shorter than 6 bytes due to
unchecked ByteUtils access before the first bounds check. GovernancePacket
had the same class of bug for inputs under 35 bytes. Both are now guarded
with early length validation that returns a clean InvalidVAA error.

quorum() returned 0 for an empty guardian set, which let unsigned VAAs
pass the signature check. Changed to return 1, and added a rejection of
empty guardian sets in the upgrade handler.

Replaced String::from_utf8().unwrap() in the governance handler with
proper error propagation via map_err.

All fixes are covered by new unit tests. Full test suite passes (60/60).

Author Checklist

I have...

  • included the correct type prefix in the PR title
  • added ! to the type prefix if API or client breaking change - not a breaking change
  • targeted the correct branch (see PR Targeting)
  • provided a link to the relevant issue or specification - does not fix an out right issue
  • included the necessary unit and integration tests
  • added a changelog entry to CHANGELOG.md - minor patch to contract functions
  • included comments for documenting Go code
  • updated the relevant documentation or specification
  • reviewed "Files changed" and left comments if necessary
  • confirmed all CI checks have passed

@chalabi2 chalabi2 requested a review from a team as a code owner March 19, 2026 22:17
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 19, 2026

Walkthrough

Added input validation to VAA and governance-packet deserialization to avoid panics, switched governance module decoding to a fallible UTF-8 conversion, enforced non-empty guardian sets, and raised minimum quorum from 0 to 1. Tests were extended to cover these cases across contract entry points.

Changes

Cohort / File(s) Summary
Core validation changes
contracts/wormhole/src/state.rs, contracts/wormhole/src/contract.rs
ParsedVAA::deserialize now checks data.len() against HEADER_LEN and returns InvalidVAA for short inputs. GovernancePacket::deserialize adds MIN_LEN and checks input length; governance module bytes are decoded using String::from_utf8 (no unwrap). GuardianSetInfo::quorum returns 1 for empty addresses and vaa_update_guardian_set rejects empty guardian sets early.
Tests & harness
contracts/wormhole/src/testing.rs
Test harness imports updated to include execute and new message types. Added unit and integration tests for short/invalid VAAs, governance packet UTF-8 handling, minimum-length parsing, and quorum behavior when guardian sets are empty.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped through bytes and checked each door,

No more unwraps to make me snore,
Guardians present, quorum not nil,
Short VAAs halted, tests fit the bill,
A safer burrow—code that's sure! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and specifically describes the main security fix: hardening VAA and governance deserialization against malformed input, which is the primary objective of all changes across all three modified files.
Description check ✅ Passed The description is directly related to the changeset, providing clear context on the bugs fixed (panics on short inputs, quorum of 0 bug, unwrap panic) and confirms test coverage, aligning well with the actual code changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

You can disable poems in the walkthrough.

Disable the reviews.poem setting to disable the poems in the walkthrough.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
contracts/wormhole/src/state.rs (2)

297-301: ⚠️ Potential issue | 🟡 Minor

Missing length check in ContractUpgrade::deserialize.

data.get_u64(24) reads 8 bytes starting at offset 24, requiring at least 32 bytes. Short input could cause a panic.

🛡️ Proposed fix
 impl ContractUpgrade {
     pub fn deserialize(data: &[u8]) -> StdResult<Self> {
+        if data.len() < 32 {
+            return ContractError::InvalidVAA.std_err();
+        }
         let new_contract = data.get_u64(24);
         Ok(ContractUpgrade { new_contract })
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/wormhole/src/state.rs` around lines 297 - 301, The
ContractUpgrade::deserialize implementation is missing a bounds check before
calling data.get_u64(24); add a guard that verifies data.len() >= 32 and return
a suitable StdResult::Err (e.g., StdError::generic_err or StdError::invalid_len)
with a clear message when the input is too short, then only call
data.get_u64(24) to construct and return ContractUpgrade { new_contract } so
short slices cannot panic.

305-310: ⚠️ Potential issue | 🟡 Minor

Add minimum length validation to prevent panic on short input.

GuardianSetUpgrade::deserialize calls data.get_u32(0) and data.get_u8(4) before any bounds check. The underlying ByteUtils implementation uses slice operations that panic on out-of-bounds access. If data is shorter than 5 bytes, this will panic. Add a minimum length check upfront.

🛡️ Proposed fix
 impl GuardianSetUpgrade {
     pub fn deserialize(data: &[u8]) -> StdResult<Self> {
         const ADDRESS_LEN: usize = 20;
+        const MIN_LEN: usize = 5; // 4-byte index + 1-byte guardian count
+
+        if data.len() < MIN_LEN {
+            return ContractError::InvalidVAA.std_err();
+        }

         let new_guardian_set_index = data.get_u32(0);

         let n_guardians = data.get_u8(4);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/wormhole/src/state.rs` around lines 305 - 310, The deserialize
implementation for GuardianSetUpgrade is calling data.get_u32(0) and
data.get_u8(4) with no bounds checks, which can panic on inputs shorter than 5
bytes; update GuardianSetUpgrade::deserialize to first verify data.len() >= 5
(or a computed minimum if you need ADDRESS_LEN and n_guardians later) and return
an Err(StdError::generic_err(...)) via StdResult<Self> when the slice is too
short before calling get_u32/get_u8; reference the ADDRESS_LEN constant and the
get_u32/get_u8 calls so the check is placed immediately before those calls to
prevent any out-of-bounds slice access.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@contracts/wormhole/src/state.rs`:
- Around line 297-301: The ContractUpgrade::deserialize implementation is
missing a bounds check before calling data.get_u64(24); add a guard that
verifies data.len() >= 32 and return a suitable StdResult::Err (e.g.,
StdError::generic_err or StdError::invalid_len) with a clear message when the
input is too short, then only call data.get_u64(24) to construct and return
ContractUpgrade { new_contract } so short slices cannot panic.
- Around line 305-310: The deserialize implementation for GuardianSetUpgrade is
calling data.get_u32(0) and data.get_u8(4) with no bounds checks, which can
panic on inputs shorter than 5 bytes; update GuardianSetUpgrade::deserialize to
first verify data.len() >= 5 (or a computed minimum if you need ADDRESS_LEN and
n_guardians later) and return an Err(StdError::generic_err(...)) via
StdResult<Self> when the slice is too short before calling get_u32/get_u8;
reference the ADDRESS_LEN constant and the get_u32/get_u8 calls so the check is
placed immediately before those calls to prevent any out-of-bounds slice access.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f3040419-88d7-4bae-8fcb-ea59fadbaa62

📥 Commits

Reviewing files that changed from the base of the PR and between 176e3b7 and 6afce84.

📒 Files selected for processing (3)
  • contracts/wormhole/src/contract.rs
  • contracts/wormhole/src/state.rs
  • contracts/wormhole/src/testing.rs

…ormed input

ParsedVAA::deserialize panicked on inputs shorter than 6 bytes due to
unchecked ByteUtils access before the first bounds check. GovernancePacket
had the same class of bug for inputs under 35 bytes. Both are now guarded
with early length validation that returns a clean InvalidVAA error.

quorum() returned 0 for an empty guardian set, which let unsigned VAAs
pass the signature check. Changed to return 1, and added a rejection of
empty guardian sets in the upgrade handler.

Replaced String::from_utf8().unwrap() in the governance handler with
proper error propagation via map_err.

All fixes are covered by new unit tests. Full test suite passes (60/60).

Made-with: Cursor
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
contracts/wormhole/src/state.rs (1)

265-275: Use MIN_LEN consistently to avoid constant drift.

Line 275 still slices with a hardcoded 35; tie it to Self::MIN_LEN so future format changes stay safe.

♻️ Suggested small refactor
-        let payload = data[35..].to_vec();
+        let payload = data[Self::MIN_LEN..].to_vec();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/wormhole/src/state.rs` around lines 265 - 275, The code hardcodes
the payload start index as 35 instead of using the MIN_LEN constant; in
deserialize (and any related offsets) replace the hardcoded slice with
Self::MIN_LEN (e.g., payload = data[Self::MIN_LEN..].to_vec()) so the header
length is consistently derived from MIN_LEN and won't drift if MIN_LEN changes;
keep the existing get_bytes32/get_u8/get_u16 uses as-is or compute offsets from
Self::MIN_LEN if you prefer a single derived header_len variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@contracts/wormhole/src/state.rs`:
- Around line 265-275: The code hardcodes the payload start index as 35 instead
of using the MIN_LEN constant; in deserialize (and any related offsets) replace
the hardcoded slice with Self::MIN_LEN (e.g., payload =
data[Self::MIN_LEN..].to_vec()) so the header length is consistently derived
from MIN_LEN and won't drift if MIN_LEN changes; keep the existing
get_bytes32/get_u8/get_u16 uses as-is or compute offsets from Self::MIN_LEN if
you prefer a single derived header_len variable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 95e2347c-5c61-438b-952c-3d63f8d1f84d

📥 Commits

Reviewing files that changed from the base of the PR and between 6afce84 and ba219d8.

📒 Files selected for processing (3)
  • contracts/wormhole/src/contract.rs
  • contracts/wormhole/src/state.rs
  • contracts/wormhole/src/testing.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • contracts/wormhole/src/contract.rs
  • contracts/wormhole/src/testing.rs

@troian troian merged commit c21e4db into akash-network:main Mar 20, 2026
16 of 17 checks passed
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.

2 participants