Skip to content

fix(composer-update): reach 4-segment hotfixes for <= advisory bounds (+ test suite)#35

Merged
oxyc merged 2 commits into
masterfrom
fix/min-safe-inclusive-hotfix
May 29, 2026
Merged

fix(composer-update): reach 4-segment hotfixes for <= advisory bounds (+ test suite)#35
oxyc merged 2 commits into
masterfrom
fix/min-safe-inclusive-hotfix

Conversation

@oxyc
Copy link
Copy Markdown
Member

@oxyc oxyc commented May 29, 2026

Problem

compute-min-safe-constraints.php derived ~X.Y.(Z+1) for an inclusive (<=X.Y.Z) advisory bound. That skips a 4-segment hotfix like X.Y.Z.1, which is >X.Y.Z but <X.Y.(Z+1): the constraint matches no published version, the update no-ops, and the vulnerability goes unpatched — no PR is opened, just a daily red scan + Chat alert.

Real case — generoi/suomentyokalu

wpackagist-plugin/seo-by-rank-math  —  CVE-2025-12714  (advisory: <=1.0.271)

The published fix is 1.0.271.1. The derived ~1.0.272 could not resolve (wpackagist's highest is 1.0.271.1), so every strategy failed:

composer update -W ...:~1.0.272            → conflict
per-package (tight/loose)                  → conflict
composer require -W ...:^1.0.272           → conflict (no matching version)

Fix

Emit >X.Y.Z,<X.(Y+1).0 for inclusive bounds instead of ~X.Y.(Z+1):

  • strict-greater lower bound → matches 1.0.271.1 (and any later patch/hotfix) while still excluding the vulnerable boundary 1.0.271 itself;
  • still minor-capped → preserves the original over-bump protection.

loosen_constraint() and build_widen_arg() in lib.sh learn the new range shape (major-cap widen/loosen, boundary stays excluded).

Verified with composer/semver: 1.0.271.1 satisfies >1.0.271,<1.1.0 ✓, 1.0.271 does not ✓.

Tests

Note: this branch is based on test/composer-update-suite, so it also brings in that branch's test harness + helper extraction (lib.sh). Merging this lands the full composer-update test suite and the fix together.

  • Regression test for the exact case: <=1.0.271 → >1.0.271,<1.1.0, plus Semver assertions that 1.0.271.1 is reachable and 1.0.271 is excluded.
  • Updated the two stale <= assertions to the corrected range output.
  • Helper-fn coverage for the new range shape in loosen_constraint / build_widen_arg.

Full suite: 5 files, 0 failures.

Note

Takes effect for downstream repos only once the v2 tag is moved to the merge commit. suomentyokalu's red run clears on its next scan after that.

🤖 Generated with Claude Code

test and others added 2 commits May 21, 2026 09:35
…ib.sh

The composer-update logic had no tests and was fragile (every recent fix —
#20#31 — was a production-only discovery). Add a real test suite and make
the logic testable.

- Extract the helper functions (build_pkg_arg, build_widen_arg,
  find_direct_ancestors, loosen_constraint, is_still_vulnerable,
  expand_args_for, get_lock_version) from update.sh into a sourceable
  scripts/lib.sh. update.sh sources it; behavior is unchanged (the existing
  no-widen integration test still passes).

- Unit tests for the PHP helpers (the fragile version-range logic):
  - compute-min-safe-constraints: exclusive/inclusive bounds, missing version
    components, multi-range selection, no-entry fall-throughs.
  - is-still-vulnerable: in/out of range, multi-range, junk-input fail-safe.

- Unit tests for the bash helpers, each tied to the edge case it guards:
  #27 build_pkg_arg trailing newline, #29 build_widen_arg caret widen,
  #28 loosen_constraint, #22/#26 find_direct_ancestors BFS + expand_args_for.

- Integration tests driving the real update.sh with a fake composer:
  #24 downgrade revert, #21 dev-* revert, #23 per-package retry isolation,
  #20 no-widen honored as a JSON array. (Plus the existing no-widen test.)

- Fix surfaced by the tests: compute-min-safe-constraints emitted `[]` for an
  empty result, but update.sh string-indexes that map with `jq '.[$pkg]'`,
  which errors on a JSON array under `set -eo pipefail`. Encode as `{}`.

- CI: composer-update-tests.yml now sets up PHP and runs tests/run.sh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ips 4-segment hotfixes

compute-min-safe-constraints derived ~X.Y.(Z+1) for a <=X.Y.Z advisory.
That skips a 4-segment hotfix like X.Y.Z.1, which is >X.Y.Z but
<X.Y.(Z+1): the constraint matched no published version, the update
no-opped, and the vuln went unpatched (no PR opened).

Real case: suomentyokalu / seo-by-rank-math, advisory <=1.0.271, fixed
in 1.0.271.1. The derived ~1.0.272 could not resolve.

Emit >X.Y.Z,<X.(Y+1).0 instead — a strict-greater lower bound that
matches the hotfix while still excluding the vulnerable boundary X.Y.Z,
still minor-capped. Teach loosen_constraint() and build_widen_arg() the
new range shape (major-cap widen/loosen, boundary stays excluded).

Tests: regression case asserting <=1.0.271 -> >1.0.271,<1.1.0 plus
Semver checks that 1.0.271.1 is reachable and 1.0.271 is excluded;
updated existing <= assertions; helper-fn coverage for the range shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@oxyc oxyc merged commit f1ae5f5 into master May 29, 2026
1 check passed
@oxyc oxyc deleted the fix/min-safe-inclusive-hotfix branch May 29, 2026 13:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant