Harden the global gem cache - #9824
Open
hsbt wants to merge 3 commits into
Open
Conversation
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
force-pushed
the
global-cache-hardening
branch
from
August 28, 2026 08:40
31b13c8 to
ca3c7c0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Gem::RemoteFetcher#downloadcopied every gem into the shared global cache whenglobal_gem_cacheis 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. Onlyhttp,https, ands3sources now reach the global cache.The same expression shadowed the
Dir.pwd == install_dirbranch thatgem fetchdepends on, sogem fetchwrote into the cache instead of the working directory, and it had no fallback when the cache directory is unwritable, aborting with a rawErrno::EACCES. Both now behave as they do with the global cache disabled.The
fileandnilbranches copy throughGem::AtomicFileWriterinstead ofFileUtils.cpso that a concurrent install cannot read a half-written.gem.The flat layout of
Gem.global_gem_cache_pathstill lets two remote sources collide on the same name and version, unlike Bundler's per-sourcecache_slug. Changing it strands existing entries and belongs in a separate change.