From 5e98d9e4b818f41a4a55aaa22aa9c6f18dbe5332 Mon Sep 17 00:00:00 2001 From: Jo D Date: Tue, 24 Feb 2026 13:56:31 -0500 Subject: [PATCH 1/2] fix(security): prevent timelock bypass via u64-to-i64 silent truncation Replace the silent `as i64` cast in TimelockData::validate() with `i64::try_from()`, returning ArithmeticOverflow for any lock_duration > i64::MAX instead of wrapping to a negative value. Add a defense-in-depth guard in AddTimelockData::try_from() that rejects lock_duration > i64::MAX at instruction parse time with InvalidInstructionData, so a malicious value never reaches storage. Fixes GHSA-qgq6-7hc8-rqx6 --- program/src/instructions/extensions/add_timelock/data.rs | 7 ++++++- program/src/state/extensions/timelock.rs | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/program/src/instructions/extensions/add_timelock/data.rs b/program/src/instructions/extensions/add_timelock/data.rs index 2ae914e..f29b08e 100644 --- a/program/src/instructions/extensions/add_timelock/data.rs +++ b/program/src/instructions/extensions/add_timelock/data.rs @@ -19,7 +19,12 @@ impl<'a> TryFrom<&'a [u8]> for AddTimelockData { fn try_from(data: &'a [u8]) -> Result { require_len!(data, Self::LEN); - Ok(Self { extensions_bump: data[0], lock_duration: u64::from_le_bytes(data[1..9].try_into().unwrap()) }) + let lock_duration = u64::from_le_bytes(data[1..9].try_into().unwrap()); + if lock_duration > i64::MAX as u64 { + return Err(ProgramError::InvalidInstructionData); + } + + Ok(Self { extensions_bump: data[0], lock_duration }) } } diff --git a/program/src/state/extensions/timelock.rs b/program/src/state/extensions/timelock.rs index 1c6c433..6c0779d 100644 --- a/program/src/state/extensions/timelock.rs +++ b/program/src/state/extensions/timelock.rs @@ -37,8 +37,8 @@ impl TimelockData { return Ok(()); } - let unlock_time = - deposited_at.checked_add(self.lock_duration as i64).ok_or(ProgramError::ArithmeticOverflow)?; + let lock_duration_i64 = i64::try_from(self.lock_duration).map_err(|_| ProgramError::ArithmeticOverflow)?; + let unlock_time = deposited_at.checked_add(lock_duration_i64).ok_or(ProgramError::ArithmeticOverflow)?; let clock = Clock::get()?; if clock.unix_timestamp < unlock_time { return Err(EscrowProgramError::TimelockNotExpired.into()); From a504f0024c9b4c999c2e21ac095fec7b794bfb42 Mon Sep 17 00:00:00 2001 From: Jo D Date: Tue, 24 Feb 2026 14:12:25 -0500 Subject: [PATCH 2/2] test(timelock): update lock_duration boundary tests for i64::MAX guard Replace u64::MAX with i64::MAX as the upper success case and add test_add_timelock_rejects_lock_duration_exceeding_i64_max to cover i64::MAX+1 and u64::MAX, both of which must now return InvalidInstructionData. --- .../src/test_add_timelock.rs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/integration-tests/src/test_add_timelock.rs b/tests/integration-tests/src/test_add_timelock.rs index 1c9eea4..ccfdefd 100644 --- a/tests/integration-tests/src/test_add_timelock.rs +++ b/tests/integration-tests/src/test_add_timelock.rs @@ -134,7 +134,7 @@ fn test_add_timelock_success() { #[test] fn test_add_timelock_success_lock_duration_values() { - for lock_duration in [0u64, 1, 60, 3600, 86400, u64::MAX] { + for lock_duration in [0u64, 1, 60, 3600, 86400, i64::MAX as u64] { let mut ctx = TestContext::new(); let escrow_ix = CreateEscrowFixture::build_valid(&mut ctx); @@ -152,3 +152,21 @@ fn test_add_timelock_success_lock_duration_values() { assert_timelock_extension(&ctx, &extensions_pda, lock_duration); } } + +#[test] +fn test_add_timelock_rejects_lock_duration_exceeding_i64_max() { + for lock_duration in [i64::MAX as u64 + 1, u64::MAX] { + let mut ctx = TestContext::new(); + + let escrow_ix = CreateEscrowFixture::build_valid(&mut ctx); + let admin = escrow_ix.signers[0].insecure_clone(); + let escrow_seed = escrow_ix.signers[1].pubkey(); + escrow_ix.send_expect_success(&mut ctx); + + let (escrow_pda, _) = find_escrow_pda(&escrow_seed); + + let test_ix = AddTimelockFixture::build_with_escrow(&mut ctx, escrow_pda, admin, lock_duration); + let error = test_ix.send_expect_error(&mut ctx); + assert_instruction_error(error, InstructionError::InvalidInstructionData); + } +}