Skip to content

Commit fb9d8cf

Browse files
committed
Merge BlockstreamResearch#271: bit machine: fix some overflows with massive types
b2d83e2 test: add unit test which causes addition overflow in type size computation (Andrew Poelstra) b1f3396 bit machine: check resource bounds on construction (Andrew Poelstra) d21486a types: use saturating addition in bit width computation during type construction (Andrew Poelstra) Pull request description: There probably are other things we can break with this test program, but these are the ones I encountered just now. ACKs for top commit: uncomputable: ACK b2d83e2 Tree-SHA512: 5c2b978d082d47a808d3510e94ed7141b5f84d454cecd5ad204e60aea75e708df7516dfbc43b1c46faaab5b76857cdef8349866f5c26380bdf6b37134915762b
2 parents 62278bc + b2d83e2 commit fb9d8cf

File tree

9 files changed

+195
-21
lines changed

9 files changed

+195
-21
lines changed

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::collections::HashSet;
1213
use std::error;
@@ -20,6 +21,8 @@ use crate::{analysis, Imr};
2021
use crate::{Cmr, FailEntropy, Value};
2122
use frame::Frame;
2223

24+
pub use self::limits::LimitError;
25+
2326
/// An execution context for a Simplicity program
2427
pub struct BitMachine {
2528
/// Space for bytes that read and write frames point to.
@@ -37,16 +40,17 @@ pub struct BitMachine {
3740

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

43-
Self {
47+
Ok(Self {
4448
data: vec![0; (io_width + program.bounds().extra_cells + 7) / 8],
4549
next_frame_start: 0,
4650
read: Vec::with_capacity(program.bounds().extra_frames + analysis::IO_EXTRA_FRAMES),
4751
write: Vec::with_capacity(program.bounds().extra_frames + analysis::IO_EXTRA_FRAMES),
4852
source_ty: program.arrow().source.clone(),
49-
}
53+
})
5054
}
5155

5256
#[cfg(test)]
@@ -61,7 +65,7 @@ impl BitMachine {
6165
.expect("finalizing types")
6266
.finalize(&mut SimpleFinalizer::new(None.into_iter()))
6367
.expect("finalizing");
64-
let mut mac = BitMachine::for_program(&prog);
68+
let mut mac = BitMachine::for_program(&prog).expect("program has reasonable bounds");
6569
mac.exec(&prog, env)
6670
}
6771

@@ -569,6 +573,8 @@ pub enum ExecutionError {
569573
ReachedFailNode(FailEntropy),
570574
/// Reached a pruned branch
571575
ReachedPrunedBranch(Cmr),
576+
/// Exceeded some program limit
577+
LimitExceeded(LimitError),
572578
/// Jet failed during execution
573579
JetFailed(JetFailed),
574580
}
@@ -585,12 +591,29 @@ impl fmt::Display for ExecutionError {
585591
ExecutionError::ReachedPrunedBranch(hash) => {
586592
write!(f, "Execution reached a pruned branch: {}", hash)
587593
}
588-
ExecutionError::JetFailed(jet_failed) => fmt::Display::fmt(jet_failed, f),
594+
ExecutionError::LimitExceeded(e) => e.fmt(f),
595+
ExecutionError::JetFailed(jet_failed) => jet_failed.fmt(f),
589596
}
590597
}
591598
}
592599

593-
impl error::Error for ExecutionError {}
600+
impl error::Error for ExecutionError {
601+
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
602+
match self {
603+
Self::InputWrongType(..)
604+
| Self::ReachedFailNode(..)
605+
| Self::ReachedPrunedBranch(..) => None,
606+
Self::LimitExceeded(ref e) => Some(e),
607+
Self::JetFailed(ref e) => Some(e),
608+
}
609+
}
610+
}
611+
612+
impl From<LimitError> for ExecutionError {
613+
fn from(e: LimitError) -> Self {
614+
ExecutionError::LimitExceeded(e)
615+
}
616+
}
594617

