diff --git a/internal/cbm/lsp/py_lsp.c b/internal/cbm/lsp/py_lsp.c index 5af66cf3..75ecb1f0 100644 --- a/internal/cbm/lsp/py_lsp.c +++ b/internal/cbm/lsp/py_lsp.c @@ -23,6 +23,8 @@ * 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_inner(PyLSPContext *ctx, TSNode node); @@ -41,6 +43,7 @@ static void py_resolve_calls_in(PyLSPContext *ctx, TSNode node) { ctx->walk_depth--; } 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); @@ -708,6 +711,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 2efbab60..0a0da907 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; + // AST-walk recursion depth for py_resolve_calls_in (guards stack overflow on // deeply-nested/cyclic files; see cbm_lsp_max_walk_depth). Zero via memset. int walk_depth; diff --git a/tests/test_stack_overflow.c b/tests/test_stack_overflow.c index 31fdf5a8..afed46ed 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 @@ -623,6 +644,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(lsp_python_deep_nesting_no_crash); RUN_TEST(lsp_go_deep_nesting_no_crash);