Skip to content

Commit 4979b95

Browse files
authored
Program: add custom error codes for readonly writes (#21)
* program: add custom error codes for executable, ro writes * check in new compute unit benchmarks * drop executable code * check in new compute unit benchmark
1 parent e2024b2 commit 4979b95

File tree

8 files changed

+158
-14
lines changed

8 files changed

+158
-14
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

program/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ test-sbf = []
1717

1818
[dependencies]
1919
bincode = "1.3.3"
20+
num-derive = "0.4"
21+
num-traits = "0.2"
2022
serde = { version = "1.0.193", features = ["derive"] }
2123
solana-program = "2.0.1"
24+
thiserror = "1.0.61"
2225

2326
[dev-dependencies]
2427
mollusk-svm = { version = "0.0.5", features = ["fuzz"] }

program/benches/compute_units.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,44 @@
1+
#### Compute Units: 2024-11-08 12:36:57.438693 UTC
2+
3+
| Name | CUs | Delta |
4+
|------|------|-------|
5+
| config_small_init_0_keys | 616 | +4 |
6+
| config_small_init_1_keys | 1247 | +4 |
7+
| config_small_init_5_keys | 2866 | +4 |
8+
| config_small_init_10_keys | 4936 | +4 |
9+
| config_small_init_25_keys | 11778 | +4 |
10+
| config_small_init_37_keys | 16781 | +4 |
11+
| config_small_store_0_keys | 616 | +4 |
12+
| config_small_store_1_keys | 1501 | +4 |
13+
| config_small_store_5_keys | 4036 | +4 |
14+
| config_small_store_10_keys | 7251 | +4 |
15+
| config_small_store_25_keys | 17528 | +4 |
16+
| config_small_store_37_keys | 25279 | +4 |
17+
| config_medium_init_0_keys | 607 | +4 |
18+
| config_medium_init_1_keys | 1194 | +4 |
19+
| config_medium_init_5_keys | 2866 | +4 |
20+
| config_medium_init_10_keys | 4936 | +4 |
21+
| config_medium_init_25_keys | 11778 | +4 |
22+
| config_medium_init_37_keys | 16781 | +4 |
23+
| config_medium_store_0_keys | 607 | +4 |
24+
| config_medium_store_1_keys | 1448 | +4 |
25+
| config_medium_store_5_keys | 4036 | +4 |
26+
| config_medium_store_10_keys | 7251 | +4 |
27+
| config_medium_store_25_keys | 17528 | +4 |
28+
| config_medium_store_37_keys | 25279 | +4 |
29+
| config_large_init_0_keys | 728 | +4 |
30+
| config_large_init_1_keys | 1315 | +4 |
31+
| config_large_init_5_keys | 2987 | +4 |
32+
| config_large_init_10_keys | 5058 | +4 |
33+
| config_large_init_25_keys | 11902 | +4 |
34+
| config_large_init_37_keys | 16906 | +4 |
35+
| config_large_store_0_keys | 728 | +4 |
36+
| config_large_store_1_keys | 1569 | +4 |
37+
| config_large_store_5_keys | 4157 | +4 |
38+
| config_large_store_10_keys | 7373 | +4 |
39+
| config_large_store_25_keys | 17652 | +4 |
40+
| config_large_store_37_keys | 25404 | +4 |
41+
142
#### Compute Units: 2024-11-04 12:41:50.422792 UTC
243

344
| Name | CUs | Delta |

program/src/entrypoint.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
//! Program entrypoint.
22
33
use {
4-
crate::processor,
5-
solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey},
4+
crate::{error::ConfigError, processor},
5+
solana_program::{
6+
account_info::AccountInfo, entrypoint::ProgramResult, program_error::PrintProgramError,
7+
pubkey::Pubkey,
8+
},
69
};
710

811
solana_program::entrypoint!(process_instruction);
@@ -11,5 +14,9 @@ fn process_instruction(
1114
accounts: &[AccountInfo],
1215
instruction_data: &[u8],
1316
) -> ProgramResult {
14-
processor::process(program_id, accounts, instruction_data)
17+
if let Err(error) = processor::process(program_id, accounts, instruction_data) {
18+
error.print::<ConfigError>();
19+
return Err(error);
20+
}
21+
Ok(())
1522
}

