-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Description
Summary
The Anchor Framework's IDL interface Definition Language array type parser contains an unhandled error condition that causes a panic when processing malformed array type definitions.
This vulnerability exists in anchor-lang-idl-spec/src/lib.rs lines 363-364 and can be exploited by crafting malicious IDL files to cause denial of service during the build process.
Vulnerable Code
https://github.com/solana-foundation/anchor/blob/master/idl/spec/src/lib.rs
Location: idl/spec/src/lib.rs,
inside IdlType::from_str() implementation
rustimpl FromStr for IdlType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut s = s.to_owned();
s.retain(|c| !c.is_whitespace());
let r = match s.as_str() {
// ... primitive type matching ...
_ => {
// ... Option and Vec handling ...
if s.starts_with('[') {
fn array_from_str(inner: &str) -> IdlType {
match inner.strip_suffix(']') {
Some(nested_inner) => array_from_str(&nested_inner[1..]),
None => {
// VULNERABLE CODE - LINE 363
// Panics if semicolon is not present
let (raw_type, raw_length) = inner.rsplit_once(';').unwrap();
// VULNERABLE CODE - LINE 364 ⚠️
// Panics if type parsing fails
let ty = IdlType::from_str(raw_type).unwrap();
let len = match raw_length.replace('_', "").parse::<usize>() {
Ok(len) => IdlArrayLen::Value(len),
Err(_) => IdlArrayLen::Generic(raw_length.to_owned()),
};
IdlType::Array(Box::new(ty), len)
}
}
}
return Ok(array_from_str(&s));
}
// ... rest of parsing logic ...
}
};
Ok(r)
}
}
Detailed Vulnerability Explanation
Line 363:
Missing Semicolon Panic
rustlet (raw_type, raw_length) = inner.rsplit_once(';').unwrap();
What it does:
Attempts to split array syntax like [u8; 32] into type (u8) and length (32)
Uses rsplit_once(';') to find the semicolon separator
Calls .unwrap() without checking if semicolon exists
The Problem:
If the semicolon is missing (e.g., [u8 32]), rsplit_once() returns None
Calling .unwrap() on None causes a panic
The entire build process crashes with an unhelpful error message
Attack Vector:
rust// Valid input (works)
"[u8; 32]" → Some(("u8", "32")) → unwrap() → OK
// Malicious input (panics)
"[u8 32]" → None → unwrap() → PANIC!
"[u8:32]" → None → unwrap() → PANIC!
"[u832]" → None → unwrap() → PANIC!
Line 364: Invalid Type Panic
rustlet ty = IdlType::from_str(raw_type).unwrap();
What it does:
Recursively parses the array element type
Calls IdlType::from_str() which returns Result<IdlType, Error>
Calls .unwrap() without handling the error case
The Problem:
If the type is invalid (e.g., NonExistentType), from_str() returns Err
Calling .unwrap() on Err causes a panic
No error recovery or helpful message provided
Attack Vector:
rust// Valid input
"[u8; 32]" → from_str("u8") → Ok(U8) → unwrap() → OK
"[Pubkey; 10]" → from_str("Pubkey") → Ok(Pubkey) → unwrap() → OK
// Malicious input (panics)
"[InvalidType; 32]" → from_str("InvalidType") → Err → unwrap() → PANIC!
"[123; 32]" → from_str("123") → Err → unwrap() → PANIC!
"[@#$; 32]" → from_str("@#$") → Err → unwrap() → PANIC!
Proof of Concept
PoC #1: Missing Semicolon Attack
Test Code
rustuse anchor_lang_idl_spec::IdlType;
use std::str::FromStr;
fn main() {
println!("Testing malformed array without semicolon...");
// This will panic the program
let malformed = "[u8 32]";
let result = IdlType::from_str(malformed);
println!("Result: {:?}", result);
}
Expected Output:
Testing malformed array without semicolon
thread main panicked at idl/spec/src/lib.rs:363:62:
called Option::unwrap() on a None value
note: run with RUST_BACKTRACE=1 for a backtrace Impact:
All builds fail with cryptic panic messages
Hours wasted debugging the issue
Unable to identify root cause without deep investigation
Productivity loss across entire team
Root Cause
The vulnerability stems from three fundamental issues:
Defensive Programming Failure
assumes input is always valid
let (raw_type, raw_length) = inner.rsplit_once(';').unwrap();
// ^^^^^^^^
// Assumes semicolon always exists
Proper Approach:
rust// Should handle None case explicitly
let (raw_type, raw_length) = inner.rsplit_once(';')
.ok_or_else(|| anyhow!("Invalid array syntax: missing semicolon") Recommended Fix
Complete Secure Implementation
rustimpl FromStr for IdlType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Input validation
if s.is_empty() {
return Err(anyhow!("Type string cannot be empty"));
}
if s.len() > 1000 {
return Err(anyhow!("Type string too long (max 1000 chars)"));
}
let mut s = s.to_owned();
s.retain(|c| !c.is_whitespace());
let r = match s.as_str() {
"bool" => IdlType::Bool,
"u8" => IdlType::U8,
"i8" => IdlType::I8,
"u16" => IdlType::U16,
"i16" => IdlType::I16,
"u32" => IdlType::U32,
"i32" => IdlType::I32,
"f32" => IdlType::F32,
"u64" => IdlType::U64,
"i64" => IdlType::I64,
"f64" => IdlType::F64,
"u128" => IdlType::U128,
"i128" => IdlType::I128,
"u256" => IdlType::U256,
"i256" => IdlType::I256,
"Vec<u8>" => IdlType::Bytes,
"String" | "&str" | "&'staticstr" => IdlType::String,
"Pubkey" => IdlType::Pubkey,
_ => {
if let Some(inner) = s.strip_prefix("Option<") {
let inner_ty = Self::from_str(
inner
.strip_suffix('>')
.ok_or_else(|| anyhow!("Invalid Option syntax: missing '>'"))?,
)?;
return Ok(IdlType::Option(Box::new(inner_ty)));
}
if let Some(inner) = s.strip_prefix("Vec<") {
let inner_ty = Self::from_str(
inner
.strip_suffix('>')
.ok_or_else(|| anyhow!("Invalid Vec syntax: missing '>'"))?,
)?;
return Ok(IdlType::Vec(Box::new(inner_ty)));
}
if s.starts_with('[') {
// FIXED: Now returns Result instead of panicking
return array_from_str(&s);
}
// ... rest of parsing logic
IdlType::Defined {
name: s.to_owned(),
generics: vec![],
}
}
};
Ok(r)
}
}
FIXED CODE.
Changed return type to Result
fn array_from_str(inner: &str) -> Result<IdlType, anyhow::Error> {
match inner.strip_suffix(']') {
Some(nested_inner) => {
// Validate nested array has content
if nested_inner.len() <= 1 {
return Err(anyhow!("Invalid nested array syntax"));
}
// Recursive call, now propagates errors
array_from_str(&nested_inner[1..])
}
None => {
// FIXED: Properly handle missing semicolon
let (raw_type, raw_length) = inner
.rsplit_once(';')
.ok_or_else(|| anyhow!(
"Invalid array syntax: expected '[type; length]', found '{}'",
inner
))?;
// Validate type is not empty
let raw_type = raw_type.trim();
if raw_type.is_empty() {
return Err(anyhow!("Array type cannot be empty"));
}
// FIXED: Properly handle invalid type
let ty = IdlType::from_str(raw_type)
.map_err(|e| anyhow!(
"Invalid array element type '{}': {}",
raw_type, e
))?;
// Validate length is not empty
let raw_length = raw_length.trim();
if raw_length.is_empty() {
return Err(anyhow!("Array length cannot be empty"));
}
let len = match raw_length.replace('_', "").parse::<usize>() {
Ok(len) => {
// Validate reasonable array size
if len == 0 {
return Err(anyhow!("Array length cannot be zero"));
}
if len > 1_000_000 {
return Err(anyhow!("Array length too large (max 1,000,000)"));
}
IdlArrayLen::Value(len)
}
Err(_) => {
// Validate generic name format
if !raw_length.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err(anyhow!(
"Invalid array length or generic name: '{}'",
raw_length
));
}
IdlArrayLen::Generic(raw_length.to_owned())
}
};
Ok(IdlType::Array(Box::new(ty), len))
}
}
}
Key Improvements
Error Propagation: Changed array_from_str to return Result
Explicit Error Handling:
Replaced all .unwrap() with .ok_or_else()
Input Validation: Added checks for empty strings, size limits
Metadata
Metadata
Assignees
Labels
Type
Projects
Status