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<Fmt>(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<Formatter, T&> 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).
This commit is contained in:
Martin Vogel committed 2026-06-10 12:11:02 +02:00
1 parent 9f981a1612
commit bb7341dc07
3 files changed
+54 -5

No files matched your search

+14 -4
View File
@@ -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);
+14 -1
View File
@@ -2,6 +2,10 @@
#include <stdint.h>
#include <string.h>
/* 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++;
}
+26
View File
@@ -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<Fmt>(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<F, T&> */
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);
}