1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
use super::*;
/// NeoVM's `ExecutionEngineLimits.MaxStackSize`. Every live StackItem — eval
/// stack, all slots, and every element nested inside arrays/maps — counts
/// toward it; exceeding it FAULTs.
pub(crate) const NEOVM_MAX_STACK_SIZE: usize = 2048;
impl ExecutionContext {
pub(crate) fn new_array0(&mut self) -> Result<(), RuntimeError> {
self.push_stack(StackItem::array(Vec::new()))
}
/// Count the total number of live StackItems reachable from every root
/// (eval stack + indexed slots + saved call frames), recursing into
/// arrays/maps, stopping once `cap` is exceeded.
///
/// Shared array/map/bytestring objects are de-duplicated by pointer
/// identity (mirroring NeoVM's per-item ReferenceCounter), and map keys —
/// stored here as raw bytes rather than primitive items — are not counted.
/// The result is therefore conservative w.r.t. compound/byte storage; a
/// `> NEOVM_MAX_STACK_SIZE` result is dominated by genuinely-distinct
/// collection elements (the only way a contract realistically reaches the
/// limit), so it signals a real on-chain MaxStackSize fault rather than a
/// false positive on a valid contract.
pub(crate) fn live_stack_item_count(&self, cap: usize) -> usize {
use std::rc::Rc;
let mut seen: std::collections::HashSet<usize> = std::collections::HashSet::new();
let mut count = 0usize;
let mut work: Vec<StackItem> = Vec::new();
work.extend(self.stack.iter().cloned());
work.extend(self.locals.iter().cloned());
work.extend(self.args.iter().cloned());
work.extend(self.static_fields.iter().cloned());
// Indexed slots saved on the call stack. `local_variables` (a parallel
// named map) is intentionally NOT walked — it can alias `saved_locals`
// and would double-count.
for frame in &self.call_stack {
work.extend(frame.saved_locals.iter().cloned());
work.extend(frame.saved_args.iter().cloned());
}
while let Some(item) = work.pop() {
if count > cap {
return count;
}
match item {
StackItem::Array(rc) => {
if seen.insert(Rc::as_ptr(&rc) as usize) {
count += 1;
work.extend(rc.borrow().iter().cloned());
}
}
StackItem::Map(rc) => {
if seen.insert(Rc::as_ptr(&rc) as usize) {
count += 1;
work.extend(rc.borrow().values().cloned());
}
}
StackItem::ByteArray(rc) => {
if seen.insert(Rc::as_ptr(&rc) as usize) {
count += 1;
}
}
_ => count += 1,
}
}
count
}
/// Fault when the live item count exceeds NeoVM's MaxStackSize. The global
/// backstop to the cheap single-collection checks in `new_array` /
/// `append_item`: catches the case where many separate collections, each
/// individually under the limit, together exceed it (e.g. an array of many
/// arrays). A real node FAULTs here; without this the simulator could
/// report success for a script that reverts on-chain.
pub(crate) fn enforce_global_stack_limit(&self) -> Result<(), RuntimeError> {
if self.live_stack_item_count(NEOVM_MAX_STACK_SIZE) > NEOVM_MAX_STACK_SIZE {
return Err(RuntimeError::ExecutionError {
message: format!(
"total live stack items exceed NeoVM MaxStackSize ({NEOVM_MAX_STACK_SIZE})"
),
});
}
Ok(())
}
pub(crate) fn new_struct0(&mut self) -> Result<(), RuntimeError> {
self.push_stack(StackItem::array(Vec::new()))
}
pub(crate) fn new_struct(&mut self) -> Result<(), RuntimeError> {
self.new_array()
}
pub(crate) fn new_array(&mut self) -> Result<(), RuntimeError> {
let count = self.pop_usize("NEWARRAY")?;
// NeoVM's ExecutionEngineLimits.MaxStackSize (2048) counts EVERY item,
// including elements inside arrays/maps/structs. A single collection that
// exceeds 2048 therefore always blows the global limit and FAULTs on a
// real node — model that here so the simulator does not hide an on-chain
// fault by reporting success. (Conservative: a valid contract can never
// hold a 2048+-element collection, so this never false-faults.)
if count > 2048 {
return Err(RuntimeError::ExecutionError {
message: format!("NEWARRAY: {count} elements exceeds NeoVM MaxStackSize (2048)"),
});
}
// Bound user-supplied count against memory_limit. Without this check,
// a malicious PUSHINT + NEWARRAY/NEWSTRUCT/NEWARRAY_T sequence can
// request ~2^31 elements and OOM-abort the host before gas accounting
// fires. Mirrors the PUSHDATA4 length check in instruction/push.rs.
let item_size = std::mem::size_of::<StackItem>();
let required = count.saturating_mul(item_size);
if required > self.memory_limit {
return Err(RuntimeError::ExecutionError {
message: format!(
"NEWARRAY: requested {} items ({} bytes) exceeds memory limit {}",
count, required, self.memory_limit
),
});
}
let items = vec![StackItem::Null; count];
self.push_stack(StackItem::array(items))?;
// Global backstop for accumulating many separate collections.
self.enforce_global_stack_limit()
}
pub(crate) fn new_map(&mut self) -> Result<(), RuntimeError> {
self.push_stack(StackItem::map(std::collections::HashMap::new()))
}
}