diff --git a/internal/slicer/slicer.go b/internal/slicer/slicer.go index ce7911621..6e7ec433c 100644 --- a/internal/slicer/slicer.go +++ b/internal/slicer/slicer.go @@ -127,6 +127,7 @@ func Run(options *RunOptions) error { } extractPackage[sourcePath] = append(extractPackage[sourcePath], tarball.ExtractInfo{ Path: targetPath, + Mode: pathInfo.Mode, Context: slice, }) } else { diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index d6ef9ca0d..0214ddd25 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -454,6 +454,304 @@ var slicerTests = []slicerTest{{ "/dir/text-file-1": "file 0644 5b41362b {test-package_myslice}", "/foo/text-file-2": "file 0644 d98cf53e 5b41362b {test-package_myslice}", }, +}, { + summary: "Copied file and directory take the listed mode", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /dir/: {mode: 0700} + /dir/file: {mode: 0600} + /other-dir/file-copy: {copy: /dir/file, mode: 0640} + `, + }, + filesystem: map[string]string{ + "/dir/": "dir 0700", + "/dir/file": "file 0600 cc55e2ec", + "/other-dir/": "dir 0755", + "/other-dir/file-copy": "file 0640 cc55e2ec", + }, + manifestPaths: map[string]string{ + "/dir/": "dir 0700 {test-package_myslice}", + "/dir/file": "file 0600 cc55e2ec {test-package_myslice}", + "/other-dir/file-copy": "file 0640 cc55e2ec {test-package_myslice}", + }, +}, { + summary: "Same source copied twice, listed mode applies to one destination only", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /copy-a: {copy: /dir/file, mode: 0600} + /copy-b: {copy: /dir/file} + `, + }, + filesystem: map[string]string{ + "/copy-a": "file 0600 cc55e2ec", + "/copy-b": "file 0644 cc55e2ec", + }, + manifestPaths: map[string]string{ + "/copy-a": "file 0600 cc55e2ec {test-package_myslice}", + "/copy-b": "file 0644 cc55e2ec {test-package_myslice}", + }, +}, { + summary: "Hard link extracted on its own takes the listed mode", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Reg(0644, "./file", "foo"), + testutil.Hrd(0644, "./hardlink", "./file"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /hardlink: {mode: 0600} + `, + }, + filesystem: map[string]string{ + "/hardlink": "file 0600 2c26b46b", + }, + manifestPaths: map[string]string{ + "/hardlink": "file 0600 2c26b46b {test-package_myslice}", + }, +}, { + summary: "Hard link with a mode other than its target's is a file of its own", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Reg(0644, "./file", "foo"), + testutil.Hrd(0644, "./hardlink", "./file"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /file: + /hardlink: {mode: 0600} + `, + }, + filesystem: map[string]string{ + "/file": "file 0644 2c26b46b", + "/hardlink": "file 0600 2c26b46b", + }, + manifestPaths: map[string]string{ + "/file": "file 0644 2c26b46b {test-package_myslice}", + "/hardlink": "file 0600 2c26b46b {test-package_myslice}", + }, +}, { + summary: "Pending hard links with different modes do not share an inode", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Reg(0644, "./file", "foo"), + testutil.Hrd(0644, "./hardlink1", "./file"), + testutil.Hrd(0644, "./hardlink2", "./file"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /hardlink1: {mode: 0600} + /hardlink2: + `, + }, + filesystem: map[string]string{ + "/hardlink1": "file 0600 2c26b46b", + "/hardlink2": "file 0644 2c26b46b", + }, + manifestPaths: map[string]string{ + "/hardlink1": "file 0600 2c26b46b {test-package_myslice}", + "/hardlink2": "file 0644 2c26b46b {test-package_myslice}", + }, +}, { + summary: "Pending hard links with the same mode share an inode", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Reg(0644, "./file", "foo"), + testutil.Hrd(0644, "./hardlink1", "./file"), + testutil.Hrd(0644, "./hardlink2", "./file"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /hardlink1: {mode: 0600} + /hardlink2: {mode: 0600} + `, + }, + filesystem: map[string]string{ + "/hardlink1": "file 0600 2c26b46b <1>", + "/hardlink2": "file 0600 2c26b46b <1>", + }, + manifestPaths: map[string]string{ + "/hardlink1": "file 0600 2c26b46b <1> {test-package_myslice}", + "/hardlink2": "file 0600 2c26b46b <1> {test-package_myslice}", + }, +}, { + summary: "Hard link with the same listed mode as its target shares its inode", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Reg(0644, "./file", "foo"), + testutil.Hrd(0644, "./hardlink", "./file"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /file: {mode: 0600} + /hardlink: {mode: 0600} + `, + }, + filesystem: map[string]string{ + "/file": "file 0600 2c26b46b <1>", + "/hardlink": "file 0600 2c26b46b <1>", + }, + manifestPaths: map[string]string{ + "/file": "file 0600 2c26b46b <1> {test-package_myslice}", + "/hardlink": "file 0600 2c26b46b <1> {test-package_myslice}", + }, +}, { + summary: "Hard link with the same listed setuid mode as its target shares its inode", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Reg(0644, "./file", "foo"), + testutil.Hrd(0644, "./hardlink", "./file"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /file: {mode: 04755} + /hardlink: {mode: 04755} + `, + }, + // The dump does not show the setuid bit; the shared inode is the point. + filesystem: map[string]string{ + "/file": "file 0755 2c26b46b <1>", + "/hardlink": "file 0755 2c26b46b <1>", + }, + manifestPaths: map[string]string{ + "/file": "file 0755 2c26b46b <1> {test-package_myslice}", + "/hardlink": "file 0755 2c26b46b <1> {test-package_myslice}", + }, +}, { + summary: "Hard link without the setuid bit of its target is a file of its own", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Reg(0644, "./file", "foo"), + testutil.Hrd(0644, "./hardlink", "./file"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /file: {mode: 04755} + /hardlink: {mode: 0755} + `, + }, + filesystem: map[string]string{ + "/file": "file 0755 2c26b46b", + "/hardlink": "file 0755 2c26b46b", + }, + manifestPaths: map[string]string{ + "/file": "file 0755 2c26b46b {test-package_myslice}", + "/hardlink": "file 0755 2c26b46b {test-package_myslice}", + }, +}, { + summary: "Hard link with a listed mode under a directory the tarball does not carry", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Reg(0644, "./file", "foo"), + testutil.Hrd(0644, "./dir/hardlink", "./file"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /file: + /dir/hardlink: {mode: 0600} + `, + }, + filesystem: map[string]string{ + "/file": "file 0644 2c26b46b", + "/dir/": "dir 0755", + "/dir/hardlink": "file 0600 2c26b46b", + }, + manifestPaths: map[string]string{ + "/file": "file 0644 2c26b46b {test-package_myslice}", + "/dir/hardlink": "file 0600 2c26b46b {test-package_myslice}", + }, +}, { + summary: "Listed mode on a path a glob also matches is a diverging mode", + slices: []setup.SliceKey{ + {"test-package", "myslice1"}, + {"test-package", "myslice2"}, + }, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice1: + contents: + /dir/fil*: + myslice2: + contents: + /dir/file: {mode: 0600} + `, + }, + error: `cannot extract from package "test-package": path /dir/file requested twice with diverging mode: 0600 != 0000`, }, { summary: "Script: use 'until' to remove file after mutate", slices: []setup.SliceKey{{"test-package", "myslice"}}, diff --git a/internal/tarball/extract.go b/internal/tarball/extract.go index 40db2cbac..ff1dff07c 100644 --- a/internal/tarball/extract.go +++ b/internal/tarball/extract.go @@ -180,19 +180,17 @@ func extractData(pkgReader io.ReadSeeker, options *ExtractOptions) error { } var pathReader io.Reader = tarReader + tarMode := tarHeader.Mode for targetPath, extractInfos := range targetPaths { if contentIsCached { pathReader = bytes.NewReader(contentCache) } - mode := extractInfos[0].Mode - for _, extractInfo := range extractInfos { - if extractInfo.Mode != mode { - if mode < extractInfo.Mode { - mode, extractInfo.Mode = extractInfo.Mode, mode - } - return fmt.Errorf("path %s requested twice with diverging mode: 0%03o != 0%03o", targetPath, mode, extractInfo.Mode) - } + mode, err := listedMode(targetPath, extractInfos) + if err != nil { + return err } + // Each target starts from the mode in the tarball. + tarHeader.Mode = tarMode if mode != 0 { tarHeader.Mode = int64(mode) } @@ -220,23 +218,35 @@ func extractData(pkgReader io.ReadSeeker, options *ExtractOptions) error { } } link := tarHeader.Linkname + deferLink := false if tarHeader.Typeflag == tar.TypeLink { // A hard link requires the real path of the target file. link = filepath.Join(options.TargetDir, link) + if mode != 0 { + // A hard link shares the mode of its target, so one + // with a listed mode of its own is extracted as a file + // of its own in the second pass. + info, err := os.Lstat(link) + if err == nil && modeBits(info.Mode()) != modeBits(tarHeader.FileInfo().Mode()) { + deferLink = true + } + } } - // Create the entry itself. - createOptions := &fsutil.CreateOptions{ - Root: options.TargetDir, - Path: targetPath, - Mode: tarHeader.FileInfo().Mode(), - Data: pathReader, - Link: link, - MakeParents: true, - OverrideMode: true, + if !deferLink { + // Create the entry itself. + createOptions := &fsutil.CreateOptions{ + Root: options.TargetDir, + Path: targetPath, + Mode: tarHeader.FileInfo().Mode(), + Data: pathReader, + Link: link, + MakeParents: true, + OverrideMode: true, + } + err = options.Create(extractInfos, createOptions) } - err := options.Create(extractInfos, createOptions) - if err != nil && os.IsNotExist(err) && tarHeader.Typeflag == tar.TypeLink { + if deferLink || 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. @@ -326,36 +336,66 @@ func extractHardLinks(pkgReader io.ReadSeeker, opts *extractHardLinkOptions) err continue } - // 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. - createOptions := &fsutil.CreateOptions{ - Root: opts.TargetDir, - Path: links[0].path, - Mode: tarHeader.FileInfo().Mode(), - Data: tarReader, + // The pending links are grouped by listed mode. The first entry of a + // group is created as a file with the content of the target path and + // the rest of the group are hard links to it. Groups with different + // modes cannot share an inode, so each gets a file of its own. + groups := make(map[uint][]pendingHardLink) + var modes []uint + for _, link := range links { + mode, err := listedMode(link.path, link.extractInfos) + if err != nil { + return err + } + if _, ok := groups[mode]; !ok { + modes = append(modes, mode) + } + groups[mode] = append(groups[mode], link) } - err = opts.Create(links[0].extractInfos, createOptions) - if err != nil { - return err + var content []byte + if len(groups) > 1 { + content, err = io.ReadAll(tarReader) + if err != nil { + return err + } } - - // Create the remaining hard links. - for _, link := range links[1:] { + tarMode := tarHeader.Mode + for _, mode := range modes { + group := groups[mode] + tarHeader.Mode = tarMode + if mode != 0 { + tarHeader.Mode = int64(mode) + } + var data io.Reader = tarReader + if content != nil { + data = bytes.NewReader(content) + } + absLink := filepath.Join(opts.TargetDir, group[0].path) createOptions := &fsutil.CreateOptions{ - Root: opts.TargetDir, - Path: link.path, - Mode: tarHeader.FileInfo().Mode(), - // Link to the first file extracted for the hard links. - Link: absLink, + Root: opts.TargetDir, + Path: group[0].path, + Mode: tarHeader.FileInfo().Mode(), + Data: data, + MakeParents: true, } - err := opts.Create(link.extractInfos, createOptions) + err = opts.Create(group[0].extractInfos, createOptions) if err != nil { return err } + for _, link := range group[1:] { + createOptions := &fsutil.CreateOptions{ + Root: opts.TargetDir, + Path: link.path, + Mode: tarHeader.FileInfo().Mode(), + // Link to the first file extracted for the group. + Link: absLink, + MakeParents: true, + } + err = opts.Create(link.extractInfos, createOptions) + if err != nil { + return err + } + } } delete(opts.pendingLinks, sourcePath) } @@ -375,6 +415,25 @@ func extractHardLinks(pkgReader io.ReadSeeker, opts *extractHardLinkOptions) err return nil } +// modeBits returns the permission and special bits of a mode. +func modeBits(mode fs.FileMode) fs.FileMode { + return mode & (fs.ModePerm | fs.ModeSetuid | fs.ModeSetgid | fs.ModeSticky) +} + +// listedMode returns the mode the extract infos ask for, 0 when none does. +func listedMode(targetPath string, extractInfos []ExtractInfo) (uint, error) { + mode := extractInfos[0].Mode + for _, extractInfo := range extractInfos { + if extractInfo.Mode != mode { + if mode < extractInfo.Mode { + mode, extractInfo.Mode = extractInfo.Mode, mode + } + return 0, fmt.Errorf("path %s requested twice with diverging mode: 0%03o != 0%03o", targetPath, mode, extractInfo.Mode) + } + } + return mode, nil +} + func parentDirs(path string) []string { path = filepath.Clean(path) parents := make([]string, strings.Count(path, "/"))