Skip to content

Commit 6226972

Browse files
committed
fix(lsp): guard trailing append in TS type-text union/intersection parser
parse_ts_type_text in ts_lsp.c builds a 16-slot CBMType* members[] for union (|) and intersection (&) types. The in-loop branch correctly caps additions at mc<15, but the trailing post-loop append that collects the final member after the last separator was unconditional, and the subsequent members[mc] = NULL sentinel could then write to members[16] — one past the array end. Caught by UBSan on the zod codebase (a TS union with >=16 members exists). Non-deterministic crash signature under pthread-parallel extraction (12 workers): exit 134 / SIGABRT with no stack, because libc's allocator detected the heap corruption and called abort(). Single-fixture-file test suite never exercised the parallel path or a wide-enough union, so existing ASan/UBSan coverage missed it. Fix: mirror the in-loop guard on the trailing append (cap at mc<15). Add regression test tslsp_union_many_members_no_overflow that constructs a 20-member string-literal union — extracting must not crash. UBSan: clean. Full suite: 3616 passed, 0 failed.
1 parent d12e889 commit 6226972

2 files changed

Lines changed: 25 additions & 2 deletions

File tree

internal/cbm/lsp/ts_lsp.c

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,8 +315,14 @@ static const CBMType* parse_ts_type_text(CBMArena* arena, const char* text, cons
315315
}
316316
}
317317
if (mc > 0 && (is_union || is_inter)) {
318-
char* part = cbm_arena_strndup(arena, text + start, len - start);
319-
members[mc++] = parse_ts_type_text(arena, part, module_qn);
318+
// Cap mirrors the in-loop guard: members[16] holds at most 15
319+
// real entries + the trailing NULL sentinel. Without this guard,
320+
// a union/intersection with >=16 members overflows the array
321+
// (UBSan: index 16 out of bounds for 'const CBMType *[16]').
322+
if (mc < 15) {
323+
char* part = cbm_arena_strndup(arena, text + start, len - start);
324+
members[mc++] = parse_ts_type_text(arena, part, module_qn);
325+
}
320326
members[mc] = NULL;
321327
return is_union ? cbm_type_union(arena, members, mc)
322328
: cbm_type_intersection(arena, members, mc);

tests/test_ts_lsp.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,22 @@ TEST(tslsp_union_common_method) {
800800
PASS();
801801
}
802802

803+
TEST(tslsp_union_many_members_no_overflow) {
804+
// Regression: a union with >=16 members used to overflow the
805+
// 16-slot members[] array in parse_ts_type_text() — the trailing
806+
// post-loop append wrote past the end (UBSan: index 16 out of
807+
// bounds). The crash was non-deterministic under pthread parallel
808+
// extraction, surfaced by indexing the zod TS codebase. Just
809+
// extracting must not crash; resolution behavior is unconstrained.
810+
CBMFileResult *r = extract_ts(
811+
"function go(x: 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' "
812+
"| 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' "
813+
"| 's' | 't') { return x; }\n");
814+
ASSERT_NOT_NULL(r);
815+
cbm_free_result(r);
816+
PASS();
817+
}
818+
803819
/* ── Category 9: more class patterns ───────────────────────────────────────── */
804820

805821
TEST(tslsp_class_static_method) {
@@ -4017,6 +4033,7 @@ SUITE(ts_lsp) {
40174033

40184034
/* Category 16: union deeper */
40194035
RUN_TEST(tslsp_union_common_method);
4036+
RUN_TEST(tslsp_union_many_members_no_overflow);
40204037

40214038
/* Category 9: more class patterns */
40224039
RUN_TEST(tslsp_class_static_method);

0 commit comments

Comments
 (0)