Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions internal/cbm/lsp/php_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -3459,13 +3459,31 @@ static void flatten_trait_into_class(PHPLSPContext *ctx, CBMTypeRegistry *reg, c
t = lookup_type_with_project(ctx, trait_qn);
const char *canonical_trait_qn = t ? t->qualified_name : trait_qn;

/* A trait cannot meaningfully flatten into itself. PHP itself rejects
* `trait T { use T; }` ("Trait T cannot use itself"), and an aliased
* `use X as Y; use Y;` can resolve back onto the enclosing trait by short
* name. Without this guard the loop below copies the trait's own methods
* back onto it with receiver_type == canonical_trait_qn; because each copy
* then re-matches the loop's filter while cbm_registry_add_func() keeps
* growing reg->func_count, the iteration never terminates — it arena-
* allocates a fresh method every pass until the process exhausts all
* memory (observed: 40 GB+, freezing the host). See regression test
* phplsp_trait_self_use_terminates. */
if (strcmp(class_qn, canonical_trait_qn) == 0)
return;

/* Iterate registry funcs whose receiver_type is the trait.
*
* Self-substitution: when the trait's method has a return type that
* names the trait itself (e.g. `tap(): self` registered as
* NAMED(trait_qn)), rewrite it to NAMED(using_class_qn) so chains
* like `$c->tap()->classMethod()` resolve correctly. */
for (int i = 0; i < reg->func_count; i++) {
* like `$c->tap()->classMethod()` resolve correctly.
*
* Snapshot the count before iterating: cbm_registry_add_func() below
* appends to reg->funcs, so the loop must never visit entries it adds
* during iteration (a mutate-while-iterating hazard). */
const int func_count_before = reg->func_count;
for (int i = 0; i < func_count_before; i++) {
const CBMRegisteredFunc *src = &reg->funcs[i];
if (!src->receiver_type || strcmp(src->receiver_type, canonical_trait_qn) != 0) {
continue;
Expand Down
26 changes: 26 additions & 0 deletions tests/test_php_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,31 @@ TEST(phplsp_trait_method_flattened) {
PASS();
}

/* ── 23b. Regression: a trait that uses itself must terminate ─────
*
* `trait T { use T; ... }` — directly, or via an alias that resolves back
* to T by short name (`use Other\T as A; use A;`) — previously sent
* flatten_trait_into_class() into an unbounded loop: each copied method
* re-matched the loop filter while cbm_registry_add_func() grew the
* registry, exhausting all memory (40 GB+, freezing the host). The fix
* short-circuits self-flattening and snapshots the loop bound. The
* assertion is simply that extraction returns: pre-fix this never
* completed, and under the ASan test build a regression would surface as
* an OOM/abort here rather than a silent hang. */
TEST(phplsp_trait_self_use_terminates) {
const char *src =
"<?php\n"
"namespace App;\n"
"trait EnumTrait {\n"
" use EnumTrait;\n"
" public function getRandom(): int { return 1; }\n"
"}\n";
CBMFileResult *r = extract_php(src);
ASSERT(r);
cbm_free_result(r);
PASS();
}

/* ── 24. Match expression result type ──────────────────────────── */

TEST(phplsp_match_result_type) {
Expand Down Expand Up @@ -5160,6 +5185,7 @@ SUITE(php_lsp) {
RUN_TEST(phplsp_phpdoc_property_class_tag);
RUN_TEST(phplsp_phpdoc_method_class_tag);
RUN_TEST(phplsp_trait_method_flattened);
RUN_TEST(phplsp_trait_self_use_terminates);
RUN_TEST(phplsp_match_result_type);
RUN_TEST(phplsp_ternary_result_type);
RUN_TEST(phplsp_method_chain_depth_three);
Expand Down
Loading