Skip to content

Commit cba7234

Browse files
committed
program: add custom error codes for executable, ro writes
1 parent e2024b2 commit cba7234

File tree

7 files changed

+152
-15
lines changed

7 files changed

+152
-15
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/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: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 changed executable account's data.
17+
#[error("Instruction changed executable account's data")]
18+
ExecutableDataModified,
19+
/// Instruction modified data of a read-only account.
20+
#[error("Instruction modified data of a read-only account")]
21+
ReadonlyDataModified,
22+
}
23+
24+
impl PrintProgramError for ConfigError {
25+
fn print<E>(&self) {
26+
msg!(&self.to_string());
27+
}
28+
}
29+
30+
impl From<ConfigError> for ProgramError {
31+
fn from(e: ConfigError) -> Self {
32+
ProgramError::Custom(e as u32)
33+
}
34+
}
35+
36+
impl<T> DecodeError<T> for ConfigError {
37+
fn type_of() -> &'static str {
38+
"ConfigError"
39+
}
40+
}

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: 28 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,33 @@ 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 these checks from `TransactionContext` to
177+
// the program directly, to throw even when the data being written is the
178+
// same as what's currently in the account.
179+
//
180+
// Unfortunately, the two `InstructionError` variants thrown do not have
181+
// `ProgramError` counterparts, so we mock them out with custom error
182+
// codes.
183+
if config_account.executable {
184+
return Err(ConfigError::ExecutableDataModified.into());
185+
}
186+
if !config_account.is_writable {
187+
return Err(ConfigError::ReadonlyDataModified.into());
188+
}
189+
163190
config_account.try_borrow_mut_data()?[..input.len()].copy_from_slice(input);
164191

165192
Ok(())

program/tests/functional.rs

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ 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
},
1112
solana_sdk::{
12-
account::AccountSharedData,
13+
account::{AccountSharedData, WritableAccount},
1314
instruction::{AccountMeta, Instruction},
1415
program_error::ProgramError,
1516
pubkey::Pubkey,
@@ -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(619),
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(619),
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_274),
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_274)],
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_275),
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_274)],
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_370),
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_370),
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_282)],
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_282)],
641642
);
642643

643644
// Use this for next invoke.
@@ -748,3 +749,58 @@ 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_executable() {
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 mut config_account = create_config_account(&mollusk, keys.clone());
763+
764+
// Make the config account executable.
765+
config_account.set_executable(true);
766+
767+
// Pass the exact same data (`MyConfig::default()`) to the instruction,
768+
// which we'll attempt to write into the account.
769+
let instruction = config_instruction::store(&config, true, keys.clone(), &MyConfig::default());
770+
771+
mollusk.process_and_validate_instruction(
772+
&instruction,
773+
&[(config, config_account)],
774+
&[Check::err(ProgramError::Custom(
775+
ConfigError::ExecutableDataModified as u32,
776+
))],
777+
);
778+
}
779+
780+
// Backwards compatibility test case.
781+
#[test]
782+
fn test_write_same_data_to_readonly() {
783+
let mollusk = setup();
784+
785+
let config = Pubkey::new_unique();
786+
let keys = vec![];
787+
788+
// Creates a config account with `MyConfig::default()`.
789+
let config_account = create_config_account(&mollusk, keys.clone());
790+
791+
// Pass the exact same data (`MyConfig::default()`) to the instruction,
792+
// which we'll attempt to write into the account.
793+
let mut instruction =
794+
config_instruction::store(&config, true, keys.clone(), &MyConfig::default());
795+
796+
// Make the config account read-only.
797+
instruction.accounts[0].is_writable = false;
798+
799+
mollusk.process_and_validate_instruction(
800+
&instruction,
801+
&[(config, config_account)],
802+
&[Check::err(ProgramError::Custom(
803+
ConfigError::ReadonlyDataModified as u32,
804+
))],
805+
);
806+
}

0 commit comments

Comments
 (0)