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
+ );
+ }
+}