From ba931ce7ab65d76b13e38ff424e2717f312becb8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Mar 2026 01:56:29 +0000 Subject: [PATCH 1/3] fix(python): add depth limit to py_to_json/json_to_py conversion Both functions now fail with a clear error beyond 64 nesting levels, preventing stack overflow from deeply nested Python/JSON structures. Closes #427 --- crates/bashkit-python/src/lib.rs | 37 ++++++++++++++++++--- crates/bashkit-python/tests/test_bashkit.py | 12 +++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/crates/bashkit-python/src/lib.rs b/crates/bashkit-python/src/lib.rs index 1d139d56..e060d468 100644 --- a/crates/bashkit-python/src/lib.rs +++ b/crates/bashkit-python/src/lib.rs @@ -21,7 +21,22 @@ use tokio::sync::Mutex; // ============================================================================ /// Convert serde_json::Value → Py +const MAX_NESTING_DEPTH: usize = 64; + fn json_to_py(py: Python<'_>, val: &serde_json::Value) -> PyResult> { + json_to_py_inner(py, val, 0) +} + +fn json_to_py_inner( + py: Python<'_>, + val: &serde_json::Value, + depth: usize, +) -> PyResult> { + if depth > MAX_NESTING_DEPTH { + return Err(pyo3::exceptions::PyValueError::new_err( + "JSON nesting depth exceeds maximum of 64", + )); + } match val { serde_json::Value::Null => Ok(py.None()), serde_json::Value::Bool(b) => Ok(b.into_pyobject(py)?.to_owned().into_any().unbind()), @@ -38,14 +53,14 @@ fn json_to_py(py: Python<'_>, val: &serde_json::Value) -> PyResult> { serde_json::Value::Array(arr) => { let items: Vec> = arr .iter() - .map(|v| json_to_py(py, v)) + .map(|v| json_to_py_inner(py, v, depth + 1)) .collect::>()?; Ok(PyList::new(py, &items)?.into_any().unbind()) } serde_json::Value::Object(map) => { let dict = PyDict::new(py); for (k, v) in map { - dict.set_item(k, json_to_py(py, v)?)?; + dict.set_item(k, json_to_py_inner(py, v, depth + 1)?)?; } Ok(dict.into_any().unbind()) } @@ -53,8 +68,20 @@ fn json_to_py(py: Python<'_>, val: &serde_json::Value) -> PyResult> { } /// Convert Py → serde_json::Value (for schema dicts) -#[allow(clippy::only_used_in_recursion)] fn py_to_json(py: Python<'_>, obj: &Bound<'_, pyo3::PyAny>) -> PyResult { + py_to_json_inner(py, obj, 0) +} + +fn py_to_json_inner( + py: Python<'_>, + obj: &Bound<'_, pyo3::PyAny>, + depth: usize, +) -> PyResult { + if depth > MAX_NESTING_DEPTH { + return Err(pyo3::exceptions::PyValueError::new_err( + "Python object nesting depth exceeds maximum of 64", + )); + } if obj.is_none() { return Ok(serde_json::Value::Null); } @@ -73,7 +100,7 @@ fn py_to_json(py: Python<'_>, obj: &Bound<'_, pyo3::PyAny>) -> PyResult() { let arr: Vec = list .iter() - .map(|item| py_to_json(py, &item)) + .map(|item| py_to_json_inner(py, &item, depth + 1)) .collect::>()?; return Ok(serde_json::Value::Array(arr)); } @@ -81,7 +108,7 @@ fn py_to_json(py: Python<'_>, obj: &Bound<'_, pyo3::PyAny>) -> PyResult Date: Mon, 2 Mar 2026 02:20:18 +0000 Subject: [PATCH 2/3] fix(python): allow clippy only_used_in_recursion for py parameter --- crates/bashkit-python/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bashkit-python/src/lib.rs b/crates/bashkit-python/src/lib.rs index e060d468..3c240c69 100644 --- a/crates/bashkit-python/src/lib.rs +++ b/crates/bashkit-python/src/lib.rs @@ -72,6 +72,7 @@ fn py_to_json(py: Python<'_>, obj: &Bound<'_, pyo3::PyAny>) -> PyResult, obj: &Bound<'_, pyo3::PyAny>, From d4c5918e80bec44c5dd21462a6e0b7f72b5fc07a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Mar 2026 02:25:58 +0000 Subject: [PATCH 3/3] style(python): fix formatting --- crates/bashkit-python/src/lib.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/bashkit-python/src/lib.rs b/crates/bashkit-python/src/lib.rs index 3c240c69..83472770 100644 --- a/crates/bashkit-python/src/lib.rs +++ b/crates/bashkit-python/src/lib.rs @@ -27,11 +27,7 @@ fn json_to_py(py: Python<'_>, val: &serde_json::Value) -> PyResult> { json_to_py_inner(py, val, 0) } -fn json_to_py_inner( - py: Python<'_>, - val: &serde_json::Value, - depth: usize, -) -> PyResult> { +fn json_to_py_inner(py: Python<'_>, val: &serde_json::Value, depth: usize) -> PyResult> { if depth > MAX_NESTING_DEPTH { return Err(pyo3::exceptions::PyValueError::new_err( "JSON nesting depth exceeds maximum of 64",