github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/oci/layer/tar_extract_test.go (about)

     1  /*
     2   * umoci: Umoci Modifies Open Containers' Images
     3   * Copyright (C) 2016-2020 SUSE LLC
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *    http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  
    18  package layer
    19  
    20  import (
    21  	"archive/tar"
    22  	"bytes"
    23  	"crypto/rand"
    24  	"io"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"runtime"
    29  	"strings"
    30  	"testing"
    31  	"time"
    32  
    33  	rspec "github.com/opencontainers/runtime-spec/specs-go"
    34  	"github.com/opencontainers/umoci/pkg/testutils"
    35  	"github.com/pkg/errors"
    36  	"golang.org/x/sys/unix"
    37  )
    38  
    39  // TODO: Test the parent directory metadata is kept the same when unpacking.
    40  // TODO: Add tests for metadata and consistency.
    41  
    42  // testUnpackEntrySanitiseHelper is a basic helper to check that a tar header
    43  // with the given prefix will resolve to the same path without it during
    44  // unpacking. The "unsafe" version should resolve to the parent directory
    45  // (which will be checked). The rootfs is assumed to be <dir>/rootfs.
    46  func testUnpackEntrySanitiseHelper(t *testing.T, dir, file, prefix string) {
    47  	hostValue := []byte("host content")
    48  	ctrValue := []byte("container content")
    49  
    50  	rootfs := filepath.Join(dir, "rootfs")
    51  
    52  	// Create a host file that we want to make sure doesn't get overwrittern.
    53  	if err := ioutil.WriteFile(filepath.Join(dir, "file"), hostValue, 0644); err != nil {
    54  		t.Fatal(err)
    55  	}
    56  
    57  	// Create our header. We raw prepend the prefix because we are generating
    58  	// invalid tar headers.
    59  	hdr := &tar.Header{
    60  		Name:       prefix + "/" + filepath.Base(file),
    61  		Uid:        os.Getuid(),
    62  		Gid:        os.Getgid(),
    63  		Mode:       0644,
    64  		Size:       int64(len(ctrValue)),
    65  		Typeflag:   tar.TypeReg,
    66  		ModTime:    time.Now(),
    67  		AccessTime: time.Now(),
    68  		ChangeTime: time.Now(),
    69  	}
    70  
    71  	te := NewTarExtractor(UnpackOptions{})
    72  	if err := te.UnpackEntry(rootfs, hdr, bytes.NewBuffer(ctrValue)); err != nil {
    73  		t.Fatalf("unexpected UnpackEntry error: %s", err)
    74  	}
    75  
    76  	hostValueGot, err := ioutil.ReadFile(filepath.Join(dir, "file"))
    77  	if err != nil {
    78  		t.Fatalf("unexpected readfile error on host: %s", err)
    79  	}
    80  
    81  	ctrValueGot, err := ioutil.ReadFile(filepath.Join(rootfs, "file"))
    82  	if err != nil {
    83  		t.Fatalf("unexpected readfile error in ctr: %s", err)
    84  	}
    85  
    86  	if !bytes.Equal(ctrValue, ctrValueGot) {
    87  		t.Errorf("ctr path was not updated: expected='%s' got='%s'", string(ctrValue), string(ctrValueGot))
    88  	}
    89  	if !bytes.Equal(hostValue, hostValueGot) {
    90  		t.Errorf("HOST PATH WAS CHANGED! THIS IS A PATH ESCAPE! expected='%s' got='%s'", string(hostValue), string(hostValueGot))
    91  	}
    92  }
    93  
    94  // TestUnpackEntrySanitiseScoping makes sure that path sanitisation is done
    95  // safely with regards to /../../ prefixes in invalid tar archives.
    96  func TestUnpackEntrySanitiseScoping(t *testing.T) {
    97  	for _, test := range []struct {
    98  		name   string
    99  		prefix string
   100  	}{
   101  		{"GarbagePrefix", "/.."},
   102  		{"DotDotPrefix", ".."},
   103  	} {
   104  		t.Run(test.name, func(t *testing.T) {
   105  			dir, err := ioutil.TempDir("", "umoci-TestUnpackEntrySanitiseScoping")
   106  			if err != nil {
   107  				t.Fatal(err)
   108  			}
   109  			defer os.RemoveAll(dir)
   110  
   111  			rootfs := filepath.Join(dir, "rootfs")
   112  			if err := os.Mkdir(rootfs, 0755); err != nil {
   113  				t.Fatal(err)
   114  			}
   115  
   116  			testUnpackEntrySanitiseHelper(t, dir, filepath.Join("/", test.prefix, "file"), test.prefix)
   117  		})
   118  	}
   119  }
   120  
   121  // TestUnpackEntrySymlinkScoping makes sure that path sanitisation is done
   122  // safely with regards to symlinks path components set to /.. and similar
   123  // prefixes in invalid tar archives (a regular tar archive won't contain stuff
   124  // like that).
   125  func TestUnpackEntrySymlinkScoping(t *testing.T) {
   126  	for _, test := range []struct {
   127  		name   string
   128  		prefix string
   129  	}{
   130  		{"RootPrefix", "/"},
   131  		{"GarbagePrefix1", "/../"},
   132  		{"GarbagePrefix2", "/../../../../../../../../../../../../../../../"},
   133  		{"GarbagePrefix3", "/./.././.././.././.././.././.././.././.././../"},
   134  		{"DotDotPrefix", ".."},
   135  	} {
   136  		t.Run(test.name, func(t *testing.T) {
   137  			dir, err := ioutil.TempDir("", "umoci-TestUnpackEntrySymlinkScoping")
   138  			if err != nil {
   139  				t.Fatal(err)
   140  			}
   141  			defer os.RemoveAll(dir)
   142  
   143  			rootfs := filepath.Join(dir, "rootfs")
   144  			if err := os.Mkdir(rootfs, 0755); err != nil {
   145  				t.Fatal(err)
   146  			}
   147  
   148  			// Create the symlink.
   149  			if err := os.Symlink(test.prefix, filepath.Join(rootfs, "link")); err != nil {
   150  				t.Fatal(err)
   151  			}
   152  
   153  			testUnpackEntrySanitiseHelper(t, dir, filepath.Join("/", test.prefix, "file"), "link")
   154  		})
   155  	}
   156  }
   157  
   158  // TestUnpackEntryParentDir ensures that when UnpackEntry hits a path that
   159  // doesn't have its leading directories, we create all of the parent
   160  // directories.
   161  func TestUnpackEntryParentDir(t *testing.T) {
   162  	dir, err := ioutil.TempDir("", "umoci-TestUnpackEntryParentDir")
   163  	if err != nil {
   164  		t.Fatal(err)
   165  	}
   166  	defer os.RemoveAll(dir)
   167  
   168  	rootfs := filepath.Join(dir, "rootfs")
   169  	if err := os.Mkdir(rootfs, 0755); err != nil {
   170  		t.Fatal(err)
   171  	}
   172  
   173  	ctrValue := []byte("creating parentdirs")
   174  
   175  	// Create our header. We raw prepend the prefix because we are generating
   176  	// invalid tar headers.
   177  	hdr := &tar.Header{
   178  		Name:       "a/b/c/file",
   179  		Uid:        os.Getuid(),
   180  		Gid:        os.Getgid(),
   181  		Mode:       0644,
   182  		Size:       int64(len(ctrValue)),
   183  		Typeflag:   tar.TypeReg,
   184  		ModTime:    time.Now(),
   185  		AccessTime: time.Now(),
   186  		ChangeTime: time.Now(),
   187  	}
   188  
   189  	te := NewTarExtractor(UnpackOptions{})
   190  	if err := te.UnpackEntry(rootfs, hdr, bytes.NewBuffer(ctrValue)); err != nil {
   191  		t.Fatalf("unexpected UnpackEntry error: %s", err)
   192  	}
   193  
   194  	ctrValueGot, err := ioutil.ReadFile(filepath.Join(rootfs, "a/b/c/file"))
   195  	if err != nil {
   196  		t.Fatalf("unexpected readfile error: %s", err)
   197  	}
   198  
   199  	if !bytes.Equal(ctrValue, ctrValueGot) {
   200  		t.Errorf("ctr path was not updated: expected='%s' got='%s'", string(ctrValue), string(ctrValueGot))
   201  	}
   202  }
   203  
   204  // TestUnpackEntryWhiteout checks whether whiteout handling is done correctly,
   205  // as well as ensuring that the metadata of the parent is maintained.
   206  func TestUnpackEntryWhiteout(t *testing.T) {
   207  	for _, test := range []struct {
   208  		name string
   209  		path string
   210  		dir  bool // TODO: Switch to Typeflag
   211  	}{
   212  		{"FileInRoot", "rootpath", false},
   213  		{"HiddenFileInRoot", ".hiddenroot", false},
   214  		{"FileInSubdir", "some/path/file", false},
   215  		{"HiddenFileInSubdir", "another/path/.hiddenfile", false},
   216  		{"DirInRoot", "rootpath", true},
   217  		{"HiddenDirInRoot", ".hiddenroot", true},
   218  		{"DirInSubdir", "some/path/dir", true},
   219  		{"HiddenDirInSubdir", "another/path/.hiddendir", true},
   220  	} {
   221  		t.Run(test.name, func(t *testing.T) {
   222  			testMtime := testutils.Unix(123, 456)
   223  			testAtime := testutils.Unix(789, 111)
   224  
   225  			dir, err := ioutil.TempDir("", "umoci-TestUnpackEntryWhiteout")
   226  			if err != nil {
   227  				t.Fatal(err)
   228  			}
   229  			defer os.RemoveAll(dir)
   230  
   231  			rawDir, rawFile := filepath.Split(test.path)
   232  			wh := filepath.Join(rawDir, whPrefix+rawFile)
   233  
   234  			// Create the parent directory.
   235  			if err := os.MkdirAll(filepath.Join(dir, rawDir), 0755); err != nil {
   236  				t.Fatal(err)
   237  			}
   238  
   239  			// Create the path itself.
   240  			if test.dir {
   241  				if err := os.Mkdir(filepath.Join(dir, test.path), 0755); err != nil {
   242  					t.Fatal(err)
   243  				}
   244  				// Make some subfiles and directories.
   245  				if err := ioutil.WriteFile(filepath.Join(dir, test.path, "file1"), []byte("some value"), 0644); err != nil {
   246  					t.Fatal(err)
   247  				}
   248  				if err := ioutil.WriteFile(filepath.Join(dir, test.path, "file2"), []byte("some value"), 0644); err != nil {
   249  					t.Fatal(err)
   250  				}
   251  				if err := os.Mkdir(filepath.Join(dir, test.path, ".subdir"), 0755); err != nil {
   252  					t.Fatal(err)
   253  				}
   254  				if err := ioutil.WriteFile(filepath.Join(dir, test.path, ".subdir", "file3"), []byte("some value"), 0644); err != nil {
   255  					t.Fatal(err)
   256  				}
   257  			} else {
   258  				if err := ioutil.WriteFile(filepath.Join(dir, test.path), []byte("some value"), 0644); err != nil {
   259  					t.Fatal(err)
   260  				}
   261  			}
   262  
   263  			// Set the modified time of the directory itself.
   264  			if err := os.Chtimes(filepath.Join(dir, rawDir), testAtime, testMtime); err != nil {
   265  				t.Fatal(err)
   266  			}
   267  
   268  			// Whiteout the path.
   269  			hdr := &tar.Header{
   270  				Name:     wh,
   271  				Typeflag: tar.TypeReg,
   272  			}
   273  
   274  			te := NewTarExtractor(UnpackOptions{})
   275  			if err := te.UnpackEntry(dir, hdr, nil); err != nil {
   276  				t.Fatalf("unexpected error in UnpackEntry: %s", err)
   277  			}
   278  
   279  			// Make sure that the path is gone.
   280  			if _, err := os.Lstat(filepath.Join(dir, test.path)); !os.IsNotExist(err) {
   281  				if err != nil {
   282  					t.Fatalf("unexpected error checking whiteout out path: %s", err)
   283  				}
   284  				t.Errorf("path was not removed by whiteout: %s", test.path)
   285  			}
   286  
   287  			// Make sure the parent directory wasn't modified.
   288  			if fi, err := os.Lstat(filepath.Join(dir, rawDir)); err != nil {
   289  				t.Fatalf("error checking parent directory of whiteout: %s", err)
   290  			} else {
   291  				hdr, err := tar.FileInfoHeader(fi, "")
   292  				if err != nil {
   293  					t.Fatalf("error generating header from fileinfo of parent directory of whiteout: %s", err)
   294  				}
   295  
   296  				if !hdr.ModTime.Equal(testMtime) {
   297  					t.Errorf("mtime of parent directory changed after whiteout: got='%s' expected='%s'", hdr.ModTime, testMtime)
   298  				}
   299  				if !hdr.AccessTime.Equal(testAtime) {
   300  					t.Errorf("atime of parent directory changed after whiteout: got='%s' expected='%s'", hdr.ModTime, testAtime)
   301  				}
   302  			}
   303  		})
   304  	}
   305  }
   306  
   307  type pseudoHdr struct {
   308  	path     string
   309  	linkname string
   310  	typeflag byte
   311  	upper    bool
   312  }
   313  
   314  func fromPseudoHdr(ph pseudoHdr) (*tar.Header, io.Reader) {
   315  	var r io.Reader
   316  	var size int64
   317  	if ph.typeflag == tar.TypeReg || ph.typeflag == tar.TypeRegA {
   318  		size = 256 * 1024
   319  		r = &io.LimitedReader{
   320  			R: rand.Reader,
   321  			N: size,
   322  		}
   323  	}
   324  
   325  	mode := os.FileMode(0777)
   326  	if ph.typeflag == tar.TypeDir {
   327  		mode |= os.ModeDir
   328  	}
   329  
   330  	return &tar.Header{
   331  		Name:       ph.path,
   332  		Linkname:   ph.linkname,
   333  		Typeflag:   ph.typeflag,
   334  		Mode:       int64(mode),
   335  		Size:       size,
   336  		ModTime:    testutils.Unix(1210393, 4528036),
   337  		AccessTime: testutils.Unix(7892829, 2341211),
   338  		ChangeTime: testutils.Unix(8731293, 8218947),
   339  	}, r
   340  }
   341  
   342  // TestUnpackOpaqueWhiteout checks whether *opaque* whiteout handling is done
   343  // correctly, as well as ensuring that the metadata of the parent is
   344  // maintained -- and that upperdir entries are handled.
   345  func TestUnpackOpaqueWhiteout(t *testing.T) {
   346  	for _, test := range []struct {
   347  		name          string
   348  		ignoreExist   bool // ignore if extra upper files exist
   349  		pseudoHeaders []pseudoHdr
   350  	}{
   351  		{"EmptyDir", false, nil},
   352  		{"OneLevel", false, []pseudoHdr{
   353  			{"file", "", tar.TypeReg, false},
   354  			{"link", "..", tar.TypeSymlink, true},
   355  			{"badlink", "./nothing", tar.TypeSymlink, true},
   356  			{"fifo", "", tar.TypeFifo, false},
   357  		}},
   358  		{"OneLevelNoUpper", false, []pseudoHdr{
   359  			{"file", "", tar.TypeReg, false},
   360  			{"link", "..", tar.TypeSymlink, false},
   361  			{"badlink", "./nothing", tar.TypeSymlink, false},
   362  			{"fifo", "", tar.TypeFifo, false},
   363  		}},
   364  		{"TwoLevel", false, []pseudoHdr{
   365  			{"file", "", tar.TypeReg, true},
   366  			{"link", "..", tar.TypeSymlink, false},
   367  			{"badlink", "./nothing", tar.TypeSymlink, false},
   368  			{"dir", "", tar.TypeDir, true},
   369  			{"dir/file", "", tar.TypeRegA, true},
   370  			{"dir/link", "../badlink", tar.TypeSymlink, false},
   371  			{"dir/verybadlink", "../../../../../../../../../../../../etc/shadow", tar.TypeSymlink, true},
   372  			{"dir/verybadlink2", "/../../../../../../../../../../../../etc/shadow", tar.TypeSymlink, false},
   373  		}},
   374  		{"TwoLevelNoUpper", false, []pseudoHdr{
   375  			{"file", "", tar.TypeReg, false},
   376  			{"link", "..", tar.TypeSymlink, false},
   377  			{"badlink", "./nothing", tar.TypeSymlink, false},
   378  			{"dir", "", tar.TypeDir, false},
   379  			{"dir/file", "", tar.TypeRegA, false},
   380  			{"dir/link", "../badlink", tar.TypeSymlink, false},
   381  			{"dir/verybadlink", "../../../../../../../../../../../../etc/shadow", tar.TypeSymlink, false},
   382  			{"dir/verybadlink2", "/../../../../../../../../../../../../etc/shadow", tar.TypeSymlink, false},
   383  		}},
   384  		{"MultiLevel", false, []pseudoHdr{
   385  			{"level1_file", "", tar.TypeReg, true},
   386  			{"level1_link", "..", tar.TypeSymlink, false},
   387  			{"level1a", "", tar.TypeDir, true},
   388  			{"level1a/level2_file", "", tar.TypeRegA, false},
   389  			{"level1a/level2_link", "../../../", tar.TypeSymlink, true},
   390  			{"level1a/level2a", "", tar.TypeDir, false},
   391  			{"level1a/level2a/level3_fileA", "", tar.TypeReg, false},
   392  			{"level1a/level2a/level3_fileB", "", tar.TypeReg, false},
   393  			{"level1a/level2b", "", tar.TypeDir, true},
   394  			{"level1a/level2b/level3_fileA", "", tar.TypeReg, true},
   395  			{"level1a/level2b/level3_fileB", "", tar.TypeReg, false},
   396  			{"level1a/level2b/level3", "", tar.TypeDir, false},
   397  			{"level1a/level2b/level3/level4", "", tar.TypeDir, false},
   398  			{"level1a/level2b/level3/level4", "", tar.TypeDir, false},
   399  			{"level1a/level2b/level3_fileA", "", tar.TypeReg, true},
   400  			{"level1b", "", tar.TypeDir, false},
   401  			{"level1b/level2_fileA", "", tar.TypeReg, false},
   402  			{"level1b/level2_fileB", "", tar.TypeReg, false},
   403  			{"level1b/level2", "", tar.TypeDir, false},
   404  			{"level1b/level2/level3_file", "", tar.TypeReg, false},
   405  		}},
   406  		{"MultiLevelNoUpper", false, []pseudoHdr{
   407  			{"level1_file", "", tar.TypeReg, false},
   408  			{"level1_link", "..", tar.TypeSymlink, false},
   409  			{"level1a", "", tar.TypeDir, false},
   410  			{"level1a/level2_file", "", tar.TypeRegA, false},
   411  			{"level1a/level2_link", "../../../", tar.TypeSymlink, false},
   412  			{"level1a/level2a", "", tar.TypeDir, false},
   413  			{"level1a/level2a/level3_fileA", "", tar.TypeReg, false},
   414  			{"level1a/level2a/level3_fileB", "", tar.TypeReg, false},
   415  			{"level1a/level2b", "", tar.TypeDir, false},
   416  			{"level1a/level2b/level3_fileA", "", tar.TypeReg, false},
   417  			{"level1a/level2b/level3_fileB", "", tar.TypeReg, false},
   418  			{"level1a/level2b/level3", "", tar.TypeDir, false},
   419  			{"level1a/level2b/level3/level4", "", tar.TypeDir, false},
   420  			{"level1a/level2b/level3/level4", "", tar.TypeDir, false},
   421  			{"level1a/level2b/level3_fileA", "", tar.TypeReg, false},
   422  			{"level1b", "", tar.TypeDir, false},
   423  			{"level1b/level2_fileA", "", tar.TypeReg, false},
   424  			{"level1b/level2_fileB", "", tar.TypeReg, false},
   425  			{"level1b/level2", "", tar.TypeDir, false},
   426  			{"level1b/level2/level3_file", "", tar.TypeReg, false},
   427  		}},
   428  		{"MissingUpperAncestor", true, []pseudoHdr{
   429  			{"some", "", tar.TypeDir, false},
   430  			{"some/dir", "", tar.TypeDir, false},
   431  			{"some/dir/somewhere", "", tar.TypeReg, true},
   432  			{"another", "", tar.TypeDir, false},
   433  			{"another/dir", "", tar.TypeDir, false},
   434  			{"another/dir/somewhere", "", tar.TypeReg, false},
   435  		}},
   436  		{"UpperWhiteout", false, []pseudoHdr{
   437  			{whPrefix + "fileB", "", tar.TypeReg, true},
   438  			{"fileA", "", tar.TypeReg, true},
   439  			{"fileB", "", tar.TypeReg, true},
   440  			{"fileC", "", tar.TypeReg, false},
   441  			{whPrefix + "fileA", "", tar.TypeReg, true},
   442  			{whPrefix + "fileC", "", tar.TypeReg, true},
   443  		}},
   444  		// XXX: What umoci should do here is not really defined by the
   445  		//      spec. In particular, whether you need a whiteout for every
   446  		//      sub-path or just the path itself is not well-defined. This
   447  		//      code assumes that you *do not*.
   448  		{"UpperDirWhiteout", false, []pseudoHdr{
   449  			{whPrefix + "dir2", "", tar.TypeReg, true},
   450  			{"file", "", tar.TypeReg, false},
   451  			{"dir1", "", tar.TypeDir, true},
   452  			{"dir1/file", "", tar.TypeRegA, true},
   453  			{"dir1/link", "../badlink", tar.TypeSymlink, false},
   454  			{"dir1/verybadlink", "../../../../../../../../../../../../etc/shadow", tar.TypeSymlink, true},
   455  			{"dir1/verybadlink2", "/../../../../../../../../../../../../etc/shadow", tar.TypeSymlink, false},
   456  			{whPrefix + "dir1", "", tar.TypeReg, true},
   457  			{"dir2", "", tar.TypeDir, true},
   458  			{"dir2/file", "", tar.TypeRegA, true},
   459  			{"dir2/link", "../badlink", tar.TypeSymlink, false},
   460  		}},
   461  	} {
   462  		t.Run(test.name, func(t *testing.T) {
   463  			unpackOptions := UnpackOptions{
   464  				MapOptions: MapOptions{
   465  					Rootless: os.Geteuid() != 0,
   466  				},
   467  			}
   468  
   469  			dir, err := ioutil.TempDir("", "umoci-TestUnpackOpaqueWhiteout")
   470  			if err != nil {
   471  				t.Fatal(err)
   472  			}
   473  			defer os.RemoveAll(dir)
   474  
   475  			// We do all whiteouts in a subdirectory.
   476  			whiteoutDir := "test-subdir"
   477  			whiteoutRoot := filepath.Join(dir, whiteoutDir)
   478  			if err := os.MkdirAll(whiteoutRoot, 0755); err != nil {
   479  				t.Fatal(err)
   480  			}
   481  
   482  			// Track if we have upper entries.
   483  			numUpper := 0
   484  
   485  			// First we apply the non-upper files in a new TarExtractor.
   486  			te := NewTarExtractor(unpackOptions)
   487  			for _, ph := range test.pseudoHeaders {
   488  				// Skip upper.
   489  				if ph.upper {
   490  					numUpper++
   491  					continue
   492  				}
   493  				hdr, rdr := fromPseudoHdr(ph)
   494  				hdr.Name = filepath.Join(whiteoutDir, hdr.Name)
   495  				if err := te.UnpackEntry(dir, hdr, rdr); err != nil {
   496  					t.Errorf("UnpackEntry %s failed: %v", hdr.Name, err)
   497  				}
   498  			}
   499  
   500  			// Now we apply the upper files in another TarExtractor.
   501  			te = NewTarExtractor(unpackOptions)
   502  			for _, ph := range test.pseudoHeaders {
   503  				// Skip non-upper.
   504  				if !ph.upper {
   505  					continue
   506  				}
   507  				hdr, rdr := fromPseudoHdr(ph)
   508  				hdr.Name = filepath.Join(whiteoutDir, hdr.Name)
   509  				if err := te.UnpackEntry(dir, hdr, rdr); err != nil {
   510  					t.Errorf("UnpackEntry %s failed: %v", hdr.Name, err)
   511  				}
   512  			}
   513  
   514  			// And now apply a whiteout for the whiteoutRoot.
   515  			whHdr := &tar.Header{
   516  				Name:     filepath.Join(whiteoutDir, whOpaque),
   517  				Typeflag: tar.TypeReg,
   518  			}
   519  			if err := te.UnpackEntry(dir, whHdr, nil); err != nil {
   520  				t.Fatalf("unpack whiteout %s failed: %v", whiteoutRoot, err)
   521  			}
   522  
   523  			// Now we double-check it worked. If the file was in "upper" it
   524  			// should have survived. If it was in lower it shouldn't. We don't
   525  			// bother checking the contents here.
   526  			for _, ph := range test.pseudoHeaders {
   527  				// If there's an explicit whiteout in the headers we ignore it
   528  				// here, since it won't be on the filesystem.
   529  				if strings.HasPrefix(filepath.Base(ph.path), whPrefix) {
   530  					t.Logf("ignoring whiteout entry %q during post-check", ph.path)
   531  					continue
   532  				}
   533  
   534  				fullPath := filepath.Join(whiteoutRoot, ph.path)
   535  				_, err := te.fsEval.Lstat(fullPath)
   536  				if err != nil && !os.IsNotExist(errors.Cause(err)) {
   537  					t.Errorf("unexpected lstat error of %s: %v", ph.path, err)
   538  				} else if ph.upper && err != nil {
   539  					t.Errorf("expected upper %s to exist: got %v", ph.path, err)
   540  				} else if !ph.upper && err == nil {
   541  					if !test.ignoreExist {
   542  						t.Errorf("expected lower %s to not exist", ph.path)
   543  					}
   544  				}
   545  			}
   546  
   547  			// Make sure the whiteoutRoot still exists.
   548  			if fi, err := te.fsEval.Lstat(whiteoutRoot); err != nil {
   549  				if os.IsNotExist(errors.Cause(err)) {
   550  					t.Errorf("expected whiteout root to still exist: %v", err)
   551  				} else {
   552  					t.Errorf("unexpected error in lstat of whiteout root: %v", err)
   553  				}
   554  			} else if !fi.IsDir() {
   555  				t.Errorf("expected whiteout root to still be a directory")
   556  			}
   557  
   558  			// Check that the directory is empty if there's no uppers.
   559  			if numUpper == 0 {
   560  				if fd, err := os.Open(whiteoutRoot); err != nil {
   561  					t.Errorf("unexpected error opening whiteoutRoot: %v", err)
   562  				} else if names, err := fd.Readdirnames(-1); err != nil {
   563  					t.Errorf("unexpected error reading dirnames: %v", err)
   564  				} else if len(names) != 0 {
   565  					t.Errorf("expected empty opaque'd dir: got %v", names)
   566  				}
   567  			}
   568  		})
   569  	}
   570  }
   571  
   572  // TestUnpackHardlink makes sure that hardlinks are correctly unpacked in all
   573  // cases. In particular when it comes to hardlinks to symlinks.
   574  func TestUnpackHardlink(t *testing.T) {
   575  	// Create the files we're going to play with.
   576  	dir, err := ioutil.TempDir("", "umoci-TestUnpackHardlink")
   577  	if err != nil {
   578  		t.Fatal(err)
   579  	}
   580  	defer os.RemoveAll(dir)
   581  
   582  	var (
   583  		hdr *tar.Header
   584  
   585  		// On MacOS, this might not work.
   586  		hardlinkToSymlinkSupported = true
   587  
   588  		ctrValue  = []byte("some content we won't check")
   589  		regFile   = "regular"
   590  		symFile   = "link"
   591  		hardFileA = "hard link"
   592  		hardFileB = "hard link to symlink"
   593  	)
   594  
   595  	te := NewTarExtractor(UnpackOptions{})
   596  
   597  	// Regular file.
   598  	hdr = &tar.Header{
   599  		Name:       regFile,
   600  		Uid:        os.Getuid(),
   601  		Gid:        os.Getgid(),
   602  		Mode:       0644,
   603  		Size:       int64(len(ctrValue)),
   604  		Typeflag:   tar.TypeReg,
   605  		ModTime:    time.Now(),
   606  		AccessTime: time.Now(),
   607  		ChangeTime: time.Now(),
   608  	}
   609  	if err := te.UnpackEntry(dir, hdr, bytes.NewBuffer(ctrValue)); err != nil {
   610  		t.Fatalf("regular: unexpected UnpackEntry error: %s", err)
   611  	}
   612  
   613  	// Hardlink to regFile.
   614  	hdr = &tar.Header{
   615  		Name:     hardFileA,
   616  		Typeflag: tar.TypeLink,
   617  		Linkname: filepath.Join("/", regFile),
   618  		// These should **not** be applied.
   619  		Uid: os.Getuid() + 1337,
   620  		Gid: os.Getgid() + 2020,
   621  	}
   622  	if err := te.UnpackEntry(dir, hdr, nil); err != nil {
   623  		t.Fatalf("hardlinkA: unexpected UnpackEntry error: %s", err)
   624  	}
   625  
   626  	// Symlink to regFile.
   627  	hdr = &tar.Header{
   628  		Name:     symFile,
   629  		Uid:      os.Getuid(),
   630  		Gid:      os.Getgid(),
   631  		Typeflag: tar.TypeSymlink,
   632  		Linkname: filepath.Join("../../../", regFile),
   633  	}
   634  	if err := te.UnpackEntry(dir, hdr, nil); err != nil {
   635  		t.Fatalf("symlink: unexpected UnpackEntry error: %s", err)
   636  	}
   637  
   638  	// Hardlink to symlink.
   639  	hdr = &tar.Header{
   640  		Name:     hardFileB,
   641  		Typeflag: tar.TypeLink,
   642  		Linkname: filepath.Join("../../../", symFile),
   643  		// These should **really not** be applied.
   644  		Uid: os.Getuid() + 1337,
   645  		Gid: os.Getgid() + 2020,
   646  	}
   647  	if err := te.UnpackEntry(dir, hdr, nil); err != nil {
   648  		// On Travis' setup, hardlinks to symlinks are not permitted under
   649  		// MacOS. That's fine.
   650  		if runtime.GOOS == "darwin" && errors.Is(err, unix.ENOTSUP) {
   651  			hardlinkToSymlinkSupported = false
   652  			t.Logf("hardlinks to symlinks unsupported -- skipping that part of the test")
   653  		} else {
   654  			t.Fatalf("hardlinkB: unexpected UnpackEntry error: %s", err)
   655  		}
   656  	}
   657  
   658  	// Make sure that the contents are as expected.
   659  	ctrValueGot, err := ioutil.ReadFile(filepath.Join(dir, regFile))
   660  	if err != nil {
   661  		t.Fatalf("regular file was not created: %s", err)
   662  	}
   663  	if !bytes.Equal(ctrValueGot, ctrValue) {
   664  		t.Fatalf("regular file did not have expected values: expected=%s got=%s", ctrValue, ctrValueGot)
   665  	}
   666  
   667  	// Now we have to check the inode numbers.
   668  	var regFi, symFi, hardAFi unix.Stat_t
   669  
   670  	if err := unix.Lstat(filepath.Join(dir, regFile), &regFi); err != nil {
   671  		t.Fatalf("could not stat regular file: %s", err)
   672  	}
   673  	if err := unix.Lstat(filepath.Join(dir, symFile), &symFi); err != nil {
   674  		t.Fatalf("could not stat symlink: %s", err)
   675  	}
   676  	if err := unix.Lstat(filepath.Join(dir, hardFileA), &hardAFi); err != nil {
   677  		t.Fatalf("could not stat hardlinkA: %s", err)
   678  	}
   679  
   680  	if regFi.Ino == symFi.Ino {
   681  		t.Errorf("regular and symlink have the same inode! ino=%d", regFi.Ino)
   682  	}
   683  	if hardAFi.Ino != regFi.Ino {
   684  		t.Errorf("hardlink to regular has a different inode: reg=%d hard=%d", regFi.Ino, hardAFi.Ino)
   685  	}
   686  
   687  	if hardlinkToSymlinkSupported {
   688  		var hardBFi unix.Stat_t
   689  
   690  		if err := unix.Lstat(filepath.Join(dir, hardFileB), &hardBFi); err != nil {
   691  			t.Fatalf("could not stat hardlinkB: %s", err)
   692  		}
   693  
   694  		// Check inode numbers of hardlink-to-symlink.
   695  		if hardAFi.Ino == hardBFi.Ino {
   696  			t.Errorf("both hardlinks have the same inode! ino=%d", hardAFi.Ino)
   697  		}
   698  		if hardBFi.Ino != symFi.Ino {
   699  			t.Errorf("hardlink to symlink has a different inode: sym=%d hard=%d", symFi.Ino, hardBFi.Ino)
   700  		}
   701  
   702  		// Double-check readlink.
   703  		linknameA, err := os.Readlink(filepath.Join(dir, symFile))
   704  		if err != nil {
   705  			t.Errorf("unexpected error reading symlink: %s", err)
   706  		}
   707  		linknameB, err := os.Readlink(filepath.Join(dir, hardFileB))
   708  		if err != nil {
   709  			t.Errorf("unexpected error reading hardlink to symlink: %s", err)
   710  		}
   711  		if linknameA != linknameB {
   712  			t.Errorf("hardlink to symlink doesn't match linkname: link=%s hard=%s", linknameA, linknameB)
   713  		}
   714  	}
   715  
   716  	// Make sure that uid and gid don't apply to hardlinks.
   717  	if int(regFi.Uid) != os.Getuid() {
   718  		t.Errorf("regular file: uid was changed by hardlink unpack: expected=%d got=%d", os.Getuid(), regFi.Uid)
   719  	}
   720  	if int(regFi.Gid) != os.Getgid() {
   721  		t.Errorf("regular file: gid was changed by hardlink unpack: expected=%d got=%d", os.Getgid(), regFi.Gid)
   722  	}
   723  	if int(symFi.Uid) != os.Getuid() {
   724  		t.Errorf("symlink: uid was changed by hardlink unpack: expected=%d got=%d", os.Getuid(), symFi.Uid)
   725  	}
   726  	if int(symFi.Gid) != os.Getgid() {
   727  		t.Errorf("symlink: gid was changed by hardlink unpack: expected=%d got=%d", os.Getgid(), symFi.Gid)
   728  	}
   729  }
   730  
   731  // TestUnpackEntryMap checks that the mapOptions handling works.
   732  func TestUnpackEntryMap(t *testing.T) {
   733  	if os.Geteuid() != 0 {
   734  		t.Skip("mapOptions tests only work with root privileges")
   735  	}
   736  
   737  	for _, test := range []struct {
   738  		name   string
   739  		uidMap rspec.LinuxIDMapping
   740  		gidMap rspec.LinuxIDMapping
   741  	}{
   742  		{"IdentityRoot",
   743  			rspec.LinuxIDMapping{HostID: 0, ContainerID: 0, Size: 100},
   744  			rspec.LinuxIDMapping{HostID: 0, ContainerID: 0, Size: 100}},
   745  		{"MapSelfToRoot",
   746  			rspec.LinuxIDMapping{HostID: uint32(os.Getuid()), ContainerID: 0, Size: 100},
   747  			rspec.LinuxIDMapping{HostID: uint32(os.Getgid()), ContainerID: 0, Size: 100}},
   748  		{"MapOtherToRoot",
   749  			rspec.LinuxIDMapping{HostID: uint32(os.Getuid() + 100), ContainerID: 0, Size: 100},
   750  			rspec.LinuxIDMapping{HostID: uint32(os.Getgid() + 200), ContainerID: 0, Size: 100}},
   751  	} {
   752  		t.Run(test.name, func(t *testing.T) {
   753  			// Create the files we're going to play with.
   754  			dir, err := ioutil.TempDir("", "umoci-TestUnpackEntryMap")
   755  			if err != nil {
   756  				t.Fatal(err)
   757  			}
   758  			defer os.RemoveAll(dir)
   759  
   760  			var (
   761  				hdrUID, hdrGID, uUID, uGID int
   762  				hdr                        *tar.Header
   763  				fi                         unix.Stat_t
   764  
   765  				ctrValue = []byte("some content we won't check")
   766  				regFile  = "regular"
   767  				symFile  = "link"
   768  				regDir   = " a directory"
   769  				symDir   = "link-dir"
   770  			)
   771  
   772  			te := NewTarExtractor(UnpackOptions{MapOptions: MapOptions{
   773  				UIDMappings: []rspec.LinuxIDMapping{test.uidMap},
   774  				GIDMappings: []rspec.LinuxIDMapping{test.gidMap},
   775  			}})
   776  
   777  			// Regular file.
   778  			hdrUID, hdrGID = 0, 0
   779  			hdr = &tar.Header{
   780  				Name:       regFile,
   781  				Uid:        hdrUID,
   782  				Gid:        hdrGID,
   783  				Mode:       0644,
   784  				Size:       int64(len(ctrValue)),
   785  				Typeflag:   tar.TypeReg,
   786  				ModTime:    time.Now(),
   787  				AccessTime: time.Now(),
   788  				ChangeTime: time.Now(),
   789  			}
   790  			if err := te.UnpackEntry(dir, hdr, bytes.NewBuffer(ctrValue)); err != nil {
   791  				t.Fatalf("regfile: unexpected UnpackEntry error: %s", err)
   792  			}
   793  
   794  			if err := unix.Lstat(filepath.Join(dir, hdr.Name), &fi); err != nil {
   795  				t.Errorf("failed to lstat %s: %s", hdr.Name, err)
   796  			} else {
   797  				uUID = int(fi.Uid)
   798  				uGID = int(fi.Gid)
   799  				if uUID != int(test.uidMap.HostID)+hdrUID {
   800  					t.Errorf("file %s has the wrong uid mapping: got=%d expected=%d", hdr.Name, uUID, int(test.uidMap.HostID)+hdrUID)
   801  				}
   802  				if uGID != int(test.gidMap.HostID)+hdrGID {
   803  					t.Errorf("file %s has the wrong gid mapping: got=%d expected=%d", hdr.Name, uGID, int(test.gidMap.HostID)+hdrGID)
   804  				}
   805  			}
   806  
   807  			// Regular directory.
   808  			hdrUID, hdrGID = 13, 42
   809  			hdr = &tar.Header{
   810  				Name:       regDir,
   811  				Uid:        hdrUID,
   812  				Gid:        hdrGID,
   813  				Mode:       0755,
   814  				Typeflag:   tar.TypeDir,
   815  				ModTime:    time.Now(),
   816  				AccessTime: time.Now(),
   817  				ChangeTime: time.Now(),
   818  			}
   819  			if err := te.UnpackEntry(dir, hdr, bytes.NewBuffer(ctrValue)); err != nil {
   820  				t.Fatalf("regdir: unexpected UnpackEntry error: %s", err)
   821  			}
   822  
   823  			if err := unix.Lstat(filepath.Join(dir, hdr.Name), &fi); err != nil {
   824  				t.Errorf("failed to lstat %s: %s", hdr.Name, err)
   825  			} else {
   826  				uUID = int(fi.Uid)
   827  				uGID = int(fi.Gid)
   828  				if uUID != int(test.uidMap.HostID)+hdrUID {
   829  					t.Errorf("file %s has the wrong uid mapping: got=%d expected=%d", hdr.Name, uUID, int(test.uidMap.HostID)+hdrUID)
   830  				}
   831  				if uGID != int(test.gidMap.HostID)+hdrGID {
   832  					t.Errorf("file %s has the wrong gid mapping: got=%d expected=%d", hdr.Name, uGID, int(test.gidMap.HostID)+hdrGID)
   833  				}
   834  			}
   835  
   836  			// Symlink to file.
   837  			hdrUID, hdrGID = 23, 22
   838  			hdr = &tar.Header{
   839  				Name:       symFile,
   840  				Uid:        hdrUID,
   841  				Gid:        hdrGID,
   842  				Typeflag:   tar.TypeSymlink,
   843  				Linkname:   regFile,
   844  				ModTime:    time.Now(),
   845  				AccessTime: time.Now(),
   846  				ChangeTime: time.Now(),
   847  			}
   848  			if err := te.UnpackEntry(dir, hdr, bytes.NewBuffer(ctrValue)); err != nil {
   849  				t.Fatalf("regdir: unexpected UnpackEntry error: %s", err)
   850  			}
   851  
   852  			if err := unix.Lstat(filepath.Join(dir, hdr.Name), &fi); err != nil {
   853  				t.Errorf("failed to lstat %s: %s", hdr.Name, err)
   854  			} else {
   855  				uUID = int(fi.Uid)
   856  				uGID = int(fi.Gid)
   857  				if uUID != int(test.uidMap.HostID)+hdrUID {
   858  					t.Errorf("file %s has the wrong uid mapping: got=%d expected=%d", hdr.Name, uUID, int(test.uidMap.HostID)+hdrUID)
   859  				}
   860  				if uGID != int(test.gidMap.HostID)+hdrGID {
   861  					t.Errorf("file %s has the wrong gid mapping: got=%d expected=%d", hdr.Name, uGID, int(test.gidMap.HostID)+hdrGID)
   862  				}
   863  			}
   864  
   865  			// Symlink to director.
   866  			hdrUID, hdrGID = 99, 88
   867  			hdr = &tar.Header{
   868  				Name:       symDir,
   869  				Uid:        hdrUID,
   870  				Gid:        hdrGID,
   871  				Typeflag:   tar.TypeSymlink,
   872  				Linkname:   regDir,
   873  				ModTime:    time.Now(),
   874  				AccessTime: time.Now(),
   875  				ChangeTime: time.Now(),
   876  			}
   877  			if err := te.UnpackEntry(dir, hdr, bytes.NewBuffer(ctrValue)); err != nil {
   878  				t.Fatalf("regdir: unexpected UnpackEntry error: %s", err)
   879  			}
   880  
   881  			if err := unix.Lstat(filepath.Join(dir, hdr.Name), &fi); err != nil {
   882  				t.Errorf("failed to lstat %s: %s", hdr.Name, err)
   883  			} else {
   884  				uUID = int(fi.Uid)
   885  				uGID = int(fi.Gid)
   886  				if uUID != int(test.uidMap.HostID)+hdrUID {
   887  					t.Errorf("file %s has the wrong uid mapping: got=%d expected=%d", hdr.Name, uUID, int(test.uidMap.HostID)+hdrUID)
   888  				}
   889  				if uGID != int(test.gidMap.HostID)+hdrGID {
   890  					t.Errorf("file %s has the wrong gid mapping: got=%d expected=%d", hdr.Name, uGID, int(test.gidMap.HostID)+hdrGID)
   891  				}
   892  			}
   893  		})
   894  	}
   895  }
   896  
   897  func TestIsDirlink(t *testing.T) {
   898  	dir, err := ioutil.TempDir("", "umoci-TestDirLink")
   899  	if err != nil {
   900  		t.Fatal(err)
   901  	}
   902  	defer os.RemoveAll(dir)
   903  
   904  	if err := os.Mkdir(filepath.Join(dir, "test_dir"), 0755); err != nil {
   905  		t.Fatal(err)
   906  	}
   907  	if file, err := os.Create(filepath.Join(dir, "test_file")); err != nil {
   908  		t.Fatal(err)
   909  	} else {
   910  		file.Close()
   911  	}
   912  	if err := os.Symlink("test_dir", filepath.Join(dir, "link")); err != nil {
   913  		t.Fatal(err)
   914  	}
   915  
   916  	te := NewTarExtractor(UnpackOptions{})
   917  	// Basic symlink usage.
   918  	if dirlink, err := te.isDirlink(dir, filepath.Join(dir, "link")); err != nil {
   919  		t.Errorf("symlink failed: %v", err)
   920  	} else if !dirlink {
   921  		t.Errorf("dirlink test failed")
   922  	}
   923  
   924  	// "Read" a non-existent link.
   925  	if _, err := te.isDirlink(dir, filepath.Join(dir, "doesnt-exist")); err == nil {
   926  		t.Errorf("read non-existent dirlink")
   927  	}
   928  	// "Read" a directory.
   929  	if _, err := te.isDirlink(dir, filepath.Join(dir, "test_dir")); err == nil {
   930  		t.Errorf("read non-link dirlink")
   931  	}
   932  	// "Read" a file.
   933  	if _, err := te.isDirlink(dir, filepath.Join(dir, "test_file")); err == nil {
   934  		t.Errorf("read non-link dirlink")
   935  	}
   936  
   937  	// Break the symlink.
   938  	if err := os.Remove(filepath.Join(dir, "test_dir")); err != nil {
   939  		t.Fatal(err)
   940  	}
   941  	if dirlink, err := te.isDirlink(dir, filepath.Join(dir, "link")); err != nil {
   942  		t.Errorf("broken symlink failed: %v", err)
   943  	} else if dirlink {
   944  		t.Errorf("broken dirlink test failed")
   945  	}
   946  
   947  	// Point the symlink to a file.
   948  	if err := os.Remove(filepath.Join(dir, "link")); err != nil {
   949  		t.Fatal(err)
   950  	}
   951  	if err := os.Symlink("test_file", filepath.Join(dir, "link")); err != nil {
   952  		t.Fatal(err)
   953  	}
   954  	if dirlink, err := te.isDirlink(dir, filepath.Join(dir, "link")); err != nil {
   955  		t.Errorf("file symlink failed: %v", err)
   956  	} else if dirlink {
   957  		t.Errorf("file dirlink test failed")
   958  	}
   959  }