Skip to content

Hosted gem VEX attests not_affected for an unpatched install when .bundle/config sets an out-of-tree path (absolute or ~/…), because the skipped bundle root counts as "nothing installed" #709

Description

[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).

Summary

bundle config set --local path <dir> with <dir> outside the project (an absolute path such as /opt/bundle or /tmp/x, or ~/.bundle-store) is an ordinary Bundler setup. The ruby crawler deliberately refuses a config-sourced BUNDLE_PATH outside the project root, because that root is also an apply write target (resolve_config_bundle_path, containment guard). For hosted mode, though, that refusal means:

  1. The stale-install guard never probes the real install root, so a scan on a project whose unpatched gem is already installed there gives no redirect_gem_stale_install.
  2. The in-run scan --mode hosted --vex attests the redirected gem not_affected.
  3. The next bundle install prints Using colorize 0.8.1 and keeps the unpatched bytes, because they're already in the configured path (the case the stale guard exists for).
  4. A standalone, hash-verifying vex (default verify mode) also attests not_affected (redirected). It finds no installed copy (it never looked in the configured root) and falls into the "absence of any installed copy is excused / lockfile basis" branch. vex prints no warning at all. scan prints a run-level gem_bundle_config_path_ignored warning, but that warning only says the root is ignored "as an install root", not that the redirect and VEX are unverified.

The comment at crates/socket-patch-cli/src/commands/vex.rs:588 says ""Absent" must mean the crawler LOOKED". It handles --ecosystems scoping, but a bundle root the crawler refused to look in also counts as "absent".

Impact

A false not_affected VEX statement for code that is still vulnerable and loaded at runtime (bundle exec loads <path>/ruby/3.3.0/gems/colorize-0.8.1/lib). This holds both in the in-run attestation and in the post-install verified attestation that's meant to catch exactly this. An env-sourced BUNDLE_PATH pointing at the same directory is handled correctly (stale warning, and vex refuses with not_applied), so only the .bundle/config spelling is affected.

Repro (Linux, Ruby 3.3.6, real rubygems.org upstream, local mock of the patch API + patch registry)

The mock serves /v0/orgs/org/patches/{batch,by-package,package,view} for pkg:gem/colorize@0.8.1 and a compact index at /patch-registry/gem/tok/<uuid>/ serving a rebuilt colorize-0.8.1.gem whose lib/colorize.rb ends in # SOCKET_PATCHED. That's the same shape as the fixture in e2e_redirect_gem_build.rs.

OUT=/tmp/outside-bundle; rm -rf proj "$OUT"; mkdir proj && cd proj
printf 'source "https://rubygems.org"\n\ngem "colorize", "0.8.1"\n' > Gemfile
bundle config set --local path "$OUT"          # or: path '~/.bundle-store'
bundle install && bundle lock --add-checksums
A="--api-url http://127.0.0.1:18765 --org org --api-token fake"
socket-patch scan --mode hosted --json --yes --cwd . $A --vex out.vex.json --vex-product pkg:generic/app@1
#  redirect.redirected = 1, redirect.warnings = []   <- no redirect_gem_stale_install
#  out.vex.json: GHSA-… not_affected
bundle install                                     # "Using colorize 0.8.1", exit 0
grep -c SOCKET_PATCHED "$OUT"/ruby/3.3.0/gems/colorize-0.8.1/lib/colorize.rb   # 0 -> unpatched
socket-patch vex --output post.json --product pkg:generic/app@1 --cwd . --patch-server-url http://127.0.0.1:18765 $A
#  exit 0, "Wrote OpenVEX document with 1 statement", status not_affected,
#  impact_statement "Patched via Socket patch <uuid> (redirected)"

Expected vs actual

  • Expected (CLI_CONTRACT.md, "Gem stale-install guard"): a materialization bundle install would reuse is flagged redirect_gem_stale_install, and "a stale-flagged purl is additionally excluded from the same run's --vex assume_applied set — the envelope must never attest a CVE its own warning says is live". The post-install vex should refuse (not_applied), as it does for the same directory supplied through env BUNDLE_PATH. At the very least, when the crawler skipped a configured bundle root it shouldn't treat the gem as "not installed" for the lockfile-basis excuse, and vex should say so. The containment guard protects writes; reading the root to verify or to detect staleness doesn't write anything.
  • Actual: no stale warning; in-run and post-install VEX both attest not_affected while Bundler installs and loads the unpatched gem.

