Skip to content

Commit aa6677a

Browse files
authored
Limit stack depth of asc_get (#4576)
1 parent ce693d8 commit aa6677a

File tree

9 files changed

+121
-37
lines changed

9 files changed

+121
-37
lines changed

chain/ethereum/src/runtime/abi.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,14 @@ impl FromAscObj<AscUnresolvedContractCall_0_0_4> for UnresolvedContractCall {
138138
asc_call: AscUnresolvedContractCall_0_0_4,
139139
heap: &H,
140140
gas: &GasCounter,
141+
depth: usize,
141142
) -> Result<Self, DeterministicHostError> {
142143
Ok(UnresolvedContractCall {
143-
contract_name: asc_get(heap, asc_call.contract_name, gas)?,
144-
contract_address: asc_get(heap, asc_call.contract_address, gas)?,
145-
function_name: asc_get(heap, asc_call.function_name, gas)?,
146-
function_signature: Some(asc_get(heap, asc_call.function_signature, gas)?),
147-
function_args: asc_get(heap, asc_call.function_args, gas)?,
144+
contract_name: asc_get(heap, asc_call.contract_name, gas, depth)?,
145+
contract_address: asc_get(heap, asc_call.contract_address, gas, depth)?,
146+
function_name: asc_get(heap, asc_call.function_name, gas, depth)?,
147+
function_signature: Some(asc_get(heap, asc_call.function_signature, gas, depth)?),
148+
function_args: asc_get(heap, asc_call.function_args, gas, depth)?,
148149
})
149150
}
150151
}
@@ -163,13 +164,14 @@ impl FromAscObj<AscUnresolvedContractCall> for UnresolvedContractCall {
163164
asc_call: AscUnresolvedContractCall,
164165
heap: &H,
165166
gas: &GasCounter,
167+
depth: usize,
166168
) -> Result<Self, DeterministicHostError> {
167169
Ok(UnresolvedContractCall {
168-
contract_name: asc_get(heap, asc_call.contract_name, gas)?,
169-
contract_address: asc_get(heap, asc_call.contract_address, gas)?,
170-
function_name: asc_get(heap, asc_call.function_name, gas)?,
170+
contract_name: asc_get(heap, asc_call.contract_name, gas, depth)?,
171+
contract_address: asc_get(heap, asc_call.contract_address, gas, depth)?,
172+
function_name: asc_get(heap, asc_call.function_name, gas, depth)?,
171173
function_signature: None,
172-
function_args: asc_get(heap, asc_call.function_args, gas)?,
174+
function_args: asc_get(heap, asc_call.function_args, gas, depth)?,
173175
})
174176
}
175177
}

chain/ethereum/src/runtime/runtime_adapter.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ fn ethereum_call(
7777
// function signature; subgraphs using an apiVersion < 0.0.4 don't pass
7878
// the signature along with the call.
7979
let call: UnresolvedContractCall = if ctx.heap.api_version() >= Version::new(0, 0, 4) {
80-
asc_get::<_, AscUnresolvedContractCall_0_0_4, _>(ctx.heap, wasm_ptr.into(), &ctx.gas)?
80+
asc_get::<_, AscUnresolvedContractCall_0_0_4, _>(ctx.heap, wasm_ptr.into(), &ctx.gas, 0)?
8181
} else {
82-
asc_get::<_, AscUnresolvedContractCall, _>(ctx.heap, wasm_ptr.into(), &ctx.gas)?
82+
asc_get::<_, AscUnresolvedContractCall, _>(ctx.heap, wasm_ptr.into(), &ctx.gas, 0)?
8383
};
8484

8585
let result = eth_call(

graph/src/runtime/asc_heap.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ use super::{
66
gas::GasCounter, AscIndexId, AscPtr, AscType, DeterministicHostError, HostExportError,
77
IndexForAscTypeId,
88
};
9+
10+
// A 128 limit is plenty for any subgraph, while the `fn recursion_limit` test ensures it is not
11+
// large enough to cause stack overflows.
12+
const MAX_RECURSION_DEPTH: usize = 128;
13+
914
/// A type that can read and write to the Asc heap. Call `asc_new` and `asc_get`
1015
/// for reading and writing Rust structs from and to Asc.
1116
///
@@ -95,12 +100,21 @@ pub fn asc_get<T, C, H: AscHeap + ?Sized>(
95100
heap: &H,
96101
asc_ptr: AscPtr<C>,
97102
gas: &GasCounter,
103+
mut depth: usize,
98104
) -> Result<T, DeterministicHostError>
99105
where
100106
C: AscType + AscIndexId,
101107
T: FromAscObj<C>,
102108
{
103-
T::from_asc_obj(asc_ptr.read_ptr(heap, gas)?, heap, gas)
109+
depth += 1;
110+
111+
if depth > MAX_RECURSION_DEPTH {
112+
return Err(DeterministicHostError::Other(anyhow::anyhow!(
113+
"recursion limit reached"
114+
)));
115+
}
116+
117+
T::from_asc_obj(asc_ptr.read_ptr(heap, gas)?, heap, gas, depth)
104118
}
105119

106120
/// Type that can be converted to an Asc object of class `C`.
@@ -133,10 +147,15 @@ impl ToAscObj<bool> for bool {
133147
}
134148

135149
/// Type that can be converted from an Asc object of class `C`.
150+
///
151+
/// ### Overflow protection
152+
/// The `depth` parameter is used to prevent stack overflows, it measures how many `asc_get` calls
153+
/// have been made. `from_asc_obj` does not need to increment the depth, only pass it through.
136154
pub trait FromAscObj<C: AscType>: Sized {
137155
fn from_asc_obj<H: AscHeap + ?Sized>(
138156
obj: C,
139157
heap: &H,
140158
gas: &GasCounter,
159+
depth: usize,
141160
) -> Result<Self, DeterministicHostError>;
142161
}

runtime/test/src/test.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,3 +1157,21 @@ async fn test_boolean() {
11571157
.is_err());
11581158
}
11591159
}
1160+
1161+
#[tokio::test]
1162+
async fn recursion_limit() {
1163+
let module = test_module_latest("RecursionLimit", "recursion_limit.wasm").await;
1164+
1165+
// An error about 'unknown key' means the entity was fully read with no stack overflow.
1166+
module
1167+
.invoke_export1_val_void("recursionLimit", 128)
1168+
.unwrap_err()
1169+
.to_string()
1170+
.contains("Unknown key `foobar`");
1171+
1172+
assert!(module
1173+
.invoke_export1_val_void("recursionLimit", 129)
1174+
.unwrap_err()
1175+
.to_string()
1176+
.contains("recursion limit reached"));
1177+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export * from './common/global';
2+
3+
import { Entity, Value } from './common/types'
4+
5+
declare namespace store {
6+
function get(entity: string, id: string): Entity | null
7+
function set(entity: string, id: string, data: Entity): void
8+
function remove(entity: string, id: string): void
9+
}
10+
11+
export function recursionLimit(depth: i32): void {
12+
let user = new Entity();
13+
var val = Value.fromI32(7);
14+
for (let i = 0; i < depth; i++) {
15+
val = Value.fromArray([val]);
16+
}
17+
user.set("foobar", val);
18+
store.set("User", "user_id", user);
19+
}
Binary file not shown.

runtime/wasm/src/module/mod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use graph::data::subgraph::schema::SubgraphError;
2222
use graph::data_source::{offchain, MappingTrigger, TriggerWithHandler};
2323
use graph::prelude::*;
2424
use graph::runtime::{
25-
asc_get, asc_new,
25+
asc_new,
2626
gas::{self, Gas, GasCounter, SaturatingInto},
2727
AscHeap, AscIndexId, AscType, DeterministicHostError, FromAscObj, HostExportError,
2828
IndexForAscTypeId, ToAscObj,
@@ -45,6 +45,19 @@ pub mod stopwatch;
4545

4646
pub const TRAP_TIMEOUT: &str = "trap: interrupt";
4747

48+
// Convenience for a 'top-level' asc_get, with depth 0.
49+
fn asc_get<T, C: AscType, H: AscHeap + ?Sized>(
50+
heap: &H,
51+
ptr: AscPtr<C>,
52+
gas: &GasCounter,
53+
) -> Result<T, DeterministicHostError>
54+
where
55+
C: AscType + AscIndexId,
56+
T: FromAscObj<C>,
57+
{
58+
graph::runtime::asc_get(heap, ptr, gas, 0)
59+
}
60+
4861
pub trait IntoTrap {
4962
fn determinism_level(&self) -> DeterminismLevel;
5063
fn into_trap(self) -> Trap;

runtime/wasm/src/to_from/external.rs

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ impl FromAscObj<Uint8Array> for web3::H160 {
2727
typed_array: Uint8Array,
2828
heap: &H,
2929
gas: &GasCounter,
30+
depth: usize,
3031
) -> Result<Self, DeterministicHostError> {
31-
let data = <[u8; 20]>::from_asc_obj(typed_array, heap, gas)?;
32+
let data = <[u8; 20]>::from_asc_obj(typed_array, heap, gas, depth)?;
3233
Ok(Self(data))
3334
}
3435
}
@@ -38,8 +39,9 @@ impl FromAscObj<Uint8Array> for web3::H256 {
3839
typed_array: Uint8Array,
3940
heap: &H,
4041
gas: &GasCounter,
42+
depth: usize,
4143
) -> Result<Self, DeterministicHostError> {
42-
let data = <[u8; 32]>::from_asc_obj(typed_array, heap, gas)?;
44+
let data = <[u8; 32]>::from_asc_obj(typed_array, heap, gas, depth)?;
4345
Ok(Self(data))
4446
}
4547
}
@@ -82,8 +84,9 @@ impl FromAscObj<AscBigInt> for BigInt {
8284
array_buffer: AscBigInt,
8385
heap: &H,
8486
gas: &GasCounter,
87+
depth: usize,
8588
) -> Result<Self, DeterministicHostError> {
86-
let bytes = <Vec<u8>>::from_asc_obj(array_buffer, heap, gas)?;
89+
let bytes = <Vec<u8>>::from_asc_obj(array_buffer, heap, gas, depth)?;
8790
Ok(BigInt::from_signed_bytes_le(&bytes))
8891
}
8992
}
@@ -109,9 +112,10 @@ impl FromAscObj<AscBigDecimal> for BigDecimal {
109112
big_decimal: AscBigDecimal,
110113
heap: &H,
111114
gas: &GasCounter,
115+
depth: usize,
112116
) -> Result<Self, DeterministicHostError> {
113-
let digits: BigInt = asc_get(heap, big_decimal.digits, gas)?;
114-
let exp: BigInt = asc_get(heap, big_decimal.exp, gas)?;
117+
let digits: BigInt = asc_get(heap, big_decimal.digits, gas, depth)?;
118+
let exp: BigInt = asc_get(heap, big_decimal.exp, gas, depth)?;
115119

116120
let bytes = exp.to_signed_bytes_le();
117121
let mut byte_array = if exp >= 0.into() { [0; 8] } else { [255; 8] };
@@ -188,6 +192,7 @@ impl FromAscObj<AscEnum<EthereumValueKind>> for ethabi::Token {
188192
asc_enum: AscEnum<EthereumValueKind>,
189193
heap: &H,
190194
gas: &GasCounter,
195+
depth: usize,
191196
) -> Result<Self, DeterministicHostError> {
192197
use ethabi::Token;
193198

@@ -196,41 +201,41 @@ impl FromAscObj<AscEnum<EthereumValueKind>> for ethabi::Token {
196201
EthereumValueKind::Bool => Token::Bool(bool::from(payload)),
197202
EthereumValueKind::Address => {
198203
let ptr: AscPtr<AscAddress> = AscPtr::from(payload);
199-
Token::Address(asc_get(heap, ptr, gas)?)
204+
Token::Address(asc_get(heap, ptr, gas, depth)?)
200205
}
201206
EthereumValueKind::FixedBytes => {
202207
let ptr: AscPtr<Uint8Array> = AscPtr::from(payload);
203-
Token::FixedBytes(asc_get(heap, ptr, gas)?)
208+
Token::FixedBytes(asc_get(heap, ptr, gas, depth)?)
204209
}
205210
EthereumValueKind::Bytes => {
206211
let ptr: AscPtr<Uint8Array> = AscPtr::from(payload);
207-
Token::Bytes(asc_get(heap, ptr, gas)?)
212+
Token::Bytes(asc_get(heap, ptr, gas, depth)?)
208213
}
209214
EthereumValueKind::Int => {
210215
let ptr: AscPtr<AscBigInt> = AscPtr::from(payload);
211-
let n: BigInt = asc_get(heap, ptr, gas)?;
216+
let n: BigInt = asc_get(heap, ptr, gas, depth)?;
212217
Token::Int(n.to_signed_u256())
213218
}
214219
EthereumValueKind::Uint => {
215220
let ptr: AscPtr<AscBigInt> = AscPtr::from(payload);
216-
let n: BigInt = asc_get(heap, ptr, gas)?;
221+
let n: BigInt = asc_get(heap, ptr, gas, depth)?;
217222
Token::Uint(n.to_unsigned_u256())
218223
}
219224
EthereumValueKind::String => {
220225
let ptr: AscPtr<AscString> = AscPtr::from(payload);
221-
Token::String(asc_get(heap, ptr, gas)?)
226+
Token::String(asc_get(heap, ptr, gas, depth)?)
222227
}
223228
EthereumValueKind::FixedArray => {
224229
let ptr: AscEnumArray<EthereumValueKind> = AscPtr::from(payload);
225-
Token::FixedArray(asc_get(heap, ptr, gas)?)
230+
Token::FixedArray(asc_get(heap, ptr, gas, depth)?)
226231
}
227232
EthereumValueKind::Array => {
228233
let ptr: AscEnumArray<EthereumValueKind> = AscPtr::from(payload);
229-
Token::Array(asc_get(heap, ptr, gas)?)
234+
Token::Array(asc_get(heap, ptr, gas, depth)?)
230235
}
231236
EthereumValueKind::Tuple => {
232237
let ptr: AscEnumArray<EthereumValueKind> = AscPtr::from(payload);
233-
Token::Tuple(asc_get(heap, ptr, gas)?)
238+
Token::Tuple(asc_get(heap, ptr, gas, depth)?)
234239
}
235240
})
236241
}
@@ -241,34 +246,35 @@ impl FromAscObj<AscEnum<StoreValueKind>> for store::Value {
241246
asc_enum: AscEnum<StoreValueKind>,
242247
heap: &H,
243248
gas: &GasCounter,
249+
depth: usize,
244250
) -> Result<Self, DeterministicHostError> {
245251
use self::store::Value;
246252

247253
let payload = asc_enum.payload;
248254
Ok(match asc_enum.kind {
249255
StoreValueKind::String => {
250256
let ptr: AscPtr<AscString> = AscPtr::from(payload);
251-
Value::String(asc_get(heap, ptr, gas)?)
257+
Value::String(asc_get(heap, ptr, gas, depth)?)
252258
}
253259
StoreValueKind::Int => Value::Int(i32::from(payload)),
254260
StoreValueKind::BigDecimal => {
255261
let ptr: AscPtr<AscBigDecimal> = AscPtr::from(payload);
256-
Value::BigDecimal(asc_get(heap, ptr, gas)?)
262+
Value::BigDecimal(asc_get(heap, ptr, gas, depth)?)
257263
}
258264
StoreValueKind::Bool => Value::Bool(bool::from(payload)),
259265
StoreValueKind::Array => {
260266
let ptr: AscEnumArray<StoreValueKind> = AscPtr::from(payload);
261-
Value::List(asc_get(heap, ptr, gas)?)
267+
Value::List(asc_get(heap, ptr, gas, depth)?)
262268
}
263269
StoreValueKind::Null => Value::Null,
264270
StoreValueKind::Bytes => {
265271
let ptr: AscPtr<Uint8Array> = AscPtr::from(payload);
266-
let array: Vec<u8> = asc_get(heap, ptr, gas)?;
272+
let array: Vec<u8> = asc_get(heap, ptr, gas, depth)?;
267273
Value::Bytes(array.as_slice().into())
268274
}
269275
StoreValueKind::BigInt => {
270276
let ptr: AscPtr<AscBigInt> = AscPtr::from(payload);
271-
let array: Vec<u8> = asc_get(heap, ptr, gas)?;
277+
let array: Vec<u8> = asc_get(heap, ptr, gas, depth)?;
272278
Value::BigInt(store::scalar::BigInt::from_signed_bytes_le(&array))
273279
}
274280
})

runtime/wasm/src/to_from/mod.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ impl<T: AscValue> FromAscObj<TypedArray<T>> for Vec<T> {
3232
typed_array: TypedArray<T>,
3333
heap: &H,
3434
gas: &GasCounter,
35+
_depth: usize,
3536
) -> Result<Self, DeterministicHostError> {
3637
typed_array.to_vec(heap, gas)
3738
}
@@ -42,6 +43,7 @@ impl<T: AscValue + Send + Sync, const LEN: usize> FromAscObj<TypedArray<T>> for
4243
typed_array: TypedArray<T>,
4344
heap: &H,
4445
gas: &GasCounter,
46+
_depth: usize,
4547
) -> Result<Self, DeterministicHostError> {
4648
let v = typed_array.to_vec(heap, gas)?;
4749
let array = <[T; LEN]>::try_from(v)
@@ -88,6 +90,7 @@ impl FromAscObj<AscString> for String {
8890
asc_string: AscString,
8991
_: &H,
9092
_gas: &GasCounter,
93+
_depth: usize,
9194
) -> Result<Self, DeterministicHostError> {
9295
let mut string = String::from_utf16(asc_string.content())
9396
.map_err(|e| DeterministicHostError::from(anyhow::Error::from(e)))?;
@@ -105,8 +108,9 @@ impl FromAscObj<AscString> for Word {
105108
asc_string: AscString,
106109
heap: &H,
107110
gas: &GasCounter,
111+
depth: usize,
108112
) -> Result<Self, DeterministicHostError> {
109-
let string = String::from_asc_obj(asc_string, heap, gas)?;
113+
let string = String::from_asc_obj(asc_string, heap, gas, depth)?;
110114

111115
Ok(Word::from(string))
112116
}
@@ -129,11 +133,12 @@ impl<C: AscType + AscIndexId, T: FromAscObj<C>> FromAscObj<Array<AscPtr<C>>> for
129133
array: Array<AscPtr<C>>,
130134
heap: &H,
131135
gas: &GasCounter,
136+
depth: usize,
132137
) -> Result<Self, DeterministicHostError> {
133138
array
134139
.to_vec(heap, gas)?
135140
.into_iter()
136-
.map(|x| asc_get(heap, x, gas))
141+
.map(|x| asc_get(heap, x, gas, depth))
137142
.collect()
138143
}
139144
}
@@ -145,10 +150,11 @@ impl<K: AscType + AscIndexId, V: AscType + AscIndexId, T: FromAscObj<K>, U: From
145150
asc_entry: AscTypedMapEntry<K, V>,
146151
heap: &H,
147152
gas: &GasCounter,
153+
depth: usize,
148154
) -> Result<Self, DeterministicHostError> {
149155
Ok((
150-
asc_get(heap, asc_entry.key, gas)?,
151-
asc_get(heap, asc_entry.value, gas)?,
156+
asc_get(heap, asc_entry.key, gas, depth)?,
157+
asc_get(heap, asc_entry.value, gas, depth)?,
152158
))
153159
}
154160
}
@@ -182,8 +188,9 @@ where
182188
asc_map: AscTypedMap<K, V>,
183189
heap: &H,
184190
gas: &GasCounter,
191+
depth: usize,
185192
) -> Result<Self, DeterministicHostError> {
186-
let entries: Vec<(T, U)> = asc_get(heap, asc_map.entries, gas)?;
193+
let entries: Vec<(T, U)> = asc_get(heap, asc_map.entries, gas, depth)?;
187194
Ok(HashMap::from_iter(entries.into_iter()))
188195
}
189196
}

0 commit comments

Comments
 (0)