Skip to content

Commit 744bc2f

Browse files
authored
Fix stack overflow when decoding deeply nested structures like arrays and maps (#51)
1 parent 7bf9fb9 commit 744bc2f

7 files changed

+62
-15
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
�`������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������e$typerapp.bsky.feed.psot
File renamed without changes.

data/torture_nested_lists.dagcbor

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

data/torture_nested_maps.dagcbor

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

pytests/conftest.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,19 @@ def load_cbor_data_fixtures(dir_path: str) -> List[Tuple[str, Any]]:
3535
return fixtures
3636

3737

38-
def load_car_fixture(did: str, path: str) -> bytes:
38+
def load_car_fixture(_: str, path: str) -> bytes:
3939
if os.path.exists(path):
4040
with open(path, 'rb') as f:
4141
return f.read()
4242

43-
contents = urllib.request.urlopen(f'https://bsky.network/xrpc/com.atproto.sync.getRepo?did={did}').read()
43+
url = 'https://github.com/MarshalX/python-libipld/releases/download/v1.0.0/test_huge_repo.car'
44+
45+
# Bsky team disabled the endpoint below.
46+
# We could not rely on it anymore.
47+
# Request forbidden by administrative rules (403 Forbidden).
48+
# url = f'https://bsky.network/xrpc/com.atproto.sync.getRepo?did={did}'
49+
50+
contents = urllib.request.urlopen(url).read()
4451
with open(path, 'wb') as f:
4552
f.write(contents)
4653

pytests/test_dag_cbor.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55

66
from conftest import load_cbor_data_fixtures, load_json_data_fixtures
77

8-
_ROUNDTRIP_DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data', 'roundtrip')
98
_REAL_DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data')
10-
_FIXTURES_DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data', 'fixtures')
11-
_CIDS_DAG_CBOR_PATH = os.path.join(os.path.dirname(__file__), '..', 'data', 'torture_cids.dag-cbor')
9+
_ROUNDTRIP_DATA_DIR = os.path.join(_REAL_DATA_DIR, 'roundtrip')
10+
_FIXTURES_DATA_DIR = os.path.join(_REAL_DATA_DIR, 'fixtures')
11+
_TORTURE_CIDS_DAG_CBOR_PATH = os.path.join(_REAL_DATA_DIR, 'torture_cids.dagcbor')
12+
_TORTURE_NESTED_LISTS_DAG_CBOR_PATH = os.path.join(_REAL_DATA_DIR, 'torture_nested_lists.dagcbor')
13+
_TORTURE_NESTED_MAPS_DAG_CBOR_PATH = os.path.join(_REAL_DATA_DIR, 'torture_nested_maps.dagcbor')
1214

1315

1416
def _dag_cbor_encode(benchmark, data) -> None:
@@ -115,5 +117,21 @@ def test_dag_cbor_decode_fixtures(benchmark, data) -> None:
115117

116118

117119
def test_dag_cbor_decode_torture_cids(benchmark) -> None:
118-
dag_cbor = open(_CIDS_DAG_CBOR_PATH, 'rb').read()
120+
dag_cbor = open(_TORTURE_CIDS_DAG_CBOR_PATH, 'rb').read()
119121
benchmark(libipld.decode_dag_cbor, dag_cbor)
122+
123+
124+
def test_recursion_limit_exceed_on_nested_lists() -> None:
125+
dag_cbor = open(_TORTURE_NESTED_LISTS_DAG_CBOR_PATH, 'rb').read()
126+
with pytest.raises(RecursionError) as exc_info:
127+
libipld.decode_dag_cbor(dag_cbor)
128+
129+
assert 'in DAG-CBOR decoding' in str(exc_info.value)
130+
131+
132+
def test_recursion_limit_exceed_on_nested_maps() -> None:
133+
dag_cbor = open(_TORTURE_NESTED_MAPS_DAG_CBOR_PATH, 'rb').read()
134+
with pytest.raises(RecursionError) as exc_info:
135+
libipld.decode_dag_cbor(dag_cbor)
136+
137+
assert 'in DAG-CBOR decoding' in str(exc_info.value)

src/lib.rs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,24 @@ fn string_new_bound<'py>(py: Python<'py>, s: &[u8]) -> Bound<'py, PyString> {
105105
}
106106
}
107107

108-
109108
fn decode_dag_cbor_to_pyobject<R: Read + Seek>(
110109
py: Python,
111110
r: &mut R,
112-
deep: usize,
111+
depth: usize,
113112
) -> Result<PyObject> {
113+
unsafe {
114+
if depth > ffi::Py_GetRecursionLimit() as usize {
115+
PyErr::new::<pyo3::exceptions::PyRecursionError, _>(
116+
"RecursionError: maximum recursion depth exceeded in DAG-CBOR decoding",
117+
).restore(py);
118+
119+
return Err(anyhow!("Maximum recursion depth exceeded"));
120+
}
121+
}
122+
114123
let major = decode::read_major(r)?;
115124
Ok(match major.kind() {
116-
MajorKind::UnsignedInt => (decode::read_uint(r, major)?).to_object(py),
125+
MajorKind::UnsignedInt => decode::read_uint(r, major)?.to_object(py),
117126
MajorKind::NegativeInt => (-1 - decode::read_uint(r, major)? as i64).to_object(py),
118127
MajorKind::ByteString => {
119128
let len = decode::read_uint(r, major)?;
@@ -130,7 +139,7 @@ fn decode_dag_cbor_to_pyobject<R: Read + Seek>(
130139
let ptr = ffi::PyList_New(len);
131140

132141
for i in 0..len {
133-
ffi::PyList_SET_ITEM(ptr, i, decode_dag_cbor_to_pyobject(py, r, deep + 1)?.into_ptr());
142+
ffi::PyList_SET_ITEM(ptr, i, decode_dag_cbor_to_pyobject(py, r, depth + 1)?.into_ptr());
134143
}
135144

136145
let list: Bound<'_, PyList> = Bound::from_owned_ptr(py, ptr).downcast_into_unchecked();
@@ -162,8 +171,8 @@ fn decode_dag_cbor_to_pyobject<R: Read + Seek>(
162171
let key_py = string_new_bound(py, key.as_slice()).to_object(py);
163172
prev_key = Some(key);
164173

165-
let value = decode_dag_cbor_to_pyobject(py, r, deep + 1)?;
166-
dict.set_item(key_py, value).unwrap();
174+
let value_py = decode_dag_cbor_to_pyobject(py, r, depth + 1)?;
175+
dict.set_item(key_py, value_py)?;
167176
}
168177

169178
dict.to_object(py)
@@ -184,7 +193,7 @@ fn decode_dag_cbor_to_pyobject<R: Read + Seek>(
184193
cbor::NULL => py.None(),
185194
cbor::F32 => decode::read_f32(r)?.to_object(py),
186195
cbor::F64 => decode::read_f64(r)?.to_object(py),
187-
_ => return Err(anyhow!(format!("Unsupported major type"))),
196+
_ => return Err(anyhow!("Unsupported major type".to_string())),
188197
},
189198
})
190199
}
@@ -456,10 +465,20 @@ pub fn decode_dag_cbor(py: Python, data: &[u8]) -> PyResult<PyObject> {
456465
if let Ok(py_object) = py_object {
457466
Ok(py_object)
458467
} else {
459-
Err(get_err(
468+
let err = get_err(
460469
"Failed to decode DAG-CBOR",
461470
py_object.unwrap_err().to_string(),
462-
))
471+
);
472+
473+
if let Some(py_err) = PyErr::take(py) {
474+
py_err.set_cause(py, Option::from(err));
475+
// in case something set global interpreter’s error,
476+
// for example C FFI function, we should return it
477+
// the real case: RecursionError (set by Py_EnterRecursiveCall)
478+
Err(py_err)
479+
} else {
480+
Err(err)
481+
}
463482
}
464483
}
465484

0 commit comments

Comments
 (0)