Skip to main content

sbpf_runtime/cpi/
validate.rs

1use {
2    crate::{
3        cpi::request::CpiRequest,
4        errors::{RuntimeError, RuntimeResult},
5    },
6    solana_account::Account,
7    solana_address::Address,
8    solana_instruction::AccountMeta,
9};
10
11/// Validates that the CPI request doesn't escalate privileges.
12pub fn check_privileges(
13    request: &CpiRequest,
14    caller_account_metas: &[AccountMeta],
15) -> RuntimeResult<()> {
16    for cpi_meta in &request.accounts {
17        let caller_meta = caller_account_metas
18            .iter()
19            .find(|m| m.pubkey == cpi_meta.pubkey);
20
21        match caller_meta {
22            Some(cm) => {
23                // Check signer.
24                if cpi_meta.is_signer
25                    && !cm.is_signer
26                    && !request.signers.contains(&cpi_meta.pubkey)
27                {
28                    return Err(RuntimeError::PrivilegeEscalation(
29                        "signer".to_string(),
30                        cpi_meta.pubkey.to_string(),
31                    ));
32                }
33
34                // Check writable.
35                if cpi_meta.is_writable && !cm.is_writable {
36                    return Err(RuntimeError::PrivilegeEscalation(
37                        "writable".to_string(),
38                        cpi_meta.pubkey.to_string(),
39                    ));
40                }
41            }
42            None => {
43                return Err(RuntimeError::MissingAccount(cpi_meta.pubkey.to_string()));
44            }
45        }
46    }
47    Ok(())
48}
49
50/// Validates that a single account change follows ownership rules.
51pub fn check_account_change(
52    callee_program_id: &Address,
53    pubkey: &Address,
54    account: &Account,
55    new_owner: &Address,
56    new_lamports: u64,
57    new_data: &[u8],
58) -> RuntimeResult<()> {
59    let is_owner = account.owner == *callee_program_id;
60
61    // Only the owner can change the owner.
62    if *new_owner != account.owner && !is_owner {
63        return Err(RuntimeError::PrivilegeEscalation(
64            "non-owner changed owner".to_string(),
65            pubkey.to_string(),
66        ));
67    }
68
69    // Only the owner can debit lamports.
70    if new_lamports < account.lamports && !is_owner {
71        return Err(RuntimeError::ExternalAccountLamportSpend(
72            pubkey.to_string(),
73        ));
74    }
75
76    // Only the owner can modify data.
77    if new_data != account.data.as_slice() && !is_owner {
78        return Err(RuntimeError::PrivilegeEscalation(
79            "non-owner modified data".to_string(),
80            pubkey.to_string(),
81        ));
82    }
83
84    // Executable accounts cannot have their data modified.
85    if account.executable && new_data != account.data.as_slice() {
86        return Err(RuntimeError::PrivilegeEscalation(
87            "executable account modified".to_string(),
88            pubkey.to_string(),
89        ));
90    }
91
92    Ok(())
93}
94
95#[cfg(test)]
96mod tests {
97    use {
98        super::*,
99        crate::cpi::request::{CpiAccountMeta, CpiRequest},
100    };
101
102    const PROGRAM_A: Address = Address::new_from_array([1u8; 32]);
103    const PROGRAM_B: Address = Address::new_from_array([2u8; 32]);
104    const ACCT_1: Address = Address::new_from_array([10u8; 32]);
105    const ACCT_2: Address = Address::new_from_array([11u8; 32]);
106    const PDA: Address = Address::new_from_array([20u8; 32]);
107
108    fn make_request(accounts: Vec<CpiAccountMeta>, signers: Vec<Address>) -> CpiRequest {
109        CpiRequest {
110            program_id: PROGRAM_B,
111            accounts,
112            data: vec![],
113            caller_accounts: vec![],
114            signers,
115        }
116    }
117
118    fn make_account(owner: Address, lamports: u64, data: &[u8]) -> Account {
119        Account {
120            lamports,
121            data: data.to_vec(),
122            owner,
123            executable: false,
124            rent_epoch: 0,
125        }
126    }
127
128    // Check privileges.
129
130    #[test]
131    fn check_privileges_ok() {
132        let request = make_request(
133            vec![CpiAccountMeta {
134                pubkey: ACCT_1,
135                is_signer: true,
136                is_writable: true,
137            }],
138            vec![],
139        );
140        let caller_metas = vec![AccountMeta {
141            pubkey: ACCT_1,
142            is_signer: true,
143            is_writable: true,
144        }];
145        assert!(check_privileges(&request, &caller_metas).is_ok());
146    }
147
148    #[test]
149    fn check_privileges_signer_escalation() {
150        let request = make_request(
151            vec![CpiAccountMeta {
152                pubkey: ACCT_1,
153                is_signer: true,
154                is_writable: false,
155            }],
156            vec![],
157        );
158        let caller_metas = vec![AccountMeta {
159            pubkey: ACCT_1,
160            is_signer: false,
161            is_writable: false,
162        }];
163        assert!(check_privileges(&request, &caller_metas).is_err());
164    }
165
166    #[test]
167    fn check_privileges_signer_via_pda() {
168        let request = make_request(
169            vec![CpiAccountMeta {
170                pubkey: PDA,
171                is_signer: true,
172                is_writable: false,
173            }],
174            vec![PDA],
175        );
176        let caller_metas = vec![AccountMeta {
177            pubkey: PDA,
178            is_signer: false,
179            is_writable: false,
180        }];
181        assert!(check_privileges(&request, &caller_metas).is_ok());
182    }
183
184    #[test]
185    fn check_privileges_writable_escalation() {
186        let request = make_request(
187            vec![CpiAccountMeta {
188                pubkey: ACCT_1,
189                is_signer: false,
190                is_writable: true,
191            }],
192            vec![],
193        );
194        let caller_metas = vec![AccountMeta {
195            pubkey: ACCT_1,
196            is_signer: false,
197            is_writable: false,
198        }];
199        assert!(check_privileges(&request, &caller_metas).is_err());
200    }
201
202    #[test]
203    fn check_privileges_missing_account() {
204        let request = make_request(
205            vec![CpiAccountMeta {
206                pubkey: ACCT_2,
207                is_signer: false,
208                is_writable: false,
209            }],
210            vec![],
211        );
212        let caller_metas = vec![AccountMeta {
213            pubkey: ACCT_1,
214            is_signer: false,
215            is_writable: false,
216        }];
217        assert!(check_privileges(&request, &caller_metas).is_err());
218    }
219
220    // Check account change.
221
222    #[test]
223    fn check_account_change_owner_modifies_data() {
224        let acct = make_account(PROGRAM_A, 100, b"hello");
225        assert!(
226            check_account_change(&PROGRAM_A, &ACCT_1, &acct, &PROGRAM_A, 100, b"world").is_ok()
227        );
228    }
229
230    #[test]
231    fn check_account_change_non_owner_modifies_data() {
232        let acct = make_account(PROGRAM_A, 100, b"hello");
233        assert!(
234            check_account_change(&PROGRAM_B, &ACCT_1, &acct, &PROGRAM_A, 100, b"world").is_err()
235        );
236    }
237
238    #[test]
239    fn check_account_change_owner_changes_owner() {
240        let acct = make_account(PROGRAM_A, 100, b"");
241        assert!(check_account_change(&PROGRAM_A, &ACCT_1, &acct, &PROGRAM_B, 100, b"").is_ok());
242    }
243
244    #[test]
245    fn check_account_change_non_owner_changes_owner() {
246        let acct = make_account(PROGRAM_A, 100, b"");
247        assert!(check_account_change(&PROGRAM_B, &ACCT_1, &acct, &PROGRAM_B, 100, b"").is_err());
248    }
249
250    #[test]
251    fn check_account_change_owner_debits_lamports() {
252        let acct = make_account(PROGRAM_A, 100, b"");
253        assert!(check_account_change(&PROGRAM_A, &ACCT_1, &acct, &PROGRAM_A, 50, b"").is_ok());
254    }
255
256    #[test]
257    fn check_account_change_non_owner_debits_lamports() {
258        let acct = make_account(PROGRAM_A, 100, b"");
259        assert!(check_account_change(&PROGRAM_B, &ACCT_1, &acct, &PROGRAM_A, 50, b"").is_err());
260    }
261
262    #[test]
263    fn check_account_change_non_owner_credits_lamports() {
264        let acct = make_account(PROGRAM_A, 100, b"");
265        assert!(check_account_change(&PROGRAM_B, &ACCT_1, &acct, &PROGRAM_A, 200, b"").is_ok());
266    }
267
268    #[test]
269    fn check_account_change_executable_data_rejected() {
270        let mut acct = make_account(PROGRAM_A, 100, b"code");
271        acct.executable = true;
272        assert!(
273            check_account_change(&PROGRAM_A, &ACCT_1, &acct, &PROGRAM_A, 100, b"hack").is_err()
274        );
275    }
276
277    #[test]
278    fn check_account_change_no_modifications() {
279        let acct = make_account(PROGRAM_A, 100, b"data");
280        assert!(check_account_change(&PROGRAM_B, &ACCT_1, &acct, &PROGRAM_A, 100, b"data").is_ok());
281    }
282}