Matrix (Linux, Ruby 3.3.6; every cell ends in a real bundle install)

Bundle root spelling Bundler 4.0.17 Bundler 2.6.9 Bundler 2.4.22 (no CHECKSUMS)
config path absolute, outside the project fail (×2) fail fail
config path ~/.bh-bundle fail untested untested
config path relative vendor/bundle pass (stale warning; vex refuses not_applied) – –
config path absolute, inside the project pass – –
env BUNDLE_PATH absolute, outside the project pass – –

macOS / Windows: untested, but the containment check is lexical and OS-independent. On Windows, a drive-letter path to another folder (C:\bundle) would take the same branch.

First bad version: not bisected. The containment guard and the lockfile-basis VEX excuse both predate this ledger.

Suspect code

  • crates/socket-patch-core/src/crawlers/ruby_crawler.rs:341: the refused config root is recorded as skipped_config_path and never probed (resolve_config_bundle_path, :1092).
  • crates/socket-patch-cli/src/commands/vex.rs:581-600: the hosted lockfile-basis excuse applies to a gem whose bundle root was skipped, and vex never surfaces gem_bundle_config_path_ignored (only scan/mod.rs:1602 and apply.rs:1813 do).
  • The hosted stale-install probe (CLI_CONTRACT.md "Gem stale-install guard") reuses the same discovery, so it misses the root too.

Related: #686 is the Composer twin (absolute or ~ config.vendor-dir). #577 is a different missing layer (Bundler's global config).

Activity

  1. mikolalysenko commented on Oct 3, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Cross-OS confirmation from probe run https://github.com/SocketDev/socket-patch/actions/runs/37137283662 (main 045d7ec, Bundler 4.0.17, the same mock as above). .bundle/config path was set to $RUNNER_TEMP/outside bundle, i.e. outside the project and containing a space.

    Runner Ruby config BUNDLE_PATH scan warnings in-run VEX installed copy patched? post-install vex
    windows-latest 3.3 D:/a/_temp/outside bundle gem_bundle_config_path_ignored only 1 statement no exit 0, not_affected
    windows-latest 3.4 same same 1 statement no exit 0, not_affected
    macos-latest 3.3 /Users/runner/work/_temp/outside bundle same 1 statement no exit 0, not_affected
    macos-latest 3.4 same same 1 statement no exit 0, not_affected

    So it reproduces on all three OSes, including a Windows drive-letter path.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 3, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged priority:p1 (Bundler). Confirmed against main 045d7ec. The ruby crawler refuses a config-sourced BUNDLE_PATH outside the project root (resolve_config_bundle_path) and never probes it. The hosted lockfile-basis excuse in crates/socket-patch-cli/src/commands/vex.rs (~L581-600) then accepts that package_not_found as "nothing installed". Its guard only excludes purls that --ecosystems scoped out. It does not exclude install roots the crawler declined to look in.

    Not a duplicate, and no open or merged PR touches it. Related but not clustered: #686 (Composer absolute or ~ vendor-dir) ends in the same vex excuse, but the discovery fix sits in a different crawler (composer_crawler.rs::resolve_local_vendor_dir, already clustered with #439). A shared fail-closed rule in vex for "the crawler skipped a configured root" would cover both. Whoever fixes either one should consider doing it at that boundary.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 3, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: hosted gem verification never probes a .bundle/config path outside the project, so stale-install and VEX treat the gem as not installed). Branch: agent/fix-gem-config-path-verification. Claim-ID: 2026-10-03T18:20:52Z-60d5bd


    Generated by Claude Code

  4. mikolalysenko commented on Oct 3, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #712


    Generated by Claude Code

  5. added 4 commits that reference this issue on Oct 3, 2026
    ee980ba
    64fbacc
    01cfe55
    55a297f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentpm:bundlerBundler (RubyGems)priority:p1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions