-
Notifications
You must be signed in to change notification settings - Fork 65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ensure files and links have missing parent dirs created. Support hard links. #1179
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -412,6 +412,13 @@ func untar(ctx context.Context, dst string, r io.Reader) error { | |
|
||
// if it's a file create it | ||
case tar.TypeReg: | ||
// If the file's parent dir doesn't exist, create it. | ||
dirname := filepath.Dir(target) | ||
if _, err := os.Stat(dirname); err != nil { | ||
if err := os.MkdirAll(dirname, 0o755); err != nil { | ||
return err | ||
} | ||
} | ||
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) | ||
if err != nil { | ||
return err | ||
|
@@ -429,15 +436,66 @@ func untar(ctx context.Context, dst string, r io.Reader) error { | |
|
||
// if it's a link create it | ||
case tar.TypeSymlink: | ||
err := os.Symlink(header.Linkname, filepath.Join(dst, header.Name)) | ||
nobaseLinkname, nobaseName := resolveLinkPaths(header.Linkname, header.Name) | ||
fullLinkname := filepath.Join(dst, nobaseLinkname) | ||
fullName := filepath.Join(dst, nobaseName) | ||
// Safeguard for cases where we're trying to link to something | ||
// outside of our base fs. | ||
if !strings.HasPrefix(fullLinkname, dst) { | ||
logger.V(log.DBG).Info("Error processing symlink. Symlink would reach outside of the image archive. Skipping this link", "link", header.Name, "linkedTo", header.Linkname, "resolvedTo", fullLinkname) | ||
continue | ||
} | ||
// Create the new link's directory if it doesn't exist. | ||
dirname := filepath.Dir(fullName) | ||
if _, err := os.Stat(dirname); err != nil { | ||
if err := os.MkdirAll(dirname, 0o755); err != nil { | ||
return err | ||
} | ||
} | ||
err := os.Symlink(fullLinkname, fullName) | ||
if err != nil { | ||
logger.V(log.DBG).Info(fmt.Sprintf("Error creating symlink: %s. Ignoring.", header.Name), "link", fullName, "linkedTo", fullLinkname, "reason", err) | ||
continue | ||
} | ||
case tar.TypeLink: | ||
// We assume hard links will not contain relative pathing in the archive. | ||
original := filepath.Join(dst, header.Linkname) | ||
// Create the new link's directory if it doesn't exist. | ||
dirname := filepath.Dir(target) | ||
if _, err := os.Stat(dirname); err != nil { | ||
if err := os.MkdirAll(dirname, 0o755); err != nil { | ||
return err | ||
} | ||
} | ||
err := os.Link(original, target) | ||
if err != nil { | ||
logger.V(log.DBG).Info(fmt.Sprintf("Error creating link: %s. Ignoring.", header.Name)) | ||
logger.V(log.DBG).Info(fmt.Sprintf("Error creating hard link: %s. Ignoring.", header.Name), "link", target, "linkedTo", original, "reason", err) | ||
continue | ||
} | ||
} | ||
} | ||
} | ||
|
||
// resolveLinkPaths determines if oldname is an absolute path or a relative | ||
// path, and returns oldname relative to newname if necessary. | ||
func resolveLinkPaths(oldname, newname string) (string, string) { | ||
if filepath.IsAbs(oldname) { | ||
return oldname, newname | ||
} | ||
|
||
linkDir := filepath.Dir(newname) | ||
// If the newname is at the root of the filesystem, but the oldname is | ||
// relative, we'll swap out the value we get from filepath.Dir for a / to | ||
// allow relative pathing to resolve. This strips `..` references given the | ||
// link exists at the very base of the filesystem. In effect, it converts | ||
// oldname to an absolute path | ||
if linkDir == "." { | ||
linkDir = "/" | ||
} | ||
|
||
return filepath.Join(linkDir, oldname), newname | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is incorrect for hard links: their targets are relative to the root directory of the layer archive. For confirmation, an example of an image with such hard links is docker.io/library/busybox. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure why busybox would be relevant here, since all our images must be based off of UBI...but if you look at busybox...it doesn't have an
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The busybox image is relevant in that it provides an example of how hard links are stored in tar archives and container image layers, and it was an image that I remembered included hard links in its layers. If it has to be UBI-based to matter, feel free to build this and examine the added layers: FROM registry.access.redhat.com/ubi8
RUN echo hello > /usr/local/hello.txt
RUN ln /usr/local/hello.txt /usr/local/hello2.txt There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 @nalind, thanks. I misread my test tar archives - I only treated link origins with Latest push should implement this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think what was previously done (passing the value from the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nalind I believe this resolves correctly now for both symlinks and hardlinks. From what I see, an image that looks like this: FROM registry.access.redhat.com/ubi8
RUN ln -v /etc/os-release /os-release
RUN ln -sf /etc/os-release /os-release-soft Has no issue linking the symlinks and hard links. E.g.
You might notice /os-release is hardlinked to /etc/os-release, which itself is a link. I see some issues with links to links to links (spanning link types), but GNU tar seems to exhibit the same issue, and the running container based on the image also sees the broken link. To that end, I'm thinking this is close enough to what we expect to be written to disk for most tasks Preflight needs to execute. Let me know if you see something different. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really think that trying to mix the handling of symbolic links with the handling of hard links is adding more complexity than it's removing. For symbolic links, the If it's possible that any of the images being analyzed will attempt tricks by having the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair enough. I've split the two, and as discussed, added a few safeguards around the writing of symlinks that may be broken as present in the archive, but may point to things outside of our extraction base directory. I was unable to get rid of the link resolution code even with splitting it out, but overall I think the hardlink logic functions a bit better (I saw a hardlink testcase resolve that was previously broken). PTAL. |
||
} | ||
|
||
// writeCertImage takes imageRef and writes it to disk as JSON representing a pyxis.CertImage | ||
// struct. The file is written at path certification.DefaultCertImageFilename. | ||
// | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now happening in every branch. Perhaps it should be outside the switch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you mean, but I think it has to happen in the switch because it conditionally happens in some of the cases (e.g. Symlinks), and must happen (though we could probably make it work) in others (e.g. Directories)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't conditionally happen though. It happens in every case. The only thing that changes is that
target
is either the real thing in the case of tar.TypeDir, or just the Dir portion of the path.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Synced up with @bcrochet but to document it here: Symlinks are skipped if they refer to a path outside of the tarball root once fully resolved (e.g. by way of
../../../../
). We skip them without writing anything. For this reason, we cannot write before the case statement.