Skip to content

Commit eb7f460

Browse files
authored
Lazer solana audit fixes (#2250)
* fix(lazer): verify account size before applying migration * fix(lazer): fetch message offset and data from sysvar and verify message data * test(lazer): add test with wrong message offset
1 parent afcb132 commit eb7f460

File tree

3 files changed

+206
-38
lines changed

3 files changed

+206
-38
lines changed

lazer/contracts/solana/programs/pyth-lazer-solana-contract/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ pub mod pyth_lazer_solana_contract {
103103
if old_data[0..ANCHOR_DISCRIMINATOR_BYTES] != Storage::DISCRIMINATOR {
104104
return Err(ProgramError::InvalidAccountData.into());
105105
}
106+
if old_data.len() != StorageV010::SERIALIZED_LEN + ANCHOR_DISCRIMINATOR_BYTES {
107+
return Err(ProgramError::InvalidAccountData.into());
108+
}
106109
let old_storage = StorageV010::deserialize(&mut &old_data[ANCHOR_DISCRIMINATOR_BYTES..])?;
107110
if old_storage.top_authority != ctx.accounts.top_authority.key() {
108111
return Err(ProgramError::MissingRequiredSignature.into());
@@ -194,7 +197,6 @@ pub mod pyth_lazer_solana_contract {
194197
message_data: Vec<u8>,
195198
ed25519_instruction_index: u16,
196199
signature_index: u8,
197-
message_offset: u16,
198200
) -> Result<VerifiedMessage> {
199201
system_program::transfer(
200202
CpiContext::new(
@@ -213,7 +215,6 @@ pub mod pyth_lazer_solana_contract {
213215
&message_data,
214216
ed25519_instruction_index,
215217
signature_index,
216-
message_offset,
217218
)
218219
.map_err(|err| {
219220
msg!("signature verification error: {:?}", err);

lazer/contracts/solana/programs/pyth-lazer-solana-contract/src/signature.rs

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use {
22
crate::Storage,
33
anchor_lang::{
44
prelude::{borsh, AccountInfo, Clock, ProgramError, Pubkey, SolanaSysvar},
5-
solana_program::{ed25519_program, pubkey::PUBKEY_BYTES, sysvar},
5+
solana_program::{
6+
ed25519_program, program_memory::sol_memcmp, pubkey::PUBKEY_BYTES, sysvar,
7+
},
68
AnchorDeserialize, AnchorSerialize,
79
},
810
bytemuck::{cast_slice, checked::try_cast_slice, Pod, Zeroable},
@@ -127,6 +129,8 @@ pub enum SignatureVerificationError {
127129
InvalidStorageData,
128130
#[error("not a trusted signer")]
129131
NotTrustedSigner,
132+
#[error("invalid message data")]
133+
InvalidMessageData,
130134
}
131135

132136
impl From<SignatureVerificationError> for ProgramError {
@@ -148,6 +152,10 @@ impl From<SignatureVerificationError> for anchor_lang::error::Error {
148152
}
149153
}
150154

155+
fn slice_eq(a: &[u8], b: &[u8]) -> bool {
156+
a.len() == b.len() && sol_memcmp(a, b, a.len()) == 0
157+
}
158+
151159
/// Verifies a ed25519 signature on Solana by checking that the transaction contains
152160
/// a correct call to the built-in `ed25519_program`.
153161
///
@@ -163,7 +171,6 @@ pub fn verify_message(
163171
message_data: &[u8],
164172
ed25519_instruction_index: u16,
165173
signature_index: u8,
166-
message_offset: u16,
167174
) -> Result<VerifiedMessage, SignatureVerificationError> {
168175
const SOLANA_FORMAT_MAGIC_LE: u32 = 2182742457;
169176

@@ -175,25 +182,25 @@ pub fn verify_message(
175182
return Err(SignatureVerificationError::Ed25519InstructionMustPrecedeCurrentInstruction);
176183
}
177184

178-
let instruction = sysvar::instructions::load_instruction_at_checked(
185+
let ed25519_instruction = sysvar::instructions::load_instruction_at_checked(
179186
ed25519_instruction_index.into(),
180187
instructions_sysvar,
181188
)
182189
.map_err(SignatureVerificationError::LoadInstructionAtFailed)?;
183190

184-
if instruction.program_id != ed25519_program::ID {
191+
if ed25519_instruction.program_id != ed25519_program::ID {
185192
return Err(SignatureVerificationError::InvalidEd25519InstructionProgramId);
186193
}
187-
if instruction.data.len() < ED25519_PROGRAM_INPUT_HEADER_LEN {
194+
if ed25519_instruction.data.len() < ED25519_PROGRAM_INPUT_HEADER_LEN {
188195
return Err(SignatureVerificationError::InvalidEd25519InstructionDataLength);
189196
}
190197

191-
let num_signatures = instruction.data[0];
198+
let num_signatures = ed25519_instruction.data[0];
192199
if signature_index >= num_signatures {
193200
return Err(SignatureVerificationError::InvalidSignatureIndex);
194201
}
195202
let args: &[Ed25519SignatureOffsets] =
196-
try_cast_slice(&instruction.data[ED25519_PROGRAM_INPUT_HEADER_LEN..])
203+
try_cast_slice(&ed25519_instruction.data[ED25519_PROGRAM_INPUT_HEADER_LEN..])
197204
.map_err(|_| SignatureVerificationError::InvalidEd25519InstructionDataLength)?;
198205

199206
let args_len = args
@@ -205,19 +212,37 @@ pub fn verify_message(
205212
}
206213
let offsets = &args[usize::from(signature_index)];
207214

208-
let expected_signature_offset = message_offset
209-
.checked_add(MAGIC_LEN)
215+
let message_offset = offsets
216+
.signature_offset
217+
.checked_sub(MAGIC_LEN)
210218
.ok_or(SignatureVerificationError::MessageOffsetOverflow)?;
211-
if offsets.signature_offset != expected_signature_offset {
212-
return Err(SignatureVerificationError::InvalidSignatureOffset);
219+
220+
let self_instruction = sysvar::instructions::load_instruction_at_checked(
221+
self_instruction_index.into(),
222+
instructions_sysvar,
223+
)
224+
.map_err(SignatureVerificationError::LoadInstructionAtFailed)?;
225+
226+
let message_end_offset = offsets
227+
.message_data_offset
228+
.checked_add(offsets.message_data_size)
229+
.ok_or(SignatureVerificationError::MessageOffsetOverflow)?;
230+
let expected_message_data = self_instruction
231+
.data
232+
.get(usize::from(message_offset)..usize::from(message_end_offset))
233+
.ok_or(SignatureVerificationError::InvalidMessageOffset)?;
234+
235+
if !slice_eq(expected_message_data, message_data) {
236+
return Err(SignatureVerificationError::InvalidMessageData);
213237
}
214238

215239
let magic = LE::read_u32(&message_data[..MAGIC_LEN.into()]);
216240
if magic != SOLANA_FORMAT_MAGIC_LE {
217241
return Err(SignatureVerificationError::FormatMagicMismatch);
218242
}
219243

220-
let expected_public_key_offset = expected_signature_offset
244+
let expected_public_key_offset = offsets
245+
.signature_offset
221246
.checked_add(SIGNATURE_LEN)
222247
.ok_or(SignatureVerificationError::MessageOffsetOverflow)?;
223248
if offsets.public_key_offset != expected_public_key_offset {

lazer/contracts/solana/programs/pyth-lazer-solana-contract/tests/test1.rs

Lines changed: 166 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
use {
22
anchor_lang::{prelude::AccountMeta, InstructionData},
33
pyth_lazer_solana_contract::{ed25519_program_args, ANCHOR_DISCRIMINATOR_BYTES},
4-
solana_program_test::{BanksClient, ProgramTest},
4+
solana_program_test::{BanksClient, BanksClientError, ProgramTest},
55
solana_sdk::{
66
account::Account,
77
ed25519_program,
88
hash::Hash,
9-
instruction::Instruction,
9+
instruction::{Instruction, InstructionError},
1010
pubkey::{Pubkey, PUBKEY_BYTES},
1111
signature::Keypair,
1212
signer::Signer,
1313
system_instruction, system_program, system_transaction, sysvar,
14-
transaction::Transaction,
14+
transaction::{Transaction, TransactionError},
1515
},
1616
std::env,
1717
};
@@ -103,24 +103,45 @@ impl Setup {
103103
}
104104

105105
async fn verify_message(&mut self, message: &[u8], treasury: Pubkey) {
106+
let treasury_starting_lamports = self
107+
.banks_client
108+
.get_account(treasury)
109+
.await
110+
.unwrap()
111+
.unwrap()
112+
.lamports;
113+
114+
// 8 bytes for Anchor header, 4 bytes for Vec length.
115+
self.verify_message_with_offset(message, treasury, 12)
116+
.await
117+
.unwrap();
118+
119+
assert_eq!(
120+
self.banks_client
121+
.get_account(treasury)
122+
.await
123+
.unwrap()
124+
.unwrap()
125+
.lamports,
126+
treasury_starting_lamports + 1,
127+
);
128+
}
129+
130+
async fn verify_message_with_offset(
131+
&mut self,
132+
message: &[u8],
133+
treasury: Pubkey,
134+
message_offset: u16,
135+
) -> Result<(), BanksClientError> {
106136
// Instruction #0 will be ed25519 instruction;
107137
// Instruction #1 will be our contract instruction.
108138
let instruction_index = 1;
109-
// 8 bytes for Anchor header, 4 bytes for Vec length.
110-
let message_offset = 12;
111139
let ed25519_args = dbg!(pyth_lazer_solana_contract::Ed25519SignatureOffsets::new(
112140
message,
113141
instruction_index,
114142
message_offset,
115143
));
116144

117-
let treasury_starting_lamports = self
118-
.banks_client
119-
.get_account(treasury)
120-
.await
121-
.unwrap()
122-
.unwrap()
123-
.lamports;
124145
let mut transaction_verify = Transaction::new_with_payer(
125146
&[
126147
Instruction::new_with_bytes(
@@ -134,7 +155,6 @@ impl Setup {
134155
message_data: message.to_vec(),
135156
ed25519_instruction_index: 0,
136157
signature_index: 0,
137-
message_offset,
138158
}
139159
.data(),
140160
vec![
@@ -152,17 +172,6 @@ impl Setup {
152172
self.banks_client
153173
.process_transaction(transaction_verify)
154174
.await
155-
.unwrap();
156-
157-
assert_eq!(
158-
self.banks_client
159-
.get_account(treasury)
160-
.await
161-
.unwrap()
162-
.unwrap()
163-
.lamports,
164-
treasury_starting_lamports + 1,
165-
);
166175
}
167176
}
168177

@@ -207,6 +216,84 @@ async fn test_basic() {
207216
setup.verify_message(&message, treasury).await;
208217
}
209218

219+
#[tokio::test]
220+
async fn test_rejects_wrong_offset() {
221+
let mut setup = Setup::new().await;
222+
let treasury = setup.create_treasury().await;
223+
224+
let mut transaction_init_contract = Transaction::new_with_payer(
225+
&[Instruction::new_with_bytes(
226+
pyth_lazer_solana_contract::ID,
227+
&pyth_lazer_solana_contract::instruction::Initialize {
228+
top_authority: setup.payer.pubkey(),
229+
treasury,
230+
}
231+
.data(),
232+
vec![
233+
AccountMeta::new(setup.payer.pubkey(), true),
234+
AccountMeta::new(pyth_lazer_solana_contract::STORAGE_ID, false),
235+
AccountMeta::new_readonly(system_program::ID, false),
236+
],
237+
)],
238+
Some(&setup.payer.pubkey()),
239+
);
240+
transaction_init_contract.sign(&[&setup.payer], setup.recent_blockhash);
241+
setup
242+
.banks_client
243+
.process_transaction(transaction_init_contract)
244+
.await
245+
.unwrap();
246+
247+
let verifying_key =
248+
hex::decode("74313a6525edf99936aa1477e94c72bc5cc617b21745f5f03296f3154461f214").unwrap();
249+
let verifying_key_2 =
250+
hex::decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
251+
let message = hex::decode(
252+
[
253+
// --- First copy of the message (this data is returned by the Lazer contract)
254+
255+
// SOLANA_FORMAT_MAGIC_LE
256+
"b9011a82",
257+
// Signature
258+
"e5cddee2c1bd364c8c57e1c98a6a28d194afcad410ff412226c8b2ae931ff59a57147cb47c7307afc2a0a1abec4dd7e835a5b7113cf5aeac13a745c6bed6c600",
259+
// Pubkey
260+
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
261+
// Payload length (could be adjusted)
262+
"1c00",
263+
// Payload
264+
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
265+
266+
// --- Second copy of the message (this data is used by the ed25519 program)
267+
268+
// Unused, was SOLANA_FORMAT_MAGIC_LE, could be removed, left it for slightly easier offset adjustments
269+
"AABBCCDD",
270+
// Signature
271+
"e5cddee2c1bd364c8c57e1c98a6a28d194afcad410ff412226c8b2ae931ff59a57147cb47c7307afc2a0a1abec4dd7e835a5b7113cf5aeac13a745c6bed6c600",
272+
// Pubkey
273+
"74313a6525edf99936aa1477e94c72bc5cc617b21745f5f03296f3154461f214",
274+
// Payload length
275+
"1c00",
276+
// Payload
277+
"75d3c7931c9773f30a240600010102000000010000e1f50500000000"
278+
].concat()
279+
)
280+
.unwrap();
281+
282+
setup.set_trusted(verifying_key.try_into().unwrap()).await;
283+
setup.set_trusted(verifying_key_2.try_into().unwrap()).await;
284+
let err = setup
285+
.verify_message_with_offset(&message, treasury, 12 + 130)
286+
.await
287+
.unwrap_err();
288+
assert!(matches!(
289+
err,
290+
BanksClientError::TransactionError(TransactionError::InstructionError(
291+
1,
292+
InstructionError::InvalidInstructionData
293+
))
294+
));
295+
}
296+
210297
#[tokio::test]
211298
async fn test_migrate_from_0_1_0() {
212299
let mut program_test = program_test();
@@ -277,3 +364,58 @@ async fn test_migrate_from_0_1_0() {
277364
// because it was present in the original storage PDA data.
278365
setup.verify_message(&message, treasury).await;
279366
}
367+
368+
#[tokio::test]
369+
async fn test_disallows_extra_migrate() {
370+
let mut setup = Setup::new().await;
371+
let treasury = setup.create_treasury().await;
372+
373+
let mut transaction_init_contract = Transaction::new_with_payer(
374+
&[Instruction::new_with_bytes(
375+
pyth_lazer_solana_contract::ID,
376+
&pyth_lazer_solana_contract::instruction::Initialize {
377+
top_authority: setup.payer.pubkey(),
378+
treasury,
379+
}
380+
.data(),
381+
vec![
382+
AccountMeta::new(setup.payer.pubkey(), true),
383+
AccountMeta::new(pyth_lazer_solana_contract::STORAGE_ID, false),
384+
AccountMeta::new_readonly(system_program::ID, false),
385+
],
386+
)],
387+
Some(&setup.payer.pubkey()),
388+
);
389+
transaction_init_contract.sign(&[&setup.payer], setup.recent_blockhash);
390+
setup
391+
.banks_client
392+
.process_transaction(transaction_init_contract)
393+
.await
394+
.unwrap();
395+
396+
let mut transaction_migrate_contract = Transaction::new_with_payer(
397+
&[Instruction::new_with_bytes(
398+
pyth_lazer_solana_contract::ID,
399+
&pyth_lazer_solana_contract::instruction::MigrateFrom010 { treasury }.data(),
400+
vec![
401+
AccountMeta::new(setup.payer.pubkey(), true),
402+
AccountMeta::new(pyth_lazer_solana_contract::STORAGE_ID, false),
403+
AccountMeta::new_readonly(system_program::ID, false),
404+
],
405+
)],
406+
Some(&setup.payer.pubkey()),
407+
);
408+
transaction_migrate_contract.sign(&[&setup.payer], setup.recent_blockhash);
409+
let err = setup
410+
.banks_client
411+
.process_transaction(transaction_migrate_contract)
412+
.await
413+
.unwrap_err();
414+
assert!(matches!(
415+
err,
416+
BanksClientError::TransactionError(TransactionError::InstructionError(
417+
0,
418+
InstructionError::InvalidAccountData
419+
))
420+
));
421+
}

0 commit comments

Comments
 (0)