[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:
- 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.
- The in-run
scan --mode hosted --vex attests the redirected gem not_affected.
- 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).
- 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).
[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/bundleor/tmp/x, or~/.bundle-store) is an ordinary Bundler setup. The ruby crawler deliberately refuses a config-sourcedBUNDLE_PATHoutside 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:redirect_gem_stale_install.scan --mode hosted --vexattests the redirected gemnot_affected.bundle installprintsUsing colorize 0.8.1and keeps the unpatched bytes, because they're already in the configured path (the case the stale guard exists for).vex(default verify mode) also attestsnot_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.vexprints no warning at all.scanprints a run-levelgem_bundle_config_path_ignoredwarning, 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:588says ""Absent" must mean the crawler LOOKED". It handles--ecosystemsscoping, but a bundle root the crawler refused to look in also counts as "absent".Impact
A false
not_affectedVEX statement for code that is still vulnerable and loaded at runtime (bundle execloads<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-sourcedBUNDLE_PATHpointing at the same directory is handled correctly (stale warning, andvexrefuses withnot_applied), so only the.bundle/configspelling 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}forpkg:gem/colorize@0.8.1and a compact index at/patch-registry/gem/tok/<uuid>/serving a rebuiltcolorize-0.8.1.gemwhoselib/colorize.rbends in# SOCKET_PATCHED. That's the same shape as the fixture ine2e_redirect_gem_build.rs.Expected vs actual
bundle installwould reuse is flaggedredirect_gem_stale_install, and "a stale-flagged purl is additionally excluded from the same run's--vexassume_appliedset — the envelope must never attest a CVE its own warning says is live". The post-installvexshould refuse (not_applied), as it does for the same directory supplied through envBUNDLE_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, andvexshould say so. The containment guard protects writes; reading the root to verify or to detect staleness doesn't write anything.not_affectedwhile Bundler installs and loads the unpatched gem.Matrix (Linux, Ruby 3.3.6; every cell ends in a real
bundle install)pathabsolute, outside the projectpath~/.bh-bundlepathrelativevendor/bundlevexrefusesnot_applied)pathabsolute, inside the projectBUNDLE_PATHabsolute, outside the projectmacOS / 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 asskipped_config_pathand 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, andvexnever surfacesgem_bundle_config_path_ignored(onlyscan/mod.rs:1602andapply.rs:1813do).Related: #686 is the Composer twin (absolute or
~config.vendor-dir). #577 is a different missing layer (Bundler's global config).