From b63fcbe69a114e84c3acef5bfae5650656275a51 Mon Sep 17 00:00:00 2001 From: Umar Sheik Date: Fri, 6 Mar 2026 10:16:14 +0530 Subject: [PATCH 1/2] feat(missing_account_reload): LazyAccount support --- anchor-lints-utils/src/diag_items.rs | 19 +++++-- lints/missing_account_reload/src/lib.rs | 42 +++++++++++++- .../missing_account_reload/src/utils/paths.rs | 8 ++- .../tests/test_program/src/lib.rs | 56 ++++++++++++++++++- 4 files changed, 114 insertions(+), 11 deletions(-) diff --git a/anchor-lints-utils/src/diag_items.rs b/anchor-lints-utils/src/diag_items.rs index 6241819..a2f0b68 100644 --- a/anchor-lints-utils/src/diag_items.rs +++ b/anchor-lints-utils/src/diag_items.rs @@ -78,6 +78,8 @@ pub enum DiagnoticItem { SplTokenAccount, /// `spl_token::state::Mint` SplTokenMint, + /// `anchor_lang::prelude::LazyAccount` + AnchorLazyAccount, } impl DiagnoticItem { @@ -149,6 +151,9 @@ impl DiagnoticItem { DiagnoticItem::SplTokenMint => { return None; } + DiagnoticItem::AnchorLazyAccount => { + return None; + } }) } @@ -255,6 +260,7 @@ impl DiagnoticItem { } DiagnoticItem::SplTokenAccount => &["spl_token::state::Account"], DiagnoticItem::SplTokenMint => &["spl_token::state::Mint"], + DiagnoticItem::AnchorLazyAccount => &["anchor_lang::prelude::LazyAccount"], } } @@ -463,10 +469,10 @@ pub fn is_solana_instruction_type(tcx: TyCtxt, ty: Ty) -> bool { pub fn is_box_type(tcx: TyCtxt, ty: Ty) -> bool { let ty = ty.peel_refs(); - if let ty::Adt(adt_def, _) = ty.kind() { - if let Some(box_def_id) = tcx.lang_items().owned_box() { - return adt_def.did() == box_def_id; - } + if let ty::Adt(adt_def, _) = ty.kind() + && let Some(box_def_id) = tcx.lang_items().owned_box() + { + return adt_def.did() == box_def_id; } false } @@ -492,3 +498,8 @@ pub fn is_cpi_builder_constructor_fn(tcx: TyCtxt, def_id: DefId) -> bool { || path.contains("UnlockV1") || path.contains("RevokeStaking") } + +pub fn is_anchor_lazy_account_type(tcx: TyCtxt, ty: Ty) -> bool { + let ty = ty.peel_refs(); + DiagnoticItem::AnchorLazyAccount.defid_is_type(tcx, ty) +} diff --git a/lints/missing_account_reload/src/lib.rs b/lints/missing_account_reload/src/lib.rs index 160a826..7e4cc31 100644 --- a/lints/missing_account_reload/src/lib.rs +++ b/lints/missing_account_reload/src/lib.rs @@ -10,7 +10,7 @@ use std::collections::{HashMap, HashSet}; use anchor_lints_utils::utils::should_skip_function; use anchor_lints_utils::{ - diag_items::{DiagnoticItem, is_cpi_invoke_fn}, + diag_items::{DiagnoticItem, is_anchor_lazy_account_type, is_cpi_invoke_fn}, mir_analyzer::{AnchorContextInfo, MirAnalyzer}, utils::get_hir_body_from_local_def_id, }; @@ -124,6 +124,25 @@ impl<'tcx> LateLintPass<'tcx> for MissingAccountReload { } } } + // Check for LazyAccount::load/LazyAccount::load_mut (reloads for LazyAccount) + else if let Some(name) = cx.tcx.opt_item_name(*fn_def_id) + && (name.as_str() == "load" || name.as_str() == "load_mut") + && let Some(receiver) = args.first() + && let Operand::Move(place) | Operand::Copy(place) = &receiver.node + && let Some(local) = place.as_local() + && let Some(decl) = mir.local_decls.get(local) + { + let receiver_ty = decl.ty.peel_refs(); + if is_anchor_lazy_account_type(cx.tcx, receiver_ty) + && let Some(account_name_and_local) = + mir_analyzer.extract_account_name_from_local(&local, false) + { + account_reloads + .entry(account_name_and_local.account_name) + .or_default() + .insert(bb); + } + } // Or a CPI invoke function else if is_cpi_invoke_fn(cx.tcx, *fn_def_id) || mir_analyzer.takes_cpi_context(args) @@ -375,6 +394,27 @@ pub fn analyze_nested_function_operations<'tcx>( nested_function_blocks.push(block); } } + // Check for LazyAccount::load/LazyAccount::load_mut (reloads for LazyAccount) + else if let Some(name) = cx.tcx.opt_item_name(*def_id) + && (name.as_str() == "load" || name.as_str() == "load_mut") + && let Some(receiver) = args.first() + && let Operand::Move(place) | Operand::Copy(place) = &receiver.node + && let Some(local) = place.as_local() + && let Some(decl) = mir_body.local_decls.get(local) + { + let receiver_ty = decl.ty.peel_refs(); + if is_anchor_lazy_account_type(cx.tcx, receiver_ty) + && let Some(block) = handle_account_reload_in_nested_function( + &mir_analyzer, + mir_body, + args, + *fn_span, + bb, + ) + { + nested_function_blocks.push(block); + } + } // Handle account access (deref) in nested function else if cx .tcx diff --git a/lints/missing_account_reload/src/utils/paths.rs b/lints/missing_account_reload/src/utils/paths.rs index 32c117b..5015b53 100644 --- a/lints/missing_account_reload/src/utils/paths.rs +++ b/lints/missing_account_reload/src/utils/paths.rs @@ -1,8 +1,8 @@ use anchor_lints_utils::{ diag_items::{ is_account_info_type, is_anchor_account_loader_type, is_anchor_account_type, - is_anchor_interface_account_type, is_anchor_signer_type, is_anchor_system_account_type, - is_anchor_unchecked_account_type, is_box_type, + is_anchor_interface_account_type, is_anchor_lazy_account_type, is_anchor_signer_type, + is_anchor_system_account_type, is_anchor_unchecked_account_type, is_box_type, }, mir_analyzer::AnchorContextInfo, }; @@ -58,7 +58,9 @@ pub fn contains_deserialized_data<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> if is_anchor_account_loader_type(cx.tcx, ty) { return false; } - if is_anchor_interface_account_type(cx.tcx, ty) || is_anchor_system_account_type(cx.tcx, ty) + if is_anchor_interface_account_type(cx.tcx, ty) + || is_anchor_system_account_type(cx.tcx, ty) + || is_anchor_lazy_account_type(cx.tcx, ty) { return true; } diff --git a/lints/missing_account_reload/tests/test_program/src/lib.rs b/lints/missing_account_reload/tests/test_program/src/lib.rs index 9fbe52c..40b5670 100644 --- a/lints/missing_account_reload/tests/test_program/src/lib.rs +++ b/lints/missing_account_reload/tests/test_program/src/lib.rs @@ -4,7 +4,7 @@ use anchor_lang::solana_program::{ system_instruction, }; -use anchor_lang::system_program::{transfer, Transfer}; +use anchor_lang::system_program::{Transfer, transfer}; declare_id!("11111111111111111111111111111111"); @@ -319,18 +319,56 @@ pub mod missing_account_reload_tests { } // Pattern 17: CPI call with self implementation - safe - pub fn invoke_with_self_implementation_safe(ctx: Context, amount: u64) -> Result<()> { + pub fn invoke_with_self_implementation_safe( + ctx: Context, + amount: u64, + ) -> Result<()> { ctx.accounts.cpi_call_safe(amount)?; let _final_data = ctx.accounts.pda_account.data; // [safe_account_accessed] Ok(()) } // Pattern 18: CPI call with self implementation - unsafe - pub fn invoke_with_self_implementation_unsafe(ctx: Context, amount: u64) -> Result<()> { + pub fn invoke_with_self_implementation_unsafe( + ctx: Context, + amount: u64, + ) -> Result<()> { ctx.accounts.cpi_call_unsafe(amount)?; let _final_data = ctx.accounts.pda_account.data; // [unsafe_account_accessed] Ok(()) } + + /// Patten 19: load_mut() then CPI — unsafe. + pub fn test_lazy_account_unsafe( + ctx: Context, + amount: u64, + ) -> Result<()> { + let data = ctx.accounts.lazy_acc.load_mut()?; // ref held across CPI + let _ = data.value; + let new_space = ctx.accounts.lazy_acc.to_account_info().data_len() as u64 + amount; + let ix = system_instruction::allocate(&ctx.accounts.lazy_acc.key(), new_space); + let account_infos = vec![ + ctx.accounts.lazy_acc.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ]; + invoke(&ix, &account_infos)?; // [cpi_call] + let _ = data.value; // [unsafe_account_accessed] + Ok(()) + } + + // Pattern 20: load_mut() then CPI with load_mut() — safe + pub fn test_lazy_account_safe(ctx: Context, amount: u64) -> Result<()> { + let new_space = ctx.accounts.lazy_acc.to_account_info().data_len() as u64 + amount; + let ix = system_instruction::allocate(&ctx.accounts.lazy_acc.key(), new_space); + let account_infos = vec![ + ctx.accounts.lazy_acc.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ]; + invoke(&ix, &account_infos)?; + let data = ctx.accounts.lazy_acc.load_mut()?; + let _ = data.value; // [safe_account_accessed] + Ok(()) + } } pub fn cpi_call_safe(ctx_a: &mut Context, amount: u64) -> Result<()> { let from_pubkey = ctx_a.accounts.pda_account.to_account_info(); @@ -614,6 +652,18 @@ pub struct UserState { pub data: u64, } +#[account] +pub struct LazyLoadable { + pub value: u64, +} + +#[derive(Accounts)] +pub struct LazyAccountCpiAccounts<'info> { + #[account(mut)] + pub lazy_acc: LazyAccount<'info, LazyLoadable>, + pub system_program: Program<'info, System>, +} + #[account] pub struct InnerAccount { pub data: u64, From 20f7064928ba1628a9025ab7f6a18888a0644c04 Mon Sep 17 00:00:00 2001 From: Umar Sheik Date: Wed, 11 Mar 2026 22:11:54 +0530 Subject: [PATCH 2/2] fix: cargo test fail --- lints/missing_account_reload/tests/test_program/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lints/missing_account_reload/tests/test_program/Cargo.toml b/lints/missing_account_reload/tests/test_program/Cargo.toml index f6f1223..4127dfa 100644 --- a/lints/missing_account_reload/tests/test_program/Cargo.toml +++ b/lints/missing_account_reload/tests/test_program/Cargo.toml @@ -9,4 +9,4 @@ workspace = "../../../../tests" crate-type = ["cdylib"] [dependencies] -anchor-lang = { workspace = true } +anchor-lang = { workspace = true, features = ["lazy-account"] }