Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bench/programs/multisig/anchor-v2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub mod multisig_v2 {

#[discrim = 0]
pub fn create(ctx: &mut Context<Create>, threshold: u8) -> Result<()> {
let remaining = ctx.remaining_accounts();
let remaining = ctx.remaining_accounts()?;
ctx.accounts.create_multisig(threshold, &remaining)?;
ctx.accounts.config.bump = ctx.bumps.config;
Ok(())
Expand All @@ -34,7 +34,7 @@ pub mod multisig_v2 {

#[discrim = 3]
pub fn execute_transfer(ctx: &mut Context<ExecuteTransfer>, amount: u64) -> Result<()> {
let remaining = ctx.remaining_accounts();
let remaining = ctx.remaining_accounts()?;
ctx.accounts.verify_and_transfer(amount, ctx.bumps.vault, &remaining)
}
}
4 changes: 4 additions & 0 deletions lang-v2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,7 @@ required-features = ["testing"]
[[test]]
name = "miri_wrapper_accounts"
required-features = ["testing"]

[[test]]
name = "remaining_accounts_mut_check"
required-features = ["testing"]
62 changes: 62 additions & 0 deletions lang-v2/derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,62 @@ fn impl_accounts(input: &DeriveInput) -> TokenStream2 {
}
}
};
let dynamic_mut_mask_terms: Vec<proc_macro2::TokenStream> = fields
.iter()
.filter_map(|f| {
if f.contributes_active_mut_bit {
Some(quote::quote! { true })
} else {
parse::extract_nested_inner_type(&f.ty).map(|inner_ty| {
quote::quote! {
<#inner_ty as anchor_lang_v2::TryAccounts>::HAS_DYNAMIC_MUT_MASK
}
})
}
})
.collect();
let has_dynamic_mut_mask_expr = if dynamic_mut_mask_terms.is_empty() {
quote::quote! { false }
} else {
quote::quote! {
false #(|| #dynamic_mut_mask_terms)*
}
};
let active_mut_mask_steps: Vec<proc_macro2::TokenStream> = fields
.iter()
.filter_map(|f| {
let field_name = &f.name;
let offset = &f.offset_expr;
if f.contributes_active_mut_bit {
Some(quote! {
if self.#field_name.is_some() {
__mask = anchor_lang_v2::mut_mask_set_bit(__mask, #offset);
}
})
} else if let Some(inner_ty) = parse::extract_nested_inner_type(&f.ty) {
Some(quote! {
if <#inner_ty as anchor_lang_v2::TryAccounts>::HAS_DYNAMIC_MUT_MASK {
__mask = anchor_lang_v2::mut_mask_or_shifted(
__mask,
<#inner_ty as anchor_lang_v2::TryAccounts>::active_mut_mask(&self.#field_name.0),
#offset,
);
}
})
} else {
None
}
})
.collect();
let active_mut_mask_body = if active_mut_mask_steps.is_empty() {
quote::quote! { Self::MUT_MASK }
} else {
quote::quote! {
let mut __mask = Self::MUT_MASK;
#(#active_mut_mask_steps)*
__mask
}
};