595618
impl From<JetFailed> for ExecutionError {
596619
fn from(jet_failed: JetFailed) -> Self {
@@ -600,7 +623,6 @@ impl From<JetFailed> for ExecutionError {
600623

601624
#[cfg(test)]
602625
mod tests {
603-
#[cfg(feature = "elements")]
604626
use super::*;
605627

606628
#[cfg(feature = "elements")]
@@ -656,7 +678,9 @@ mod tests {
656678

657679
// Try to run it on the bit machine and return the result
658680
let env = ElementsEnv::dummy();
659-
BitMachine::for_program(&prog).exec(&prog, &env)
681+
BitMachine::for_program(&prog)
682+
.expect("program has reasonable bounds")
683+
.exec(&prog, &env)
660684
}
661685

662686
#[test]
@@ -685,4 +709,20 @@ mod tests {
685709
);
686710
assert_eq!(res.unwrap(), Value::unit());
687711
}
712+
713+
#[test]
714+
fn crash_regression2() {
715+
use crate::node::{CoreConstructible as _, JetConstructible as _};
716+
717+
type Node = Arc<crate::ConstructNode<crate::jet::Core>>;
718+
719+
let mut bomb = Node::jet(
720+
&crate::types::Context::new(),
721+
crate::jet::Core::Ch8, // arbitrary jet with nonzero output size
722+
);
723+
for _ in 0..100 {
724+
bomb = Node::pair(&bomb, &bomb).unwrap();
725+
}
726+
let _ = bomb.finalize_pruned(&());
727+
}
688728
}

src/human_encoding/mod.rs

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

@@ -260,7 +260,7 @@ mod tests {
260260
return;
261261
}
262262
};
263-
let mut mac = BitMachine::for_program(&program);
263+
let mut mac = BitMachine::for_program(&program).expect("program has reasonable bounds");
264264
match mac.exec(&program, env) {
265265
Ok(_) => panic!("Execution is expected to fail"),
266266
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
@@ -595,7 +595,8 @@ mod tests {
595595
.finalize_unpruned()
596596
.expect("finalize");
597597

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

src/node/commit.rs

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

546546
// Execute the program to confirm that it worked
547-
let mut mac = BitMachine::for_program(&diff1_final);
547+
let mut mac =
548+
BitMachine::for_program(&diff1_final).expect("program has reasonable bounds");
548549
mac.exec(&diff1_final, &()).unwrap();
549550
}
550551
}

src/node/redeem.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ impl<J: Jet> RedeemNode<J> {
418418

419419
// 1) Run the Bit Machine and mark (un)used branches.
420420
// This is the only fallible step in the pruning process.
421-
let mut mac = BitMachine::for_program(self);
421+
let mut mac = BitMachine::for_program(self)?;
422422
let tracker = mac.exec_prune(self, env)?;
423423

424424
// 2) Prune out unused case branches.
@@ -920,7 +920,8 @@ mod tests {
920920
.finalize_unpruned()
921921
.expect("expected pruned program should finalize");
922922

923-
let mut mac = BitMachine::for_program(&unpruned_program);
923+
let mut mac = BitMachine::for_program(&unpruned_program)
924+
.expect("unpruned program has reasonable bounds");
924925
let unpruned_output = mac
925926
.exec(&unpruned_program, env)
926927
.expect("unpruned program should run without failure");
@@ -934,7 +935,8 @@ mod tests {
934935
"pruning result differs from expected result"
935936
);
936937

937-
let mut mac = BitMachine::for_program(&pruned_program);
938+
let mut mac =
939+
BitMachine::for_program(&pruned_program).expect("pruned program has reasonable bounds");
938940
let pruned_output = mac
939941
.exec(&pruned_program, env)
940942
.expect("pruned program should run without failure");

src/policy/satisfy.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -351,15 +351,15 @@ mod tests {
351351
program: Arc<RedeemNode<Elements>>,
352352
env: &ElementsEnv<Arc<elements::Transaction>>,
353353
) {
354-
let mut mac = BitMachine::for_program(&program);
354+
let mut mac = BitMachine::for_program(&program).unwrap();
355355
assert!(mac.exec(&program, env).is_ok());
356356
}
357357

358358
fn execute_unsuccessful(
359359
program: Arc<RedeemNode<Elements>>,
360360
env: &ElementsEnv<Arc<elements::Transaction>>,
361361
) {
362-
let mut mac = BitMachine::for_program(&program);
362+
let mut mac = BitMachine::for_program(&program).unwrap();
363363
assert!(mac.exec(&program, env).is_err());
364364
}
365365

src/policy/serialize.rs

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

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

src/types/final_data.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -194,19 +194,24 @@ impl Final {
194194

195195
/// Create the sum of the given `left` and `right` types.
196196
pub fn sum(left: Arc<Self>, right: Arc<Self>) -> Arc<Self> {
197+
// Use saturating_add for bitwidths. If the user has overflowed usize, even on a 32-bit
198+
// system this means that they have a 4-gigabit type and their program should be rejected
199+
// by a sanity check somewhere. However, if we panic here, the user cannot finalize their
200+
// program and cannot even tell that this resource limit has been hit before panicking.
197201
Arc::new(Final {
198202
tmr: Tmr::sum(left.tmr, right.tmr),
199-
bit_width: 1 + cmp::max(left.bit_width, right.bit_width),
203+
bit_width: cmp::max(left.bit_width, right.bit_width).saturating_add(1),
200204
has_padding: left.has_padding || right.has_padding || left.bit_width != right.bit_width,
201205
bound: CompleteBound::Sum(left, right),
202206
})
203207
}
204208

205209
/// Create the product of the given `left` and `right` types.
206210
pub fn product(left: Arc<Self>, right: Arc<Self>) -> Arc<Self> {
211+
// See comment in `sum` about use of saturating add.
207212
Arc::new(Final {
208213
tmr: Tmr::product(left.tmr, right.tmr),
209-
bit_width: left.bit_width + right.bit_width,
214+
bit_width: left.bit_width.saturating_add(right.bit_width),
210215
has_padding: left.has_padding || right.has_padding,
211216
bound: CompleteBound::Product(left, right),
212217
})

0 commit comments

Comments
 (0)