Skip to content

PHP: trait whose name collides with an aliased use … as import it composes → unbounded recursion in definitions pass, OOM (SIGKILL) #765

Description

@KasaharaDefries

Version

codebase-memory-mcp 0.8.1

Platform

macOS (Apple Silicon)

Install channel

GitHub release archive / install.sh / install.ps1

Binary variant

ui

What happened, and what did you expect?

A single ~6-line PHP trait drives the indexer into unbounded memory growth during the definitions pass: RSS climbs to ~50% of machine RAM (~18–20 GB on a 36 GB Mac) and the process is OOM-killed (SIGKILL, exit 137).

The trigger is a name collision between a trait and an aliased use … as … import that the trait composes:

  • trait Auditable
  • … that does use \Vendor\Pkg\Auditable as AuditableBase; (imported short name Auditable == trait name)
  • … and composes it with use AuditableBase;
  • … and has at least one method with a non-empty body.

The extractor appears to ignore the as alias and resolve the composed trait back to the same-short-name local trait — i.e. it sees trait Auditable composing Auditable (itself), a self-referential trait — then recurses without bound while expanding it to process a method body.

This reproduces with mode: fast, and with CBM_LSP_DISABLED=1 and CBM_DISABLE_LSP_CROSS=1 — so it is not in the LSP resolvers that received depth guards in #720. It is in the definitions pass and needs its own recursion/expansion guard.

Expected: the file indexes instantly (it's 6 lines), as it does the moment any one of the three ingredients is removed.

Reproduction

No proprietary code needed. Create a directory with one file Auditable.php:

<?php
namespace App\Demo;

use Vendor\Pkg\Auditable as AuditableBase;  // aliased import; short name "Auditable" == trait name below

trait Auditable {
    use AuditableBase;                       // composes the aliased trait

    function f() { $x = 1; return $x; }      // any method with a non-empty body
}

Run:

CBM_LSP_DISABLED=1 codebase-memory-mcp cli index_repository \
  '{"repo_path":"/path/to/that/dir","mode":"fast"}'

→ RSS climbs to ~18–20 GB and the process is killed (exit 137). Vendor\Pkg\Auditable does not need to exist — it is an unresolved, pure short-name collision.

All three ingredients are required. Removing any one makes it index instantly at ~0 GB (verified, each run monitored for peak RSS):

Change from the repro above Result
rename the trait so its name ≠ Auditable ✅ OK, ~0 GB
drop use AuditableBase; (import present but not composed) ✅ OK, ~0 GB
empty method body {} (or no method at all) ✅ OK, ~0 GB
all three present (as above) ❌ OOM, ~20 GB, exit 137

Note: $this is not required — a body of just $x = 1; is enough.

Real-world origin: this is the shape of a trait that wraps the popular owen-it/laravel-auditing package, e.g. use OwenIt\Auditing\Auditable as AuditableBase; inside a local trait Auditable. So it hits real Laravel apps, not just synthetic input.

Logs

From the minimal single-file repro above — it dies right at the start of the definitions pass:

level=info msg=mem.init budget_mb=18432 total_ram_mb=36864
level=info msg=pipeline.discover files=1 elapsed_ms=0
level=info msg=pipeline.route path=full
level=info msg=pass.start pass=structure files=1
level=info msg=pass.done pass=structure nodes=2 edges=1
level=info msg=pass.timing pass=structure elapsed_ms=0
level=info msg=pipeline.mode mode=sequential files=1
level=info msg=pkgmap.scan_repo manifests=0
level=info msg=pkgmap.scan manifests_from_files=0 manifests_from_walk=0 entries=0
level=info msg=pass.start pass=definitions files=1
<no further output — SIGKILL / OOM, exit 137>

Anything else?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions