From d4dbf03884769b61c8faa89c494575b7d1919893 Mon Sep 17 00:00:00 2001 From: "Jose R. Gonzalez" Date: Wed, 26 Jun 2024 15:54:37 -0500 Subject: [PATCH] Ensure files and links have missing parent dirs created. Support hard links Signed-off-by: Jose R. Gonzalez --- internal/engine/engine.go | 66 ++++++++++++++++++++++++++++++++-- internal/engine/engine_test.go | 15 ++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 546302cb..7cbd6f97 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -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,70 @@ 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. + if !strings.HasPrefix(original, 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", original) + continue + } + 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 +} + // writeCertImage takes imageRef and writes it to disk as JSON representing a pyxis.CertImage // struct. The file is written at path certification.DefaultCertImageFilename. // diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index 5f2ea471..834b0ccb 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -379,6 +379,21 @@ var _ = Describe("Check Name Queries", func() { }) }) +var _ = Describe("Link Path Resolution", func() { + DescribeTable( + "Link targets should resolve correctly", + func(old, new, expectedOld, expectedNew string) { + resO, resN := resolveLinkPaths(old, new) + Expect(resO).To(Equal(expectedOld)) + Expect(resN).To(Equal(expectedNew)) + }, + Entry("Link at root with relative origin", "../usr/lib/file", "file", "/usr/lib/file", "file"), + Entry("Origin is absolute", "/usr/lib/file", "file", "/usr/lib/file", "file"), + Entry("Link in dir with relative origin", "../usr/lib/file", "etc/file", "usr/lib/file", "etc/file"), + Entry("Link in dir with relative origin and up multiple levels", "../../cfg/file", "etc/foo/file", "cfg/file", "etc/foo/file"), + ) +}) + // writeTarball writes a tar archive to out with filename containing contents at the base path // with extra bytes written at the end of length extraBytes. // note: this should only be used as a helper function in tests