Skip to content

Commit 55b50ff

Browse files
committed
Merge #283: Backport #271: integer overflow checks
34aa968 bump version to 0.3.1 (Andrew Poelstra) 7f03b7e test: add unit test which causes addition overflow in type size computation (Andrew Poelstra) e439bc0 bit machine: check resource bounds on construction (Andrew Poelstra) 4f147ca types: use saturating addition in bit width computation during type construction (Andrew Poelstra) Pull request description: Backports several integer overflow checks to the currently-released version of 0.3. These can be used to crash rust-simplicity which is making it hard to write regression fuzztests. Also changes the version to 0.3.1 so we can release immediately. Should be easy enough to review with `git range-diff pr/271/head...pr/283/head`. ACKs for top commit: uncomputable: ACK 34aa968 Tree-SHA512: 2df90cb26d0cbe50e15381c1b8368ca394ee592937f0f173068d2b891bec138e7e23aefcb19c58c51868e373c7d35f7b09930afcc31ebfb86f859fb2f54db657
2 parents 9eed013 + 34aa968 commit 55b50ff

File tree

10 files changed

+192
-20
lines changed

10 files changed

+192
-20
lines changed

Cargo-recent.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ dependencies = [
435435

436436
[[package]]
437437
name = "simplicity-lang"
438-
version = "0.3.0"
438+
version = "0.3.1"
439439
dependencies = [
440440
"bitcoin",
441441
"bitcoin_hashes",

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "simplicity-lang"
3-
version = "0.3.0"
3+
version = "0.3.1"
44
authors = ["Andrew Poelstra <[email protected]>"]
55
license = "CC0-1.0"
66
homepage = "https://github.com/BlockstreamResearch/rust-simplicity/"

src/bit_machine/limits.rs

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
//! Bit Machine Resource Limits
4+
//!
5+
//! Implementation of the Bit Machine, without TCO, as TCO precludes some
6+
//! frame management optimizations which can be used to great benefit.
7+
//!
8+
9+
use core::fmt;
10+
use std::error;
11+
12+
/// The maximum number of cells needed by the bit machine.
13+
///
14+
/// This roughly corresponds to the maximum size of any type used by a
15+
/// program, in bits. In a blockchain context this should be limited by
16+
/// the transaction's weight budget.
17+
///
18+
/// The limit here is an absolute limit enforced by the library to avoid
19+
/// unbounded allocations.
20+
// This value must be less than usize::MAX / 2 to avoid panics in this module.
21+
const MAX_CELLS: usize = 2 * 1024 * 1024 * 1024 - 1;
22+
23+
/// The maximum number of frames needed by the bit machine.
24+
///
25+
/// This roughly corresponds to the maximum depth of nested `comp` and
26+
/// `disconnect` combinators. In a blockchain context this should be
27+
/// limited by the transaction's weight budget.
28+
///
29+
/// The limit here is an absolute limit enforced by the library to avoid
30+
/// unbounded allocations.
31+
// This value must be less than usize::MAX / 2 to avoid panics in this module.
32+
const MAX_FRAMES: usize = 1024 * 1024;
33+
34+
#[non_exhaustive]
35+
#[derive(Clone, Debug)]
36+
pub enum LimitError {
37+
MaxCellsExceeded {
38+
/// The number of cells needed by the program.
39+
got: usize,
40+
/// The maximum allowed number of cells.
41+
max: usize,
42+
/// A description of which cell count exceeded the limit.
43+
bound: &'static str,
44+
},
45+
MaxFramesExceeded {
46+
/// The number of frames needed by the program.
47+
got: usize,
48+
/// The maximum allowed number of frames.
49+
max: usize,
50+
/// A description of which frame count exceeded the limit.
51+
bound: &'static str,
52+
},
53+
}
54+
55+
impl fmt::Display for LimitError {
56+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57+
let (limit, got, max, bound) = match self {
58+
LimitError::MaxCellsExceeded { got, max, bound } => ("cells", got, max, bound),
59+
LimitError::MaxFramesExceeded { got, max, bound } => ("frames", got, max, bound),
60+
};
61+
write!(
62+
f,
63+
"maximum number of {} exceeded (needed {}, maximum {}) (bound: {})",
64+
limit, got, max, bound,
65+
)
66+
}
67+
}
68+
impl error::Error for LimitError {}
69+
70+
impl LimitError {
71+
fn check_max_cells(got: usize, bound: &'static str) -> Result<(), Self> {
72+
if got > MAX_CELLS {
73+
Err(Self::MaxCellsExceeded {
74+
got,
75+
max: MAX_CELLS,
76+
bound,
77+
})
78+
} else {
79+
Ok(())
80+
}
81+
}
82+
83+
fn check_max_frames(got: usize, bound: &'static str) -> Result<(), Self> {
84+
if got > MAX_FRAMES {
85+
Err(Self::MaxFramesExceeded {
86+
got,
87+
max: MAX_CELLS,
88+
bound,
89+
})
90+
} else {
91+
Ok(())
92+
}
93+
}
94+
95+
/// Helper function to check every value and sum for being within bounds.
96+
pub(super) fn check_program<J: crate::jet::Jet>(
97+
program: &crate::RedeemNode<J>,
98+
) -> Result<(), Self> {
99+
let source_ty_width = program.arrow().source.bit_width();
100+
let target_ty_width = program.arrow().target.bit_width();
101+
let bounds = program.bounds();
102+
103+
Self::check_max_cells(source_ty_width, "source type width")?;
104+
Self::check_max_cells(target_ty_width, "target type width")?;
105+
Self::check_max_cells(bounds.extra_cells, "extra cells")?;
106+
Self::check_max_cells(
107+
source_ty_width + target_ty_width,
108+
"source + target type widths",
109+
)?;
110+
Self::check_max_cells(
111+
source_ty_width + target_ty_width + bounds.extra_cells,
112+
"source + target type widths + extra cells",
113+
)?;
114+
115+
Self::check_max_frames(bounds.extra_frames, "extra frames")?;
116+
Self::check_max_frames(
117+
bounds.extra_frames + crate::analysis::IO_EXTRA_FRAMES,
118+
"extra frames + fixed overhead",
119+
)?;
120+
Ok(())
121+
}
122+
}

src/bit_machine/mod.rs

+49-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
// SPDX-License-Identifier: CC0-1.0
22

3-
//! # Simplicity Execution
3+
//! Simplicity Execution
44
//!
55
//! Implementation of the Bit Machine, without TCO, as TCO precludes some
66
//! frame management optimizations which can be used to great benefit.
77
//!
88
99
mod frame;
10+
mod limits;
1011

1112
use std::error;
1213
use std::fmt;
@@ -19,6 +20,8 @@ use crate::types::Final;
1920
use crate::{Cmr, FailEntropy, Value};
2021
use frame::Frame;
2122

23+
pub use self::limits::LimitError;
24+
2225
/// An execution context for a Simplicity program
2326
pub struct BitMachine {
2427
/// Space for bytes that read and write frames point to.
@@ -36,16 +39,17 @@ pub struct BitMachine {
3639

3740
impl BitMachine {
3841
/// Construct a Bit Machine with enough space to execute the given program.
39-
pub fn for_program<J: Jet>(program: &RedeemNode<J>) -> Self {
42+
pub fn for_program<J: Jet>(program: &RedeemNode<J>) -> Result<Self, LimitError> {
43+
LimitError::check_program(program)?;
4044
let io_width = program.arrow().source.bit_width() + program.arrow().target.bit_width();
4145

42-
Self {
46+
Ok(Self {
4347
data: vec![0; (io_width + program.bounds().extra_cells + 7) / 8],
4448
next_frame_start: 0,
4549
read: Vec::with_capacity(program.bounds().extra_frames + analysis::IO_EXTRA_FRAMES),
4650
write: Vec::with_capacity(program.bounds().extra_frames + analysis::IO_EXTRA_FRAMES),
4751
source_ty: program.arrow().source.clone(),
48-
}
52+
})
4953
}
5054

5155
#[cfg(test)]
@@ -60,7 +64,7 @@ impl BitMachine {
6064
.expect("finalizing types")
6165
.finalize(&mut SimpleFinalizer::new(None.into_iter()))
6266
.expect("finalizing");
63-
let mut mac = BitMachine::for_program(&prog);
67+
let mut mac = BitMachine::for_program(&prog).expect("program has reasonable bounds");
6468
mac.exec(&prog, env)
6569
}
6670

@@ -481,6 +485,8 @@ pub enum ExecutionError {
481485
ReachedFailNode(FailEntropy),
482486
/// Reached a pruned branch
483487
ReachedPrunedBranch(Cmr),
488+
/// Exceeded some program limit
489+
LimitExceeded(LimitError),
484490
/// Jet failed during execution
485491
JetFailed(JetFailed),
486492
}
@@ -497,12 +503,29 @@ impl fmt::Display for ExecutionError {
497503
ExecutionError::ReachedPrunedBranch(hash) => {
498504
write!(f, "Execution reached a pruned branch: {}", hash)
499505
}
500-
ExecutionError::JetFailed(jet_failed) => fmt::Display::fmt(jet_failed, f),
506+
ExecutionError::LimitExceeded(e) => e.fmt(f),
507+
ExecutionError::JetFailed(jet_failed) => jet_failed.fmt(f),
501508
}
502509
}
503510
}
504511

505-
impl error::Error for ExecutionError {}
512+
impl error::Error for ExecutionError {
513+
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
514+
match self {
515+
Self::InputWrongType(..)
516+
| Self::ReachedFailNode(..)
517+
| Self::ReachedPrunedBranch(..) => None,
518+
Self::LimitExceeded(ref e) => Some(e),
519+
Self::JetFailed(ref e) => Some(e),
520+
}
521+
}
522+
}
523+
524+
impl From<LimitError> for ExecutionError {
525+
fn from(e: LimitError) -> Self {
526+
ExecutionError::LimitExceeded(e)
527+
}
528+
}
506529

507530
impl From<JetFailed> for ExecutionError {
508531
fn from(jet_failed: JetFailed) -> Self {
@@ -512,7 +535,6 @@ impl From<JetFailed> for ExecutionError {
512535

513536
#[cfg(test)]
514537
mod tests {
515-
#[cfg(feature = "elements")]
516538
use super::*;
517539

518540
#[cfg(feature = "elements")]
@@ -568,7 +590,9 @@ mod tests {
568590

569591
// Try to run it on the bit machine and return the result
570592
let env = ElementsEnv::dummy();
571-
BitMachine::for_program(&prog).exec(&prog, &env)
593+
BitMachine::for_program(&prog)
594+
.expect("program has reasonable bounds")
595+
.exec(&prog, &env)
572596
}
573597

574598
#[test]
@@ -597,4 +621,20 @@ mod tests {
597621
);
598622
assert_eq!(res.unwrap(), Value::unit());
599623
}
624+
625+
#[test]
626+
fn crash_regression2() {
627+
use crate::node::{CoreConstructible as _, JetConstructible as _};
628+
629+
type Node = Arc<crate::ConstructNode<crate::jet::Core>>;
630+
631+
let mut bomb = Node::jet(
632+
&crate::types::Context::new(),
633+
crate::jet::Core::Ch8, // arbitrary jet with nonzero output size
634+
);
635+
for _ in 0..100 {
636+
bomb = Node::pair(&bomb, &bomb).unwrap();
637+
}
638+
let _ = bomb.finalize_types();
639+
}
600640
}

src/human_encoding/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ mod tests {
239239
.expect("Forest is missing expected root")
240240
.finalize()
241241
.expect("Failed to finalize");
242-
let mut mac = BitMachine::for_program(&program);
242+
let mut mac = BitMachine::for_program(&program).expect("program has reasonable bounds");
243243
mac.exec(&program, env).expect("Failed to run program");
244244
}
245245

@@ -261,7 +261,7 @@ mod tests {
261261
return;
262262
}
263263
};
264-
let mut mac = BitMachine::for_program(&program);
264+
let mut mac = BitMachine::for_program(&program).expect("program has reasonable bounds");
265265
match mac.exec(&program, env) {
266266
Ok(_) => panic!("Execution is expected to fail"),
267267
Err(error) => assert_eq!(&error.to_string(), err_msg),

src/human_encoding/parse/mod.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,8 @@ mod tests {
594594
.finalize()
595595
.expect("finalize");
596596

597-
let mut mac = BitMachine::for_program(&program);
597+
let mut mac =
598+
BitMachine::for_program(&program).expect("program has reasonable bounds");
598599
mac.exec(&program, env).expect("execute");
599600
}
600601
Err(errs) => {

src/node/commit.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,8 @@ mod tests {
539539
assert_eq!(counter, 0..98);
540540

541541
// Execute the program to confirm that it worked
542-
let mut mac = BitMachine::for_program(&diff1_final);
542+
let mut mac =
543+
BitMachine::for_program(&diff1_final).expect("program has reasonable bounds");
543544
mac.exec(&diff1_final, &()).unwrap();
544545
}
545546
}

src/policy/satisfy.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -318,15 +318,15 @@ mod tests {
318318
program: Arc<RedeemNode<Elements>>,
319319
env: &ElementsEnv<Arc<elements::Transaction>>,
320320
) {
321-
let mut mac = BitMachine::for_program(&program);
321+
let mut mac = BitMachine::for_program(&program).unwrap();
322322
assert!(mac.exec(&program, env).is_ok());
323323
}
324324

325325
fn execute_unsuccessful(
326326
program: Arc<RedeemNode<Elements>>,
327327
env: &ElementsEnv<Arc<elements::Transaction>>,
328328
) {
329-
let mut mac = BitMachine::for_program(&program);
329+
let mut mac = BitMachine::for_program(&program).unwrap();
330330
assert!(mac.exec(&program, env).is_err());
331331
}
332332

src/policy/serialize.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,10 @@ mod tests {
287287
let finalized = commit
288288
.finalize(&mut SimpleFinalizer::new(witness.into_iter()))
289289
.expect("finalize");
290-
let mut mac = BitMachine::for_program(&finalized);
290+
let mut mac = match BitMachine::for_program(&finalized) {
291+
Ok(mac) => mac,
292+
Err(_) => return false,
293+
};
291294

292295
match mac.exec(&finalized, env) {
293296
Ok(output) => output == Value::unit(),

src/types/final_data.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -189,18 +189,23 @@ impl Final {
189189

190190
/// Create the sum of the given `left` and `right` types.
191191
pub fn sum(left: Arc<Self>, right: Arc<Self>) -> Arc<Self> {
192+
// Use saturating_add for bitwidths. If the user has overflowed usize, even on a 32-bit
193+
// system this means that they have a 4-gigabit type and their program should be rejected
194+
// by a sanity check somewhere. However, if we panic here, the user cannot finalize their
195+
// program and cannot even tell that this resource limit has been hit before panicking.
192196
Arc::new(Final {
193197
tmr: Tmr::sum(left.tmr, right.tmr),
194-
bit_width: 1 + cmp::max(left.bit_width, right.bit_width),
198+
bit_width: cmp::max(left.bit_width, right.bit_width).saturating_add(1),
195199
bound: CompleteBound::Sum(left, right),
196200
})
197201
}
198202

199203
/// Create the product of the given `left` and `right` types.
200204
pub fn product(left: Arc<Self>, right: Arc<Self>) -> Arc<Self> {
205+
// See comment in `sum` about use of saturating add.
201206
Arc::new(Final {
202207
tmr: Tmr::product(left.tmr, right.tmr),
203-
bit_width: left.bit_width + right.bit_width,
208+
bit_width: left.bit_width.saturating_add(right.bit_width),
204209
bound: CompleteBound::Product(left, right),
205210
})
206211
}

0 commit comments

Comments
 (0)