solana_program_runtime/
pre_account.rs

1use {
2    crate::timings::ExecuteDetailsTimings,
3    solana_sdk::{
4        account::{AccountSharedData, ReadableAccount, WritableAccount},
5        instruction::InstructionError,
6        pubkey::Pubkey,
7        rent::Rent,
8        system_instruction::MAX_PERMITTED_DATA_LENGTH,
9    },
10    std::fmt::Debug,
11};
12
13// The relevant state of an account before an Instruction executes, used
14// to verify account integrity after the Instruction completes
15#[derive(Clone, Debug, Default)]
16pub struct PreAccount {
17    key: Pubkey,
18    account: AccountSharedData,
19    changed: bool,
20}
21impl PreAccount {
22    pub fn new(key: &Pubkey, account: AccountSharedData) -> Self {
23        Self {
24            key: *key,
25            account,
26            changed: false,
27        }
28    }
29
30    pub fn verify(
31        &self,
32        program_id: &Pubkey,
33        is_writable: bool,
34        rent: &Rent,
35        post: &AccountSharedData,
36        timings: &mut ExecuteDetailsTimings,
37        outermost_call: bool,
38    ) -> Result<(), InstructionError> {
39        let pre = &self.account;
40
41        // Only the owner of the account may change owner and
42        //   only if the account is writable and
43        //   only if the account is not executable and
44        //   only if the data is zero-initialized or empty
45        let owner_changed = pre.owner() != post.owner();
46        if owner_changed
47            && (!is_writable // line coverage used to get branch coverage
48                || pre.executable()
49                || program_id != pre.owner()
50            || !Self::is_zeroed(post.data()))
51        {
52            return Err(InstructionError::ModifiedProgramId);
53        }
54
55        // An account not assigned to the program cannot have its balance decrease.
56        if program_id != pre.owner() // line coverage used to get branch coverage
57         && pre.lamports() > post.lamports()
58        {
59            return Err(InstructionError::ExternalAccountLamportSpend);
60        }
61
62        // The balance of read-only and executable accounts may not change
63        let lamports_changed = pre.lamports() != post.lamports();
64        if lamports_changed {
65            if !is_writable {
66                return Err(InstructionError::ReadonlyLamportChange);
67            }
68            if pre.executable() {
69                return Err(InstructionError::ExecutableLamportChange);
70            }
71        }
72
73        // Account data size cannot exceed a maxumum length
74        if post.data().len() > MAX_PERMITTED_DATA_LENGTH as usize {
75            return Err(InstructionError::InvalidRealloc);
76        }
77
78        // The owner of the account can change the size of the data
79        let data_len_changed = pre.data().len() != post.data().len();
80        if data_len_changed && program_id != pre.owner() {
81            return Err(InstructionError::AccountDataSizeChanged);
82        }
83
84        // Only the owner may change account data
85        //   and if the account is writable
86        //   and if the account is not executable
87        if !(program_id == pre.owner()
88            && is_writable  // line coverage used to get branch coverage
89            && !pre.executable())
90            && pre.data() != post.data()
91        {
92            if pre.executable() {
93                return Err(InstructionError::ExecutableDataModified);
94            } else if is_writable {
95                return Err(InstructionError::ExternalAccountDataModified);
96            } else {
97                return Err(InstructionError::ReadonlyDataModified);
98            }
99        }
100
101        // executable is one-way (false->true) and only the account owner may set it.
102        let executable_changed = pre.executable() != post.executable();
103        if executable_changed {
104            if !rent.is_exempt(post.lamports(), post.data().len()) {
105                return Err(InstructionError::ExecutableAccountNotRentExempt);
106            }
107            if !is_writable // line coverage used to get branch coverage
108                || pre.executable()
109                || program_id != post.owner()
110            {
111                return Err(InstructionError::ExecutableModified);
112            }
113        }
114
115        // No one modifies rent_epoch (yet).
116        let rent_epoch_changed = pre.rent_epoch() != post.rent_epoch();
117        if rent_epoch_changed {
118            return Err(InstructionError::RentEpochModified);
119        }
120
121        if outermost_call {
122            timings.total_account_count = timings.total_account_count.saturating_add(1);
123            timings.total_data_size = timings.total_data_size.saturating_add(post.data().len());
124            if owner_changed
125                || lamports_changed
126                || data_len_changed
127                || executable_changed
128                || rent_epoch_changed
129                || self.changed
130            {
131                timings.changed_account_count = timings.changed_account_count.saturating_add(1);
132                timings.data_size_changed =
133                    timings.data_size_changed.saturating_add(post.data().len());
134            }
135        }
136
137        Ok(())
138    }
139
140    pub fn update(&mut self, account: AccountSharedData) {
141        let rent_epoch = self.account.rent_epoch();
142        self.account = account;
143        self.account.set_rent_epoch(rent_epoch);
144
145        self.changed = true;
146    }
147
148    pub fn key(&self) -> &Pubkey {
149        &self.key
150    }
151
152    pub fn data(&self) -> &[u8] {
153        self.account.data()
154    }
155
156    pub fn lamports(&self) -> u64 {
157        self.account.lamports()
158    }
159
160    pub fn executable(&self) -> bool {
161        self.account.executable()
162    }
163
164    pub fn is_zeroed(buf: &[u8]) -> bool {
165        const ZEROS_LEN: usize = 1024;
166        static ZEROS: [u8; ZEROS_LEN] = [0; ZEROS_LEN];
167        let mut chunks = buf.chunks_exact(ZEROS_LEN);
168
169        #[allow(clippy::indexing_slicing)]
170        {
171            chunks.all(|chunk| chunk == &ZEROS[..])
172                && chunks.remainder() == &ZEROS[..chunks.remainder().len()]
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use {
180        super::*,
181        solana_sdk::{account::Account, instruction::InstructionError, system_program},
182    };
183
184    #[test]
185    fn test_is_zeroed() {
186        const ZEROS_LEN: usize = 1024;
187        let mut buf = [0; ZEROS_LEN];
188        assert!(PreAccount::is_zeroed(&buf));
189        buf[0] = 1;
190        assert!(!PreAccount::is_zeroed(&buf));
191
192        let mut buf = [0; ZEROS_LEN - 1];
193        assert!(PreAccount::is_zeroed(&buf));
194        buf[0] = 1;
195        assert!(!PreAccount::is_zeroed(&buf));
196
197        let mut buf = [0; ZEROS_LEN + 1];
198        assert!(PreAccount::is_zeroed(&buf));
199        buf[0] = 1;
200        assert!(!PreAccount::is_zeroed(&buf));
201
202        let buf = vec![];
203        assert!(PreAccount::is_zeroed(&buf));
204    }
205
206    struct Change {
207        program_id: Pubkey,
208        is_writable: bool,
209        rent: Rent,
210        pre: PreAccount,
211        post: AccountSharedData,
212    }
213    impl Change {
214        pub fn new(owner: &Pubkey, program_id: &Pubkey) -> Self {
215            Self {
216                program_id: *program_id,
217                rent: Rent::default(),
218                is_writable: true,
219                pre: PreAccount::new(
220                    &solana_sdk::pubkey::new_rand(),
221                    AccountSharedData::from(Account {
222                        owner: *owner,
223                        lamports: std::u64::MAX,
224                        ..Account::default()
225                    }),
226                ),
227                post: AccountSharedData::from(Account {
228                    owner: *owner,
229                    lamports: std::u64::MAX,
230                    ..Account::default()
231                }),
232            }
233        }
234        pub fn read_only(mut self) -> Self {
235            self.is_writable = false;
236            self
237        }
238        pub fn executable(mut self, pre: bool, post: bool) -> Self {
239            self.pre.account.set_executable(pre);
240            self.post.set_executable(post);
241            self
242        }
243        pub fn lamports(mut self, pre: u64, post: u64) -> Self {
244            self.pre.account.set_lamports(pre);
245            self.post.set_lamports(post);
246            self
247        }
248        pub fn owner(mut self, post: &Pubkey) -> Self {
249            self.post.set_owner(*post);
250            self
251        }
252        pub fn data(mut self, pre: Vec<u8>, post: Vec<u8>) -> Self {
253            self.pre.account.set_data(pre);
254            self.post.set_data(post);
255            self
256        }
257        pub fn rent_epoch(mut self, pre: u64, post: u64) -> Self {
258            self.pre.account.set_rent_epoch(pre);
259            self.post.set_rent_epoch(post);
260            self
261        }
262        pub fn verify(&self) -> Result<(), InstructionError> {
263            self.pre.verify(
264                &self.program_id,
265                self.is_writable,
266                &self.rent,
267                &self.post,
268                &mut ExecuteDetailsTimings::default(),
269                false,
270            )
271        }
272    }
273
274    #[test]
275    fn test_verify_account_changes_owner() {
276        let system_program_id = system_program::id();
277        let alice_program_id = solana_sdk::pubkey::new_rand();
278        let mallory_program_id = solana_sdk::pubkey::new_rand();
279
280        assert_eq!(
281            Change::new(&system_program_id, &system_program_id)
282                .owner(&alice_program_id)
283                .verify(),
284            Ok(()),
285            "system program should be able to change the account owner"
286        );
287        assert_eq!(
288            Change::new(&system_program_id, &system_program_id)
289                .owner(&alice_program_id)
290                .read_only()
291                .verify(),
292            Err(InstructionError::ModifiedProgramId),
293            "system program should not be able to change the account owner of a read-only account"
294        );
295        assert_eq!(
296            Change::new(&mallory_program_id, &system_program_id)
297                .owner(&alice_program_id)
298                .verify(),
299            Err(InstructionError::ModifiedProgramId),
300            "system program should not be able to change the account owner of a non-system account"
301        );
302        assert_eq!(
303            Change::new(&mallory_program_id, &mallory_program_id)
304                .owner(&alice_program_id)
305                .verify(),
306            Ok(()),
307            "mallory should be able to change the account owner, if she leaves clear data"
308        );
309        assert_eq!(
310            Change::new(&mallory_program_id, &mallory_program_id)
311                .owner(&alice_program_id)
312                .data(vec![42], vec![0])
313                .verify(),
314            Ok(()),
315            "mallory should be able to change the account owner, if she leaves clear data"
316        );
317        assert_eq!(
318            Change::new(&mallory_program_id, &mallory_program_id)
319                .owner(&alice_program_id)
320                .executable(true, true)
321                .data(vec![42], vec![0])
322                .verify(),
323            Err(InstructionError::ModifiedProgramId),
324            "mallory should not be able to change the account owner, if the account executable"
325        );
326        assert_eq!(
327            Change::new(&mallory_program_id, &mallory_program_id)
328                .owner(&alice_program_id)
329                .data(vec![42], vec![42])
330                .verify(),
331            Err(InstructionError::ModifiedProgramId),
332            "mallory should not be able to inject data into the alice program"
333        );
334    }
335
336    #[test]
337    fn test_verify_account_changes_executable() {
338        let owner = solana_sdk::pubkey::new_rand();
339        let mallory_program_id = solana_sdk::pubkey::new_rand();
340        let system_program_id = system_program::id();
341
342        assert_eq!(
343            Change::new(&owner, &system_program_id)
344                .executable(false, true)
345                .verify(),
346            Err(InstructionError::ExecutableModified),
347            "system program can't change executable if system doesn't own the account"
348        );
349        assert_eq!(
350            Change::new(&owner, &system_program_id)
351                .executable(true, true)
352                .data(vec![1], vec![2])
353                .verify(),
354            Err(InstructionError::ExecutableDataModified),
355            "system program can't change executable data if system doesn't own the account"
356        );
357        assert_eq!(
358            Change::new(&owner, &owner).executable(false, true).verify(),
359            Ok(()),
360            "owner should be able to change executable"
361        );
362        assert_eq!(
363            Change::new(&owner, &owner)
364                .executable(false, true)
365                .read_only()
366                .verify(),
367            Err(InstructionError::ExecutableModified),
368            "owner can't modify executable of read-only accounts"
369        );
370        assert_eq!(
371            Change::new(&owner, &owner).executable(true, false).verify(),
372            Err(InstructionError::ExecutableModified),
373            "owner program can't reverse executable"
374        );
375        assert_eq!(
376            Change::new(&owner, &mallory_program_id)
377                .executable(false, true)
378                .verify(),
379            Err(InstructionError::ExecutableModified),
380            "malicious Mallory should not be able to change the account executable"
381        );
382        assert_eq!(
383            Change::new(&owner, &owner)
384                .executable(false, true)
385                .data(vec![1], vec![2])
386                .verify(),
387            Ok(()),
388            "account data can change in the same instruction that sets the bit"
389        );
390        assert_eq!(
391            Change::new(&owner, &owner)
392                .executable(true, true)
393                .data(vec![1], vec![2])
394                .verify(),
395            Err(InstructionError::ExecutableDataModified),
396            "owner should not be able to change an account's data once its marked executable"
397        );
398        assert_eq!(
399            Change::new(&owner, &owner)
400                .executable(true, true)
401                .lamports(1, 2)
402                .verify(),
403            Err(InstructionError::ExecutableLamportChange),
404            "owner should not be able to add lamports once marked executable"
405        );
406        assert_eq!(
407            Change::new(&owner, &owner)
408                .executable(true, true)
409                .lamports(1, 2)
410                .verify(),
411            Err(InstructionError::ExecutableLamportChange),
412            "owner should not be able to add lamports once marked executable"
413        );
414        assert_eq!(
415            Change::new(&owner, &owner)
416                .executable(true, true)
417                .lamports(2, 1)
418                .verify(),
419            Err(InstructionError::ExecutableLamportChange),
420            "owner should not be able to subtract lamports once marked executable"
421        );
422        let data = vec![1; 100];
423        let min_lamports = Rent::default().minimum_balance(data.len());
424        assert_eq!(
425            Change::new(&owner, &owner)
426                .executable(false, true)
427                .lamports(0, min_lamports)
428                .data(data.clone(), data.clone())
429                .verify(),
430            Ok(()),
431        );
432        assert_eq!(
433            Change::new(&owner, &owner)
434                .executable(false, true)
435                .lamports(0, min_lamports - 1)
436                .data(data.clone(), data)
437                .verify(),
438            Err(InstructionError::ExecutableAccountNotRentExempt),
439            "owner should not be able to change an account's data once its marked executable"
440        );
441    }
442
443    #[test]
444    fn test_verify_account_changes_data_len() {
445        let alice_program_id = solana_sdk::pubkey::new_rand();
446
447        assert_eq!(
448            Change::new(&system_program::id(), &system_program::id())
449                .data(vec![0], vec![0, 0])
450                .verify(),
451            Ok(()),
452            "system program should be able to change the data len"
453        );
454        assert_eq!(
455            Change::new(&alice_program_id, &system_program::id())
456            .data(vec![0], vec![0,0])
457            .verify(),
458        Err(InstructionError::AccountDataSizeChanged),
459        "system program should not be able to change the data length of accounts it does not own"
460        );
461    }
462
463    #[test]
464    fn test_verify_account_changes_data() {
465        let alice_program_id = solana_sdk::pubkey::new_rand();
466        let mallory_program_id = solana_sdk::pubkey::new_rand();
467
468        assert_eq!(
469            Change::new(&alice_program_id, &alice_program_id)
470                .data(vec![0], vec![42])
471                .verify(),
472            Ok(()),
473            "alice program should be able to change the data"
474        );
475        assert_eq!(
476            Change::new(&mallory_program_id, &alice_program_id)
477                .data(vec![0], vec![42])
478                .verify(),
479            Err(InstructionError::ExternalAccountDataModified),
480            "non-owner mallory should not be able to change the account data"
481        );
482        assert_eq!(
483            Change::new(&alice_program_id, &alice_program_id)
484                .data(vec![0], vec![42])
485                .read_only()
486                .verify(),
487            Err(InstructionError::ReadonlyDataModified),
488            "alice isn't allowed to touch a CO account"
489        );
490    }
491
492    #[test]
493    fn test_verify_account_changes_rent_epoch() {
494        let alice_program_id = solana_sdk::pubkey::new_rand();
495
496        assert_eq!(
497            Change::new(&alice_program_id, &system_program::id()).verify(),
498            Ok(()),
499            "nothing changed!"
500        );
501        assert_eq!(
502            Change::new(&alice_program_id, &system_program::id())
503                .rent_epoch(0, 1)
504                .verify(),
505            Err(InstructionError::RentEpochModified),
506            "no one touches rent_epoch"
507        );
508    }
509
510    #[test]
511    fn test_verify_account_changes_deduct_lamports_and_reassign_account() {
512        let alice_program_id = solana_sdk::pubkey::new_rand();
513        let bob_program_id = solana_sdk::pubkey::new_rand();
514
515        // positive test of this capability
516        assert_eq!(
517            Change::new(&alice_program_id, &alice_program_id)
518            .owner(&bob_program_id)
519            .lamports(42, 1)
520            .data(vec![42], vec![0])
521            .verify(),
522        Ok(()),
523        "alice should be able to deduct lamports and give the account to bob if the data is zeroed",
524    );
525    }
526
527    #[test]
528    fn test_verify_account_changes_lamports() {
529        let alice_program_id = solana_sdk::pubkey::new_rand();
530
531        assert_eq!(
532            Change::new(&alice_program_id, &system_program::id())
533                .lamports(42, 0)
534                .read_only()
535                .verify(),
536            Err(InstructionError::ExternalAccountLamportSpend),
537            "debit should fail, even if system program"
538        );
539        assert_eq!(
540            Change::new(&alice_program_id, &alice_program_id)
541                .lamports(42, 0)
542                .read_only()
543                .verify(),
544            Err(InstructionError::ReadonlyLamportChange),
545            "debit should fail, even if owning program"
546        );
547        assert_eq!(
548            Change::new(&alice_program_id, &system_program::id())
549                .lamports(42, 0)
550                .owner(&system_program::id())
551                .verify(),
552            Err(InstructionError::ModifiedProgramId),
553            "system program can't debit the account unless it was the pre.owner"
554        );
555        assert_eq!(
556            Change::new(&system_program::id(), &system_program::id())
557                .lamports(42, 0)
558                .owner(&alice_program_id)
559                .verify(),
560            Ok(()),
561            "system can spend (and change owner)"
562        );
563    }
564
565    #[test]
566    fn test_verify_account_changes_data_size_changed() {
567        let alice_program_id = solana_sdk::pubkey::new_rand();
568
569        assert_eq!(
570            Change::new(&alice_program_id, &system_program::id())
571                .data(vec![0], vec![0, 0])
572                .verify(),
573            Err(InstructionError::AccountDataSizeChanged),
574            "system program should not be able to change another program's account data size"
575        );
576        assert_eq!(
577            Change::new(&alice_program_id, &solana_sdk::pubkey::new_rand())
578                .data(vec![0], vec![0, 0])
579                .verify(),
580            Err(InstructionError::AccountDataSizeChanged),
581            "one program should not be able to change another program's account data size"
582        );
583        assert_eq!(
584            Change::new(&alice_program_id, &alice_program_id)
585                .data(vec![0], vec![0, 0])
586                .verify(),
587            Ok(()),
588            "programs can change their own data size"
589        );
590        assert_eq!(
591            Change::new(&system_program::id(), &system_program::id())
592                .data(vec![0], vec![0, 0])
593                .verify(),
594            Ok(()),
595            "system program should be able to change account data size"
596        );
597    }
598
599    #[test]
600    fn test_verify_account_changes_owner_executable() {
601        let alice_program_id = solana_sdk::pubkey::new_rand();
602        let bob_program_id = solana_sdk::pubkey::new_rand();
603
604        assert_eq!(
605            Change::new(&alice_program_id, &alice_program_id)
606                .owner(&bob_program_id)
607                .executable(false, true)
608                .verify(),
609            Err(InstructionError::ExecutableModified),
610            "program should not be able to change owner and executable at the same time"
611        );
612    }
613}