From 090dfbf4994165ebaf2680f3add313236f620983 Mon Sep 17 00:00:00 2001 From: febo Date: Fri, 8 May 2026 13:07:14 +0100 Subject: [PATCH 1/4] Add rent exempt check --- program/src/processor/allocate.rs | 11 +++++++---- program/src/processor/extend.rs | 21 ++++++++++++++++----- program/src/processor/initialize.rs | 18 ++++++++++++------ program/src/processor/trim.rs | 16 ++++++++++++---- program/src/processor/write.rs | 12 +++++++++++- 5 files changed, 58 insertions(+), 20 deletions(-) diff --git a/program/src/processor/allocate.rs b/program/src/processor/allocate.rs index 4065635..67c5b0d 100644 --- a/program/src/processor/allocate.rs +++ b/program/src/processor/allocate.rs @@ -3,6 +3,7 @@ use pinocchio::{ instruction::{Seed, Signer}, program_error::ProgramError, pubkey::find_program_address, + sysvars::{rent::Rent, Sysvar}, ProgramResult, }; use pinocchio_system::instructions::{Allocate, Assign}; @@ -81,10 +82,6 @@ pub fn allocate(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramRes // Allocates the space for the buffer account and assigns it to // the program. - if buffer.lamports() == 0 { - return Err(ProgramError::AccountNotRentExempt); - } - let space = Buffer::LEN as u64; match (is_pda, canonical) { @@ -138,6 +135,12 @@ pub fn allocate(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramRes _ => return Err(ProgramError::InvalidAccountData), } + let minimum_balance = Rent::get()?.minimum_balance(buffer.data_len()); + + if buffer.lamports() < minimum_balance { + return Err(ProgramError::AccountNotRentExempt); + } + // Writes the buffer header. // SAFETY: single mutable borrow of the `buffer` account data. The legth of the buffer account diff --git a/program/src/processor/extend.rs b/program/src/processor/extend.rs index 20fa3fa..eb6ff5f 100644 --- a/program/src/processor/extend.rs +++ b/program/src/processor/extend.rs @@ -1,5 +1,10 @@ use core::mem::size_of; -use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, +}; use crate::state::{buffer::Buffer, AccountDiscriminator}; @@ -54,11 +59,17 @@ pub fn extend(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResul } } - // Reallocates the account size. - // The length of the data is never more than `10_000_000`; adding a `u16` // will never overflow the `usize` limit. - account.realloc(account.data_len() + extend_length as usize, false)?; + let length = account.data_len() + extend_length as usize; + + let minimum_balance = Rent::get()?.minimum_balance(length); + + if account.lamports() < minimum_balance { + return Err(ProgramError::AccountNotRentExempt); + } + + // Reallocates the account size. - Ok(()) + account.realloc(length, false) } diff --git a/program/src/processor/initialize.rs b/program/src/processor/initialize.rs index 2aad5d4..6481f55 100644 --- a/program/src/processor/initialize.rs +++ b/program/src/processor/initialize.rs @@ -4,7 +4,9 @@ use pinocchio::{ memory::sol_memcpy, program_error::ProgramError, pubkey::{find_program_address, Pubkey}, - seeds, ProgramResult, + seeds, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, }; use pinocchio_system::instructions::{Allocate, Assign}; @@ -101,9 +103,6 @@ pub fn initialize(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramR return Err(ProgramError::AccountAlreadyInitialized) } None => { - if metadata.lamports() == 0 { - return Err(ProgramError::AccountNotRentExempt); - } // Ensure remaining data is provided. if remaining_data.is_empty() { return Err(ProgramError::InvalidInstructionData); @@ -124,11 +123,12 @@ pub fn initialize(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramR ) }; let signer = &[Signer::from(signer_seeds)]; + // Instruction data is limited to ~1232 bytes. + let space = Header::LEN + remaining_data.len(); Allocate { account: metadata, - // Instruction data is limited to 1232 bytes. - space: (Header::LEN + remaining_data.len()) as u64, + space: space as u64, } .invoke_signed(signer)?; @@ -138,6 +138,12 @@ pub fn initialize(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramR } .invoke_signed(signer)?; + let minimum_balance = Rent::get()?.minimum_balance(space); + + if metadata.lamports() < minimum_balance { + return Err(ProgramError::AccountNotRentExempt); + } + // SAFETY: scoped mutable borrow of `metadata` account data. The data is // guaranteed to be allocated and assigned to the program. let metadata_account_data = unsafe { metadata.borrow_mut_data_unchecked() }; diff --git a/program/src/processor/trim.rs b/program/src/processor/trim.rs index f835bdc..279d833 100644 --- a/program/src/processor/trim.rs +++ b/program/src/processor/trim.rs @@ -53,19 +53,27 @@ pub fn trim(accounts: &[AccountInfo]) -> ProgramResult { let minimum_balance = { // SAFETY: single immutable borrow of `rent_sysver` account data. - let rent = unsafe { Rent::from_bytes(rent_sysvar.borrow_data_unchecked())? }; + let rent = unsafe { Rent::from_account_info_unchecked(rent_sysvar)? }; rent.minimum_balance(length) }; account.realloc(length, false)?; + // Current lamports should always be greater than or equal to the minimum + // balance since the account must be rent exempt. + let excess_lamports = account + .lamports() + .checked_sub(minimum_balance) + .ok_or(ProgramError::AccountNotRentExempt)?; + // SAFETY: single mutable borrow if `account` and `destination` lamports. unsafe { let account_lamports = account.borrow_mut_lamports_unchecked(); let destination_lamports = destination.borrow_mut_lamports_unchecked(); - // Current lamports should always be greater than or equal to the minimum - // balance since the account is rent exempt. - *destination_lamports += *account_lamports - minimum_balance; + + *destination_lamports = destination_lamports + .checked_add(excess_lamports) + .ok_or(ProgramError::ArithmeticOverflow)?; *account_lamports = minimum_balance; } diff --git a/program/src/processor/write.rs b/program/src/processor/write.rs index b216e5d..098875a 100644 --- a/program/src/processor/write.rs +++ b/program/src/processor/write.rs @@ -1,7 +1,11 @@ use core::cmp::max; use pinocchio::{ - account_info::AccountInfo, memory::sol_memcpy, program_error::ProgramError, ProgramResult, + account_info::AccountInfo, + memory::sol_memcpy, + program_error::ProgramError, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, }; use crate::state::{buffer::Buffer, header::Header, AccountDiscriminator}; @@ -78,6 +82,12 @@ pub fn write(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult (max(data.len(), offset + source_data.len()), source_data) }; + let minimum_balance = Rent::get()?.minimum_balance(required_length); + + if target_buffer.lamports() < minimum_balance { + return Err(ProgramError::AccountNotRentExempt); + } + // Writes the source data to the buffer account. target_buffer.realloc(required_length, false)?; From 8093547a7e7cbfccfc143eb9a031818484c1e2d1 Mon Sep 17 00:00:00 2001 From: febo Date: Fri, 8 May 2026 13:07:30 +0100 Subject: [PATCH 2/4] Add test --- program/tests/trim.rs | 50 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/program/tests/trim.rs b/program/tests/trim.rs index b0bac0d..4d33120 100644 --- a/program/tests/trim.rs +++ b/program/tests/trim.rs @@ -3,8 +3,9 @@ pub use setup::*; use mollusk_svm::{program::keyed_account_for_system_program, result::Check}; use solana_account::Account; +use solana_program_error::ProgramError; use solana_pubkey::Pubkey; -use solana_sdk_ids::system_program; +use solana_sdk_ids::{system_program, sysvar::rent}; use spl_program_metadata::state::{buffer::Buffer, header::Header}; const EXCESS_LAMPORTS: usize = 90; @@ -256,3 +257,50 @@ fn test_trim_buffer() { ], ); } + +#[test] +fn fail_trim_non_rent_exempt_account() { + let destination_key = Pubkey::new_unique(); + let destination_account = create_empty_account(Buffer::LEN, PROGRAM_ID); + + // A program-owned account non-rent exempt on purpose to try trigger + // an underflow + let fake_buffer_key = Pubkey::new_unique(); + let mut fake_buffer_account = Account { + lamports: 0, + owner: PROGRAM_ID, + data: vec![1; Buffer::LEN], // buffer discriminator + ..Default::default() + }; + // Set the authority to a non-zero value to pass the authority validation. + fake_buffer_account.data[33..65].copy_from_slice(fake_buffer_key.as_array()); + + let destination_lamports = destination_account.lamports; + + process_instructions( + &[( + &trim( + &fake_buffer_key, + &fake_buffer_key, + None, + None, + &destination_key, + ) + .unwrap(), + &[ + Check::err(ProgramError::AccountNotRentExempt), + Check::account(&destination_key) + .lamports(destination_lamports) + .build(), + Check::account(&fake_buffer_key).lamports(0).build(), + ], + )], + &[ + (fake_buffer_key, fake_buffer_account), + (PROGRAM_ID, Account::default()), + (destination_key, destination_account), + keyed_account_for_system_program(), + (rent::ID, rent_sysvar()), + ], + ); +} From 04d329ba09b6b5df67f61c75a7613df73821e206 Mon Sep 17 00:00:00 2001 From: febo Date: Fri, 8 May 2026 21:36:10 +0100 Subject: [PATCH 3/4] Update comments --- program/src/processor/extend.rs | 2 +- program/src/processor/write.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/program/src/processor/extend.rs b/program/src/processor/extend.rs index eb6ff5f..29b1530 100644 --- a/program/src/processor/extend.rs +++ b/program/src/processor/extend.rs @@ -36,7 +36,7 @@ pub fn extend(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResul // - must be a buffer or metadata account // - must have a valid authority // - must be rent exempt (pre-funded account) since we are reallocating the buffer - // account (this is tested implicity) + // account if account.data_is_empty() { return Err(ProgramError::InvalidAccountData); diff --git a/program/src/processor/write.rs b/program/src/processor/write.rs index 098875a..e3736b8 100644 --- a/program/src/processor/write.rs +++ b/program/src/processor/write.rs @@ -38,7 +38,7 @@ pub fn write(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult // target_buffer // - must be initialized // - must be rent exempt (pre-funded account) since we are reallocating the buffer - // account (this is tested implicity) + // account let (required_length, source_data) = { // SAFETY: scoped immutable borrow of `buffer` account data. There From 7b61011ec53dac96d31151274ff1344f256c5ff7 Mon Sep 17 00:00:00 2001 From: febo Date: Fri, 8 May 2026 21:43:51 +0100 Subject: [PATCH 4/4] Add audit ignores --- scripts/rust/audit.mjs | 86 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/scripts/rust/audit.mjs b/scripts/rust/audit.mjs index 43bd2cf..d517a4d 100644 --- a/scripts/rust/audit.mjs +++ b/scripts/rust/audit.mjs @@ -20,6 +20,92 @@ const advisories = [ // URL: https://rustsec.org/advisories/RUSTSEC-2024-0376 // Solution: Upgrade to >=0.12.3 'RUSTSEC-2024-0376', + + // Remove the ignores below once the dependencies are updated to versions + // that have the vulnerabilities fixed. + + // Crate: quinn-proto + // Version: 0.11.12 + // Title: Denial of service in Quinn endpoints + // Date: 2026-03-09 + // ID: RUSTSEC-2026-0037 + // URL: https://rustsec.org/advisories/RUSTSEC-2026-0037 + // Severity: 8.7 (high) + // Solution: Upgrade to >=0.11.14 + 'RUSTSEC-2026-0037', + + //Crate: rustls-webpki + // Version: 0.101.7 + // Title: Reachable panic in certificate revocation list parsing + // Date: 2026-04-22 + // ID: RUSTSEC-2026-0104 + // URL: https://rustsec.org/advisories/RUSTSEC-2026-0104 + // Solution: Upgrade to >=0.103.13, <0.104.0-alpha.1 OR >=0.104.0-alpha.7 + 'RUSTSEC-2026-0104', + + // Crate: rustls-webpki + // Version: 0.101.7 + // Title: Name constraints for URI names were incorrectly accepted + // Date: 2026-04-14 + // ID: RUSTSEC-2026-0098 + // URL: https://rustsec.org/advisories/RUSTSEC-2026-0098 + // Solution: Upgrade to >=0.103.12, <0.104.0-alpha.1 OR >=0.104.0-alpha.6 + 'RUSTSEC-2026-0098', + + // Crate: rustls-webpki + // Version: 0.101.7 + // Title: Name constraints were accepted for certificates asserting a wildcard name + // Date: 2026-04-14 + // ID: RUSTSEC-2026-0099 + // URL: https://rustsec.org/advisories/RUSTSEC-2026-0099 + // Solution: Upgrade to >=0.103.12, <0.104.0-alpha.1 OR >=0.104.0-alpha.6 + 'RUSTSEC-2026-0099', + + // Crate: rustls-webpki + // Version: 0.103.4 + // Title: Reachable panic in certificate revocation list parsing + // Date: 2026-04-22 + // ID: RUSTSEC-2026-0104 + // URL: https://rustsec.org/advisories/RUSTSEC-2026-0104 + // Solution: Upgrade to >=0.103.13, <0.104.0-alpha.1 OR >=0.104.0-alpha.7 + 'RUSTSEC-2026-0104', + + // Crate: rustls-webpki + // Version: 0.103.4 + // Title: CRLs not considered authoritative by Distribution Point due to faulty matching logic + // Date: 2026-03-20 + // ID: RUSTSEC-2026-0049 + // URL: https://rustsec.org/advisories/RUSTSEC-2026-0049 + // Solution: Upgrade to >=0.103.10 + 'RUSTSEC-2026-0049', + + // Crate: rustls-webpki + // Version: 0.103.4 + // Title: Name constraints for URI names were incorrectly accepted + // Date: 2026-04-14 + // ID: RUSTSEC-2026-0098 + // URL: https://rustsec.org/advisories/RUSTSEC-2026-0098 + // Solution: Upgrade to >=0.103.12, <0.104.0-alpha.1 OR >=0.104.0-alpha.6 + 'RUSTSEC-2026-0098', + + // Crate: rustls-webpki + // Version: 0.103.4 + // Title: Name constraints were accepted for certificates asserting a wildcard name + // Date: 2026-04-14 + // ID: RUSTSEC-2026-0099 + // URL: https://rustsec.org/advisories/RUSTSEC-2026-0099 + // Solution: Upgrade to >=0.103.12, <0.104.0-alpha.1 OR >=0.104.0-alpha.6 + 'RUSTSEC-2026-0099', + + // Crate: time + // Version: 0.3.41 + // Title: Denial of Service via Stack Exhaustion + // Date: 2026-02-05 + // ID: RUSTSEC-2026-0009 + // URL: https://rustsec.org/advisories/RUSTSEC-2026-0009 + // Severity: 6.8 (medium) + // Solution: Upgrade to >=0.3.47 + 'RUSTSEC-2026-0009' ]; const ignores = [] advisories.forEach(x => {