Keep the build context inside the build directory - #611
Open
halogenandtoast wants to merge 1 commit into
Open
halogenandtoast wants to merge 1 commit into
halogenandtoast wants to merge 1 commit into
Conversation
Docker::Util could pull files from outside the build directory into the build context in two ways. create_relative_dir_tar stats and opens each path with File.stat and File.open, both of which follow symlinks, so a link sitting in the build directory was packed with its target's bytes under the link's own name. Separately, docker_context joins each .dockerignore negation pattern onto the build directory with File.join and hands the result to Dir.glob with nothing checking that it stays underneath, so a pattern containing ../ resolved outside. Either way the file landed in the tar sent to the daemon and could be read from inside the build. docker build does not work this way: moby matches .dockerignore against paths it gets from walking the context root, so a pattern cannot name anything outside it, and a COPY that leaves the context is refused. Archive symlinks as symlinks instead of following them, as PR upserve#530 has proposed since 2018, and check that regular files really resolve under the build directory. Both are needed: lstat does not stop a ../ pattern, and validating the pattern string does not stop a symlink or a file reached through a symlinked directory. The check sits in the tar loop rather than in docker_context so that file_hash_from_paths is covered too, which reaches the same code through Container#archive_in and Image#insert_local. Using lstat also clears up the Errno::ENOENT that a dangling symlink previously raised out of build_from_dir. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Docker::Utilcan pull files from outside the build directory into the build context, in two independent ways.create_relative_dir_tarstats and opens each path withFile.statandFile.open, both of which follow symlinks, so a link sitting in the build directory is packed with its target's bytes under the link's own name. Separately,docker_contextjoins each.dockerignorenegation pattern onto the build directory withFile.joinand hands the result toDir.globwith nothing checking that it stays underneath, so a pattern containing../resolves outside. Either way the file lands in the tar sent to the daemon and can be read from inside the build.docker builddoes not work this way. Moby matches.dockerignoreagainst paths it gets from walking the context root, so a pattern cannot name anything outside it, and aCOPYthat leaves the context is refused. Anyone assuming this gem behaved like the CLI would be wrong about what their build can reach.Affected: the symlink path since 1.20.0, the
.dockerignorepath since 2.2.0. Both present in 2.4.0 and on master.The fix
Archive symlinks as symlinks instead of following them — which is what #530 has proposed since 2018 — and check that regular files really resolve under the build directory.
Both halves are needed.
lstatalone does not stop a../pattern, and validating the pattern string alone does not stop a symlink, or a file reached through a symlinked directory.The containment check sits in the tar loop rather than in
docker_contextso thatfile_hash_from_pathsis covered too, which reaches the same code throughContainer#archive_inandImage#insert_local.Using
lstatalso clears up theErrno::ENOENTthat a dangling symlink previously raised out ofbuild_from_dir. The Docker CLI archives a dangling link without complaint, so a tree that builds fine withdocker buildused to fail hard here.Behaviour change worth flagging
In-context symlinks now ship as symlink entries rather than as regular files holding a copy of the target's bytes. That matches what
docker buildproduces, but it is a visible change for anyone who was relying on the old dereferencing.Tests
Four regression tests in
spec/docker/util_spec.rb, covering both mechanisms, the composed symlinked-directory case, and the dangling-link crash. The suite had no symlink coverage at all before, which is why nothing pinned this.SingleCov.covered! uncovered: 71is unchanged and needs no edit: master measures 70 uncovered lines inutil.rb, the patch alone takes it to 74, and the four tests bring it back to exactly 71.Verified on Ruby 3.2.2. Note that the suite cannot load on Ruby 3.4 —
lib/docker.rbrequiresbase64, which left the default gems in 3.4 — but that is pre-existing and unrelated to this change.network_spec.rbandconnection_spec.rbhave four failures against a modern daemon (Docker 29.8.1). They are identical on master with this branch reverted, so they are pre-existing and not from this change.Cost
The added
realpathcall is roughly 18 µs per file: a 1,000-file context goes 76 ms to 99 ms, a 5,000-file context 259 ms to 352 ms, measured over 5 runs each with a warm cache. Proportionally real, negligible next to tarring and uploading the context.Reproduction
Exits 1 on master, 0 with this branch. No Docker daemon needed — it only inspects the tar the gem builds.
Found by AI-assisted analysis, reviewed and reproduced by hand before sending.
This overlaps #530, which fixes the symlink half. Happy to rebase onto it if you would rather land that one first.