From 17074fe61da7d8856d0bff9ad2ba1a06d3fe2fd8 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:09:54 +0000 Subject: [PATCH] fix(platform-wallet-ffi): bound bincode decode size on asset-lock proof bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `asset_lock_manager_recover` fed attacker-controlled `proof_bytes` straight into `bincode::decode_from_slice` with the unbounded `config::standard()`, so a hostile length prefix could drive an unbounded allocation across the FFI boundary (memory-exhaustion DoS). Gate `proof_len` at 16 MiB before touching the slice and hand bincode the same ceiling via `with_limit::<>`, so nested length prefixes cannot over-allocate inside the budget either. Rejection reuses the existing `ErrorInvalidParameter` code — no ABI surface change, no registry entry. 🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent --- .../src/asset_lock/sync.rs | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/rs-platform-wallet-ffi/src/asset_lock/sync.rs b/packages/rs-platform-wallet-ffi/src/asset_lock/sync.rs index 9bc819585cd..228d1589e62 100644 --- a/packages/rs-platform-wallet-ffi/src/asset_lock/sync.rs +++ b/packages/rs-platform-wallet-ffi/src/asset_lock/sync.rs @@ -9,6 +9,20 @@ use std::ffi::CString; use std::os::raw::c_char; use std::time::Duration; +const MAX_ASSET_LOCK_PROOF_SIZE_BYTES: usize = 16 * 1024 * 1024; + +fn validate_asset_lock_proof_size(proof_len: usize) -> Result<(), PlatformWalletFFIResult> { + if proof_len > MAX_ASSET_LOCK_PROOF_SIZE_BYTES { + return Err(PlatformWalletFFIResult::err( + PlatformWalletFFIResultCode::ErrorInvalidParameter, + format!( + "asset lock proof length {proof_len} exceeds the {MAX_ASSET_LOCK_PROOF_SIZE_BYTES}-byte limit" + ), + )); + } + Ok(()) +} + /// Build an `OutPoint` from a 32-byte raw txid pointer and a vout. /// /// **FFI invariant:** the `txid` parameter is typed `*const [u8; 32]`, @@ -264,10 +278,13 @@ pub unsafe extern "C" fn asset_lock_manager_recover( // Parse optional proof let proof = if !proof_bytes.is_null() && proof_len > 0 { + // A proof contains one Core transaction and, at most, one InstantLock; + // 16 MiB is generous while matching the persisted-blob ceiling. + unwrap_result_or_return!(validate_asset_lock_proof_size(proof_len)); let data = std::slice::from_raw_parts(proof_bytes, proof_len); let (p, _) = unwrap_result_or_return!(dpp::bincode::decode_from_slice( data, - dpp::bincode::config::standard() + dpp::bincode::config::standard().with_limit::() )); Some(p) } else { @@ -288,3 +305,21 @@ pub unsafe extern "C" fn asset_lock_manager_recover( unwrap_option_or_return!(option); PlatformWalletFFIResult::ok() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_reject_oversized_asset_lock_proof_before_decode() { + assert!(validate_asset_lock_proof_size(MAX_ASSET_LOCK_PROOF_SIZE_BYTES).is_ok()); + + let result = validate_asset_lock_proof_size(MAX_ASSET_LOCK_PROOF_SIZE_BYTES + 1) + .expect_err("oversized proof must fail"); + + assert_eq!( + result.code, + PlatformWalletFFIResultCode::ErrorInvalidParameter + ); + } +}