Skip to content

Commit d015f7b

Browse files
committed
program: safe deserialize config keys
1 parent a7fef8e commit d015f7b

File tree

2 files changed

+95
-11
lines changed

2 files changed

+95
-11
lines changed

program/src/processor.rs

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,43 @@ use {
1212
std::collections::BTreeSet,
1313
};
1414

15-
// [Core BPF]: Locally-implemented
16-
// `solana_sdk::program_utils::limited_deserialize`.
17-
fn limited_deserialize<T>(input: &[u8]) -> Result<T, ProgramError>
18-
where
19-
T: serde::de::DeserializeOwned,
20-
{
21-
solana_program::program_utils::limited_deserialize(
22-
input, 1232, // [Core BPF]: See `solana_sdk::packet::PACKET_DATA_SIZE`
23-
)
24-
.map_err(|_| ProgramError::InvalidInstructionData)
15+
// [Core BPF]: The original Config builtin leverages the
16+
// `solana_sdk::program_utils::limited_deserialize` method to cap the length of
17+
// the input buffer at 1232 (`solana_sdk::packet::PACKET_DATA_SIZE`). As a
18+
// result, any input buffer larger than 1232 will abort deserialization and
19+
// return `InstructionError::InvalidInstructionData`.
20+
//
21+
// Howevever, since `ConfigKeys` contains a vector of `(Pubkey, bool)`, the
22+
// `limited_deserialize` method will still read the vector's length and attempt
23+
// to allocate a vector of the designated size. For extremely large length
24+
// values, this can cause the initial allocation of a large vector to exhuast
25+
// the BPF program's heap before deserialization can proceed.
26+
//
27+
// To mitigate this memory issue, the BPF version of the Config program has
28+
// been designed to "peek" the length value, and ensure it cannot allocate a
29+
// vector that would otherwise violate the input buffer length restriction of
30+
// 1232.
31+
//
32+
// Taking the maximum input length of 1232 and subtracting (up to) 3 bytes for
33+
// the `ShortU16`, then dividing that by the size of a `(Pubkey, bool)` entry
34+
// (33), we get a maximum vector size of 37. A `ShortU16` value for 37 fits in
35+
// just one byte (`[0x25]`), so this function can simply check the first
36+
// provided byte.
37+
fn safe_deserialize_config_keys(input: &[u8]) -> Result<ConfigKeys, ProgramError> {
38+
match input.first() {
39+
Some(first_byte) if *first_byte <= 0x25 => {
40+
solana_program::program_utils::limited_deserialize::<ConfigKeys>(
41+
input, 1232, // [Core BPF]: See `solana_sdk::packet::PACKET_DATA_SIZE`
42+
)
43+
.map_err(|_| ProgramError::InvalidInstructionData)
44+
}
45+
_ => Err(ProgramError::InvalidInstructionData),
46+
}
2547
}
2648

2749
/// Config program processor.
2850
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
29-
let key_list: ConfigKeys = limited_deserialize(input)?;
51+
let key_list = safe_deserialize_config_keys(input)?;
3052

3153
let mut accounts_iter = accounts.iter();
3254
let config_account = next_account_info(&mut accounts_iter)?;

program/tests/functional.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,3 +587,65 @@ fn test_maximum_keys_input() {
587587
&[Check::err(ProgramError::InvalidInstructionData)],
588588
);
589589
}
590+
591+
#[test]
592+
fn test_safe_deserialize() {
593+
let mollusk = setup();
594+
595+
// Accounts don't matter for this test.
596+
597+
// First try to spoof the program with just `ShortU16` length values.
598+
let build_instruction =
599+
|data: &[u8]| Instruction::new_with_bytes(solana_config_program::id(), data, vec![]);
600+
601+
mollusk.process_and_validate_instruction(
602+
// Empty buffer. Not a valid `ShortU16`.
603+
&build_instruction(&[]),
604+
&[],
605+
&[Check::err(ProgramError::InvalidInstructionData)],
606+
);
607+
608+
mollusk.process_and_validate_instruction(
609+
// `ShortU16` value of 38. One byte too large.
610+
&build_instruction(&[0x26]),
611+
&[],
612+
&[Check::err(ProgramError::InvalidInstructionData)],
613+
);
614+
615+
mollusk.process_and_validate_instruction(
616+
// `ShortU16` value of 37. OK for vector size, but no keys following.
617+
&build_instruction(&[0x25]),
618+
&[],
619+
&[Check::err(ProgramError::InvalidInstructionData)],
620+
);
621+
622+
// Now try with some actual `ConfigKeys` inputs.
623+
let mut keys = Vec::new();
624+
let serialized_config_keys = |keys: &[(Pubkey, bool)]| {
625+
let config_keys = ConfigKeys {
626+
keys: keys.to_vec(),
627+
};
628+
bincode::serialize(&config_keys).unwrap()
629+
};
630+
631+
// First build out to an acceptable size of 37.
632+
(0..37).for_each(|i| keys.push((Pubkey::new_unique(), i % 2 == 0)));
633+
634+
mollusk.process_and_validate_instruction(
635+
// `ShortU16` value of 37. OK.
636+
&build_instruction(&serialized_config_keys(&keys)),
637+
&[],
638+
// Falls through to account keys failure.
639+
&[Check::err(ProgramError::NotEnoughAccountKeys)],
640+
);
641+
642+
// Add one more key, pushing the size to 38.
643+
keys.push((Pubkey::new_unique(), true));
644+
645+
mollusk.process_and_validate_instruction(
646+
// `ShortU16` value of 38. Err.
647+
&build_instruction(&serialized_config_keys(&keys)),
648+
&[],
649+
&[Check::err(ProgramError::InvalidInstructionData)],
650+
);
651+
}

0 commit comments

Comments
 (0)