From 00488bf0b52d740cb29791da4aeafbd38346b5ef Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 12 Aug 2026 11:19:04 +0100 Subject: [PATCH] Check total --- crates/xrpl-wasm-vm/src/abi.rs | 16 +++++++++++++++- crates/xrpl-wasm-vm/tests/host_calls.rs | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 57e9dd6a36..74b52944d4 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -281,6 +281,12 @@ const EXPONENT_BYTES: usize = 4; /// to a scratch buffer, so the input stays borrowed rather than copied. The two output /// regions are judged after the input, and the mantissa's region before the exponent's, /// so the first fault reported is the leftmost. +/// +/// The two widths are the ABI's rather than the guest's, so the length the host reports +/// is checked against their sum for equality rather than as a bound, and ahead of the +/// output regions: a wrong total means there is no answer to place, whatever the guest +/// declared. That is a fatal error and not a status, since the guest asked for nothing +/// wrong. pub(crate) fn write_mant_exp( caller: &mut Caller<'_, VmState<'_>>, mantissa_out: Region, @@ -299,6 +305,14 @@ pub(crate) fn write_mant_exp( let total = call(host, data, mant_buf, exp_buf)?; + // Both buffers are fixed-width and were offered whole, so the only length the host + // can correctly report is their sum. Anything else is the host contradicting the + // ABI: with the widths in doubt, part of what would be copied out is whatever the + // previous call left in the buffer, so none of it is copied. + if total != MANTISSA_BYTES + EXPONENT_BYTES { + return Err(HostError::InternalFatal.into()); + } + // Copy the mantissa, then the exponent, each only if its whole value fits its // region — a region too small is `BufferTooSmall`, with nothing written. let mant_range = mantissa_out.range()?; @@ -324,7 +338,7 @@ pub(crate) fn write_mant_exp( #[expect( clippy::cast_possible_truncation, clippy::cast_possible_wrap, - reason = "the total is 12, far inside i32" + reason = "a total other than 12 returned above, and 12 is far inside i32" )] let total = total as i32; Ok(total) diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 0d8abfda50..251b452b46 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -935,6 +935,24 @@ fn float_to_mant_exp_writes_both_regions() { assert_eq!(status(&wat, &host), 9, "the exponent's first byte"); } +/// The widths that call writes are the ABI's, so a host reporting any other total has +/// contradicted it: the regions are wide enough and the guest asked for nothing wrong, +/// yet the mantissa is short of its eight bytes, so the rest of what would be copied is +/// whatever the buffer already held. The run stops instead. +#[test] +fn float_to_mant_exp_with_a_wrong_total_stops_the_run() { + let host = FakeHost::new().answering_float_mant_exp(vec![1, 2, 3, 4], vec![9, 10, 11, 12]); + + let wat = module( + &[import::FLOAT_TO_MANT_EXP, ONE_PAGE], + "(call $float_to_mant_exp (i32.const 0) (i32.const 8) (i32.const 64) (i32.const 8) (i32.const 80) (i32.const 4))", + ); + assert!( + matches!(failure(&wat, &host).error, RunError::Internal), + "a total that is not the two widths must stop the run" + ); +} + /// A comparison that reads two float regions and returns a scalar verdict, no output /// region involved. #[test]