diff --git a/ergo-chain-types/src/digest32.rs b/ergo-chain-types/src/digest32.rs index 59b88b215..c92b83b55 100644 --- a/ergo-chain-types/src/digest32.rs +++ b/ergo-chain-types/src/digest32.rs @@ -153,7 +153,7 @@ impl ScorexSerializable for Digest { } fn scorex_parse(r: &mut R) -> Result { let mut bytes = [0; N]; - r.read_exact(&mut bytes)?; + r.get_bytes_into(&mut bytes)?; Ok(Self(bytes)) } } diff --git a/ergo-chain-types/src/ec_point.rs b/ergo-chain-types/src/ec_point.rs index a94b98f02..da503d5fa 100644 --- a/ergo-chain-types/src/ec_point.rs +++ b/ergo-chain-types/src/ec_point.rs @@ -138,7 +138,7 @@ impl ScorexSerializable for EcPoint { fn scorex_parse(r: &mut R) -> Result { let mut buf = [0; EcPoint::GROUP_SIZE]; - r.read_exact(&mut buf[..])?; + r.get_bytes_into(&mut buf[..])?; if buf[0] != 0 { let pubkey = PublicKey::from_sec1_bytes(&buf[..]).map_err(|e| { ScorexParsingError::Misc(format!("failed to parse PK from bytes: {:?}", e)) diff --git a/ergo-chain-types/src/header.rs b/ergo-chain-types/src/header.rs index 44d40b3f4..25bb32ed3 100644 --- a/ergo-chain-types/src/header.rs +++ b/ergo-chain-types/src/header.rs @@ -130,11 +130,11 @@ impl ScorexSerializable for Header { let timestamp = r.get_u64()?; let extension_root = Digest32::scorex_parse(r)?; let mut n_bits_buf = [0u8, 0, 0, 0]; - r.read_exact(&mut n_bits_buf)?; + r.get_bytes_into(&mut n_bits_buf)?; let n_bits = u32::from_be_bytes(n_bits_buf); let height = r.get_u32()?; let mut votes_bytes = [0u8, 0, 0]; - r.read_exact(&mut votes_bytes)?; + r.get_bytes_into(&mut votes_bytes)?; let votes = Votes(votes_bytes); // For block version >= 2, a new byte encodes length of possible new fields. If this byte > @@ -143,7 +143,7 @@ impl ScorexSerializable for Header { let new_field_size = r.get_u8()?; if new_field_size > 0 { let mut field_bytes: Vec = vec![0; new_field_size as usize]; - r.read_exact(&mut field_bytes)?; + r.get_bytes_into(&mut field_bytes)?; field_bytes.into() } else { Box::new([]) @@ -157,10 +157,10 @@ impl ScorexSerializable for Header { let miner_pk = EcPoint::scorex_parse(r)?.into(); let pow_onetime_pk = Some(EcPoint::scorex_parse(r)?.into()); let mut nonce: Vec = vec![0; 8]; - r.read_exact(&mut nonce)?; + r.get_bytes_into(&mut nonce)?; let d_bytes_len = r.get_u8()?; let mut d_bytes: Vec = vec![0; d_bytes_len as usize]; - r.read_exact(&mut d_bytes)?; + r.get_bytes_into(&mut d_bytes)?; let pow_distance = Some(BigUint::from_bytes_be(&d_bytes)); AutolykosSolution { miner_pk, @@ -174,7 +174,7 @@ impl ScorexSerializable for Header { let pow_distance = None; let miner_pk = EcPoint::scorex_parse(r)?.into(); let mut nonce: Vec = vec![0; 8]; - r.read_exact(&mut nonce)?; + r.get_bytes_into(&mut nonce)?; AutolykosSolution { miner_pk, pow_onetime_pk, diff --git a/ergo-lib/src/wallet/box_selector/simple.rs b/ergo-lib/src/wallet/box_selector/simple.rs index 9d9e27784..a898cab4d 100644 --- a/ergo-lib/src/wallet/box_selector/simple.rs +++ b/ergo-lib/src/wallet/box_selector/simple.rs @@ -8,7 +8,6 @@ use alloc::string::String; use alloc::vec::Vec; use ergotree_ir::chain::ergo_box::box_value::BoxValue; use ergotree_ir::chain::ergo_box::BoxTokens; -use ergotree_ir::chain::ergo_box::ErgoBox; use ergotree_ir::chain::token::Token; use ergotree_ir::chain::token::TokenAmount; use ergotree_ir::chain::token::TokenAmountError; @@ -224,7 +223,14 @@ fn check_input_preservation( #[error("Not enough coins for change box(es)")] pub struct NotEnoughCoinsForChangeBox(String); -/// Split change tokens into a multiple boxes if over ErgoBox::MAX_TOKENS_COUNT distinct tokens +/// Wallet packing policy: max tokens per change box. Deliberately NOT +/// `ErgoBox::MAX_TOKENS_COUNT` (255, the wire/type ceiling): a box that many +/// minimal 33-byte tokens would exceed `ErgoBox::MAX_BOX_SIZE` and be rejected +/// by the node's size rule. 122 is the fit-derived bound this selector has +/// always used ((4096 - ~85 bytes of other fields) / 33), kept as local policy. +const MAX_TOKENS_PER_CHANGE_BOX: usize = 122; + +/// Split change tokens into a multiple boxes if over MAX_TOKENS_PER_CHANGE_BOX distinct tokens fn make_change_boxes( change_value: BoxValue, change_tokens: HashMap, @@ -234,9 +240,9 @@ fn make_change_boxes( value: change_value, tokens: None, }]) - } else if change_tokens.len() <= ErgoBox::MAX_TOKENS_COUNT { + } else if change_tokens.len() <= MAX_TOKENS_PER_CHANGE_BOX { #[allow(clippy::unwrap_used)] - // unwrap_used is ok here because we checked that change_tokens.len() <= ErgoBox::MAX_TOKENS_COUNT + // unwrap_used is ok here because we checked that change_tokens.len() <= MAX_TOKENS_PER_CHANGE_BOX Ok(vec![ErgoBoxAssetsData { value: change_value, tokens: Some( @@ -249,9 +255,9 @@ fn make_change_boxes( change_tokens.into_iter().map(Token::from).collect(); let mut change_value_left = change_value; while !change_tokens_left.is_empty() { - if change_tokens_left.len() <= ErgoBox::MAX_TOKENS_COUNT { + if change_tokens_left.len() <= MAX_TOKENS_PER_CHANGE_BOX { #[allow(clippy::unwrap_used)] - // unwrap_used is ok here because we checked that change_tokens_left.len() <= ErgoBox::MAX_TOKENS_COUNT + // unwrap_used is ok here because we checked that change_tokens_left.len() <= MAX_TOKENS_PER_CHANGE_BOX let change_box = ErgoBoxAssetsData { value: change_value_left, tokens: Some(BoxTokens::from_vec(change_tokens_left).unwrap()), @@ -262,11 +268,11 @@ fn make_change_boxes( #[allow(clippy::unwrap_used)] // safe for the box value upper bound // doubled due to larger box size to accomodate so many tokens let value = BoxValue::SAFE_USER_MIN.checked_mul_u32(2).unwrap(); - let tokens_to_drain = ErgoBox::MAX_TOKENS_COUNT; + let tokens_to_drain = MAX_TOKENS_PER_CHANGE_BOX; let drained_tokens: Vec = change_tokens_left.drain(..tokens_to_drain).collect(); #[allow(clippy::unwrap_used)] - // safe since tokens_to_drain is ErgoBox::MAX_TOKENS_COUNT + // safe since tokens_to_drain is MAX_TOKENS_PER_CHANGE_BOX let change_box = ErgoBoxAssetsData { value, tokens: Some(BoxTokens::from_vec(drained_tokens).unwrap()), @@ -551,8 +557,8 @@ mod tests { value_range: (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into(), tokens_param: ArbTokensParam { token_id_param: ArbTokenIdParam::Arbitrary, - // with min 4 boxes below gives us minimum ErgoBox::MAX_TOKENS_COUNT * 2 distinct tokens total - token_count_range: (ErgoBox::MAX_TOKENS_COUNT/2)..ErgoBox::MAX_TOKENS_COUNT, + // with min 4 boxes below gives us minimum MAX_TOKENS_PER_CHANGE_BOX * 2 distinct tokens total + token_count_range: (MAX_TOKENS_PER_CHANGE_BOX/2)..MAX_TOKENS_PER_CHANGE_BOX, } }), 4..10 @@ -611,15 +617,15 @@ mod tests { value_range: (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into(), tokens_param: ArbTokensParam { token_id_param: ArbTokenIdParam::Arbitrary, - // with min 4 boxes below gives us minimum ErgoBox::MAX_TOKENS_COUNT * 2 distinct tokens total - token_count_range: (ErgoBox::MAX_TOKENS_COUNT/2)..ErgoBox::MAX_TOKENS_COUNT, + // with min 4 boxes below gives us minimum MAX_TOKENS_PER_CHANGE_BOX * 2 distinct tokens total + token_count_range: (MAX_TOKENS_PER_CHANGE_BOX/2)..MAX_TOKENS_PER_CHANGE_BOX, } }), 4..10 )) { let target_tokens = inputs.iter() .flat_map(|b| b.tokens().unwrap()) - .take(ErgoBox::MAX_TOKENS_COUNT + 10) + .take(MAX_TOKENS_PER_CHANGE_BOX + 10) .collect::>(); let target_balance = BoxValue::SAFE_USER_MIN.checked_mul_u32(2).unwrap(); let s = SimpleBoxSelector::new(); diff --git a/ergotree-ir/src/bigint256.rs b/ergotree-ir/src/bigint256.rs index 6c5033c1f..81b102a46 100644 --- a/ergotree-ir/src/bigint256.rs +++ b/ergotree-ir/src/bigint256.rs @@ -268,7 +268,7 @@ impl SigmaSerializable for BigInt256 { ))); } let mut buf = vec![0u8; size as usize]; - r.read_exact(&mut buf)?; + r.get_bytes_into(&mut buf)?; match BigInt256::from_be_slice(&buf) { Some(x) => Ok(x), None => Err(SigmaParsingError::ValueOutOfBounds(String::new())), diff --git a/ergotree-ir/src/chain/ergo_box.rs b/ergotree-ir/src/chain/ergo_box.rs index ddbe44748..dfa303cb8 100644 --- a/ergotree-ir/src/chain/ergo_box.rs +++ b/ergotree-ir/src/chain/ergo_box.rs @@ -80,11 +80,13 @@ pub struct ErgoBox { } impl ErgoBox { - /// Safe maximum number of tokens in the box - /// Calculated from the max box size (4kb) limit and the size of the token (32 bytes) - // (4096 - 85 bytes minimal size of the rest of the fields) / 33 token id 32 bytes + minimal token amount 1 byte = 121 tokens - // let's set to 121 + 1 to be safe - pub const MAX_TOKENS_COUNT: usize = 122; + /// Maximum number of tokens a box can carry: the wire count is a single + /// byte, so 255 — the reference impl's `SigmaConstants.MaxTokens`, which + /// binds only in SDK builders. The data layer has NO token-count consensus + /// rule; the real parse gate is the [`ErgoBox::MAX_BOX_SIZE`] position-limit + /// window over the candidate span (`ErgoBoxCandidate.parseBodyWithIndexedDigests`), + /// so how many tokens actually fit depends on the rest of the box. + pub const MAX_TOKENS_COUNT: usize = 255; /// Maximum box size in Ergo pub const MAX_BOX_SIZE: usize = 4096; /// Maximum script size @@ -346,6 +348,16 @@ pub fn parse_box_with_indexed_digests( ) -> Result { // reference implementation -https://github.com/ScorexFoundation/sigmastate-interpreter/blob/9b20cb110effd1987ff76699d637174a4b2fb441/sigmastate/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala#L144-L144 + // `ErgoBoxCandidate.parseBodyWithIndexedDigests` puts a MaxBoxSize window + // over the candidate span: each primitive read checks `position > limit` + // BEFORE reading (rule 1014), so a final field may overrun the window while + // a read STARTING past it fails. Restored on the success path only, like + // the reference impl (no `finally` there); nested candidate parses + // (Coll[Box] constants in registers) save/restore the outer window. + let previous_position_limit = r.position_limit(); + let window_limit = r.position()? + ErgoBox::MAX_BOX_SIZE as u64; + r.set_position_limit(window_limit); + let value = BoxValue::sigma_parse(r)?; let ergo_tree = ErgoTree::sigma_parse(r)?; let creation_height = r.get_u32()?; @@ -378,6 +390,7 @@ pub fn parse_box_with_indexed_digests( let additional_registers = NonMandatoryRegisters::sigma_parse(r)?; + r.set_position_limit(previous_position_limit); Ok(ErgoBoxCandidate { value, ergo_tree, @@ -443,6 +456,15 @@ pub mod arbitrary { creation_height, }, ) + // candidates whose serialization exceeds the MAX_BOX_SIZE parse + // window do not survive the consensus parser (here or on the + // JVM), so they cannot round-trip; keep generated boxes + // parseable + .prop_filter("candidate must fit the MAX_BOX_SIZE parse window", |c| { + c.sigma_serialize_bytes() + .map(|b| b.len() <= ErgoBox::MAX_BOX_SIZE) + .unwrap_or(false) + }) .boxed() } type Strategy = BoxedStrategy; @@ -483,13 +505,12 @@ pub mod arbitrary { mod tests { use super::*; - use crate::chain::token::arbitrary::ArbTokenIdParam; + use crate::chain::token::TokenAmount; + use crate::mir::expr::Expr; use crate::serialization::sigma_serialize_roundtrip; - use proptest::collection::SizeRange; use proptest::prelude::*; use sigma_test_util::force_any_val; - use sigma_test_util::force_any_val_with; #[test] fn get_register_mandatory() { @@ -522,25 +543,110 @@ mod tests { assert_eq!(b.creation_info().1, expected_bytes.to_vec().as_vec_i8()); } + // NOTE: the old `test_max_tokens` (a MAX_TOKENS_COUNT round-trip with + // arbitrary token amounts) is superseded by + // `box_window_123_minimal_tokens_fit_and_parse` below: with the + // MAX_BOX_SIZE parse window in place, "max tokens" is not a fixed count + // but whatever fits the window, exactly like the reference impl. + + /// Deterministic tiny tree so the token counts below sit on known sides of + /// the MAX_BOX_SIZE window (123 minimal tokens fit for any tree <= 31 + /// bytes; 124 cross for any tree). + fn small_tree() -> ErgoTree { + let tree = ErgoTree::try_from(Expr::Const(true.into())).unwrap(); + assert!(tree.sigma_serialize_bytes().unwrap().len() <= 31); + tree + } + + fn minimal_tokens_vec(n: usize) -> Vec { + (0..n) + .map(|i| { + let mut id = [0u8; 32]; + id[0] = i as u8; + id[1] = (i >> 8) as u8; + Token { + token_id: Digest32::from(id).into(), + // amount 1 = single VLQ byte: a minimal token is 33 bytes + amount: TokenAmount::try_from(1u64).unwrap(), + } + }) + .collect() + } + + fn windowed_candidate( + tokens: Option, + registers: NonMandatoryRegisters, + ) -> ErgoBoxCandidate { + ErgoBoxCandidate { + value: BoxValue::MIN, + ergo_tree: small_tree(), + tokens, + additional_registers: registers, + creation_height: 0, + } + } + + // The token gate is the MAX_BOX_SIZE position-limit window over the + // candidate span, not a count rule (JVM + // ErgoBoxCandidate.parseBodyWithIndexedDigests; the routed + // Global.deserializeTo_Box_token_window family). + #[test] - fn test_max_tokens() { - let tokens = force_any_val_with::>(( - SizeRange::new(ErgoBox::MAX_TOKENS_COUNT..=ErgoBox::MAX_TOKENS_COUNT), - ArbTokenIdParam::Arbitrary, - )); - let b = ErgoBox::from_box_candidate( - &ErgoBoxCandidate { - value: BoxValue::SAFE_USER_MIN, - ergo_tree: force_any_val::(), - tokens: Some(BoxTokens::from_vec(tokens).unwrap()), - additional_registers: NonMandatoryRegisters::empty(), - creation_height: 0, - }, - TxId::zero(), - 0, - ) - .unwrap(); - assert_eq!(sigma_serialize_roundtrip(&b), b); + fn box_window_123_minimal_tokens_fit_and_parse() { + let tokens = BoxTokens::from_vec(minimal_tokens_vec(123)).unwrap(); + let c = windowed_candidate(Some(tokens), NonMandatoryRegisters::empty()); + let bytes = c.sigma_serialize_bytes().unwrap(); + assert!(bytes.len() <= ErgoBox::MAX_BOX_SIZE); + let parsed = ErgoBoxCandidate::sigma_parse_bytes(&bytes).unwrap(); + assert_eq!(parsed.tokens.as_ref().unwrap().len(), 123); + } + + #[test] + fn box_window_124_minimal_tokens_cross_and_error() { + let tokens = BoxTokens::from_vec(minimal_tokens_vec(124)).unwrap(); + let c = windowed_candidate(Some(tokens), NonMandatoryRegisters::empty()); + let bytes = c.sigma_serialize_bytes().unwrap(); + assert!(bytes.len() > ErgoBox::MAX_BOX_SIZE); + // a token read starts past the window: same verdict the count cap used + // to give, now by the JVM's mechanism + assert!(ErgoBoxCandidate::sigma_parse_bytes(&bytes).is_err()); + } + + #[test] + fn box_window_escape_on_final_field() { + // the window is checked BEFORE each read: a candidate whose LAST field + // overruns 4096 still parses (the JVM's lazy per-read semantics) ... + let fat_r4 = Constant::from(vec![0i8; 4200]); + let c = windowed_candidate( + Some(BoxTokens::from_vec(minimal_tokens_vec(2)).unwrap()), + NonMandatoryRegisters::try_from(vec![fat_r4.clone()]).unwrap(), + ); + let bytes = c.sigma_serialize_bytes().unwrap(); + assert!(bytes.len() > ErgoBox::MAX_BOX_SIZE); + let parsed = ErgoBoxCandidate::sigma_parse_bytes(&bytes).unwrap(); + assert_eq!(parsed.tokens.as_ref().unwrap().len(), 2); + + // ... while a read STARTING past the window fails: a small R5 placed + // after the fat R4 + let c = windowed_candidate( + Some(BoxTokens::from_vec(minimal_tokens_vec(2)).unwrap()), + NonMandatoryRegisters::try_from(vec![fat_r4, Constant::from(1i32)]).unwrap(), + ); + let bytes = c.sigma_serialize_bytes().unwrap(); + assert!(ErgoBoxCandidate::sigma_parse_bytes(&bytes).is_err()); + } + + #[test] + fn box_tokens_relaxed_to_u8_ceiling() { + // 255 is the type bound (SigmaConstants.MaxTokens): such boxes + // serialize fine, and it is the window that rejects them at parse + let tokens = BoxTokens::from_vec(minimal_tokens_vec(255)).unwrap(); + let c = windowed_candidate(Some(tokens), NonMandatoryRegisters::empty()); + let bytes = c.sigma_serialize_bytes().unwrap(); + assert!(bytes.len() > ErgoBox::MAX_BOX_SIZE); + assert!(ErgoBoxCandidate::sigma_parse_bytes(&bytes).is_err()); + // 256 is unrepresentable (single count byte on the wire) + assert!(BoxTokens::from_vec(minimal_tokens_vec(256)).is_err()); } proptest! { diff --git a/ergotree-ir/src/ergo_tree.rs b/ergotree-ir/src/ergo_tree.rs index a1d52708d..c98957365 100644 --- a/ergotree-ir/src/ergo_tree.rs +++ b/ergotree-ir/src/ergo_tree.rs @@ -383,7 +383,7 @@ impl SigmaSerializable for ErgoTree { let tree_size_bytes = r.get_u32()?; let body_pos = r.position()?; let mut buf = vec![0u8; tree_size_bytes as usize]; - r.read_exact(buf.as_mut_slice())?; + r.get_bytes_into(buf.as_mut_slice())?; let mut inner_r = SigmaByteReader::new(Cursor::new(&mut buf[..]), ConstantStore::empty()); match inner_r.with_tree_version(header.version(), |inner_r| { @@ -394,7 +394,7 @@ impl SigmaSerializable for ErgoTree { let num_bytes = (body_pos - start_pos) + tree_size_bytes as u64; r.seek(io::SeekFrom::Start(start_pos))?; let mut bytes = vec![0; num_bytes as usize]; - r.read_exact(&mut bytes)?; + r.get_bytes_into(&mut bytes)?; Ok(ErgoTree::Unparsed { tree_bytes: bytes, error, diff --git a/ergotree-ir/src/serialization/data.rs b/ergotree-ir/src/serialization/data.rs index ff1d4b10b..237bb2952 100644 --- a/ergotree-ir/src/serialization/data.rs +++ b/ergotree-ir/src/serialization/data.rs @@ -134,7 +134,7 @@ impl DataSerializer { SString => { let len = r.get_u32()?; let mut buf = vec![0; len as usize]; - r.read_exact(&mut buf)?; + r.get_bytes_into(&mut buf)?; Literal::String(String::from_utf8_lossy(&buf).into()) } SBigInt => Literal::BigInt(BigInt256::sigma_parse(r)?), @@ -149,7 +149,7 @@ impl DataSerializer { SColl(elem_type) if **elem_type == SByte => { let len = r.get_u16()? as usize; let mut buf = vec![0u8; len]; - r.read_exact(&mut buf)?; + r.get_bytes_into(&mut buf)?; Literal::Coll(CollKind::NativeColl(NativeColl::CollByte( buf.into_iter().map(|v| v as i8).collect(), ))) diff --git a/ergotree-ir/src/serialization/sigma_byte_reader.rs b/ergotree-ir/src/serialization/sigma_byte_reader.rs index 199eeb5c6..607e62fe2 100644 --- a/ergotree-ir/src/serialization/sigma_byte_reader.rs +++ b/ergotree-ir/src/serialization/sigma_byte_reader.rs @@ -6,6 +6,7 @@ use super::val_def_type_store::ValDefTypeStore; use core2::io::Cursor; use core2::io::Read; use core2::io::Seek; +use sigma_ser::vlq_encode::PositionLimit; use sigma_ser::vlq_encode::ReadSigmaVlqExt; /// Implementation of SigmaByteRead @@ -16,6 +17,7 @@ pub struct SigmaByteReader { val_def_type_store: ValDefTypeStore, was_deserialize: bool, version: ErgoTreeVersion, + position_limit: u64, } impl SigmaByteReader { @@ -28,6 +30,7 @@ impl SigmaByteReader { val_def_type_store: ValDefTypeStore::new(), was_deserialize: false, version: ErgoTreeVersion::V0, + position_limit: u64::MAX, } } @@ -44,6 +47,7 @@ impl SigmaByteReader { val_def_type_store: ValDefTypeStore::new(), was_deserialize: false, version: ErgoTreeVersion::MAX_SCRIPT_VERSION, + position_limit: u64::MAX, } } } @@ -118,6 +122,18 @@ impl Seek for SigmaByteReader { } } +/// Real position-limit storage: this is the decorating reader the limit lives on +/// (the reference impl's `CoreByteReader.positionLimit`), while the wrapped inner +/// reader stays unchecked. +impl PositionLimit for SigmaByteReader { + fn position_limit(&self) -> u64 { + self.position_limit + } + fn set_position_limit(&mut self, limit: u64) { + self.position_limit = limit; + } +} + impl SigmaByteRead for SigmaByteReader { fn constant_store(&mut self) -> &mut ConstantStore { &mut self.constant_store diff --git a/ergotree-ir/src/types/stype_param.rs b/ergotree-ir/src/types/stype_param.rs index 2b7473be2..c2504a8f3 100644 --- a/ergotree-ir/src/types/stype_param.rs +++ b/ergotree-ir/src/types/stype_param.rs @@ -86,7 +86,7 @@ impl SigmaSerializable for STypeVar { fn sigma_parse(r: &mut R) -> Result { let name_len = r.get_u8()?; let mut bytes = vec![0; name_len as usize]; - r.read_exact(&mut bytes)?; + r.get_bytes_into(&mut bytes)?; Ok(STypeVar::new_from_bytes(bytes)?) } } diff --git a/ergotree-ir/src/unsignedbigint256.rs b/ergotree-ir/src/unsignedbigint256.rs index 86ab30120..55493ea12 100644 --- a/ergotree-ir/src/unsignedbigint256.rs +++ b/ergotree-ir/src/unsignedbigint256.rs @@ -327,7 +327,7 @@ impl SigmaSerializable for UnsignedBigInt { ))); } let mut buf = [0u8; 32]; - r.read_exact(&mut buf[32 - size..])?; + r.get_bytes_into(&mut buf[32 - size..])?; match UnsignedBigInt::from_be_slice(&buf) { Some(x) => Ok(x), None => Err(SigmaParsingError::ValueOutOfBounds("".into())), diff --git a/sigma-ser/src/vlq_encode.rs b/sigma-ser/src/vlq_encode.rs index 880e7dc50..8bc08ddc3 100644 --- a/sigma-ser/src/vlq_encode.rs +++ b/sigma-ser/src/vlq_encode.rs @@ -165,10 +165,71 @@ pub trait WriteSigmaVlqExt: io::Write { /// Mark all types implementing `Write` as implementing the extension. impl WriteSigmaVlqExt for W {} +/// Reader position-limit state, mirroring the reference implementation's +/// `CoreByteReader.positionLimit`: a soft window over a span of the input that is +/// checked once at the START of each primitive read (see +/// [`ReadSigmaVlqExt::check_position_limit`]). `u64::MAX` means "no limit"; a +/// serializer narrows it over a span and restores the previous value afterwards +/// (e.g. the box candidate parser sets `position + MaxBoxSize`). +pub trait PositionLimit { + /// Current position limit (`u64::MAX` = unlimited). + fn position_limit(&self) -> u64; + /// Set the position limit. Callers save the previous value and restore it + /// after the windowed span. + fn set_position_limit(&mut self, limit: u64); +} + +/// Bare cursors carry no limit storage: setting a limit on a `Cursor` is a no-op +/// and reads are bounded only by the buffer. Position limits are effective on +/// readers that store them (`SigmaByteReader` in `ergotree-ir`), matching the +/// reference impl where only the decorating `CoreByteReader` has a limit while +/// the underlying reader is unchecked. +impl PositionLimit for io::Cursor { + fn position_limit(&self) -> u64 { + u64::MAX + } + fn set_position_limit(&mut self, _limit: u64) {} +} + +impl PositionLimit for &mut P { + fn position_limit(&self) -> u64 { + (**self).position_limit() + } + fn set_position_limit(&mut self, limit: u64) { + (**self).set_position_limit(limit) + } +} + /// Read and decode values using VLQ (+ ZigZag for signed values) encoded and written with [`WriteSigmaVlqExt`] /// for VLQ see (GLE) /// for ZigZag see -pub trait ReadSigmaVlqExt: io::Read + io::Seek { +pub trait ReadSigmaVlqExt: io::Read + io::Seek + PositionLimit { + /// Check that the current position has not passed the position limit, failing + /// strictly on `position > limit` (a read starting exactly AT the limit is + /// allowed) — the reference impl's `CheckPositionLimit` validation rule 1014. + /// Called once at the start of each primitive read; the read itself may then + /// run past the limit unchecked, which is what lets the final field of a + /// windowed span overrun the window, exactly like the JVM's lazy per-read + /// check. + // `seek(Current(0))` instead of `stream_position()`: core2's no_std `Seek` + // has no `stream_position` (it is std-gated), and this crate is no_std-capable + #[allow(clippy::seek_from_current)] + fn check_position_limit(&mut self) -> Result<(), io::Error> { + let limit = self.position_limit(); + if limit == u64::MAX { + return Ok(()); + } + let position = self.seek(io::SeekFrom::Current(0))?; + if position > limit { + // static message: core2's no_std `io::Error::new` only takes `&'static str` + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "read position exceeds the position limit", + )); + } + Ok(()) + } + /// Read i8 without decoding fn get_i8(&mut self) -> Result { Self::get_u8(self).map(|v| v as i8) @@ -176,6 +237,7 @@ pub trait ReadSigmaVlqExt: io::Read + io::Seek { /// Read u8 without decoding fn get_u8(&mut self) -> Result { + self.check_position_limit()?; let mut slice = [0u8; 1]; self.read_exact(&mut slice)?; Ok(slice[0]) @@ -217,10 +279,16 @@ pub trait ReadSigmaVlqExt: io::Read + io::Seek { fn get_u64(&mut self) -> Result { // source: http://github.com/google/protobuf/blob/a7252bf42df8f0841cf3a0c85fdbf1a5172adecb/java/core/src/main/java/com/google/protobuf/CodedInputStream.java#L2653 // for faster version see: http://github.com/google/protobuf/blob/a7252bf42df8f0841cf3a0c85fdbf1a5172adecb/java/core/src/main/java/com/google/protobuf/CodedInputStream.java#L1085 + self.check_position_limit()?; let mut result: i64 = 0; let mut shift = 0; while shift < 64 { - let b = self.get_u8()?; + // raw (unchecked) byte read: the limit is checked once per primitive, + // so continuation bytes of a VLQ value that started within the limit + // may cross it, like the reference impl's underlying reader + let mut slice = [0u8; 1]; + self.read_exact(&mut slice)?; + let b = slice[0]; result |= ((b & 0x7F) as i64) << shift; if (b & 0x80) == 0 { return Ok(result as u64); @@ -232,6 +300,7 @@ pub trait ReadSigmaVlqExt: io::Read + io::Seek { /// Read a vector of bits with the given size fn get_bits(&mut self, size: usize) -> Result, VlqEncodingError> { + self.check_position_limit()?; let byte_num = size.div_ceil(8); let mut buf = vec![0u8; byte_num]; self.read_exact(&mut buf)?; @@ -241,6 +310,14 @@ pub trait ReadSigmaVlqExt: io::Read + io::Seek { Ok(bits.iter().map(|x| *x).collect::>()) } + /// Read exactly `buf.len()` bytes, checking the position limit once at the + /// start (the reference impl's `getBytes`): a chunk that begins within the + /// limit may run past it. + fn get_bytes_into(&mut self, buf: &mut [u8]) -> Result<(), io::Error> { + self.check_position_limit()?; + self.read_exact(buf) + } + /// Reads a string from the reader. Reads a byte (size), and the string fn get_short_string(&mut self) -> Result { let size_bytes = self.get_u8()?; @@ -265,7 +342,7 @@ pub trait ReadSigmaVlqExt: io::Read + io::Seek { } /// Mark all types implementing `Read` as implementing the extension. -impl ReadSigmaVlqExt for R {} +impl ReadSigmaVlqExt for R {} #[allow(clippy::unwrap_used)] #[cfg(test)] @@ -1013,4 +1090,98 @@ mod tests { prop_assert_eq!(&bytes_i32(i as i32), &expected_bytes); } } + + /// A reader with real position-limit storage (the shape `SigmaByteReader` + /// has in `ergotree-ir`), pinning the check semantics at this layer. + struct LimitedCursor { + inner: Cursor>, + limit: u64, + } + + impl LimitedCursor { + fn new(bytes: Vec) -> Self { + LimitedCursor { + inner: Cursor::new(bytes), + limit: u64::MAX, + } + } + } + + impl io::Read for LimitedCursor { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.inner.read(buf) + } + } + + impl io::Seek for LimitedCursor { + fn seek(&mut self, pos: io::SeekFrom) -> io::Result { + self.inner.seek(pos) + } + } + + impl PositionLimit for LimitedCursor { + fn position_limit(&self) -> u64 { + self.limit + } + fn set_position_limit(&mut self, limit: u64) { + self.limit = limit; + } + } + + #[test] + fn position_limit_boundary_is_inclusive() { + // strict `position > limit`: a primitive starting exactly AT the limit reads + let mut r = LimitedCursor::new(vec![1, 2, 3, 4, 5, 6, 7, 8]); + r.set_position_limit(4); + assert_eq!(r.get_u8().unwrap(), 1); // pos 0 + assert_eq!(r.get_u8().unwrap(), 2); // pos 1 + assert_eq!(r.get_u8().unwrap(), 3); // pos 2 + assert_eq!(r.get_u8().unwrap(), 4); // pos 3 + assert_eq!(r.get_u8().unwrap(), 5); // pos 4 == limit: allowed + assert!(r.get_u8().is_err()); // pos 5 > limit + } + + #[test] + fn position_limit_vlq_checked_once_per_primitive() { + // a VLQ u64 starting within the limit decodes even when its + // continuation bytes cross the limit + let mut bytes = bytes_u64(u64::MAX); + assert_eq!(bytes.len(), 10); + let mut r = LimitedCursor::new(bytes.clone()); + r.set_position_limit(0); + assert_eq!(r.get_u64().unwrap(), u64::MAX); // starts at 0 == limit + + // while one STARTING past the limit is rejected before any byte is read + bytes.insert(0, 0); // pad: the VLQ now starts at position 1 + let mut r = LimitedCursor::new(bytes); + r.set_position_limit(0); + assert_eq!(r.get_u8().unwrap(), 0); // consume the pad at pos 0 + assert!(r.get_u64().is_err()); // starts at 1 > limit + } + + #[test] + fn position_limit_chunk_may_overrun_but_not_start_past() { + let mut r = LimitedCursor::new(vec![0u8; 16]); + r.set_position_limit(4); + let mut buf = [0u8; 10]; + // starts at 0 <= 4: the chunk may run past the limit + r.get_bytes_into(&mut buf).unwrap(); + // the next chunk starts at 10 > 4 + assert!(r.get_bytes_into(&mut buf[..2]).is_err()); + } + + #[test] + fn position_limit_default_is_unlimited() { + let mut r = LimitedCursor::new(bytes_u64(12345)); + assert_eq!(r.get_u64().unwrap(), 12345); + } + + #[test] + fn bare_cursor_limit_is_noop() { + // documented: Cursor has no limit storage, setting one is a no-op + let mut c = Cursor::new(vec![9u8; 4]); + c.set_position_limit(0); + assert_eq!(c.position_limit(), u64::MAX); + assert_eq!(c.get_u8().unwrap(), 9); + } }