Skip to content

Commit 2154edf

Browse files
Mubarizclaude
andcommitted
fix(lsp/php): prevent OOM when a trait uses itself
flatten_trait_into_class() iterated reg->funcs with a live upper bound (reg->func_count) while cbm_registry_add_func() appended to that same array inside the loop. Each copied method is tagged with receiver_type = class_qn, so when a trait adopts itself -- directly (`trait T { use T; }`) or via an alias that resolves back to the trait by short name (`use X\T as A; use A;`) -- class_qn equals canonical_trait_qn and every appended method re-matches the loop filter, extending the iteration. The loop never terminates: it arena-allocates a fresh method each pass until the process exhausts all memory (observed: 40 GB+, freezing the host). Fix: - Short-circuit self-flattening: a trait cannot meaningfully use itself (PHP itself rejects it), so return early when class_qn equals the resolved trait QN. - Snapshot reg->func_count before the loop so entries appended during iteration are never revisited (defensive against the mutate-while- iterating hazard in general). Add regression test phplsp_trait_self_use_terminates: it hangs/OOMs on the pre-fix code and passes after. Verified end to end -- the prod binary now indexes the triggering file in <0.5s at ~6 MB RSS instead of running away. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Mubariz <mubariz@sportimport.de>
1 parent f092d92 commit 2154edf

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

internal/cbm/lsp/php_lsp.c

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3459,13 +3459,31 @@ static void flatten_trait_into_class(PHPLSPContext *ctx, CBMTypeRegistry *reg, c
34593459
t = lookup_type_with_project(ctx, trait_qn);
34603460
const char *canonical_trait_qn = t ? t->qualified_name : trait_qn;
34613461

3462+
/* A trait cannot meaningfully flatten into itself. PHP itself rejects
3463+
* `trait T { use T; }` ("Trait T cannot use itself"), and an aliased
3464+
* `use X as Y; use Y;` can resolve back onto the enclosing trait by short
3465+
* name. Without this guard the loop below copies the trait's own methods
3466+
* back onto it with receiver_type == canonical_trait_qn; because each copy
3467+
* then re-matches the loop's filter while cbm_registry_add_func() keeps
3468+
* growing reg->func_count, the iteration never terminates — it arena-
3469+
* allocates a fresh method every pass until the process exhausts all
3470+
* memory (observed: 40 GB+, freezing the host). See regression test
3471+
* phplsp_trait_self_use_terminates. */
3472+
if (strcmp(class_qn, canonical_trait_qn) == 0)
3473+
return;
3474+
34623475
/* Iterate registry funcs whose receiver_type is the trait.
34633476
*
34643477
* Self-substitution: when the trait's method has a return type that
34653478
* names the trait itself (e.g. `tap(): self` registered as
34663479
* NAMED(trait_qn)), rewrite it to NAMED(using_class_qn) so chains
3467-
* like `$c->tap()->classMethod()` resolve correctly. */
3468-
for (int i = 0; i < reg->func_count; i++) {
3480+
* like `$c->tap()->classMethod()` resolve correctly.
3481+
*
3482+
* Snapshot the count before iterating: cbm_registry_add_func() below
3483+
* appends to reg->funcs, so the loop must never visit entries it adds
3484+
* during iteration (a mutate-while-iterating hazard). */
3485+
const int func_count_before = reg->func_count;
3486+
for (int i = 0; i < func_count_before; i++) {
34693487
const CBMRegisteredFunc *src = &reg->funcs[i];
34703488
if (!src->receiver_type || strcmp(src->receiver_type, canonical_trait_qn) != 0) {
34713489
continue;

tests/test_php_lsp.c

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,31 @@ TEST(phplsp_trait_method_flattened) {
543543
PASS();
544544
}
545545

546+
/* ── 23b. Regression: a trait that uses itself must terminate ─────
547+
*
548+
* `trait T { use T; ... }` — directly, or via an alias that resolves back
549+
* to T by short name (`use Other\T as A; use A;`) — previously sent
550+
* flatten_trait_into_class() into an unbounded loop: each copied method
551+
* re-matched the loop filter while cbm_registry_add_func() grew the
552+
* registry, exhausting all memory (40 GB+, freezing the host). The fix
553+
* short-circuits self-flattening and snapshots the loop bound. The
554+
* assertion is simply that extraction returns: pre-fix this never
555+
* completed, and under the ASan test build a regression would surface as
556+
* an OOM/abort here rather than a silent hang. */
557+
TEST(phplsp_trait_self_use_terminates) {
558+
const char *src =
559+
"<?php\n"
560+
"namespace App;\n"
561+
"trait EnumTrait {\n"
562+
" use EnumTrait;\n"
563+
" public function getRandom(): int { return 1; }\n"
564+
"}\n";
565+
CBMFileResult *r = extract_php(src);
566+
ASSERT(r);
567+
cbm_free_result(r);
568+
PASS();
569+
}
570+
546571
/* ── 24. Match expression result type ──────────────────────────── */
547572

548573
TEST(phplsp_match_result_type) {
@@ -5160,6 +5185,7 @@ SUITE(php_lsp) {
51605185
RUN_TEST(phplsp_phpdoc_property_class_tag);
51615186
RUN_TEST(phplsp_phpdoc_method_class_tag);
51625187
RUN_TEST(phplsp_trait_method_flattened);
5188+
RUN_TEST(phplsp_trait_self_use_terminates);
51635189
RUN_TEST(phplsp_match_result_type);
51645190
RUN_TEST(phplsp_ternary_result_type);
51655191
RUN_TEST(phplsp_method_chain_depth_three);

0 commit comments

Comments
 (0)