Bug #22123
closedRuby::Box + `BUNDLER_SETUP` can evaluate gemspecs before main-box RubyGems initialization
Description
Ruby::Box + BUNDLER_SETUP can evaluate gemspecs before main-box RubyGems initialization¶
Subject¶
Ruby::Box + BUNDLER_SETUP can evaluate gemspecs before main-box RubyGems initialization
Description¶
When Ruby is started with RUBY_BOX=1 and Bundler's BUNDLER_SETUP environment variable is present, RubyGems can require bundler/setup while loading gem_prelude for the root box.
That can make Bundler run code through TOPLEVEL_BINDING in the main box before the main box has finished loading RubyGems. In one common case, Bundler evaluates a path gem's .gemspec, and a normal gemspec using Gem::Specification.new then fails with:
This happens during Ruby startup, before application code starts.
Environment¶
Observed with:
ruby 4.1.0dev (2026-06-13T03:25:52Z master 0e3b8918b3) +PRISM [arm64-darwin25]
macOS 26.4.1
bundler 4.1.0.dev
Ruby::Box enabled with RUBY_BOX=1
Minimal Reproduction¶
Create a tiny path gem:
Gemfile:
box_repro/box_repro.gemspec:
Gem::Specification.new do |spec|
spec.name = "box_repro"
spec.version = "0.1.0"
spec.summary = "Ruby::Box BUNDLER_SETUP repro"
spec.authors = ["repro"]
spec.files = []
end
Choose the Ruby executable being tested:
Generate the lockfile with the same Ruby/Bundler environment:
Then use the same executable to find the bundled bundler/setup path:
BUNDLER_SETUP_PATH="$("$RUBY_UNDER_TEST" -rrbconfig -e 'puts File.join(RbConfig::CONFIG["rubylibdir"], "bundler/setup")')"
Then run Ruby with RUBY_BOX=1 and BUNDLER_SETUP:
RUBY_BOX=1 \
BUNDLE_GEMFILE="$PWD/Gemfile" \
BUNDLER_SETUP="$BUNDLER_SETUP_PATH" \
"$RUBY_UNDER_TEST" -e 'puts :ok'
Actual Result¶
[!] There was an error while loading `box_repro.gemspec`: uninitialized constant Gem::Specification. Bundler cannot continue.
# from /tmp/box-bundler-repro/box_repro/box_repro.gemspec:1
# -------------------------------------------
> Gem::Specification.new do |spec|
# spec.name = "box_repro"
# -------------------------------------------
Expected Result¶
Control Cases¶
This succeeds without Ruby::Box:
BUNDLE_GEMFILE="$PWD/Gemfile" \
BUNDLER_SETUP="$BUNDLER_SETUP_PATH" \
"$RUBY_UNDER_TEST" -e 'puts :ok'
This also succeeds with Ruby::Box if Bundler is loaded later through -rbundler/setup instead of RubyGems' BUNDLER_SETUP hook:
So this does not appear to be application-specific. The failure is triggered by the Bundler startup environment, especially BUNDLER_SETUP.
Analysis¶
Ruby currently loads gem_prelude for both the root and main boxes:
gem_prelude.rb requires RubyGems:
At the end of RubyGems, Bundler may be loaded from BUNDLER_SETUP:
This means the root box's RubyGems load can trigger bundler/setup before the main box's RubyGems state is ready.
Bundler evaluates path gemspecs with TOPLEVEL_BINDING:
To confirm which box evaluated the gemspec and which RubyGems constants were available, I temporarily added the following diagnostics at the beginning of the path gem's .gemspec, before Gem::Specification.new:
if defined?(Ruby::Box)
warn "box=#{Ruby::Box.current.inspect}"
warn "root=#{Ruby::Box.root.inspect}"
warn "main=#{Ruby::Box.main.inspect}"
else
warn "box=nil"
warn "root=nil"
warn "main=nil"
end
warn "gem=#{!!defined?(Gem)}"
warn "gem_spec=#{!!defined?(Gem::Specification)}"
warn "gem_version=#{!!defined?(Gem::VERSION)}"
During the failure, that diagnostic output showed:
box=#<Ruby::Box:3,user,main>
root=#<Ruby::Box:2,root>
main=#<Ruby::Box:3,user,main>
gem=true
gem_spec=false
gem_version=false
So the gemspec is evaluated in the main box, but before the main box's RubyGems state is ready. The diagnostics suggest that Gem is present, but RubyGems constants such as Gem::Specification and Gem::VERSION are not fully initialized in the main box at that point.
Why this should not be fixed in gemspecs¶
Normal gemspecs conventionally use:
Adding require "rubygems/specification" to the gemspec does not seem like the right fix. In local testing it only moved the failure further into RubyGems initialization, with other missing pieces such as:
The problem is that Bundler is evaluating the gemspec before RubyGems is ready in the current box.
Possible Fix Direction¶
The core issue seems to be that BUNDLER_SETUP is consumed from the root box's RubyGems prelude before the main box's RubyGems state is ready.
One possible direction is to fix this in Ruby startup / Ruby::Box prelude sequencing. For example, builtin.c could prevent the root box gem_prelude from consuming BUNDLER_SETUP, while still allowing the main box gem_prelude to consume it normally.
Another possible direction is to fix this on the Bundler/RubyGems side, so that the BUNDLER_SETUP hook does not run user/top-level code in the main box before the main box RubyGems state is ready.
The invariant I think we want is:
BUNDLER_SETUP should not be able to trigger main-box code execution before the main box's RubyGems initialization is complete.
With a local prototype that temporarily hides BUNDLER_SETUP while loading the root box prelude in builtin.c, then restores it before loading the main box prelude, the minimal reproduction succeeds.
Related PR¶
A proposed fix with a regression test is available at:
Updated by hsbt (Hiroshi SHIBATA) 11 days ago
Some background on why this require exists.
error_highlight, did_you_mean and syntax_suggest used to be required at startup right after RubyGems, before RUBYOPT=-rbundler/setup ran, so a Gemfile could not control their versions ([Bug #19089]). https://github.com/ruby/rubygems/commit/963cb65a2db6117bfe4fb2900c17c0d3bd979498 made bundle exec export BUNDLER_SETUP and had rubygems.rb require it at its end, which moves bundler/setup ahead of those three.
Since [Feature #21951] the three are autoloaded and are no longer required at startup, so that early load has nothing left to protect. Removing the line does fix this issue. I confirmed that a Gemfile pinning error_highlight still takes effect on master without it, while the same setup on Ruby 3.4 fails with You have already activated error_highlight 0.7.2, but your Gemfile requires 0.7.0.
RubyGems supports Ruby 3.2 and later, and the three gems are still loaded eagerly on 3.2 through 4.0, so the line cannot be removed from rubygems.rb.
I therefore took the approach of https://github.com/ruby/ruby/pull/17323, hiding BUNDLER_SETUP while the prelude loads RubyGems, and put it in gem_prelude.rb rather than builtin.c: https://github.com/ruby/ruby/pull/18218. Doing it there also covers user boxes, which load the same prelude, so Ruby::Box.new under a bundle no longer fails with Bundler::GemNotFound or re-resolves the Gemfile.
The TOPLEVEL_BINDING TODO in doc/language/box.md remains.
Updated by hsbt (Hiroshi SHIBATA) 11 days ago
- Status changed from Open to Closed
Applied in changeset git|c3c0e280d03561e21fc0894ea693ce3c9ebada0e.
Do not consume BUNDLER_SETUP outside the main box
[Bug #22123]
Bundler evaluates gemspecs through TOPLEVEL_BINDING, which always belongs
to the main box, so consuming BUNDLER_SETUP while the root box loads
RubyGems runs Bundler code in the main box before RubyGems has finished
loading there. Skip it in every box once error_highlight, did_you_mean and
syntax_suggest are autoloaded, since nothing loaded here needs the bundle
and RUBYOPT=-rbundler/setup sets Bundler up after the boot sequence.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Updated by hsbt (Hiroshi SHIBATA) 11 days ago
- Backport changed from 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN to 3.3: DONTNEED, 3.4: DONTNEED, 4.0: REQUIRED