From dbf25909b11d1be18add36fd97b1bee95c581e01 Mon Sep 17 00:00:00 2001 From: Dustin Persek Date: Wed, 1 Jul 2026 22:57:50 -0400 Subject: [PATCH] fix(py-lsp): cap expression evaluation depth Signed-off-by: Dustin Persek --- internal/cbm/lsp/py_lsp.c | 15 +++++++++++++++ internal/cbm/lsp/py_lsp.h | 3 +++ tests/test_stack_overflow.c | 22 ++++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/internal/cbm/lsp/py_lsp.c b/internal/cbm/lsp/py_lsp.c index 83725efad..16bba7fc0 100644 --- a/internal/cbm/lsp/py_lsp.c +++ b/internal/cbm/lsp/py_lsp.c @@ -23,9 +23,12 @@ * only from py_lsp.c, never compiled standalone. */ #include "py_builtins.c" +#define PY_LSP_MAX_EVAL_DEPTH 64 + // Forward decls static void py_resolve_calls_in(PyLSPContext *ctx, TSNode node); static const CBMType *py_eval_expr_type(PyLSPContext *ctx, TSNode node); +static const CBMType *py_eval_expr_type_inner(PyLSPContext *ctx, TSNode node); static void py_process_statement(PyLSPContext *ctx, TSNode node); static const CBMRegisteredFunc *py_lookup_attribute(PyLSPContext *ctx, const char *type_qn, const char *member_name); @@ -693,6 +696,18 @@ static const CBMType *py_iterable_element_type(PyLSPContext *ctx, const CBMType } static const CBMType *py_eval_expr_type(PyLSPContext *ctx, TSNode node) { + if (!ctx || ts_node_is_null(node)) + return cbm_type_unknown(); + if (ctx->eval_depth >= PY_LSP_MAX_EVAL_DEPTH) + return cbm_type_unknown(); + + ctx->eval_depth++; + const CBMType *result = py_eval_expr_type_inner(ctx, node); + ctx->eval_depth--; + return result ? result : cbm_type_unknown(); +} + +static const CBMType *py_eval_expr_type_inner(PyLSPContext *ctx, TSNode node) { if (!ctx || ts_node_is_null(node)) return cbm_type_unknown(); diff --git a/internal/cbm/lsp/py_lsp.h b/internal/cbm/lsp/py_lsp.h index 57553fd3b..0893956d9 100644 --- a/internal/cbm/lsp/py_lsp.h +++ b/internal/cbm/lsp/py_lsp.h @@ -69,6 +69,9 @@ typedef struct { int dict_literal_count; int dict_literal_cap; + // Expression evaluator recursion depth guard. + int eval_depth; + // Debug mode (CBM_LSP_DEBUG env, shared across all language LSPs). bool debug; } PyLSPContext; diff --git a/tests/test_stack_overflow.c b/tests/test_stack_overflow.c index b8f0680ee..c5afc21b3 100644 --- a/tests/test_stack_overflow.c +++ b/tests/test_stack_overflow.c @@ -487,6 +487,27 @@ TEST(lsp_cpp_deep_expression_no_crash) { PASS(); } +TEST(lsp_python_deep_expression_no_crash) { + /* Deep parenthesized expressions force repeated py_eval_expr_type + * recursion through assignment RHS inference. The evaluator should hit + * its depth cap and fall back to unknown instead of overflowing. */ + const int DEPTH = 256; + size_t sz = (size_t)DEPTH * 2 + 256; + char *src = malloc(sz); + ASSERT_NOT_NULL(src); + char *p = src; + p += snprintf(p, sz, "def main():\n value = "); + memset(p, '(', DEPTH); + p += DEPTH; + *p++ = '1'; + memset(p, ')', DEPTH); + p += DEPTH; + snprintf(p, sz - (size_t)(p - src), "\n return value\n"); + ASSERT_FALSE(so_extract_crashes(src, CBM_LANG_PYTHON, "deep_expr.py")); + free(src); + PASS(); +} + TEST(lsp_java_lambda_args_exceed_params_no_crash) { /* A call with MORE arguments than the resolved method's declared params: * bind_lambda_args indexed the NULL-terminated signature param_types array @@ -528,6 +549,7 @@ SUITE(stack_overflow) { RUN_TEST(lsp_java_deep_nesting_no_crash); RUN_TEST(lsp_java_lambda_args_exceed_params_no_crash); RUN_TEST(lsp_cpp_deep_expression_no_crash); + RUN_TEST(lsp_python_deep_expression_no_crash); RUN_TEST(lsp_ts_cyclic_types_no_crash); RUN_TEST(js_calls_exceed_512);