program/src/error.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//! Program error types.
2+
3+
use {
4+
num_derive::FromPrimitive,
5+
solana_program::{
6+
decode_error::DecodeError,
7+
msg,
8+
program_error::{PrintProgramError, ProgramError},
9+
},
10+
thiserror::Error,
11+
};
12+
13+
/// Errors that can be returned by the Config program.
14+
#[derive(Error, Clone, Debug, Eq, PartialEq, FromPrimitive)]
15+
pub enum ConfigError {
16+
/// Instruction modified data of a read-only account.
17+
#[error("Instruction modified data of a read-only account")]
18+
ReadonlyDataModified,
19+
}
20+
21+
impl PrintProgramError for ConfigError {
22+
fn print<E>(&self) {
23+
msg!(&self.to_string());
24+
}
25+
}
26+
27+
impl From<ConfigError> for ProgramError {
28+
fn from(e: ConfigError) -> Self {
29+
ProgramError::Custom(e as u32)
30+
}
31+
}
32+
33+
impl<T> DecodeError<T> for ConfigError {
34+
fn type_of() -> &'static str {
35+
"ConfigError"
36+
}
37+
}

program/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
#[cfg(all(target_os = "solana", feature = "bpf-entrypoint"))]
44
mod entrypoint;
5+
pub mod error;
56
pub mod instruction;
67
pub mod processor;
78
pub mod state;

