Skip to content

Commit c75867f

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

File tree

7 files changed

+178
-3
lines changed

7 files changed

+178
-3
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
@@ -19,10 +19,13 @@ test-sbf = []
1919
[dependencies]
2020
bincode = "1.3.3"
2121
bytemuck = "1.14.1"
22+
num-derive = "0.4"
23+
num-traits = "0.2"
2224
serde = { version = "1.0.193", features = ["derive"] }
2325
solana-frozen-abi = { version = "2.0.1", optional = true }
2426
solana-frozen-abi-macro = { version = "2.0.1", optional = true }
2527
solana-program = "2.0.1"
28+
thiserror = "1.0.61"
2629

2730
[dev-dependencies]
2831
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::AddressLookupTableError, 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::<AddressLookupTableError>();
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 AddressLookupTableError {
16+
/// Instruction changed executable account's data.
17+
#[error("Instruction changed executable account's data")]
18+
ExecutableDataModified = 10, // Avoid collisions with SystemError from CPI.
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 AddressLookupTableError {
25+
fn print<E>(&self) {
26+
msg!(&self.to_string());
27+
}
28+
}
29+
30+
impl From<AddressLookupTableError> for ProgramError {
31+
fn from(e: AddressLookupTableError) -> Self {
32+
ProgramError::Custom(e as u32)
33+
}
34+
}
35+
36+
impl<T> DecodeError<T> for AddressLookupTableError {
37+
fn type_of() -> &'static str {
38+
"AddressLookupTableError"
39+
}
40+
}

program/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#[cfg(all(target_os = "solana", feature = "bpf-entrypoint"))]
55
mod entrypoint;
6+
pub mod error;
67
pub mod instruction;
78
pub mod processor;
89
pub mod state;

