diff --git a/clients/rust-legacy/tests/cpi_guard.rs b/clients/rust-legacy/tests/cpi_guard.rs index c2c3061fd..69ee10479 100644 --- a/clients/rust-legacy/tests/cpi_guard.rs +++ b/clients/rust-legacy/tests/cpi_guard.rs @@ -9,7 +9,9 @@ use { solana_sdk::{ instruction::InstructionError, pubkey::Pubkey, rent::Rent, signature::Signer, signer::keypair::Keypair, transaction::TransactionError, transport::TransportError, + transaction::Transaction, }, + solana_system_interface::instruction as system_instruction, spl_instruction_padding_interface::instruction::wrap_instruction, spl_token_2022_interface::{ error::TokenError, @@ -685,6 +687,57 @@ async fn test_cpi_guard_unwrap_lamports() { assert_eq!(alice_state.base.amount, amount); } +#[tokio::test] +async fn test_cpi_guard_withdraw_excess_lamports() { + let context = make_context_with_new_mint().await; + let program_context = context.context.clone(); + let TokenContext { + token, alice, bob, .. + } = context.token_context.unwrap(); + + let withdraw_excess_lamports = [wrap_instruction( + spl_instruction_padding_interface::id(), + instruction::withdraw_excess_lamports( + &spl_token_2022_interface::id(), + &alice.pubkey(), + &bob.pubkey(), + &alice.pubkey(), + &[], + ) + .unwrap(), + vec![], + 0, + ) + .unwrap()]; + + token + .enable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice]) + .await + .unwrap(); + + { + let context = program_context.lock().await; + let instructions = vec![system_instruction::transfer( + &context.payer.pubkey(), + &alice.pubkey(), + 1, + )]; + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + } + + let error = token + .process_ixs(&withdraw_excess_lamports, &[&alice]) + .await + .expect_err("expected CPI withdraw_excess_lamports to be blocked by CPI Guard"); + assert_eq!(error, client_error(TokenError::CpiGuardTransferBlocked)); +} + async fn make_close_test_account( token: &Token, owner: &S, diff --git a/clients/rust-legacy/tests/token_metadata_initialize.rs b/clients/rust-legacy/tests/token_metadata_initialize.rs index dc443df20..1936b4000 100644 --- a/clients/rust-legacy/tests/token_metadata_initialize.rs +++ b/clients/rust-legacy/tests/token_metadata_initialize.rs @@ -9,7 +9,9 @@ use { }, spl_token_2022_interface::{error::TokenError, extension::BaseStateWithExtensions}, spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - spl_token_metadata_interface::{error::TokenMetadataError, state::TokenMetadata}, + spl_token_metadata_interface::{ + error::TokenMetadataError, instruction::Initialize, state::TokenMetadata, + }, std::{convert::TryInto, sync::Arc}, }; @@ -282,3 +284,53 @@ async fn fail_without_signature() { ))) ); } + +#[tokio::test] +async fn fail_initialize_with_forged_name_length_returns_invalid_instruction_data() { + let authority = Pubkey::new_unique(); + let mint_keypair = Keypair::new(); + let mut test_context = setup(mint_keypair, &authority).await; + + let token_context = test_context.token_context.take().unwrap(); + + let name = "Name".to_string(); + let symbol = "SYM".to_string(); + let uri = "uri".to_string(); + let payload = borsh::to_vec(&Initialize { + name: name.clone(), + symbol: symbol.clone(), + uri: uri.clone(), + }) + .unwrap(); + let mut instruction = spl_token_metadata_interface::instruction::initialize( + &spl_token_2022_interface::id(), + token_context.token.get_address(), + &Pubkey::new_unique(), + token_context.token.get_address(), + &token_context.mint_authority.pubkey(), + name, + symbol, + uri, + ); + + let payload_offset = instruction.data.len().checked_sub(payload.len()).unwrap(); + let length_prefix_len = std::mem::size_of::(); + instruction.data[payload_offset..payload_offset + length_prefix_len] + .copy_from_slice(&u32::MAX.to_le_bytes()); + + let error = token_context + .token + .process_ixs(&[instruction], &[&token_context.mint_authority]) + .await + .unwrap_err(); + + // Once the metadata decoder is hardened, malformed recognized payloads + // should reject as InvalidInstructionData instead of aborting or falling + // through to another error shape. + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData) + ))) + ); +} diff --git a/clients/rust-legacy/tests/token_metadata_remove_key.rs b/clients/rust-legacy/tests/token_metadata_remove_key.rs index bff96d9fa..a3ad092c8 100644 --- a/clients/rust-legacy/tests/token_metadata_remove_key.rs +++ b/clients/rust-legacy/tests/token_metadata_remove_key.rs @@ -14,7 +14,7 @@ use { spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, spl_token_metadata_interface::{ error::TokenMetadataError, - instruction::remove_key, + instruction::{remove_key, RemoveKey}, state::{Field, TokenMetadata}, }, std::{convert::TryInto, sync::Arc}, @@ -250,3 +250,65 @@ async fn fail_authority_checks() { TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature,) ); } + +#[tokio::test] +async fn fail_remove_key_with_forged_key_length_returns_invalid_instruction_data() { + let authority = Keypair::new(); + let mint_keypair = Keypair::new(); + let mut test_context = setup(mint_keypair, &authority.pubkey()).await; + let payer_pubkey = test_context.context.lock().await.payer.pubkey(); + let token_context = test_context.token_context.take().unwrap(); + + let update_authority = Keypair::new(); + token_context + .token + .token_metadata_initialize_with_rent_transfer( + &payer_pubkey, + &update_authority.pubkey(), + &token_context.mint_authority.pubkey(), + "MySuperCoolToken".to_string(), + "MINE".to_string(), + "my.super.cool.token".to_string(), + &[&token_context.mint_authority], + ) + .await + .unwrap(); + + let key = "new_name".to_string(); + let idempotent = true; + let idempotent_prefix = borsh::to_vec(&idempotent).unwrap(); + let payload = borsh::to_vec(&RemoveKey { + key: key.clone(), + idempotent, + }) + .unwrap(); + let mut instruction = remove_key( + &spl_token_2022_interface::id(), + token_context.token.get_address(), + &update_authority.pubkey(), + key, + idempotent, + ); + + let payload_offset = instruction.data.len().checked_sub(payload.len()).unwrap(); + let key_length_offset = payload_offset + idempotent_prefix.len(); + let length_prefix_len = std::mem::size_of::(); + instruction.data[key_length_offset..key_length_offset + length_prefix_len] + .copy_from_slice(&u32::MAX.to_le_bytes()); + + let error = token_context + .token + .process_ixs(&[instruction], &[&update_authority]) + .await + .unwrap_err(); + + // Once the metadata decoder is hardened, malformed recognized payloads + // should reject as InvalidInstructionData instead of aborting or falling + // through to another error shape. + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData) + ))) + ); +} diff --git a/clients/rust-legacy/tests/token_metadata_update_field.rs b/clients/rust-legacy/tests/token_metadata_update_field.rs index a8a1d07e3..93e7167c2 100644 --- a/clients/rust-legacy/tests/token_metadata_update_field.rs +++ b/clients/rust-legacy/tests/token_metadata_update_field.rs @@ -11,7 +11,7 @@ use { spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, spl_token_metadata_interface::{ error::TokenMetadataError, - instruction::update_field, + instruction::{update_field, UpdateField}, state::{Field, TokenMetadata}, }, std::{convert::TryInto, sync::Arc}, @@ -198,3 +198,65 @@ async fn fail_authority_checks() { ))) ); } + +#[tokio::test] +async fn fail_update_field_with_forged_value_length_returns_invalid_instruction_data() { + let authority = Keypair::new(); + let mint_keypair = Keypair::new(); + let mut test_context = setup(mint_keypair, &authority.pubkey()).await; + let payer_pubkey = test_context.context.lock().await.payer.pubkey(); + let token_context = test_context.token_context.take().unwrap(); + + let update_authority = Keypair::new(); + token_context + .token + .token_metadata_initialize_with_rent_transfer( + &payer_pubkey, + &update_authority.pubkey(), + &token_context.mint_authority.pubkey(), + "MySuperCoolToken".to_string(), + "MINE".to_string(), + "my.super.cool.token".to_string(), + &[&token_context.mint_authority], + ) + .await + .unwrap(); + + let field = Field::Name; + let value = "new_name".to_string(); + let field_prefix = borsh::to_vec(&field).unwrap(); + let payload = borsh::to_vec(&UpdateField { + field: field.clone(), + value: value.clone(), + }) + .unwrap(); + let mut instruction = update_field( + &spl_token_2022_interface::id(), + token_context.token.get_address(), + &update_authority.pubkey(), + field, + value, + ); + + let payload_offset = instruction.data.len().checked_sub(payload.len()).unwrap(); + let value_length_offset = payload_offset + field_prefix.len(); + let length_prefix_len = std::mem::size_of::(); + instruction.data[value_length_offset..value_length_offset + length_prefix_len] + .copy_from_slice(&u32::MAX.to_le_bytes()); + + let error = token_context + .token + .process_ixs(&[instruction], &[&update_authority]) + .await + .unwrap_err(); + + // Once the metadata decoder is hardened, malformed recognized payloads + // should reject as InvalidInstructionData instead of aborting or falling + // through to another error shape. + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData) + ))) + ); +} diff --git a/program/src/processor.rs b/program/src/processor.rs index 7e18a9630..b64fac74b 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -73,6 +73,15 @@ use { std::convert::{TryFrom, TryInto}, }; +const TOKEN_METADATA_DISCRIMINATOR_LENGTH: usize = 8; +const U32_BYTES: usize = 4; +const TOKEN_METADATA_INITIALIZE_DISCRIMINATOR: [u8; TOKEN_METADATA_DISCRIMINATOR_LENGTH] = + [0xd2, 0xe1, 0x1e, 0xa2, 0x58, 0xb8, 0x4d, 0x8d]; +const TOKEN_METADATA_UPDATE_FIELD_DISCRIMINATOR: [u8; TOKEN_METADATA_DISCRIMINATOR_LENGTH] = + [0xdd, 0xe9, 0x31, 0x2d, 0xb5, 0xca, 0xdc, 0xc8]; +const TOKEN_METADATA_REMOVE_KEY_DISCRIMINATOR: [u8; TOKEN_METADATA_DISCRIMINATOR_LENGTH] = + [0xea, 0x12, 0x20, 0x38, 0x59, 0x8d, 0x25, 0xb5]; + pub(crate) enum TransferInstruction { Unchecked, Checked { decimals: u8 }, @@ -96,6 +105,98 @@ pub(crate) enum BurnInstructionVariant { /// Program state handler. pub struct Processor {} impl Processor { + fn read_u8(input: &[u8], cursor: &mut usize) -> Result { + let value = *input + .get(*cursor) + .ok_or(ProgramError::InvalidInstructionData)?; + *cursor = cursor + .checked_add(1) + .ok_or(ProgramError::InvalidInstructionData)?; + Ok(value) + } + + fn read_u32(input: &[u8], cursor: &mut usize) -> Result { + let end = cursor + .checked_add(U32_BYTES) + .ok_or(ProgramError::InvalidInstructionData)?; + let bytes = input + .get(*cursor..end) + .and_then(|slice| slice.try_into().ok()) + .ok_or(ProgramError::InvalidInstructionData)?; + *cursor = end; + Ok(u32::from_le_bytes(bytes)) + } + + fn skip_borsh_string(input: &[u8], cursor: &mut usize) -> ProgramResult { + let length = usize::try_from(Self::read_u32(input, cursor)?) + .map_err(|_| ProgramError::InvalidInstructionData)?; + let end = cursor + .checked_add(length) + .ok_or(ProgramError::InvalidInstructionData)?; + input + .get(*cursor..end) + .ok_or(ProgramError::InvalidInstructionData)?; + *cursor = end; + Ok(()) + } + + fn validate_no_remaining_bytes(input: &[u8], cursor: usize) -> ProgramResult { + if cursor == input.len() { + Ok(()) + } else { + Err(ProgramError::InvalidInstructionData) + } + } + + fn validate_token_metadata_initialize_payload(input: &[u8]) -> ProgramResult { + let mut cursor = 0; + Self::skip_borsh_string(input, &mut cursor)?; + Self::skip_borsh_string(input, &mut cursor)?; + Self::skip_borsh_string(input, &mut cursor)?; + Self::validate_no_remaining_bytes(input, cursor) + } + + fn validate_token_metadata_update_field_payload(input: &[u8]) -> ProgramResult { + let mut cursor = 0; + match Self::read_u8(input, &mut cursor)? { + 0..=2 => {} + 3 => Self::skip_borsh_string(input, &mut cursor)?, + _ => return Err(ProgramError::InvalidInstructionData), + } + Self::skip_borsh_string(input, &mut cursor)?; + Self::validate_no_remaining_bytes(input, cursor) + } + + fn validate_token_metadata_remove_key_payload(input: &[u8]) -> ProgramResult { + let mut cursor = 0; + match Self::read_u8(input, &mut cursor)? { + 0 | 1 => {} + _ => return Err(ProgramError::InvalidInstructionData), + } + Self::skip_borsh_string(input, &mut cursor)?; + Self::validate_no_remaining_bytes(input, cursor) + } + + fn validate_affected_token_metadata_instruction(input: &[u8]) -> Result { + if input.len() < TOKEN_METADATA_DISCRIMINATOR_LENGTH { + return Ok(false); + } + + let (discriminator, rest) = input.split_at(TOKEN_METADATA_DISCRIMINATOR_LENGTH); + if discriminator == TOKEN_METADATA_INITIALIZE_DISCRIMINATOR.as_slice() { + Self::validate_token_metadata_initialize_payload(rest)?; + Ok(true) + } else if discriminator == TOKEN_METADATA_UPDATE_FIELD_DISCRIMINATOR.as_slice() { + Self::validate_token_metadata_update_field_payload(rest)?; + Ok(true) + } else if discriminator == TOKEN_METADATA_REMOVE_KEY_DISCRIMINATOR.as_slice() { + Self::validate_token_metadata_remove_key_payload(rest)?; + Ok(true) + } else { + Ok(false) + } + } + fn _process_initialize_mint( accounts: &[AccountInfo], decimals: u8, @@ -1666,6 +1767,12 @@ impl Processor { authority_info.data_len(), account_info_iter.as_slice(), )?; + + if let Ok(cpi_guard) = account.get_extension::() { + if cpi_guard.lock_cpi.into() && in_cpi() { + return Err(TokenError::CpiGuardTransferBlocked.into()); + } + } } else if let Ok(mint) = PodStateWithExtensions::::unpack(&source_data) { match &mint.base.mint_authority { PodCOption { @@ -2113,6 +2220,10 @@ impl Processor { Self::process_unwrap_lamports(program_id, accounts, amount) } } + } else if Self::validate_affected_token_metadata_instruction(input)? { + let instruction = TokenMetadataInstruction::unpack(input) + .map_err(|_| ProgramError::InvalidInstructionData)?; + token_metadata::processor::process_instruction(program_id, accounts, instruction) } else if let Ok(instruction) = TokenMetadataInstruction::unpack(input) { token_metadata::processor::process_instruction(program_id, accounts, instruction) } else if let Ok(instruction) = TokenGroupInstruction::unpack(input) {