program/src/processor.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Config program processor.
22
33
use {
4-
crate::state::ConfigKeys,
4+
crate::{error::ConfigError, state::ConfigKeys},
55
solana_program::{
66
account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError,
77
pubkey::Pubkey,
@@ -160,6 +160,29 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P
160160
return Err(ProgramError::InvalidInstructionData);
161161
}
162162

163+
// [Core BPF]:
164+
// When a builtin program attempts to write to an executable or read-only
165+
// account, it will be immediately rejected by the `TransactionContext`.
166+
// However, BPF programs do not query the `TransactionContext` for the
167+
// ability to perform a write. Instead, they perform writes at-will, and
168+
// the loader will inspect the serialized account memory region for any
169+
// account update violations _after_ the VM has completed execution.
170+
//
171+
// The loader's inspection will catch any unauthorized modifications,
172+
// however, when the exact same data is written to the account, thus
173+
// rendering the serialized account state unchanged, the program succeeds.
174+
//
175+
// In order to maximize backwards compatibility between the BPF version and
176+
// its original builtin, we add this check from `TransactionContext` to the
177+
// program directly, to throw even when the data being written is the same
178+
// same as what's currently in the account.
179+
//
180+
// Since the account can never be executable and also owned by the config
181+
// program, we'll just focus on readonly.
182+
if !config_account.is_writable {
183+
return Err(ConfigError::ReadonlyDataModified.into());
184+
}
185+
163186
config_account.try_borrow_mut_data()?[..input.len()].copy_from_slice(input);
164187

165188
Ok(())

program/tests/functional.rs

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use {
55
mollusk_svm::{result::Check, Mollusk},
66
serde::{Deserialize, Serialize},
77
solana_config_program::{
8+
error::ConfigError,
89
instruction as config_instruction,
910
state::{ConfigKeys, ConfigState},
1011
},
@@ -83,7 +84,7 @@ fn test_process_create_ok() {
8384
&[(config, config_account)],
8485
&[
8586
Check::success(),
86-
Check::compute_units(612),
87+
Check::compute_units(616),
8788
Check::account(&config)
8889
.data(
8990
&bincode::serialize(&(ConfigKeys { keys: vec![] }, MyConfig::default()))
@@ -111,7 +112,7 @@ fn test_process_store_ok() {
111112
&[(config, config_account)],
112113
&[
113114
Check::success(),
114-
Check::compute_units(612),
115+
Check::compute_units(616),
115116
Check::account(&config)
116117
.data(&bincode::serialize(&(ConfigKeys { keys }, my_config)).unwrap())
117118
.build(),
@@ -186,7 +187,7 @@ fn test_process_store_with_additional_signers() {
186187
],
187188
&[
188189
Check::success(),
189-
Check::compute_units(3_267),
190+
Check::compute_units(3_271),
190191
Check::account(&config)
191192
.data(&bincode::serialize(&(ConfigKeys { keys }, my_config)).unwrap())
192193
.build(),
@@ -334,7 +335,7 @@ fn test_config_updates() {
334335
(signer0, AccountSharedData::default()),
335336
(signer1, AccountSharedData::default()),
336337
],
337-
&[Check::success(), Check::compute_units(3_267)],
338+
&[Check::success(), Check::compute_units(3_271)],
338339
);
339340

340341
// Use this for next invoke.
@@ -352,7 +353,7 @@ fn test_config_updates() {
352353
],
353354
&[
354355
Check::success(),
355-
Check::compute_units(3_268),
356+
Check::compute_units(3_272),
356357
Check::account(&config)
357358
.data(&bincode::serialize(&(ConfigKeys { keys }, new_config)).unwrap())
358359
.build(),
@@ -465,7 +466,7 @@ fn test_config_update_contains_duplicates_fails() {
465466
(signer0, AccountSharedData::default()),
466467
(signer1, AccountSharedData::default()),
467468
],
468-
&[Check::success(), Check::compute_units(3_267)],
469+
&[Check::success(), Check::compute_units(3_271)],
469470
);
470471

471472
// Attempt update with duplicate signer inputs.
@@ -509,7 +510,7 @@ fn test_config_updates_requiring_config() {
509510
],
510511
&[
511512
Check::success(),
512-
Check::compute_units(3_363),
513+
Check::compute_units(3_367),
513514
Check::account(&config)
514515
.data(&bincode::serialize(&(ConfigKeys { keys: keys.clone() }, my_config)).unwrap())
515516
.build(),
@@ -530,7 +531,7 @@ fn test_config_updates_requiring_config() {
530531
],
531532
&[
532533
Check::success(),
533-
Check::compute_units(3_363),
534+
Check::compute_units(3_367),
534535
Check::account(&config)
535536
.data(&bincode::serialize(&(ConfigKeys { keys }, new_config)).unwrap())
536537
.build(),
@@ -624,7 +625,7 @@ fn test_maximum_keys_input() {
624625
let result = mollusk.process_and_validate_instruction(
625626
&instruction,
626627
&[(config, config_account)],
627-
&[Check::success(), Check::compute_units(25_275)],
628+
&[Check::success(), Check::compute_units(25_279)],
628629
);
629630

630631
// Use this for next invoke.
@@ -637,7 +638,7 @@ fn test_maximum_keys_input() {
637638
let result = mollusk.process_and_validate_instruction(
638639
&instruction,
639640
&[(config, updated_config_account)],
640-
&[Check::success(), Check::compute_units(25_275)],
641+
&[Check::success(), Check::compute_units(25_279)],
641642
);
642643

643644
// Use this for next invoke.
@@ -748,3 +749,31 @@ fn test_safe_deserialize_from_state() {
748749
&[Check::err(ProgramError::InvalidAccountData)],
749750
);
750751
}
752+
753+
// Backwards compatibility test case.
754+
#[test]
755+
fn test_write_same_data_to_readonly() {
756+
let mollusk = setup();
757+
758+
let config = Pubkey::new_unique();
759+
let keys = vec![];
760+
761+
// Creates a config account with `MyConfig::default()`.
762+
let config_account = create_config_account(&mollusk, keys.clone());
763+
764+
// Pass the exact same data (`MyConfig::default()`) to the instruction,
765+
// which we'll attempt to write into the account.
766+
let mut instruction =
767+
config_instruction::store(&config, true, keys.clone(), &MyConfig::default());
768+
769+
// Make the config account read-only.
770+
instruction.accounts[0].is_writable = false;
771+
772+
mollusk.process_and_validate_instruction(
773+
&instruction,
774+
&[(config, config_account)],
775+
&[Check::err(ProgramError::Custom(
776+
ConfigError::ReadonlyDataModified as u32,
777+
))],
778+
);
779+
}

0 commit comments

Comments
 (0)