Skip to content

Fix gem apply/vex skipping platform gem copies (#1092) - #1395

Merged
Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-gem-every-platform-copy
Oct 11, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-gem-every-platform-copy

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #1092

Summary

A gem home can hold two builds of the same gem version, for example ffi-1.17.2/ (ruby platform) and ffi-1.17.2-x86_64-linux-gnu/. RubyGems and Bundler load the platform build. Before this PR, agent-mode apply, rollback, apply --check and vex only looked at the plain dir. So apply patched a copy nothing loads, apply --check reported "in sync", and vex attested not_affected while the loaded copy stayed vulnerable. Global mode (-g) had the same gap.

After this PR, every build of the version is treated as an installed copy:

  • apply patches each build and rollback restores each one.
  • apply --check and vex refuse while any build is unpatched.

Root cause

locate_gem_dir / locate_gem_dir_sync in crates/socket-patch-core/src/crawlers/ruby_crawler.rs assumed one build per gem home. They returned the plain <name>-<version>/ dir and never looked at a -<platform> sibling. The commands downstream already act on every copy they are given (the multi-store gem fix), so the crawler was the gap.

Changes

  • Crawler (ruby_crawler.rs): new find_all_by_purls returns every verifying build in a gem home. The plain dir comes first, then platform dirs in name order. The home is listed once per call. find_by_purls and find_each_by_purl keep their single-copy contract (the first copy). A platform dir counts only when the text after <name>-<version>- starts with a letter, so foo-1.0-2.0/ (the gem foo-1.0 at 2.0) is not taken as a build of foo 1.0. Dotted platforms such as universal-java-1.8 still count.
  • Dispatch (ecosystem_dispatch.rs): the gem arm and the vex verification-only stores use find_all_by_purls, and every copy reaches the apply, rollback, vex and --check maps.
  • apply (apply.rs): an unqualified gem record normally overwrites a locally modified file with a warning. That policy assumes the copy is the only candidate. It no longer applies to a build that has a sibling build in the same gem home: a mismatch there means another distribution, so the build is left untouched and the run fails (no matching variant found), the same as any primary copy no variant matches. This gate is gem-only.
  • rollback (rollback.rs): variants are chosen per copy instead of from the first copy only. A build that holds no variant, beside a sibling build that does, is skipped, because nothing was written there.
  • vex (vex.rs, vex_copy_sets): each qualified gem variant (?platform=) is judged only against the builds that hold its distribution. A build holding no variant stays on every key, so the statement is withheld.
  • CLI_CONTRACT.md: documents the several-builds rule and fixes the sentence that listed vex as a single-copy consumer.

Test evidence

Each regression test below was run against main's source (fix stashed) and failed, then passed with the fix:

Test Without fix With fix
core find_all_by_purls_returns_every_platform_copy n/a (new API) pass
core is_platform_dir_of_rejects_another_gem_sharing_the_prefix n/a (new helper) pass
cli apply_and_rollback_cover_every_platform_variant_copy fail: platform copy not patched pass
cli vex_and_check_refuse_an_unpatched_platform_variant_copy fail: vex wrote not_affected; apply --check said "Patches are in sync" pass
cli global_apply_covers_every_platform_variant_copy (-g) same crawler path as above pass
cli apply_refuses_a_platform_copy_holding_another_distribution before the gate: the native build was overwritten with the plain build's bytes pass
cli each_platform_build_uses_its_own_variant fail without the vex fix: no statement attested pass
cli rollback_skips_a_platform_copy_holding_another_distribution fail without the rollback fix: hash_mismatch on the untouched build pass

Per-issue checklist:

Local runs:

  • cargo fmt --all -- --check: clean
  • cargo clippy --workspace --all-features -- -D warnings: clean
  • cargo test --workspace --all-features: 13901 passed, 13 failed. The 13 are unrelated to this change and specific to the sandbox: write-failure tests that rely on chmod (the sandbox runs as root, which ignores it) and one bun test that needs the live patch API. No gem test among them.
  • cargo test -p socket-patch-cli --all-features --test e2e_redirect_gem_build --test e2e_vendor_gem_build -- --ignored (Ruby 3.3.6): 29 + 8 passed; e2e_redirect_gem_stale_install (non-ignored) passed in the workspace run
  • cargo test -p socket-patch-cli --test apply gem: 30 passed

Review notes

/code-review high ran on the diff. Fixed: per-copy rollback narrowing, per-variant vex copy sets, the PyPI false positive of the sibling gate, one listing per gem home, dotted platforms, the contract wording, and the duplicated merge helper. Not changed:

🤖 Generated with Claude Code

https://claude.ai/code/session_019iyRzgAFy9WDK4P1TZMAfJ


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
A gem home can hold both builds of one gem version, such as
ffi-1.17.2/ and ffi-1.17.2-x86_64-linux-gnu/. RubyGems and Bundler
load the platform build, but agent-mode apply, rollback, apply --check
and vex only ever looked at the plain dir. So apply patched a copy
nothing loads, and vex attested not_affected while the loaded copy
stayed vulnerable. Global mode (-g) had the same gap.

The gem crawler now returns every verifying build of a version in a
gem home (plain dir first, then each platform dir in name order), and
the dispatch maps feed all of them to the commands that already act on
every copy. A platform dir must parse back to the same gem name and
version, so foo-1.0-2.0/ (the gem foo-1.0 at 2.0) is never taken as a
build of foo 1.0.

When builds sit side by side, a build whose bytes match none of the
patch's variants is a different distribution rather than a locally
edited copy. Apply leaves it untouched and fails, instead of
overwriting it with another build's bytes.

Fixes #1092

Assisted-by: Claude Code:claude-opus-5-5
Now that every build of a gem version in one gem home is a copy, a
manifest with a separate record per build (?platform=ruby for
x-1.0/, ?platform=x86_64-linux for x-1.0-x86_64-linux/) must be
judged build by build:

- rollback picked a variant from the first copy and rolled it back on
  every copy, failing on the other build and dropping its record. It
  now picks variants per copy, and skips a build that holds none of
  them when a sibling build does (nothing was ever written there).
- vex judged each variant against every build, so two correctly
  patched builds were never attested. Each qualified variant now gets
  only the builds holding its distribution; a build holding none
  stays on every key, so the statement is still withheld.

Also from review: the build-sibling apply gate is gem-only (PyPI
site-packages dirs can share a parent), the gem home is listed once
per lookup instead of once per patch, a dotted platform such as
universal-java-1.8 is still recognised, and the contract no longer
lists vex as a single-copy consumer.

Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 2688988. Configure here.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] Ready for review at 2688988.

  • CI: 36/36 non-skipped checks green (50 skipped by path filters).
  • Bugbot: no findings on the head.
  • Decision for a reviewer: a coinstalled build for another platform (for example -arm64-darwin in a vendor/bundle shared with a Linux host) now fails apply when the manifest has no variant for it, under the existing primary-copy rule. See "Review notes" in the description.

Generated by Claude Code

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 11, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at 2688988.

  • CI: 36/36 non-skipped checks green on the head (50 skipped by path filters); no main-wide failures.
  • Bugbot: reviewed 2688988, no findings.
  • Merge state: mergeable (clean); already approved by a human reviewer.
  • Look at: gem apply/rollback/apply --check/vex now cover every platform build of a gem version in a gem home (e.g. ffi-1.17.2-x86_64-linux-gnu/), not only the plain dir.
  • Slack announcement not sent this run (no Slack send tool available); next run retries.

Generated by Claude Code

Merged via the queue into main with commit ff4ced8 Oct 11, 2026
86 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-gem-every-platform-copy branch October 11, 2026 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

3 participants