Skip to content

Commit fc97c60

Browse files
bit2swazclaude
andauthored
fix(interface): validate Borsh string lengths in unpack() to prevent SBF heap abort (#87)
* fix(interface): validate Borsh string lengths in unpack() to prevent SBF heap abort Forged u32 length prefixes in Initialize/UpdateField/RemoveKey payloads could trigger an oversized allocation in try_from_slice that aborts on the 32 KiB SBF heap. Validate each string length against the remaining buffer before deserializing, returning InvalidInstructionData for malformed payloads. Closes solana-program/token-2022#1152 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci: add "oversized" and "SBF" to spellcheck dictionary The doc comment on check_borsh_string uses both words; cargo spellcheck flagged them. They're a correct word and a known acronym (SBF sits alongside TLV, APY, DAO already in the dict). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent fd81046 commit fc97c60

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

interface/src/instruction.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,30 @@ pub enum TokenMetadataInstruction {
167167
/// 0. `[]` Metadata account
168168
Emit(Emit),
169169
}
170+
/// Validates that a Borsh-encoded `String` starting at `*cursor` declares a
171+
/// length that fits within `rest`, advancing `*cursor` past it on success.
172+
///
173+
/// `TokenMetadataInstruction::unpack` calls this before `try_from_slice` so a
174+
/// forged length prefix can't trigger an oversized allocation that aborts on
175+
/// the SBF heap. See <https://github.com/solana-program/token-2022/issues/1152>.
176+
fn check_borsh_string(rest: &[u8], cursor: &mut usize) -> Result<(), ProgramError> {
177+
let len_end = cursor
178+
.checked_add(core::mem::size_of::<u32>())
179+
.ok_or(ProgramError::InvalidInstructionData)?;
180+
let len_bytes = rest
181+
.get(*cursor..len_end)
182+
.ok_or(ProgramError::InvalidInstructionData)?;
183+
let len = u32::from_le_bytes(len_bytes.try_into().unwrap()) as usize;
184+
let str_end = len_end
185+
.checked_add(len)
186+
.ok_or(ProgramError::InvalidInstructionData)?;
187+
if str_end > rest.len() {
188+
return Err(ProgramError::InvalidInstructionData);
189+
}
190+
*cursor = str_end;
191+
Ok(())
192+
}
193+
170194
impl TokenMetadataInstruction {
171195
/// Unpacks a byte buffer into a
172196
/// [`TokenMetadataInstruction`](enum.TokenMetadataInstruction.html).
@@ -177,14 +201,33 @@ impl TokenMetadataInstruction {
177201
let (discriminator, rest) = input.split_at(ArrayDiscriminator::LENGTH);
178202
Ok(match discriminator {
179203
Initialize::SPL_DISCRIMINATOR_SLICE => {
204+
let mut cursor = 0usize;
205+
check_borsh_string(rest, &mut cursor)?; // name
206+
check_borsh_string(rest, &mut cursor)?; // symbol
207+
check_borsh_string(rest, &mut cursor)?; // uri
180208
let data = Initialize::try_from_slice(rest)?;
181209
Self::Initialize(data)
182210
}
183211
UpdateField::SPL_DISCRIMINATOR_SLICE => {
212+
let mut cursor = 0usize;
213+
// `Field` is a Borsh enum: 1-byte variant tag, and only the
214+
// `Key` variant (tag 3) carries a nested string.
215+
let tag = *rest
216+
.get(cursor)
217+
.ok_or(ProgramError::InvalidInstructionData)?;
218+
cursor = cursor
219+
.checked_add(1)
220+
.ok_or(ProgramError::InvalidInstructionData)?;
221+
if tag == 3 {
222+
check_borsh_string(rest, &mut cursor)?; // Field::Key(String)
223+
}
224+
check_borsh_string(rest, &mut cursor)?; // value
184225
let data = UpdateField::try_from_slice(rest)?;
185226
Self::UpdateField(data)
186227
}
187228
RemoveKey::SPL_DISCRIMINATOR_SLICE => {
229+
let mut cursor = 1usize; // skip the 1-byte `idempotent` bool
230+
check_borsh_string(rest, &mut cursor)?; // key
188231
let data = RemoveKey::try_from_slice(rest)?;
189232
Self::RemoveKey(data)
190233
}
@@ -416,6 +459,41 @@ mod test {
416459
check_pack_unpack(check, discriminator, data);
417460
}
418461

462+
#[test]
463+
fn fail_unpack_initialize_with_forged_name_length() {
464+
let mut input = vec![];
465+
input.extend_from_slice(Initialize::SPL_DISCRIMINATOR_SLICE);
466+
input.extend_from_slice(&u32::MAX.to_le_bytes()); // forged name length, no bytes follow
467+
assert_eq!(
468+
TokenMetadataInstruction::unpack(&input).unwrap_err(),
469+
ProgramError::InvalidInstructionData,
470+
);
471+
}
472+
473+
#[test]
474+
fn fail_unpack_update_field_with_forged_value_length() {
475+
let mut input = vec![];
476+
input.extend_from_slice(UpdateField::SPL_DISCRIMINATOR_SLICE);
477+
input.push(0u8); // Field::Name (tag 0, no nested string)
478+
input.extend_from_slice(&u32::MAX.to_le_bytes()); // forged value length
479+
assert_eq!(
480+
TokenMetadataInstruction::unpack(&input).unwrap_err(),
481+
ProgramError::InvalidInstructionData,
482+
);
483+
}
484+
485+
#[test]
486+
fn fail_unpack_remove_key_with_forged_key_length() {
487+
let mut input = vec![];
488+
input.extend_from_slice(RemoveKey::SPL_DISCRIMINATOR_SLICE);
489+
input.push(0u8); // idempotent = false
490+
input.extend_from_slice(&u32::MAX.to_le_bytes()); // forged key length
491+
assert_eq!(
492+
TokenMetadataInstruction::unpack(&input).unwrap_err(),
493+
ProgramError::InvalidInstructionData,
494+
);
495+
}
496+
419497
#[cfg(feature = "serde-traits")]
420498
#[test]
421499
fn initialize_serde() {

scripts/solana.dic

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,5 @@ Pedersen
6161
aes
6262
plaintext
6363
pausable
64+
oversized
65+
SBF

0 commit comments

Comments
 (0)