Bug #22123
openRuby::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:
uninitialized constant Gem::Specification
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:
rm -rf /tmp/box-bundler-repro
mkdir -p /tmp/box-bundler-repro/box_repro
cd /tmp/box-bundler-repro
Gemfile:
source "https://rubygems.org"
gem "box_repro", path: "./box_repro"
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:
RUBY_UNDER_TEST="${RUBY_UNDER_TEST:-ruby}"
Generate the lockfile with the same Ruby/Bundler environment:
"$RUBY_UNDER_TEST" -S bundle lock
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¶
ok
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:
RUBY_BOX=1 \
BUNDLE_GEMFILE="$PWD/Gemfile" \
"$RUBY_UNDER_TEST" -rbundler/setup -e 'puts :ok'
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:
// builtin.c
rb_load_gem_prelude((VALUE)rb_root_box());
rb_load_gem_prelude((VALUE)rb_main_box());
gem_prelude.rb requires RubyGems:
require "rubygems"
At the end of RubyGems, Bundler may be loaded from BUNDLER_SETUP:
# lib/rubygems.rb
require ENV["BUNDLER_SETUP"] if ENV["BUNDLER_SETUP"] && !defined?(Bundler)
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:
# lib/bundler.rb
eval(contents, TOPLEVEL_BINDING.dup, path.expand_path.to_s)
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:
Gem::Specification.new do |spec|
# ...
end
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:
Gem::Deprecate
Gem::Requirement
Gem::VERSION
Gem.platforms
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:
No data to display