// IDL collection — the accounts-JSON emission is a runtime function
// (not a `&'static str` const) so it can read
Expand Down Expand Up @@ -1158,6 +1214,12 @@ fn impl_accounts(input: &DeriveInput) -> TokenStream2 {
impl anchor_lang_v2::TryAccounts for #name {
const HEADER_SIZE: usize = #header_size_expr;
const MUT_MASK: [u64; 4] = #mut_mask_expr;
const HAS_DYNAMIC_MUT_MASK: bool = #has_dynamic_mut_mask_expr;

#[inline(always)]
fn active_mut_mask(&self) -> [u64; 4] {
#active_mut_mask_body
}

#ix_args_assoc

Expand Down
6 changes: 6 additions & 0 deletions lang-v2/derive/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,9 @@ pub struct AccountField {
/// client sends `program_id` as the address) should still silence the
/// dup check; the derive keeps the gated per-field `get()` for those.
pub contributes_mut_bit: bool,
/// `true` iff this optional field contributes to the runtime active
/// mutable mask when it loads as `Some`.
pub contributes_active_mut_bit: bool,
/// The local payer field named by this field's `init`/`init_if_needed`
/// constraint, if present.
pub init_payer: Option<String>,
Expand Down Expand Up @@ -1256,6 +1259,7 @@ pub fn parse_field(
// into the parent's; they don't set a bit at the nested field's
// own offset.
contributes_mut_bit: false,
contributes_active_mut_bit: false,
init_payer: None,
idl_writable: false,
idl_init_signer: false,
Expand Down Expand Up @@ -1985,6 +1989,7 @@ pub fn parse_field(
};

let contributes_mut_bit = attrs.is_mut && !attrs.is_dup && !is_optional;
let contributes_active_mut_bit = attrs.is_mut && !attrs.is_dup && is_optional;
let init_payer = (attrs.is_init || attrs.is_init_if_needed)
.then(|| attrs.payer.as_ref().map(ToString::to_string))
.flatten();
Expand All @@ -2001,6 +2006,7 @@ pub fn parse_field(
is_optional,
offset_expr,
contributes_mut_bit,
contributes_active_mut_bit,
init_payer,
idl_writable,
idl_init_signer,
Expand Down
130 changes: 104 additions & 26 deletions lang-v2/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use {
crate::cursor::AccountCursor,
pinocchio::{account::AccountView, address::Address},
solana_program_error::ProgramError,
};

/// Instruction-scoped context passed to every handler. Holds the
Expand All @@ -19,21 +20,56 @@ pub struct Context<'a, T: Bumps> {
/// pass them in as arguments.
pub bumps: T::Bumps,

/// Cursor into the serialized input buffer, positioned at the
/// Holds either a cursor into the serialized input buffer, pointing to the
/// *start* of the remaining-accounts region (after `try_accounts`
/// has consumed exactly `T::HEADER_SIZE` declared accounts). Used
/// by [`Self::remaining_accounts`] for on-demand walking.
cursor: &'a mut AccountCursor,
/// After `remaining_accounts` is called this holds a cache of the result.
remaining_accounts: RemainingAccounts<'a>,

/// Number of accounts after the declared region. Decremented to
/// zero once the remaining region is walked.
remaining_num: u8,
/// Mutable-account mask covering active mutable accounts in the declared
/// region. Starts from `T::MUT_MASK` and includes optional mutable fields
/// only when the loaded account is `Some`. Used by
/// [`Self::remaining_accounts`] to re-check each trailing account against
/// declared mut slots —
/// without this, a trailing account whose dup index points at a
/// mut declared account would silently alias it (bug 3: the
/// `HEADER_SIZE`-only check in `run_handler` can't see dups that
/// only surface during the trailing walk).
mut_mask: MutMask,
}

pub enum MutMask {
Static(&'static [u64; 4]),
Dynamic([u64; 4]),
}

/// Cache of the parsed remaining accounts. Populated lazily on the
/// first call to [`Self::remaining_accounts`]; subsequent calls
/// return a clone of the cached vec so the caller receives an
/// owned value (avoiding borrow conflicts with `self.accounts`).
remaining_cache: Option<alloc::vec::Vec<AccountView>>,
impl MutMask {
#[inline(always)]
fn as_ref(&self) -> &[u64; 4] {
match self {
Self::Static(mask) => mask,
Self::Dynamic(mask) => mask,
}
}
}

enum RemainingAccounts<'a> {
Unparsed {
/// Points to the `remaining-accounts` region
cursor: &'a mut AccountCursor,
/// Number of accounts remaining after the initially declared region
remaining: u8,
},
/// Cached result of walking `remaining-accounts`; will return an error if
/// duplicate mutable constraints are violated.
Cached(Result<alloc::vec::Vec<AccountView>, ProgramError>),
}

impl<'a> RemainingAccounts<'a> {
fn is_unparsed(&self) -> bool {
matches!(self, RemainingAccounts::Unparsed { .. })
}
}

impl<'a, T: Bumps> Context<'a, T> {
Expand All @@ -44,33 +80,75 @@ impl<'a, T: Bumps> Context<'a, T> {
bumps: T::Bumps,
cursor: &'a mut AccountCursor,
remaining_num: u8,
mut_mask: MutMask,
) -> Self {
Self {
program_id,
accounts,
bumps,
cursor,
remaining_num,
remaining_cache: None,
remaining_accounts: RemainingAccounts::Unparsed {
cursor,
remaining: remaining_num,
},
mut_mask,
}
}

/// Returns trailing accounts beyond the declared `T` fields as an
/// owned `Vec<AccountView>`. First call walks the cursor and caches;
/// subsequent calls clone the cache. Owned vec avoids borrow conflicts
/// with `self.accounts` / `self.bumps`.
pub fn remaining_accounts(&mut self) -> alloc::vec::Vec<AccountView> {
if self.remaining_cache.is_none() {
let mut v = alloc::vec::Vec::with_capacity(self.remaining_num as usize);
for _ in 0..self.remaining_num {
// SAFETY: cursor is positioned at the start of the
// remaining region and `remaining_num` is the exact
// number of accounts to walk.
v.push(unsafe { self.cursor.next() });
/// owned `Vec<AccountView>`. First call walks the cursor and caches
/// the resulting `Result`; subsequent calls replay the cache (clone
/// of the vec on success, clone of the error on failure). Caching
/// the error is important: the walk advances an unsafe cursor, and
/// a handler that calls this again after an error must not trigger
/// another `cursor.next()` loop.
///
/// After each cursor advance, re-tests the cursor's duplicate bitvec
/// against the active mutable mask. If a trailing account's dup index
/// resolves to a declared mut slot, returns
/// `ConstraintDuplicateMutableAccount`. The `HEADER_SIZE`-only check
/// in `run_handler` only sees duplicates that existed at the end of
/// the declared walk; trailing-region dups can only be caught here.
///
/// The mask is sized per declared field, so bits set for trailing indices
/// (past `HEADER_SIZE`) are naturally zero — the intersect only fires when
/// a trailing slot's bit overlaps with an active declared mut slot's bit,
/// which by construction means the runtime resolved the trailing slot as a
/// dup of that declared mut account.
pub fn remaining_accounts(&mut self) -> Result<alloc::vec::Vec<AccountView>, ProgramError> {
if self.remaining_accounts.is_unparsed() {
self.remaining_accounts = RemainingAccounts::Cached(self.walk_remaining());
}

match &self.remaining_accounts {
RemainingAccounts::Cached(Ok(accs)) => Ok(accs.clone()),
RemainingAccounts::Cached(Err(err)) => Err(err.clone()),
RemainingAccounts::Unparsed { .. } => unreachable!(),
}
}

fn walk_remaining(&mut self) -> Result<alloc::vec::Vec<AccountView>, ProgramError> {
let RemainingAccounts::Unparsed {
ref mut cursor,
remaining,
} = self.remaining_accounts
else {
unreachable!()
};
let mut v = alloc::vec::Vec::with_capacity(remaining as usize);
let mut_mask = self.mut_mask.as_ref();
for _ in 0..remaining {
// SAFETY: cursor is positioned at the start of the remaining
// region and `remaining_num` is the exact number of accounts
// to walk. Walking stops on the first mut-alias error so we
// never advance past the remaining region.
v.push(unsafe { cursor.next() });
if let Some(dups) = cursor.duplicates() {
if dups.intersects(mut_mask) {
return Err(crate::ErrorCode::ConstraintDuplicateMutableAccount.into());
}
}
self.remaining_cache = Some(v);
}
self.remaining_cache.as_ref().unwrap().clone()
Ok(v)
}
}

Expand Down
12 changes: 12 additions & 0 deletions lang-v2/src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ impl AccountCursor {
self.consumed
}

/// Current duplicate-tracking bitvec. `None` if the cursor has not
/// yet yielded a duplicate account (lazy allocation — see
/// [`Self::next`]). Used by
/// [`Context::remaining_accounts`](crate::context::Context::remaining_accounts)
/// to re-check `MUT_MASK` after each trailing account is walked,
/// catching aliases of declared mut accounts that only surface past
/// `HEADER_SIZE`.
#[inline(always)]
pub fn duplicates(&self) -> Option<&AccountBitvec> {
self.duplicate.as_ref()
}

/// Walk N accounts in a tight loop, storing views in the lookup array.
/// Returns a slice of the walked views and the duplicate tracking bitvec.
/// This avoids interleaving cursor math with validation logic, letting
Expand Down
26 changes: 24 additions & 2 deletions lang-v2/src/dispatch.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use {
crate::{
context::{Bumps, Context},
context::{Bumps, Context, MutMask},
cursor::{AccountBitvec, AccountCursor},
loader::AccountLoader,
},
Expand Down Expand Up @@ -31,6 +31,16 @@ pub trait TryAccounts: Bumps + Sized {
/// does today.
const MUT_MASK: [u64; 4];

/// True when [`Self::active_mut_mask`] can differ from [`Self::MUT_MASK`].
/// This lets accounts without optional mutable fields keep the old static
/// mask path.
const HAS_DYNAMIC_MUT_MASK: bool;

/// Runtime mutable-account mask for checks that happen after declared
/// accounts are loaded. This includes optional mutable accounts only when
/// they resolved to `Some`.
fn active_mut_mask(&self) -> [u64; 4];

/// Parsed instruction args carried alongside validated accounts.
/// Accounts structs without `#[instruction(...)]` use `()`.
type IxArgs<'ix>;
Expand Down Expand Up @@ -86,7 +96,19 @@ pub fn run_handler<'a, T: TryAccounts, R>(
};
const _: () = assert!(pinocchio::MAX_TX_ACCOUNTS <= u8::MAX as usize);
let remaining_num = (num_accounts - T::HEADER_SIZE) as u8;
let mut ctx = Context::new(program_id, ctx_accounts, bumps, cursor, remaining_num);
let mut_mask = if remaining_num != 0 && T::HAS_DYNAMIC_MUT_MASK {
MutMask::Dynamic(ctx_accounts.active_mut_mask())
} else {
MutMask::Static(&T::MUT_MASK)
};
let mut ctx = Context::new(
program_id,
ctx_accounts,
bumps,
cursor,
remaining_num,
mut_mask,
);
let result = handler(&mut ctx, ix_args)?;
ctx.accounts.exit_accounts()?;
Ok(result)
Expand Down
2 changes: 1 addition & 1 deletion lang-v2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ pub use {
program, Accounts, InitSpace,
},
bytemuck,
context::{Bumps, Context},
context::{Bumps, Context, MutMask},
context_cpi::{invoke_signed_fixed, CpiContext},
cpi::{
create_account, create_account_signed, create_program_address,
Expand Down
Loading
Loading