java-topology/whitepaper/outreach/composer.md
russell@unturf.com 7e7ec2c3d3 feat: add 10 outreach docs (20 defects) for 2-patch projects
amarok, arrow, audacity, cargo, clementine, composer, dask,
deluge, dosbox-x, dragonfly. All CWE-407.
2026-04-14 13:50:33 -04:00

4.1 KiB
Raw Blame History

Composer — CWE-407 Disclosure Brief

2026-04-13 · Patch available — awaiting upstream merge

Finding

Two O(n²) defects in Composer's dependency resolution: one in recursive package filtering and one in recursive dependent-package discovery. Both patched. Patches ready for upstream review.

The Defects

composer-0001 (PATCHED — MEDIUM): src/Composer/Repository/RepositoryUtils.php:34

// In filterRequiredPackages() — fires during dependency resolution:
if (!in_array($candidate, $bucket, true)) {
    $bucket[] = $candidate;
    $bucket = self::filterRequiredPackages($packages, $candidate, false, $bucket);
}

in_array($candidate, $bucket, true) is O(|bucket|) per candidate. As $bucket grows through recursive calls, each membership test scans the full accumulated list. For a project with P packages and D dependency depth, total cost reaches O(P×D×B) where B = average bucket size.

composer-0002 (PATCHED — MEDIUM): src/Composer/Repository/InstalledRepository.php:86

// In getDependents() — fires during `composer why` / `composer depends`:
if (in_array($link->getTarget(), $packagesInTree)) {
    $results[] = [$package, $link, false];
    continue;
}
$packagesInTree[] = $link->getTarget();
$dependents = $recurse ? $this->getDependents($link->getTarget(), ..., $packagesInTree) : [];

in_array() on $packagesInTree is O(N) per link. Called recursively with a growing tree. For a project with L links across R packages, total cost reaches O(L×N) where N = accumulated tree size.

Complexity Proof

composer-0001: At P=200 packages, B growing to 200:

  • Defective: 200 × 100 (avg bucket) = 20,000 comparisons
  • Fixed: 200 × O(1) = 200 lookups (SplObjectStorage)
  • ~100× op reduction per dependency resolution.

composer-0002: At R=500 packages, L=2000 links:

  • Defective: 2,000 × 250 (avg tree) = 500,000 comparisons
  • Fixed: 2,000 × O(1) = 2,000 lookups (isset on associative array)
  • ~250× op reduction per composer why invocation.

Impact

Composer is the package manager for PHP — used by virtually every PHP project. filterRequiredPackages runs during dependency resolution, affecting composer install, composer update, and composer require. getDependents powers composer why and composer depends, used to trace why a package appears in the dependency tree.

Large PHP projects (Symfony, Laravel, Drupal applications) with hundreds of packages hit both paths. composer-0001 compounds through recursive resolution; composer-0002 compounds through recursive dependent traversal.

The Fix

composer-0001: Replace in_array() with SplObjectStorage (PHP's hash-backed object set):

// Before
if (!in_array($candidate, $bucket, true)) { ... }

// After
// CWE-407 fix: SplObjectStorage for O(1) membership instead of O(|bucket|) in_array().
$bucketSet = new \SplObjectStorage();
if (!$bucketSet->contains($candidate)) {
    $bucketSet->attach($candidate);
    $bucket[] = $candidate;
}

composer-0002: Replace in_array() with isset() on a parallel associative array:

// Before
if (in_array($link->getTarget(), $packagesInTree)) { ... }

// After
// CWE-407 fix: isset() on hash set for O(1) instead of O(N) in_array().
if (isset($packagesInTreeSet[$link->getTarget()])) { ... }
$packagesInTreeSet[$link->getTarget()] = true;

Patch

Fix available: defects/composer/patch/composer-0001-filter-splatobjectstorage.patch and defects/composer/patch/composer-0002-dependents-isset.patch

Two-file patch across RepositoryUtils.php and InstalledRepository.php.

composer-0001: ~100× speedup at P=200. composer-0002: ~250× speedup at R=500, L=2000.

What We Ask

A patch is ready for review.

  1. Confirm receipt and assign an issue reference (composer/composer).
  2. Assess severity — both defects sit on dependency resolution paths used by every composer install and composer why invocation.
  3. Coordinate a disclosure date — we are targeting 90 days from first contact.
  4. We will credit the Composer team in the public disclosure. Preferred acknowledgment format welcome.

Contact: see cover email. This brief is confidential until coordinated disclosure.