Bug Description
Return<T>::get() in the CPI codegen calls sol_get_return_data() which returns (program_id, data), but discards the program_id via:
let (_key, data) = anchor_lang::solana_program::program::get_return_data().unwrap();
This means the caller has no way to verify the return data actually came from the expected program.
Impact
Solana's set_return_data() uses a last-writer-wins model. If Program A CPIs into Program B, and B CPIs into a malicious Program C, then C can call set_return_data() to overwrite B's return data. When A reads via Return<T>::get(), it receives C's spoofed data without any validation.
This affects every Anchor program that uses CPI return values — a framework-level vulnerability.
Affected Code
Both codegen paths discard the program_id:
lang/syn/src/codegen/program/cpi.rs (standard #[program])
lang/attribute/program/src/declare_program/mods/cpi.rs (declare_program!)
Internal Evidence
The same repository already implements the correct pattern in spl/src/token_2022.rs (lines 377-388), where get_return_data() validates key != ctx.program_id:
.and_then(|(key, data)| {
if key != ctx.program_id {
Err(ProgramError::IncorrectProgramId)
} else {
// ...deserialize
}
})
Reproduction
A full PoC with 3 programs (caller, callee, malicious) demonstrating the spoofing attack is included in the fix PR: #4231
Suggested Fix
- Store the expected
program_id (from CpiContext) in the Return<T> struct
- Validate in
get() that get_return_data() key matches
- Add
get_unchecked() for backward compatibility
Full write-up: https://gist.github.com/rz1989s/f0d0d217c27a28f89bbec11b3b439cb6
Bug Description
Return<T>::get()in the CPI codegen callssol_get_return_data()which returns(program_id, data), but discards theprogram_idvia:This means the caller has no way to verify the return data actually came from the expected program.
Impact
Solana's
set_return_data()uses a last-writer-wins model. If Program A CPIs into Program B, and B CPIs into a malicious Program C, then C can callset_return_data()to overwrite B's return data. When A reads viaReturn<T>::get(), it receives C's spoofed data without any validation.This affects every Anchor program that uses CPI return values — a framework-level vulnerability.
Affected Code
Both codegen paths discard the program_id:
lang/syn/src/codegen/program/cpi.rs(standard#[program])lang/attribute/program/src/declare_program/mods/cpi.rs(declare_program!)Internal Evidence
The same repository already implements the correct pattern in
spl/src/token_2022.rs(lines 377-388), whereget_return_data()validateskey != ctx.program_id:Reproduction
A full PoC with 3 programs (caller, callee, malicious) demonstrating the spoofing attack is included in the fix PR: #4231
Suggested Fix
program_id(fromCpiContext) in theReturn<T>structget()thatget_return_data()key matchesget_unchecked()for backward compatibilityFull write-up: https://gist.github.com/rz1989s/f0d0d217c27a28f89bbec11b3b439cb6