Skip to content

Conversation

@acheroncrypto
Copy link
Collaborator

@acheroncrypto acheroncrypto commented Dec 19, 2025

Problem

InterfaceAccount allows account substitution between unexpected types.

PoC

Passing AnotherAccount to an account typed as InterfaceAccount<ExpectedAccount>:

Details

The PR titled "feat(account): Check Owner on Reload" #3837 changed InterfaceAccount::try_from from:

pub fn try_from(info: &'a AccountInfo<'a>) -> Result<Account<'a, T>> {
if info.owner == &system_program::ID && info.lamports() == 0 {
return Err(ErrorCode::AccountNotInitialized.into());
}
if info.owner != &T::owner() {
return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
.with_pubkeys((*info.owner, T::owner())));
}
let mut data: &[u8] = &info.try_borrow_data()?;
Ok(Account::new(info, T::try_deserialize(&mut data)?))
}

to:

pub fn try_from(info: &'a AccountInfo<'a>) -> Result<Self> {
// `InterfaceAccount` targets foreign program accounts (e.g., SPL Token
// accounts) that do not have Anchor discriminators. Because of that, we
// intentionally skip the Anchor discriminator check here and instead:
//
// 1) Validate program ownership via `T::check_owner(info.owner)?`
// 2) Deserialize without a discriminator by delegating to
// `T::try_deserialize_unchecked`
Self::try_from_unchecked(info)
}

The author assumes InterfaceAccount is only intended to work with programs that "do not have Anchor discriminators". However, similar to Account, the InterfaceAccount implementation does not actually make any assumptions about discriminators. As its name suggests, it's for accounts that share the same interface between different programs; or in other words, it's the same as the Account type, but instead of checking only a single owner via the Owner trait, it allows multiple owners via the Owners trait.

Similar to the Account type, InterfaceAccount's documentation even has a section called Using InterfaceAccount with non-anchor programs that starts with:

/// InterfaceAccount can also be used with non-anchor programs. The data types from

which in itself should be enough to suggest that InterfaceAccount can also be used with Anchor program accounts, or generally, accounts that have discriminators.

The same erroneous understanding also resulted in changing the reload method of AccountInterface from:

self.account.reload()

which used Account::reload with checked account deserialization (AccountDeserialize::try_deserialize):

self.account = T::try_deserialize(&mut data)?;

to an implementation that uses unchecked account deserialization (AccountDeserialize::try_deserialize_unchecked):

let new_val = T::try_deserialize_unchecked(&mut data)?;

The misconception might have arisen from the fact that InterfaceAccount was initially added (in #2386) in order to make handling SPL Token and SPL Token 2022 accounts easier. However, its implementation is fully generic, just like the Account type. In fact, in its implementation PR, the first commit is titled 'Add "interface" and "interface account" concept', and it does not even touch anchor-spl.

The reason why anchor-spl types such as token::Mint and token_interface::Mint only implement try_deserialize_unchecked is because try_deserialize defaults to try_deserialize_unchecked (in this case they are all checked in reality):

anchor/lang/src/lib.rs

Lines 361 to 370 in 3a799e2

/// Deserializes previously initialized account data. Should fail for all
/// uninitialized accounts, where the bytes are zeroed. Implementations
/// should be unique to a particular account type so that one can never
/// successfully deserialize the data of one account type into another.
/// For example, if the SPL token program were to implement this trait,
/// it should be impossible to deserialize a `Mint` account into a token
/// `Account`.
fn try_deserialize(buf: &mut &[u8]) -> Result<Self> {
Self::try_deserialize_unchecked(buf)
}

This means changing try_deserialize to try_deserialize_unchecked here in the best case has no benefits (same impl), and in the worst case allows bypassing account checks.

Summary of changes (WIP)

  • Add tests that test the basic security checks of InterfaceAccount, including unexpected account substitution
  • Fix InterfaceAccount::try_from (revert to how it was before)

@vercel
Copy link

vercel bot commented Dec 19, 2025

@acheroncrypto is attempting to deploy a commit to the Solana Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@acheroncrypto
Copy link
Collaborator Author

Before the fix commit (CI):

✔ Allows old exptected accounts (48ms)
✔ Allows new exptected accounts
✔ Doesn't allow accounts owned by other programs
1) Doesn't allow unexpected accounts owned by the expected programs

After (CI):

✔ Allows old exptected accounts (49ms)
✔ Allows new exptected accounts
✔ Doesn't allow accounts owned by other programs
✔ Doesn't allow unexpected accounts owned by the expected programs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant