Skip to content

Harden the global gem cache - #9824

Open
hsbt wants to merge 3 commits into
masterfrom
global-cache-hardening
Open

Harden the global gem cache#9824
hsbt wants to merge 3 commits into
masterfrom
global-cache-hardening

Conversation

@hsbt

@hsbt hsbt commented Aug 28, 2026

Copy link
Copy Markdown
Member

Gem::RemoteFetcher#download copied every gem into the shared global cache when global_gem_cache is enabled, gems installed from a local path included. The http branch reuses an existing cache file without re-verifying it, so one local install could hand its own file to later remote installs of the same name and version in every project on the machine. Only http, https, and s3 sources now reach the global cache.

The same expression shadowed the Dir.pwd == install_dir branch that gem fetch depends on, so gem fetch wrote into the cache instead of the working directory, and it had no fallback when the cache directory is unwritable, aborting with a raw Errno::EACCES. Both now behave as they do with the global cache disabled.

The file and nil branches copy through Gem::AtomicFileWriter instead of FileUtils.cp so that a concurrent install cannot read a half-written .gem.

The flat layout of Gem.global_gem_cache_path still lets two remote sources collide on the same name and version, unlike Bundler's per-source cache_slug. Changing it strands existing entries and belongs in a separate change.

hsbt and others added 3 commits August 28, 2026 17:39
A gem installed from a local path was copied into the global cache under
its canonical file name, where later remote installs of the same name and
version would reuse it without re-verification, so one local install
could poison every other project on the machine.

Two smaller problems in the same expression go with it. The global cache
branch was evaluated before the one gem fetch relies on, so gem fetch
wrote into the cache instead of the working directory, and an unwritable
cache directory aborted the install rather than falling back the way it
does with the cache disabled. The working directory is compared by
identity because the paths can differ while naming the same place, and
because Dir.pwd raises once that directory is gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The file and path schemes wrote into the shared cache directory with a
plain FileUtils.cp, so a concurrent install could read a half-written
.gem file. Route them through Gem::AtomicFileWriter like the http scheme,
which also makes replacing a read-only cache copy work.

cp passed the source mode to File.open, so it reached only a file being
created and the umask still applied to it. Reproduce that rather than
chmodding unconditionally, which would skip the umask, carry setuid
across, and rewrite the mode of a file cp would have left alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The expected cache base was computed from Gem.global_gem_cache_path, the
same expression the code under test uses, so the assertion could never
catch an unintended path change. That method was added after 4.0 was cut,
so no released 4.0.x has it and the old gate would make the specs expect
the RubyGems layout from a Bundler that falls back to its own.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hsbt
hsbt force-pushed the global-cache-hardening branch from 31b13c8 to ca3c7c0 Compare August 28, 2026 08:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant