Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions internal/slicer/slicer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1661,6 +1661,54 @@ var slicerTests = []slicerTest{{
"/symlink": "symlink ./file <1> {test-package_myslice}",
"/hardlink": "symlink ./file <1> {test-package_myslice}",
},
}, {
summary: "Hard link target path provided by another package extracted first",
slices: []setup.SliceKey{
{"test-package2", "myslice"},
{"test-package1", "myslice"},
},
pkgs: []*testutil.TestPackage{{
Name: "test-package1",
Data: testutil.MustMakeDeb([]testutil.TarEntry{
testutil.Dir(0755, "./"),
testutil.Reg(0644, "./file", "foo"),
testutil.Hrd(0644, "./hardlink", "./file"),
}),
}, {
Name: "test-package2",
Data: testutil.MustMakeDeb([]testutil.TarEntry{
testutil.Dir(0755, "./"),
testutil.Reg(0644, "./file", "bar"),
}),
}},
release: map[string]string{
"slices/mydir/test-package1.yaml": `
package: test-package1
slices:
myslice:
essential:
- test-package2_myslice
contents:
/hardlink:
`,
"slices/mydir/test-package2.yaml": `
package: test-package2
slices:
myslice:
contents:
/file:
`,
},
// test-package2 is extracted first, so its /file is on disk when the
// hard link is extracted. The link carries test-package1's content.
filesystem: map[string]string{
"/file": "file 0644 fcde2b2e",
"/hardlink": "file 0644 2c26b46b",
},
manifestPaths: map[string]string{
"/file": "file 0644 fcde2b2e {test-package2_myslice}",
"/hardlink": "file 0644 2c26b46b {test-package1_myslice}",
},
}, {
summary: "Hard link identifiers are unique across packages",
slices: []setup.SliceKey{
Expand Down
112 changes: 92 additions & 20 deletions internal/tarball/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ func extractData(pkgReader io.ReadSeeker, options *ExtractOptions) error {
// before the entry for the file itself. This is the case for .deb files but
// not for all tarballs.
tarDirMode := make(map[string]fs.FileMode)
// Entries extracted by this package at their own path. A hard link is
// only created against one of these: anything else at the link target
// came from another package.
extracted := make(map[string]bool)
// Where each hard link entry of the tarball points, so that a link that
// could not be created can find the content at the end of a chain of
// links once the whole tarball has been seen.
linkTargets := make(map[string]string)
tarReader := tar.NewReader(dataReader)
for {
tarHeader, err := tarReader.Next()
Expand All @@ -139,6 +147,11 @@ func extractData(pkgReader io.ReadSeeker, options *ExtractOptions) error {
if sourceIsDir {
tarDirMode[sourcePath] = tarHeader.FileInfo().Mode()
}
if tarHeader.Typeflag == tar.TypeLink {
if target, ok := sanitizeLinkTarget(tarHeader.Linkname); ok {
linkTargets[sourcePath] = target
}
}

// Find all globs and copies that require this source, and map them by
// their target paths on disk.
Expand Down Expand Up @@ -221,8 +234,24 @@ func extractData(pkgReader io.ReadSeeker, options *ExtractOptions) error {
}
link := tarHeader.Linkname
if tarHeader.Typeflag == tar.TypeLink {
relLinkPath, ok := sanitizeLinkTarget(tarHeader.Linkname)
if !ok {
return fmt.Errorf("invalid link target %s", tarHeader.Linkname)
}
if !extracted[relLinkPath] {
// The content was not extracted previously, so the
// hard link entry is dealt with once the whole
// tarball has been seen.
info := pendingHardLink{
path: targetPath,
mode: tarHeader.FileInfo().Mode(),
extractInfos: extractInfos,
}
pendingHardLinks[relLinkPath] = append(pendingHardLinks[relLinkPath], info)
continue
}
// A hard link requires the real path of the target file.
link = filepath.Join(options.TargetDir, link)
link = filepath.Join(options.TargetDir, relLinkPath)
}

// Create the entry itself.
Expand All @@ -236,22 +265,45 @@ func extractData(pkgReader io.ReadSeeker, options *ExtractOptions) error {
OverrideMode: true,
}
err := options.Create(extractInfos, createOptions)
if err != nil && os.IsNotExist(err) && tarHeader.Typeflag == tar.TypeLink {
// The hard link could not be created because the content
// was not extracted previously. Add this hard link entry
// to the pending list to extract later.
relLinkPath, ok := sanitizeTarPath(tarHeader.Linkname)
if !ok {
return fmt.Errorf("invalid link target %s", tarHeader.Linkname)
if err != nil {
return err
}
if targetPath == sourcePath {
extracted[targetPath] = true
}
}
}

// A pending link waits for the content at the end of its chain of links.
// When that content was extracted by this package the link is created
// against it now; otherwise the second pass extracts the content.
for linkPath, links := range pendingHardLinks {
endPath := linkPath
for i := 0; i < len(linkTargets); i++ {
next, ok := linkTargets[endPath]
if !ok {
break
}
endPath = next
}
if extracted[endPath] {
for _, link := range links {
createOptions := &fsutil.CreateOptions{
Root: options.TargetDir,
Path: link.path,
Mode: link.mode,
Link: filepath.Join(options.TargetDir, endPath),
MakeParents: true,
}
info := pendingHardLink{
path: targetPath,
extractInfos: extractInfos,
err := options.Create(link.extractInfos, createOptions)
if err != nil {
return err
}
pendingHardLinks[relLinkPath] = append(pendingHardLinks[relLinkPath], info)
} else if err != nil {
return err
}
delete(pendingHardLinks, linkPath)
} else if endPath != linkPath {
delete(pendingHardLinks, linkPath)
pendingHardLinks[endPath] = append(pendingHardLinks[endPath], links...)
}
}

Expand Down Expand Up @@ -289,6 +341,7 @@ func extractData(pkgReader io.ReadSeeker, options *ExtractOptions) error {

type pendingHardLink struct {
path string
mode fs.FileMode
extractInfos []ExtractInfo
}

Expand Down Expand Up @@ -325,18 +378,27 @@ func extractHardLinks(pkgReader io.ReadSeeker, opts *extractHardLinkOptions) err
if len(links) == 0 {
continue
}
if tarHeader.Typeflag == tar.TypeLink {
// Only a chain of links leading nowhere ends here.
return fmt.Errorf("cannot create hard link %s: %s is a hard link", links[0].path, sourcePath)
}

// For a target path, the first hard link will be created as a file with
// the content of the target path. If there are more pending hard links,
// the remaining ones will be created as hard links with the newly
// created file as their target.
absLink := filepath.Join(opts.TargetDir, links[0].path)
// Extract the content to the first hard link path.
// Extract the content to the first hard link path. A hard link to a
// symlink becomes a symlink with the same target.
createOptions := &fsutil.CreateOptions{
Root: opts.TargetDir,
Path: links[0].path,
Mode: tarHeader.FileInfo().Mode(),
Data: tarReader,
Root: opts.TargetDir,
Path: links[0].path,
Mode: tarHeader.FileInfo().Mode(),
Data: tarReader,
MakeParents: true,
}
if tarHeader.Typeflag == tar.TypeSymlink {
createOptions.Link = tarHeader.Linkname
}
err = opts.Create(links[0].extractInfos, createOptions)
if err != nil {
Expand All @@ -350,7 +412,8 @@ func extractHardLinks(pkgReader io.ReadSeeker, opts *extractHardLinkOptions) err
Path: link.path,
Mode: tarHeader.FileInfo().Mode(),
// Link to the first file extracted for the hard links.
Link: absLink,
Link: absLink,
MakeParents: true,
}
err := opts.Create(link.extractInfos, createOptions)
if err != nil {
Expand Down Expand Up @@ -388,6 +451,15 @@ func parentDirs(path string) []string {
return parents
}

// sanitizeLinkTarget returns the clean path a hard link entry points at, which
// the tarball may spell with or without the leading "./".
func sanitizeLinkTarget(link string) (string, bool) {
if link == "" || link[0] == '/' {
return "", false
}
return filepath.Clean("/" + strings.TrimPrefix(link, "./")), true
}

// sanitizeTarPath removes the leading "./" from the source path in the tarball,
// and verifies that the path is not empty.
func sanitizeTarPath(path string) (string, bool) {
Expand Down
Loading
Loading