github.com/moby/docker@v26.1.3+incompatible/pkg/archive/archive_unix_test.go (about)

     1  //go:build !windows
     2  
     3  package archive // import "github.com/docker/docker/pkg/archive"
     4  
     5  import (
     6  	"archive/tar"
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"strings"
    14  	"syscall"
    15  	"testing"
    16  
    17  	"github.com/containerd/containerd/pkg/userns"
    18  	"github.com/docker/docker/pkg/system"
    19  	"golang.org/x/sys/unix"
    20  	"gotest.tools/v3/assert"
    21  	is "gotest.tools/v3/assert/cmp"
    22  	"gotest.tools/v3/skip"
    23  )
    24  
    25  func TestCanonicalTarName(t *testing.T) {
    26  	cases := []struct {
    27  		in       string
    28  		isDir    bool
    29  		expected string
    30  	}{
    31  		{"foo", false, "foo"},
    32  		{"foo", true, "foo/"},
    33  		{"foo/bar", false, "foo/bar"},
    34  		{"foo/bar", true, "foo/bar/"},
    35  	}
    36  	for _, v := range cases {
    37  		if canonicalTarName(v.in, v.isDir) != v.expected {
    38  			t.Fatalf("wrong canonical tar name. expected:%s got:%s", v.expected, canonicalTarName(v.in, v.isDir))
    39  		}
    40  	}
    41  }
    42  
    43  func TestChmodTarEntry(t *testing.T) {
    44  	cases := []struct {
    45  		in, expected os.FileMode
    46  	}{
    47  		{0o000, 0o000},
    48  		{0o777, 0o777},
    49  		{0o644, 0o644},
    50  		{0o755, 0o755},
    51  		{0o444, 0o444},
    52  	}
    53  	for _, v := range cases {
    54  		if out := chmodTarEntry(v.in); out != v.expected {
    55  			t.Fatalf("wrong chmod. expected:%v got:%v", v.expected, out)
    56  		}
    57  	}
    58  }
    59  
    60  func TestTarWithHardLink(t *testing.T) {
    61  	origin, err := os.MkdirTemp("", "docker-test-tar-hardlink")
    62  	assert.NilError(t, err)
    63  	defer os.RemoveAll(origin)
    64  
    65  	err = os.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0o700)
    66  	assert.NilError(t, err)
    67  
    68  	err = os.Link(filepath.Join(origin, "1"), filepath.Join(origin, "2"))
    69  	assert.NilError(t, err)
    70  
    71  	var i1, i2 uint64
    72  	i1, err = getNlink(filepath.Join(origin, "1"))
    73  	assert.NilError(t, err)
    74  
    75  	// sanity check that we can hardlink
    76  	if i1 != 2 {
    77  		t.Skipf("skipping since hardlinks don't work here; expected 2 links, got %d", i1)
    78  	}
    79  
    80  	dest, err := os.MkdirTemp("", "docker-test-tar-hardlink-dest")
    81  	assert.NilError(t, err)
    82  	defer os.RemoveAll(dest)
    83  
    84  	// we'll do this in two steps to separate failure
    85  	fh, err := Tar(origin, Uncompressed)
    86  	assert.NilError(t, err)
    87  
    88  	// ensure we can read the whole thing with no error, before writing back out
    89  	buf, err := io.ReadAll(fh)
    90  	assert.NilError(t, err)
    91  
    92  	bRdr := bytes.NewReader(buf)
    93  	err = Untar(bRdr, dest, &TarOptions{Compression: Uncompressed})
    94  	assert.NilError(t, err)
    95  
    96  	i1, err = getInode(filepath.Join(dest, "1"))
    97  	assert.NilError(t, err)
    98  
    99  	i2, err = getInode(filepath.Join(dest, "2"))
   100  	assert.NilError(t, err)
   101  
   102  	assert.Check(t, is.Equal(i1, i2))
   103  }
   104  
   105  func TestTarWithHardLinkAndRebase(t *testing.T) {
   106  	tmpDir, err := os.MkdirTemp("", "docker-test-tar-hardlink-rebase")
   107  	assert.NilError(t, err)
   108  	defer os.RemoveAll(tmpDir)
   109  
   110  	origin := filepath.Join(tmpDir, "origin")
   111  	err = os.Mkdir(origin, 0o700)
   112  	assert.NilError(t, err)
   113  
   114  	err = os.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0o700)
   115  	assert.NilError(t, err)
   116  
   117  	err = os.Link(filepath.Join(origin, "1"), filepath.Join(origin, "2"))
   118  	assert.NilError(t, err)
   119  
   120  	var i1, i2 uint64
   121  	i1, err = getNlink(filepath.Join(origin, "1"))
   122  	assert.NilError(t, err)
   123  
   124  	// sanity check that we can hardlink
   125  	if i1 != 2 {
   126  		t.Skipf("skipping since hardlinks don't work here; expected 2 links, got %d", i1)
   127  	}
   128  
   129  	dest := filepath.Join(tmpDir, "dest")
   130  	bRdr, err := TarResourceRebase(origin, "origin")
   131  	assert.NilError(t, err)
   132  
   133  	dstDir, srcBase := SplitPathDirEntry(origin)
   134  	_, dstBase := SplitPathDirEntry(dest)
   135  	content := RebaseArchiveEntries(bRdr, srcBase, dstBase)
   136  	err = Untar(content, dstDir, &TarOptions{Compression: Uncompressed, NoLchown: true, NoOverwriteDirNonDir: true})
   137  	assert.NilError(t, err)
   138  
   139  	i1, err = getInode(filepath.Join(dest, "1"))
   140  	assert.NilError(t, err)
   141  	i2, err = getInode(filepath.Join(dest, "2"))
   142  	assert.NilError(t, err)
   143  
   144  	assert.Check(t, is.Equal(i1, i2))
   145  }
   146  
   147  // TestUntarParentPathPermissions is a regression test to check that missing
   148  // parent directories are created with the expected permissions
   149  func TestUntarParentPathPermissions(t *testing.T) {
   150  	skip.If(t, os.Getuid() != 0, "skipping test that requires root")
   151  	buf := &bytes.Buffer{}
   152  	w := tar.NewWriter(buf)
   153  	err := w.WriteHeader(&tar.Header{Name: "foo/bar"})
   154  	assert.NilError(t, err)
   155  	tmpDir, err := os.MkdirTemp("", t.Name())
   156  	assert.NilError(t, err)
   157  	defer os.RemoveAll(tmpDir)
   158  	err = Untar(buf, tmpDir, nil)
   159  	assert.NilError(t, err)
   160  
   161  	fi, err := os.Lstat(filepath.Join(tmpDir, "foo"))
   162  	assert.NilError(t, err)
   163  	assert.Equal(t, fi.Mode(), 0o755|os.ModeDir)
   164  }
   165  
   166  func getNlink(path string) (uint64, error) {
   167  	stat, err := os.Stat(path)
   168  	if err != nil {
   169  		return 0, err
   170  	}
   171  	statT, ok := stat.Sys().(*syscall.Stat_t)
   172  	if !ok {
   173  		return 0, fmt.Errorf("expected type *syscall.Stat_t, got %t", stat.Sys())
   174  	}
   175  	// We need this conversion on ARM64
   176  	//nolint: unconvert
   177  	return uint64(statT.Nlink), nil
   178  }
   179  
   180  func getInode(path string) (uint64, error) {
   181  	stat, err := os.Stat(path)
   182  	if err != nil {
   183  		return 0, err
   184  	}
   185  	statT, ok := stat.Sys().(*syscall.Stat_t)
   186  	if !ok {
   187  		return 0, fmt.Errorf("expected type *syscall.Stat_t, got %t", stat.Sys())
   188  	}
   189  	return statT.Ino, nil
   190  }
   191  
   192  func TestTarWithBlockCharFifo(t *testing.T) {
   193  	skip.If(t, os.Getuid() != 0, "skipping test that requires root")
   194  	skip.If(t, userns.RunningInUserNS(), "skipping test that requires initial userns")
   195  	origin, err := os.MkdirTemp("", "docker-test-tar-hardlink")
   196  	assert.NilError(t, err)
   197  
   198  	defer os.RemoveAll(origin)
   199  	err = os.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0o700)
   200  	assert.NilError(t, err)
   201  
   202  	err = system.Mknod(filepath.Join(origin, "2"), unix.S_IFBLK, int(system.Mkdev(int64(12), int64(5))))
   203  	assert.NilError(t, err)
   204  	err = system.Mknod(filepath.Join(origin, "3"), unix.S_IFCHR, int(system.Mkdev(int64(12), int64(5))))
   205  	assert.NilError(t, err)
   206  	err = system.Mknod(filepath.Join(origin, "4"), unix.S_IFIFO, int(system.Mkdev(int64(12), int64(5))))
   207  	assert.NilError(t, err)
   208  
   209  	dest, err := os.MkdirTemp("", "docker-test-tar-hardlink-dest")
   210  	assert.NilError(t, err)
   211  	defer os.RemoveAll(dest)
   212  
   213  	// we'll do this in two steps to separate failure
   214  	fh, err := Tar(origin, Uncompressed)
   215  	assert.NilError(t, err)
   216  
   217  	// ensure we can read the whole thing with no error, before writing back out
   218  	buf, err := io.ReadAll(fh)
   219  	assert.NilError(t, err)
   220  
   221  	bRdr := bytes.NewReader(buf)
   222  	err = Untar(bRdr, dest, &TarOptions{Compression: Uncompressed})
   223  	assert.NilError(t, err)
   224  
   225  	changes, err := ChangesDirs(origin, dest)
   226  	assert.NilError(t, err)
   227  
   228  	if len(changes) > 0 {
   229  		t.Fatalf("Tar with special device (block, char, fifo) should keep them (recreate them when untar) : %v", changes)
   230  	}
   231  }
   232  
   233  // TestTarUntarWithXattr is Unix as Lsetxattr is not supported on Windows
   234  func TestTarUntarWithXattr(t *testing.T) {
   235  	skip.If(t, os.Getuid() != 0, "skipping test that requires root")
   236  	if _, err := exec.LookPath("setcap"); err != nil {
   237  		t.Skip("setcap not installed")
   238  	}
   239  	if _, err := exec.LookPath("getcap"); err != nil {
   240  		t.Skip("getcap not installed")
   241  	}
   242  
   243  	origin, err := os.MkdirTemp("", "docker-test-untar-origin")
   244  	assert.NilError(t, err)
   245  	defer os.RemoveAll(origin)
   246  	err = os.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0o700)
   247  	assert.NilError(t, err)
   248  
   249  	err = os.WriteFile(filepath.Join(origin, "2"), []byte("welcome!"), 0o700)
   250  	assert.NilError(t, err)
   251  	err = os.WriteFile(filepath.Join(origin, "3"), []byte("will be ignored"), 0o700)
   252  	assert.NilError(t, err)
   253  	// there is no known Go implementation of setcap/getcap with support for v3 file capability
   254  	out, err := exec.Command("setcap", "cap_block_suspend+ep", filepath.Join(origin, "2")).CombinedOutput()
   255  	assert.NilError(t, err, string(out))
   256  
   257  	tarball, err := Tar(origin, Uncompressed)
   258  	assert.NilError(t, err)
   259  	defer tarball.Close()
   260  	rdr := tar.NewReader(tarball)
   261  	for {
   262  		h, err := rdr.Next()
   263  		if err == io.EOF {
   264  			break
   265  		}
   266  		assert.NilError(t, err)
   267  		capability, hasxattr := h.PAXRecords["SCHILY.xattr.security.capability"]
   268  		switch h.Name {
   269  		case "2":
   270  			if assert.Check(t, hasxattr, "tar entry %q should have the 'security.capability' xattr", h.Name) {
   271  				assert.Check(t, len(capability) > 0, "tar entry %q has a blank 'security.capability' xattr value")
   272  			}
   273  		default:
   274  			assert.Check(t, !hasxattr, "tar entry %q should not have the 'security.capability' xattr", h.Name)
   275  		}
   276  	}
   277  
   278  	for _, c := range []Compression{
   279  		Uncompressed,
   280  		Gzip,
   281  	} {
   282  		changes, err := tarUntar(t, origin, &TarOptions{
   283  			Compression:     c,
   284  			ExcludePatterns: []string{"3"},
   285  		})
   286  		if err != nil {
   287  			t.Fatalf("Error tar/untar for compression %s: %s", c.Extension(), err)
   288  		}
   289  
   290  		if len(changes) != 1 || changes[0].Path != "/3" {
   291  			t.Fatalf("Unexpected differences after tarUntar: %v", changes)
   292  		}
   293  		out, err := exec.Command("getcap", filepath.Join(origin, "2")).CombinedOutput()
   294  		assert.NilError(t, err, string(out))
   295  		assert.Check(t, is.Contains(string(out), "cap_block_suspend=ep"), "untar should have kept the 'security.capability' xattr")
   296  	}
   297  }
   298  
   299  func TestCopyInfoDestinationPathSymlink(t *testing.T) {
   300  	tmpDir, _ := getTestTempDirs(t)
   301  	defer removeAllPaths(tmpDir)
   302  
   303  	root := strings.TrimRight(tmpDir, "/") + "/"
   304  
   305  	type FileTestData struct {
   306  		resource FileData
   307  		file     string
   308  		expected CopyInfo
   309  	}
   310  
   311  	testData := []FileTestData{
   312  		// Create a directory: /tmp/archive-copy-test*/dir1
   313  		// Test will "copy" file1 to dir1
   314  		{resource: FileData{filetype: Dir, path: "dir1", permissions: 0o740}, file: "file1", expected: CopyInfo{Path: root + "dir1/file1", Exists: false, IsDir: false}},
   315  
   316  		// Create a symlink directory to dir1: /tmp/archive-copy-test*/dirSymlink -> dir1
   317  		// Test will "copy" file2 to dirSymlink
   318  		{resource: FileData{filetype: Symlink, path: "dirSymlink", contents: root + "dir1", permissions: 0o600}, file: "file2", expected: CopyInfo{Path: root + "dirSymlink/file2", Exists: false, IsDir: false}},
   319  
   320  		// Create a file in tmp directory: /tmp/archive-copy-test*/file1
   321  		// Test to cover when the full file path already exists.
   322  		{resource: FileData{filetype: Regular, path: "file1", permissions: 0o600}, file: "", expected: CopyInfo{Path: root + "file1", Exists: true}},
   323  
   324  		// Create a directory: /tmp/archive-copy*/dir2
   325  		// Test to cover when the full directory path already exists
   326  		{resource: FileData{filetype: Dir, path: "dir2", permissions: 0o740}, file: "", expected: CopyInfo{Path: root + "dir2", Exists: true, IsDir: true}},
   327  
   328  		// Create a symlink to a non-existent target: /tmp/archive-copy*/symlink1 -> noSuchTarget
   329  		// Negative test to cover symlinking to a target that does not exit
   330  		{resource: FileData{filetype: Symlink, path: "symlink1", contents: "noSuchTarget", permissions: 0o600}, file: "", expected: CopyInfo{Path: root + "noSuchTarget", Exists: false}},
   331  
   332  		// Create a file in tmp directory for next test: /tmp/existingfile
   333  		{resource: FileData{filetype: Regular, path: "existingfile", permissions: 0o600}, file: "", expected: CopyInfo{Path: root + "existingfile", Exists: true}},
   334  
   335  		// Create a symlink to an existing file: /tmp/archive-copy*/symlink2 -> /tmp/existingfile
   336  		// Test to cover when the parent directory of a new file is a symlink
   337  		{resource: FileData{filetype: Symlink, path: "symlink2", contents: "existingfile", permissions: 0o600}, file: "", expected: CopyInfo{Path: root + "existingfile", Exists: true}},
   338  	}
   339  
   340  	var dirs []FileData
   341  	for _, data := range testData {
   342  		dirs = append(dirs, data.resource)
   343  	}
   344  	provisionSampleDir(t, tmpDir, dirs)
   345  
   346  	for _, info := range testData {
   347  		p := filepath.Join(tmpDir, info.resource.path, info.file)
   348  		ci, err := CopyInfoDestinationPath(p)
   349  		assert.Check(t, err)
   350  		assert.Check(t, is.DeepEqual(info.expected, ci))
   351  	}
   352  }