From bb7341dc076f1891be3c2b3614481f0282858c68 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Wed, 10 Jun 2026 12:11:02 +0200 Subject: [PATCH] Fix uninitialized template-arg arrays binding stack garbage into type graphs The C/C++ LSP's explicit-template-argument call path (template_function, e.g. Using(v)) filled a stack array const CBMType *targs[16] without NULL-terminating it before cbm_type_substitute. With more declared type params than explicit args (bitcoin src/serialize.h: Using's Wrapper with 2 params, 1 arg), the bounded args walk read uninitialized stack; any non-NULL garbage got bound to the type param and woven into the REGISTERED return-type graph, then dereferenced at a later call site - SIGSEGV (crash shape: string bytes misread as a CBMType*). All c_lsp targs arrays are now zero-filled, and cbm_type_substitute's args walk treats implausible values (misaligned / below the first page) as the terminator so contract violations can never leak garbage into a type graph. Reproduce-first: deterministic poison-args unit test (typerep_substitute_rejects_garbage_args_entries) + real-repo verification (bitcoin indexes clean under ASan; was a deterministic SIGSEGV). --- internal/cbm/lsp/c_lsp.c | 18 ++++++++++++++---- internal/cbm/lsp/type_rep.c | 15 ++++++++++++++- tests/test_type_rep.c | 26 ++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/internal/cbm/lsp/c_lsp.c b/internal/cbm/lsp/c_lsp.c index fda90c585..6e58e373d 100644 --- a/internal/cbm/lsp/c_lsp.c +++ b/internal/cbm/lsp/c_lsp.c @@ -985,7 +985,10 @@ const CBMType *c_parse_type_node(CLSPContext *ctx, TSNode node) { } // Parse template arguments - const CBMType *targs[16]; + const CBMType *targs[16] = { + NULL}; /* zero-fill: cbm_type_substitute requires NULL-terminated args + (uninitialized tail bound T to stack garbage -> corrupt type graph, + bitcoin serialize.h) */ int targ_count = 0; if (!ts_node_is_null(tmpl_args)) { uint32_t nc = ts_node_named_child_count(tmpl_args); @@ -1087,7 +1090,9 @@ const CBMType *c_parse_type_node(CLSPContext *ctx, TSNode node) { } // Parse template arguments - const CBMType *targs[16]; + const CBMType *targs[16] = { + NULL}; /* zero-fill: cbm_type_substitute requires NULL-terminated args (uninitialized + tail bound T to stack garbage -> corrupt type graph, bitcoin serialize.h) */ int targ_count = 0; if (!ts_node_is_null(args_node)) { uint32_t nc = ts_node_named_child_count(args_node); @@ -1585,7 +1590,10 @@ static const CBMType *c_eval_expr_type_inner(CLSPContext *ctx, TSNode node) { f->signature->kind == CBM_TYPE_FUNC && f->signature->data.func.return_types && f->signature->data.func.return_types[0]) { const CBMType *base_ret = f->signature->data.func.return_types[0]; - const CBMType *targs[16]; + const CBMType *targs[16] = { + NULL}; /* zero-fill: cbm_type_substitute requires NULL-terminated args + (uninitialized tail bound T to stack garbage -> corrupt type graph, + bitcoin serialize.h) */ int targ_count = 0; uint32_t tnc = ts_node_named_child_count(qi_tmpl_args); for (uint32_t ti = 0; ti < tnc && targ_count < 15; ti++) { @@ -1742,7 +1750,9 @@ static const CBMType *c_eval_expr_type_inner(CLSPContext *ctx, TSNode node) { return f->signature; // Parse explicit template arguments from <...> - const CBMType *targs[16]; + const CBMType *targs[16] = { + NULL}; /* zero-fill: cbm_type_substitute requires NULL-terminated args (uninitialized + tail bound T to stack garbage -> corrupt type graph, bitcoin serialize.h) */ int targ_count = 0; if (!ts_node_is_null(args_node)) { uint32_t nc = ts_node_named_child_count(args_node); diff --git a/internal/cbm/lsp/type_rep.c b/internal/cbm/lsp/type_rep.c index 1317e5369..edb90c50e 100644 --- a/internal/cbm/lsp/type_rep.c +++ b/internal/cbm/lsp/type_rep.c @@ -2,6 +2,10 @@ #include #include +/* No real allocation lives in the first page; values below this are garbage + * (e.g. small integers or truncated string bytes misread as pointers). */ +enum { TR_MIN_PLAUSIBLE_PTR = 4096 }; + // Singleton UNKNOWN type (no allocation needed). static const CBMType unknown_singleton = {.kind = CBM_TYPE_UNKNOWN}; @@ -769,8 +773,17 @@ const CBMType *cbm_type_substitute(CBMArena *a, const CBMType *t, const char **t while (type_params[nparams]) { nparams++; } + /* Contract: type_args must be NULL-terminated (it may be shorter than + * type_params). A misaligned or null-page value can never be a real + * CBMType* — it means a caller passed an unterminated array and the walk + * is reading uninitialized memory (seen on bitcoin's serialize.h: an + * explicit-template-arg call bound T to stack garbage that was woven into + * the registered type graph and dereferenced later -> SIGSEGV). Treat such + * values as the terminator so garbage can never enter a type graph. */ int args_len = 0; - while (args_len < nparams && type_args[args_len]) { + while (args_len < nparams && type_args[args_len] && + ((uintptr_t)type_args[args_len] & (sizeof(void *) - 1)) == 0 && + (uintptr_t)type_args[args_len] >= TR_MIN_PLAUSIBLE_PTR) { args_len++; } diff --git a/tests/test_type_rep.c b/tests/test_type_rep.c index 0bc118663..ead709d3f 100644 --- a/tests/test_type_rep.c +++ b/tests/test_type_rep.c @@ -266,6 +266,31 @@ TEST(typerep_substitute_short_args_no_oob_issue427) { PASS(); } +/* A caller that forgets to NULL-terminate type_args (a stack array) makes the + * bounded walk read uninitialized memory — and a garbage "pointer" within the + * param count would be BOUND to a type param and woven into the resulting type + * graph (bitcoin serialize.h: Using(v) with explicit args bound T to stack + * garbage; the corrupt graph was dereferenced later -> SIGSEGV). Implausible + * values (misaligned / null-page) must act as the terminator instead. */ +TEST(typerep_substitute_rejects_garbage_args_entries) { + CBMArena a; + cbm_arena_init(&a); + const CBMType *t = + cbm_type_reference(&a, cbm_type_type_param(&a, "T")); /* T& as in Wrapper */ + const char *params[] = {"F", "T", NULL}; + const CBMType *args[2]; + args[0] = cbm_type_named(&a, "proj.Fmt"); /* explicit arg for F */ + args[1] = (const CBMType *)0x37; /* simulated uninitialized stack garbage */ + const CBMType *sub = cbm_type_substitute(&a, t, params, args); + ASSERT_NOT_NULL(sub); + ASSERT_EQ(sub->kind, CBM_TYPE_REFERENCE); + /* T has no real binding: it must be preserved, never the garbage value. */ + ASSERT_TRUE(sub->data.reference.elem != (const CBMType *)0x37); + ASSERT_EQ(sub->data.reference.elem->kind, CBM_TYPE_TYPE_PARAM); + cbm_arena_destroy(&a); + PASS(); +} + /* ── Suite ─────────────────────────────────────────────────────── */ SUITE(type_rep) { @@ -293,4 +318,5 @@ SUITE(type_rep) { /* SUBSTITUTION */ RUN_TEST(typerep_substitute_unbound_param_preserved); RUN_TEST(typerep_substitute_short_args_no_oob_issue427); + RUN_TEST(typerep_substitute_rejects_garbage_args_entries); }