program/src/processor.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use {
44
crate::{
55
check_id,
6+
error::AddressLookupTableError,
67
instruction::AddressLookupTableInstruction,
78
state::{
89
AddressLookupTable, ProgramState, LOOKUP_TABLE_MAX_ADDRESSES, LOOKUP_TABLE_META_SIZE,
@@ -145,6 +146,8 @@ fn process_create_lookup_table(
145146
)?;
146147
}
147148

149+
// [Core BPF]: No need to check `is_executable` or `is_writable` here,
150+
// since they will be checked by System.
148151
invoke_signed(
149152
&system_instruction::allocate(lookup_table_info.key, lookup_table_data_len as u64),
150153
&[lookup_table_info.clone()],
@@ -205,6 +208,10 @@ fn process_freeze_lookup_table(program_id: &Pubkey, accounts: &[AccountInfo]) ->
205208
lookup_table.meta
206209
};
207210

211+
// [Core BPF]: No need to check `is_executable` or `is_writable` here,
212+
// since only non-frozen lookup tables can be frozen (never a same-data
213+
// write).
214+
208215
lookup_table_meta.authority = None;
209216
AddressLookupTable::overwrite_meta_data(
210217
&mut lookup_table_info.try_borrow_mut_data()?[..],
@@ -297,6 +304,36 @@ fn process_extend_lookup_table(
297304
)
298305
};
299306

307+
// [Core BPF]:
308+
// When a builtin program attempts to write to an executable or read-only
309+
// account, it will be immediately rejected by the `TransactionContext`.
310+
// For more information, see https://github.com/solana-program/config/pull/21.
311+
//
312+
// However, in the case of the Address Lookup Table program's
313+
// `ExtendLookupTable` instruction, since the processor rejects any
314+
// zero-length "new keys" vectors, and will gladly append the same keys
315+
// again to the table, the issue here is slightly different than the linked
316+
// PR.
317+
//
318+
// The builtin version of the Address Lookup Table program will throw
319+
// when it attempts to overwrite the metadata, while the BPF version will
320+
// continue. In the case where an executable or read-only lookup table
321+
// account is provided, and some other requirement below is violated
322+
// (ie. no payer or system program accounts provided, payer is not a
323+
// signer, payer has insufficent balance, etc.), the BPF version will throw
324+
// based on one of those violations, rather than throwing immediately when
325+
// it encounters the executable or read-only lookup table account.
326+
//
327+
// As a result, this placement of these mocked out `InstructionError`
328+
// variants ensures maximum backwards compatibility with the builtin
329+
// version.
330+
if lookup_table_info.executable {
331+
return Err(AddressLookupTableError::ExecutableDataModified.into());
332+
}
333+
if !lookup_table_info.is_writable {
334+
return Err(AddressLookupTableError::ReadonlyDataModified.into());
335+
}
336+
300337
AddressLookupTable::overwrite_meta_data(
301338
&mut lookup_table_info.try_borrow_mut_data()?[..],
302339
lookup_table_meta,
@@ -374,6 +411,11 @@ fn process_deactivate_lookup_table(program_id: &Pubkey, accounts: &[AccountInfo]
374411
};
375412

376413
let clock = <Clock as Sysvar>::get()?;
414+
415+
// [Core BPF]: No need to check `is_executable` or `is_writable` here,
416+
// since only non-deactivated lookup tables can be deactivated (never a
417+
// same-data write).
418+
377419
lookup_table_meta.deactivation_slot = clock.slot;
378420

379421
AddressLookupTable::overwrite_meta_data(
@@ -444,6 +486,10 @@ fn process_close_lookup_table(program_id: &Pubkey, accounts: &[AccountInfo]) ->
444486
}?;
445487
}
446488

489+
// [Core BPF]: No need to check `is_executable` or `is_writable` here,
490+
// since only non-closed lookup tables can be deactivated. Deserialization
491+
// would fail earlier in the processor.
492+
447493
let new_recipient_lamports = lookup_table_info
448494
.lamports()
449495
.checked_add(recipient_info.lamports())

program/tests/extend_lookup_table_ix.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use {
1010
Mollusk,
1111
},
1212
solana_address_lookup_table_program::{
13+
error::AddressLookupTableError,
1314
instruction::extend_lookup_table,
1415
state::{AddressLookupTable, LookupTableMeta},
1516
},
@@ -355,3 +356,77 @@ fn test_extend_prepaid_lookup_table_without_payer() {
355356
},
356357
);
357358
}
359+
360+
// Backwards compatibility test case.
361+
#[test]
362+
fn test_extend_executable() {
363+
let mollusk = setup();
364+
365+
let payer = Pubkey::new_unique();
366+
let authority = Pubkey::new_unique();
367+
368+
let initialized_table = new_address_lookup_table(Some(authority), 0);
369+
370+
let lookup_table_address = Pubkey::new_unique();
371+
let mut lookup_table_account = lookup_table_account(initialized_table);
372+
373+
// Make the lookup table account executable.
374+
lookup_table_account.set_executable(true);
375+
376+
let new_addresses = vec![Pubkey::new_unique()];
377+
let instruction =
378+
extend_lookup_table(lookup_table_address, authority, Some(payer), new_addresses);
379+
380+
mollusk.process_and_validate_instruction(
381+
&instruction,
382+
&[
383+
(lookup_table_address, lookup_table_account),
384+
(authority, AccountSharedData::default()),
385+
(
386+
payer,
387+
AccountSharedData::new(100_000_000, 0, &system_program::id()),
388+
),
389+
keyed_account_for_system_program(),
390+
],
391+
&[Check::err(ProgramError::Custom(
392+
AddressLookupTableError::ExecutableDataModified as u32,
393+
))],
394+
);
395+
}
396+
397+
// Backwards compatibility test case.
398+
#[test]
399+
fn test_extend_readonly() {
400+
let mollusk = setup();
401+
402+
let payer = Pubkey::new_unique();
403+
let authority = Pubkey::new_unique();
404+
405+
let initialized_table = new_address_lookup_table(Some(authority), 0);
406+
407+
let lookup_table_address = Pubkey::new_unique();
408+
let lookup_table_account = lookup_table_account(initialized_table);
409+
410+
let new_addresses = vec![Pubkey::new_unique()];
411+
let mut instruction =
412+
extend_lookup_table(lookup_table_address, authority, Some(payer), new_addresses);
413+
414+
// Make the lookup table account read-only.
415+
instruction.accounts[0].is_writable = false;
416+
417+
mollusk.process_and_validate_instruction(
418+
&instruction,
419+
&[
420+
(lookup_table_address, lookup_table_account),
421+
(authority, AccountSharedData::default()),
422+
(
423+
payer,
424+
AccountSharedData::new(100_000_000, 0, &system_program::id()),
425+
),
426+
keyed_account_for_system_program(),
427+
],
428+
&[Check::err(ProgramError::Custom(
429+
AddressLookupTableError::ReadonlyDataModified as u32,
430+
))],
431+
);
432+
}

0 commit comments

Comments
 (0)