Project

General

Profile

Actions

Bug #22123

open

Ruby::Box + `BUNDLER_SETUP` can evaluate gemspecs before main-box RubyGems initialization

Bug #22123: Ruby::Box + `BUNDLER_SETUP` can evaluate gemspecs before main-box RubyGems initialization

Added by se4weed.dev@gmail.com (Yuto NORINAGA) 5 days ago.

Status:
Open
Assignee:
-
Target version:
-
ruby -v:
ruby 4.1.0dev (2026-06-13T03:25:52Z master 0e3b8918b3) +PRISM [arm64-darwin25]
[ruby-dev:<unknown>]

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:

https://github.com/ruby/ruby/pull/17323

No data to display

Actions

Also available in: PDF Atom