github.com/SagerNet/gvisor@v0.0.0-20210707092255-7731c139d75c/pkg/sentry/fsimpl/ext/ext_test.go (about)

     1  // Copyright 2019 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package ext
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"os"
    21  	"path"
    22  	"sort"
    23  	"testing"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  	"github.com/google/go-cmp/cmp/cmpopts"
    27  	"github.com/SagerNet/gvisor/pkg/abi/linux"
    28  	"github.com/SagerNet/gvisor/pkg/context"
    29  	"github.com/SagerNet/gvisor/pkg/errors/linuxerr"
    30  	"github.com/SagerNet/gvisor/pkg/fspath"
    31  	"github.com/SagerNet/gvisor/pkg/sentry/contexttest"
    32  	"github.com/SagerNet/gvisor/pkg/sentry/fsimpl/ext/disklayout"
    33  	"github.com/SagerNet/gvisor/pkg/sentry/kernel/auth"
    34  	"github.com/SagerNet/gvisor/pkg/sentry/vfs"
    35  	"github.com/SagerNet/gvisor/pkg/test/testutil"
    36  	"github.com/SagerNet/gvisor/pkg/usermem"
    37  )
    38  
    39  const (
    40  	assetsDir = "pkg/sentry/fsimpl/ext/assets"
    41  )
    42  
    43  var (
    44  	ext2ImagePath = path.Join(assetsDir, "tiny.ext2")
    45  	ext3ImagePath = path.Join(assetsDir, "tiny.ext3")
    46  	ext4ImagePath = path.Join(assetsDir, "tiny.ext4")
    47  )
    48  
    49  // setUp opens imagePath as an ext Filesystem and returns all necessary
    50  // elements required to run tests. If error is non-nil, it also returns a tear
    51  // down function which must be called after the test is run for clean up.
    52  func setUp(t *testing.T, imagePath string) (context.Context, *vfs.VirtualFilesystem, *vfs.VirtualDentry, func(), error) {
    53  	localImagePath, err := testutil.FindFile(imagePath)
    54  	if err != nil {
    55  		return nil, nil, nil, nil, fmt.Errorf("failed to open local image at path %s: %v", imagePath, err)
    56  	}
    57  
    58  	f, err := os.Open(localImagePath)
    59  	if err != nil {
    60  		return nil, nil, nil, nil, err
    61  	}
    62  
    63  	ctx := contexttest.Context(t)
    64  	creds := auth.CredentialsFromContext(ctx)
    65  
    66  	// Create VFS.
    67  	vfsObj := &vfs.VirtualFilesystem{}
    68  	if err := vfsObj.Init(ctx); err != nil {
    69  		t.Fatalf("VFS init: %v", err)
    70  	}
    71  	vfsObj.MustRegisterFilesystemType("extfs", FilesystemType{}, &vfs.RegisterFilesystemTypeOptions{
    72  		AllowUserMount: true,
    73  	})
    74  	mntns, err := vfsObj.NewMountNamespace(ctx, creds, localImagePath, "extfs", &vfs.MountOptions{
    75  		GetFilesystemOptions: vfs.GetFilesystemOptions{
    76  			InternalData: int(f.Fd()),
    77  		},
    78  	})
    79  	if err != nil {
    80  		f.Close()
    81  		return nil, nil, nil, nil, err
    82  	}
    83  
    84  	root := mntns.Root()
    85  	root.IncRef()
    86  
    87  	tearDown := func() {
    88  		root.DecRef(ctx)
    89  
    90  		if err := f.Close(); err != nil {
    91  			t.Fatalf("tearDown failed: %v", err)
    92  		}
    93  	}
    94  	return ctx, vfsObj, &root, tearDown, nil
    95  }
    96  
    97  // TODO(b/134676337): Test vfs.FilesystemImpl.ReadlinkAt and
    98  // vfs.FilesystemImpl.StatFSAt which are not implemented in
    99  // vfs.VirtualFilesystem yet.
   100  
   101  // TestSeek tests vfs.FileDescriptionImpl.Seek functionality.
   102  func TestSeek(t *testing.T) {
   103  	type seekTest struct {
   104  		name  string
   105  		image string
   106  		path  string
   107  	}
   108  
   109  	tests := []seekTest{
   110  		{
   111  			name:  "ext4 root dir seek",
   112  			image: ext4ImagePath,
   113  			path:  "/",
   114  		},
   115  		{
   116  			name:  "ext3 root dir seek",
   117  			image: ext3ImagePath,
   118  			path:  "/",
   119  		},
   120  		{
   121  			name:  "ext2 root dir seek",
   122  			image: ext2ImagePath,
   123  			path:  "/",
   124  		},
   125  		{
   126  			name:  "ext4 reg file seek",
   127  			image: ext4ImagePath,
   128  			path:  "/file.txt",
   129  		},
   130  		{
   131  			name:  "ext3 reg file seek",
   132  			image: ext3ImagePath,
   133  			path:  "/file.txt",
   134  		},
   135  		{
   136  			name:  "ext2 reg file seek",
   137  			image: ext2ImagePath,
   138  			path:  "/file.txt",
   139  		},
   140  	}
   141  
   142  	for _, test := range tests {
   143  		t.Run(test.name, func(t *testing.T) {
   144  			ctx, vfsfs, root, tearDown, err := setUp(t, test.image)
   145  			if err != nil {
   146  				t.Fatalf("setUp failed: %v", err)
   147  			}
   148  			defer tearDown()
   149  
   150  			fd, err := vfsfs.OpenAt(
   151  				ctx,
   152  				auth.CredentialsFromContext(ctx),
   153  				&vfs.PathOperation{Root: *root, Start: *root, Path: fspath.Parse(test.path)},
   154  				&vfs.OpenOptions{},
   155  			)
   156  			if err != nil {
   157  				t.Fatalf("vfsfs.OpenAt failed: %v", err)
   158  			}
   159  
   160  			if n, err := fd.Seek(ctx, 0, linux.SEEK_SET); n != 0 || err != nil {
   161  				t.Errorf("expected seek position 0, got %d and error %v", n, err)
   162  			}
   163  
   164  			stat, err := fd.Stat(ctx, vfs.StatOptions{})
   165  			if err != nil {
   166  				t.Errorf("fd.stat failed for file %s in image %s: %v", test.path, test.image, err)
   167  			}
   168  
   169  			// We should be able to seek beyond the end of file.
   170  			size := int64(stat.Size)
   171  			if n, err := fd.Seek(ctx, size, linux.SEEK_SET); n != size || err != nil {
   172  				t.Errorf("expected seek position %d, got %d and error %v", size, n, err)
   173  			}
   174  
   175  			// EINVAL should be returned if the resulting offset is negative.
   176  			if _, err := fd.Seek(ctx, -1, linux.SEEK_SET); !linuxerr.Equals(linuxerr.EINVAL, err) {
   177  				t.Errorf("expected error EINVAL but got %v", err)
   178  			}
   179  
   180  			if n, err := fd.Seek(ctx, 3, linux.SEEK_CUR); n != size+3 || err != nil {
   181  				t.Errorf("expected seek position %d, got %d and error %v", size+3, n, err)
   182  			}
   183  
   184  			// Make sure negative offsets work with SEEK_CUR.
   185  			if n, err := fd.Seek(ctx, -2, linux.SEEK_CUR); n != size+1 || err != nil {
   186  				t.Errorf("expected seek position %d, got %d and error %v", size+1, n, err)
   187  			}
   188  
   189  			// EINVAL should be returned if the resulting offset is negative.
   190  			if _, err := fd.Seek(ctx, -(size + 2), linux.SEEK_CUR); !linuxerr.Equals(linuxerr.EINVAL, err) {
   191  				t.Errorf("expected error EINVAL but got %v", err)
   192  			}
   193  
   194  			// Make sure SEEK_END works with regular files.
   195  			if _, ok := fd.Impl().(*regularFileFD); ok {
   196  				// Seek back to 0.
   197  				if n, err := fd.Seek(ctx, -size, linux.SEEK_END); n != 0 || err != nil {
   198  					t.Errorf("expected seek position %d, got %d and error %v", 0, n, err)
   199  				}
   200  
   201  				// Seek forward beyond EOF.
   202  				if n, err := fd.Seek(ctx, 1, linux.SEEK_END); n != size+1 || err != nil {
   203  					t.Errorf("expected seek position %d, got %d and error %v", size+1, n, err)
   204  				}
   205  
   206  				// EINVAL should be returned if the resulting offset is negative.
   207  				if _, err := fd.Seek(ctx, -(size + 1), linux.SEEK_END); !linuxerr.Equals(linuxerr.EINVAL, err) {
   208  					t.Errorf("expected error EINVAL but got %v", err)
   209  				}
   210  			}
   211  		})
   212  	}
   213  }
   214  
   215  // TestStatAt tests filesystem.StatAt functionality.
   216  func TestStatAt(t *testing.T) {
   217  	type statAtTest struct {
   218  		name  string
   219  		image string
   220  		path  string
   221  		want  linux.Statx
   222  	}
   223  
   224  	tests := []statAtTest{
   225  		{
   226  			name:  "ext4 statx small file",
   227  			image: ext4ImagePath,
   228  			path:  "/file.txt",
   229  			want: linux.Statx{
   230  				Blksize: 0x400,
   231  				Nlink:   1,
   232  				UID:     0,
   233  				GID:     0,
   234  				Mode:    0644 | linux.ModeRegular,
   235  				Size:    13,
   236  			},
   237  		},
   238  		{
   239  			name:  "ext3 statx small file",
   240  			image: ext3ImagePath,
   241  			path:  "/file.txt",
   242  			want: linux.Statx{
   243  				Blksize: 0x400,
   244  				Nlink:   1,
   245  				UID:     0,
   246  				GID:     0,
   247  				Mode:    0644 | linux.ModeRegular,
   248  				Size:    13,
   249  			},
   250  		},
   251  		{
   252  			name:  "ext2 statx small file",
   253  			image: ext2ImagePath,
   254  			path:  "/file.txt",
   255  			want: linux.Statx{
   256  				Blksize: 0x400,
   257  				Nlink:   1,
   258  				UID:     0,
   259  				GID:     0,
   260  				Mode:    0644 | linux.ModeRegular,
   261  				Size:    13,
   262  			},
   263  		},
   264  		{
   265  			name:  "ext4 statx big file",
   266  			image: ext4ImagePath,
   267  			path:  "/bigfile.txt",
   268  			want: linux.Statx{
   269  				Blksize: 0x400,
   270  				Nlink:   1,
   271  				UID:     0,
   272  				GID:     0,
   273  				Mode:    0644 | linux.ModeRegular,
   274  				Size:    13042,
   275  			},
   276  		},
   277  		{
   278  			name:  "ext3 statx big file",
   279  			image: ext3ImagePath,
   280  			path:  "/bigfile.txt",
   281  			want: linux.Statx{
   282  				Blksize: 0x400,
   283  				Nlink:   1,
   284  				UID:     0,
   285  				GID:     0,
   286  				Mode:    0644 | linux.ModeRegular,
   287  				Size:    13042,
   288  			},
   289  		},
   290  		{
   291  			name:  "ext2 statx big file",
   292  			image: ext2ImagePath,
   293  			path:  "/bigfile.txt",
   294  			want: linux.Statx{
   295  				Blksize: 0x400,
   296  				Nlink:   1,
   297  				UID:     0,
   298  				GID:     0,
   299  				Mode:    0644 | linux.ModeRegular,
   300  				Size:    13042,
   301  			},
   302  		},
   303  		{
   304  			name:  "ext4 statx symlink file",
   305  			image: ext4ImagePath,
   306  			path:  "/symlink.txt",
   307  			want: linux.Statx{
   308  				Blksize: 0x400,
   309  				Nlink:   1,
   310  				UID:     0,
   311  				GID:     0,
   312  				Mode:    0777 | linux.ModeSymlink,
   313  				Size:    8,
   314  			},
   315  		},
   316  		{
   317  			name:  "ext3 statx symlink file",
   318  			image: ext3ImagePath,
   319  			path:  "/symlink.txt",
   320  			want: linux.Statx{
   321  				Blksize: 0x400,
   322  				Nlink:   1,
   323  				UID:     0,
   324  				GID:     0,
   325  				Mode:    0777 | linux.ModeSymlink,
   326  				Size:    8,
   327  			},
   328  		},
   329  		{
   330  			name:  "ext2 statx symlink file",
   331  			image: ext2ImagePath,
   332  			path:  "/symlink.txt",
   333  			want: linux.Statx{
   334  				Blksize: 0x400,
   335  				Nlink:   1,
   336  				UID:     0,
   337  				GID:     0,
   338  				Mode:    0777 | linux.ModeSymlink,
   339  				Size:    8,
   340  			},
   341  		},
   342  	}
   343  
   344  	// Ignore the fields that are not supported by filesystem.StatAt yet and
   345  	// those which are likely to change as the image does.
   346  	ignoredFields := map[string]bool{
   347  		"Attributes":     true,
   348  		"AttributesMask": true,
   349  		"Atime":          true,
   350  		"Blocks":         true,
   351  		"Btime":          true,
   352  		"Ctime":          true,
   353  		"DevMajor":       true,
   354  		"DevMinor":       true,
   355  		"Ino":            true,
   356  		"Mask":           true,
   357  		"Mtime":          true,
   358  		"RdevMajor":      true,
   359  		"RdevMinor":      true,
   360  	}
   361  
   362  	for _, test := range tests {
   363  		t.Run(test.name, func(t *testing.T) {
   364  			ctx, vfsfs, root, tearDown, err := setUp(t, test.image)
   365  			if err != nil {
   366  				t.Fatalf("setUp failed: %v", err)
   367  			}
   368  			defer tearDown()
   369  
   370  			got, err := vfsfs.StatAt(ctx,
   371  				auth.CredentialsFromContext(ctx),
   372  				&vfs.PathOperation{Root: *root, Start: *root, Path: fspath.Parse(test.path)},
   373  				&vfs.StatOptions{},
   374  			)
   375  			if err != nil {
   376  				t.Fatalf("vfsfs.StatAt failed for file %s in image %s: %v", test.path, test.image, err)
   377  			}
   378  
   379  			cmpIgnoreFields := cmp.FilterPath(func(p cmp.Path) bool {
   380  				_, ok := ignoredFields[p.String()]
   381  				return ok
   382  			}, cmp.Ignore())
   383  			if diff := cmp.Diff(got, test.want, cmpIgnoreFields, cmpopts.IgnoreUnexported(linux.Statx{})); diff != "" {
   384  				t.Errorf("stat mismatch (-want +got):\n%s", diff)
   385  			}
   386  		})
   387  	}
   388  }
   389  
   390  // TestRead tests the read functionality for vfs file descriptions.
   391  func TestRead(t *testing.T) {
   392  	type readTest struct {
   393  		name    string
   394  		image   string
   395  		absPath string
   396  	}
   397  
   398  	tests := []readTest{
   399  		{
   400  			name:    "ext4 read small file",
   401  			image:   ext4ImagePath,
   402  			absPath: "/file.txt",
   403  		},
   404  		{
   405  			name:    "ext3 read small file",
   406  			image:   ext3ImagePath,
   407  			absPath: "/file.txt",
   408  		},
   409  		{
   410  			name:    "ext2 read small file",
   411  			image:   ext2ImagePath,
   412  			absPath: "/file.txt",
   413  		},
   414  		{
   415  			name:    "ext4 read big file",
   416  			image:   ext4ImagePath,
   417  			absPath: "/bigfile.txt",
   418  		},
   419  		{
   420  			name:    "ext3 read big file",
   421  			image:   ext3ImagePath,
   422  			absPath: "/bigfile.txt",
   423  		},
   424  		{
   425  			name:    "ext2 read big file",
   426  			image:   ext2ImagePath,
   427  			absPath: "/bigfile.txt",
   428  		},
   429  	}
   430  
   431  	for _, test := range tests {
   432  		t.Run(test.name, func(t *testing.T) {
   433  			ctx, vfsfs, root, tearDown, err := setUp(t, test.image)
   434  			if err != nil {
   435  				t.Fatalf("setUp failed: %v", err)
   436  			}
   437  			defer tearDown()
   438  
   439  			fd, err := vfsfs.OpenAt(
   440  				ctx,
   441  				auth.CredentialsFromContext(ctx),
   442  				&vfs.PathOperation{Root: *root, Start: *root, Path: fspath.Parse(test.absPath)},
   443  				&vfs.OpenOptions{},
   444  			)
   445  			if err != nil {
   446  				t.Fatalf("vfsfs.OpenAt failed: %v", err)
   447  			}
   448  
   449  			// Get a local file descriptor and compare its functionality with a vfs file
   450  			// description for the same file.
   451  			localFile, err := testutil.FindFile(path.Join(assetsDir, test.absPath))
   452  			if err != nil {
   453  				t.Fatalf("testutil.FindFile failed for %s: %v", test.absPath, err)
   454  			}
   455  
   456  			f, err := os.Open(localFile)
   457  			if err != nil {
   458  				t.Fatalf("os.Open failed for %s: %v", localFile, err)
   459  			}
   460  			defer f.Close()
   461  
   462  			// Read the entire file by reading one byte repeatedly. Doing this stress
   463  			// tests the underlying file reader implementation.
   464  			got := make([]byte, 1)
   465  			want := make([]byte, 1)
   466  			for {
   467  				n, err := f.Read(want)
   468  				fd.Read(ctx, usermem.BytesIOSequence(got), vfs.ReadOptions{})
   469  
   470  				if diff := cmp.Diff(got, want); diff != "" {
   471  					t.Errorf("file data mismatch (-want +got):\n%s", diff)
   472  				}
   473  
   474  				// Make sure there is no more file data left after getting EOF.
   475  				if n == 0 || err == io.EOF {
   476  					if n, _ := fd.Read(ctx, usermem.BytesIOSequence(got), vfs.ReadOptions{}); n != 0 {
   477  						t.Errorf("extra unexpected file data in file %s in image %s", test.absPath, test.image)
   478  					}
   479  
   480  					break
   481  				}
   482  
   483  				if err != nil {
   484  					t.Fatalf("read failed: %v", err)
   485  				}
   486  			}
   487  		})
   488  	}
   489  }
   490  
   491  // iterDirentsCb is a simple callback which just keeps adding the dirents to an
   492  // internal list. Implements vfs.IterDirentsCallback.
   493  type iterDirentsCb struct {
   494  	dirents []vfs.Dirent
   495  }
   496  
   497  // Compiles only if iterDirentCb implements vfs.IterDirentsCallback.
   498  var _ vfs.IterDirentsCallback = (*iterDirentsCb)(nil)
   499  
   500  // newIterDirentsCb is the iterDirent
   501  func newIterDirentCb() *iterDirentsCb {
   502  	return &iterDirentsCb{dirents: make([]vfs.Dirent, 0)}
   503  }
   504  
   505  // Handle implements vfs.IterDirentsCallback.Handle.
   506  func (cb *iterDirentsCb) Handle(dirent vfs.Dirent) error {
   507  	cb.dirents = append(cb.dirents, dirent)
   508  	return nil
   509  }
   510  
   511  // TestIterDirents tests the FileDescriptionImpl.IterDirents functionality.
   512  func TestIterDirents(t *testing.T) {
   513  	type iterDirentTest struct {
   514  		name  string
   515  		image string
   516  		path  string
   517  		want  []vfs.Dirent
   518  	}
   519  
   520  	wantDirents := []vfs.Dirent{
   521  		{
   522  			Name: ".",
   523  			Type: linux.DT_DIR,
   524  		},
   525  		{
   526  			Name: "..",
   527  			Type: linux.DT_DIR,
   528  		},
   529  		{
   530  			Name: "lost+found",
   531  			Type: linux.DT_DIR,
   532  		},
   533  		{
   534  			Name: "file.txt",
   535  			Type: linux.DT_REG,
   536  		},
   537  		{
   538  			Name: "bigfile.txt",
   539  			Type: linux.DT_REG,
   540  		},
   541  		{
   542  			Name: "symlink.txt",
   543  			Type: linux.DT_LNK,
   544  		},
   545  	}
   546  	tests := []iterDirentTest{
   547  		{
   548  			name:  "ext4 root dir iteration",
   549  			image: ext4ImagePath,
   550  			path:  "/",
   551  			want:  wantDirents,
   552  		},
   553  		{
   554  			name:  "ext3 root dir iteration",
   555  			image: ext3ImagePath,
   556  			path:  "/",
   557  			want:  wantDirents,
   558  		},
   559  		{
   560  			name:  "ext2 root dir iteration",
   561  			image: ext2ImagePath,
   562  			path:  "/",
   563  			want:  wantDirents,
   564  		},
   565  	}
   566  
   567  	for _, test := range tests {
   568  		t.Run(test.name, func(t *testing.T) {
   569  			ctx, vfsfs, root, tearDown, err := setUp(t, test.image)
   570  			if err != nil {
   571  				t.Fatalf("setUp failed: %v", err)
   572  			}
   573  			defer tearDown()
   574  
   575  			fd, err := vfsfs.OpenAt(
   576  				ctx,
   577  				auth.CredentialsFromContext(ctx),
   578  				&vfs.PathOperation{Root: *root, Start: *root, Path: fspath.Parse(test.path)},
   579  				&vfs.OpenOptions{},
   580  			)
   581  			if err != nil {
   582  				t.Fatalf("vfsfs.OpenAt failed: %v", err)
   583  			}
   584  
   585  			cb := &iterDirentsCb{}
   586  			if err = fd.IterDirents(ctx, cb); err != nil {
   587  				t.Fatalf("dir fd.IterDirents() failed: %v", err)
   588  			}
   589  
   590  			sort.Slice(cb.dirents, func(i int, j int) bool { return cb.dirents[i].Name < cb.dirents[j].Name })
   591  			sort.Slice(test.want, func(i int, j int) bool { return test.want[i].Name < test.want[j].Name })
   592  
   593  			// Ignore the inode number and offset of dirents because those are likely to
   594  			// change as the underlying image changes.
   595  			cmpIgnoreFields := cmp.FilterPath(func(p cmp.Path) bool {
   596  				return p.String() == "Ino" || p.String() == "NextOff"
   597  			}, cmp.Ignore())
   598  			if diff := cmp.Diff(cb.dirents, test.want, cmpIgnoreFields); diff != "" {
   599  				t.Errorf("dirents mismatch (-want +got):\n%s", diff)
   600  			}
   601  		})
   602  	}
   603  }
   604  
   605  // TestRootDir tests that the root directory inode is correctly initialized and
   606  // returned from setUp.
   607  func TestRootDir(t *testing.T) {
   608  	type inodeProps struct {
   609  		Mode      linux.FileMode
   610  		UID       auth.KUID
   611  		GID       auth.KGID
   612  		Size      uint64
   613  		InodeSize uint16
   614  		Links     uint16
   615  		Flags     disklayout.InodeFlags
   616  	}
   617  
   618  	type rootDirTest struct {
   619  		name      string
   620  		image     string
   621  		wantInode inodeProps
   622  	}
   623  
   624  	tests := []rootDirTest{
   625  		{
   626  			name:  "ext4 root dir",
   627  			image: ext4ImagePath,
   628  			wantInode: inodeProps{
   629  				Mode:      linux.ModeDirectory | 0755,
   630  				Size:      0x400,
   631  				InodeSize: 0x80,
   632  				Links:     3,
   633  				Flags:     disklayout.InodeFlags{Extents: true},
   634  			},
   635  		},
   636  		{
   637  			name:  "ext3 root dir",
   638  			image: ext3ImagePath,
   639  			wantInode: inodeProps{
   640  				Mode:      linux.ModeDirectory | 0755,
   641  				Size:      0x400,
   642  				InodeSize: 0x80,
   643  				Links:     3,
   644  			},
   645  		},
   646  		{
   647  			name:  "ext2 root dir",
   648  			image: ext2ImagePath,
   649  			wantInode: inodeProps{
   650  				Mode:      linux.ModeDirectory | 0755,
   651  				Size:      0x400,
   652  				InodeSize: 0x80,
   653  				Links:     3,
   654  			},
   655  		},
   656  	}
   657  
   658  	for _, test := range tests {
   659  		t.Run(test.name, func(t *testing.T) {
   660  			_, _, vd, tearDown, err := setUp(t, test.image)
   661  			if err != nil {
   662  				t.Fatalf("setUp failed: %v", err)
   663  			}
   664  			defer tearDown()
   665  
   666  			d, ok := vd.Dentry().Impl().(*dentry)
   667  			if !ok {
   668  				t.Fatalf("ext dentry of incorrect type: %T", vd.Dentry().Impl())
   669  			}
   670  
   671  			// Offload inode contents into local structs for comparison.
   672  			gotInode := inodeProps{
   673  				Mode:      d.inode.diskInode.Mode(),
   674  				UID:       d.inode.diskInode.UID(),
   675  				GID:       d.inode.diskInode.GID(),
   676  				Size:      d.inode.diskInode.Size(),
   677  				InodeSize: d.inode.diskInode.InodeSize(),
   678  				Links:     d.inode.diskInode.LinksCount(),
   679  				Flags:     d.inode.diskInode.Flags(),
   680  			}
   681  
   682  			if diff := cmp.Diff(gotInode, test.wantInode); diff != "" {
   683  				t.Errorf("inode mismatch (-want +got):\n%s", diff)
   684  			}
   685  		})
   686  	}
   687  }
   688  
   689  // TestFilesystemInit tests that the filesystem superblock and block group
   690  // descriptors are correctly read in and initialized.
   691  func TestFilesystemInit(t *testing.T) {
   692  	// sb only contains the immutable properties of the superblock.
   693  	type sb struct {
   694  		InodesCount      uint32
   695  		BlocksCount      uint64
   696  		MaxMountCount    uint16
   697  		FirstDataBlock   uint32
   698  		BlockSize        uint64
   699  		BlocksPerGroup   uint32
   700  		ClusterSize      uint64
   701  		ClustersPerGroup uint32
   702  		InodeSize        uint16
   703  		InodesPerGroup   uint32
   704  		BgDescSize       uint16
   705  		Magic            uint16
   706  		Revision         disklayout.SbRevision
   707  		CompatFeatures   disklayout.CompatFeatures
   708  		IncompatFeatures disklayout.IncompatFeatures
   709  		RoCompatFeatures disklayout.RoCompatFeatures
   710  	}
   711  
   712  	// bg only contains the immutable properties of the block group descriptor.
   713  	type bg struct {
   714  		InodeTable      uint64
   715  		BlockBitmap     uint64
   716  		InodeBitmap     uint64
   717  		ExclusionBitmap uint64
   718  		Flags           disklayout.BGFlags
   719  	}
   720  
   721  	type fsInitTest struct {
   722  		name    string
   723  		image   string
   724  		wantSb  sb
   725  		wantBgs []bg
   726  	}
   727  
   728  	tests := []fsInitTest{
   729  		{
   730  			name:  "ext4 filesystem init",
   731  			image: ext4ImagePath,
   732  			wantSb: sb{
   733  				InodesCount:      0x10,
   734  				BlocksCount:      0x40,
   735  				MaxMountCount:    0xffff,
   736  				FirstDataBlock:   0x1,
   737  				BlockSize:        0x400,
   738  				BlocksPerGroup:   0x2000,
   739  				ClusterSize:      0x400,
   740  				ClustersPerGroup: 0x2000,
   741  				InodeSize:        0x80,
   742  				InodesPerGroup:   0x10,
   743  				BgDescSize:       0x40,
   744  				Magic:            linux.EXT_SUPER_MAGIC,
   745  				Revision:         disklayout.DynamicRev,
   746  				CompatFeatures: disklayout.CompatFeatures{
   747  					ExtAttr:     true,
   748  					ResizeInode: true,
   749  					DirIndex:    true,
   750  				},
   751  				IncompatFeatures: disklayout.IncompatFeatures{
   752  					DirentFileType: true,
   753  					Extents:        true,
   754  					Is64Bit:        true,
   755  					FlexBg:         true,
   756  				},
   757  				RoCompatFeatures: disklayout.RoCompatFeatures{
   758  					Sparse:       true,
   759  					LargeFile:    true,
   760  					HugeFile:     true,
   761  					DirNlink:     true,
   762  					ExtraIsize:   true,
   763  					MetadataCsum: true,
   764  				},
   765  			},
   766  			wantBgs: []bg{
   767  				{
   768  					InodeTable:  0x23,
   769  					BlockBitmap: 0x3,
   770  					InodeBitmap: 0x13,
   771  					Flags: disklayout.BGFlags{
   772  						InodeZeroed: true,
   773  					},
   774  				},
   775  			},
   776  		},
   777  		{
   778  			name:  "ext3 filesystem init",
   779  			image: ext3ImagePath,
   780  			wantSb: sb{
   781  				InodesCount:      0x10,
   782  				BlocksCount:      0x40,
   783  				MaxMountCount:    0xffff,
   784  				FirstDataBlock:   0x1,
   785  				BlockSize:        0x400,
   786  				BlocksPerGroup:   0x2000,
   787  				ClusterSize:      0x400,
   788  				ClustersPerGroup: 0x2000,
   789  				InodeSize:        0x80,
   790  				InodesPerGroup:   0x10,
   791  				BgDescSize:       0x20,
   792  				Magic:            linux.EXT_SUPER_MAGIC,
   793  				Revision:         disklayout.DynamicRev,
   794  				CompatFeatures: disklayout.CompatFeatures{
   795  					ExtAttr:     true,
   796  					ResizeInode: true,
   797  					DirIndex:    true,
   798  				},
   799  				IncompatFeatures: disklayout.IncompatFeatures{
   800  					DirentFileType: true,
   801  				},
   802  				RoCompatFeatures: disklayout.RoCompatFeatures{
   803  					Sparse:    true,
   804  					LargeFile: true,
   805  				},
   806  			},
   807  			wantBgs: []bg{
   808  				{
   809  					InodeTable:  0x5,
   810  					BlockBitmap: 0x3,
   811  					InodeBitmap: 0x4,
   812  					Flags: disklayout.BGFlags{
   813  						InodeZeroed: true,
   814  					},
   815  				},
   816  			},
   817  		},
   818  		{
   819  			name:  "ext2 filesystem init",
   820  			image: ext2ImagePath,
   821  			wantSb: sb{
   822  				InodesCount:      0x10,
   823  				BlocksCount:      0x40,
   824  				MaxMountCount:    0xffff,
   825  				FirstDataBlock:   0x1,
   826  				BlockSize:        0x400,
   827  				BlocksPerGroup:   0x2000,
   828  				ClusterSize:      0x400,
   829  				ClustersPerGroup: 0x2000,
   830  				InodeSize:        0x80,
   831  				InodesPerGroup:   0x10,
   832  				BgDescSize:       0x20,
   833  				Magic:            linux.EXT_SUPER_MAGIC,
   834  				Revision:         disklayout.DynamicRev,
   835  				CompatFeatures: disklayout.CompatFeatures{
   836  					ExtAttr:     true,
   837  					ResizeInode: true,
   838  					DirIndex:    true,
   839  				},
   840  				IncompatFeatures: disklayout.IncompatFeatures{
   841  					DirentFileType: true,
   842  				},
   843  				RoCompatFeatures: disklayout.RoCompatFeatures{
   844  					Sparse:    true,
   845  					LargeFile: true,
   846  				},
   847  			},
   848  			wantBgs: []bg{
   849  				{
   850  					InodeTable:  0x5,
   851  					BlockBitmap: 0x3,
   852  					InodeBitmap: 0x4,
   853  					Flags: disklayout.BGFlags{
   854  						InodeZeroed: true,
   855  					},
   856  				},
   857  			},
   858  		},
   859  	}
   860  
   861  	for _, test := range tests {
   862  		t.Run(test.name, func(t *testing.T) {
   863  			_, _, vd, tearDown, err := setUp(t, test.image)
   864  			if err != nil {
   865  				t.Fatalf("setUp failed: %v", err)
   866  			}
   867  			defer tearDown()
   868  
   869  			fs, ok := vd.Mount().Filesystem().Impl().(*filesystem)
   870  			if !ok {
   871  				t.Fatalf("ext filesystem of incorrect type: %T", vd.Mount().Filesystem().Impl())
   872  			}
   873  
   874  			// Offload superblock and block group descriptors contents into
   875  			// local structs for comparison.
   876  			totalFreeInodes := uint32(0)
   877  			totalFreeBlocks := uint64(0)
   878  			gotSb := sb{
   879  				InodesCount:      fs.sb.InodesCount(),
   880  				BlocksCount:      fs.sb.BlocksCount(),
   881  				MaxMountCount:    fs.sb.MaxMountCount(),
   882  				FirstDataBlock:   fs.sb.FirstDataBlock(),
   883  				BlockSize:        fs.sb.BlockSize(),
   884  				BlocksPerGroup:   fs.sb.BlocksPerGroup(),
   885  				ClusterSize:      fs.sb.ClusterSize(),
   886  				ClustersPerGroup: fs.sb.ClustersPerGroup(),
   887  				InodeSize:        fs.sb.InodeSize(),
   888  				InodesPerGroup:   fs.sb.InodesPerGroup(),
   889  				BgDescSize:       fs.sb.BgDescSize(),
   890  				Magic:            fs.sb.Magic(),
   891  				Revision:         fs.sb.Revision(),
   892  				CompatFeatures:   fs.sb.CompatibleFeatures(),
   893  				IncompatFeatures: fs.sb.IncompatibleFeatures(),
   894  				RoCompatFeatures: fs.sb.ReadOnlyCompatibleFeatures(),
   895  			}
   896  			gotNumBgs := len(fs.bgs)
   897  			gotBgs := make([]bg, gotNumBgs)
   898  			for i := 0; i < gotNumBgs; i++ {
   899  				gotBgs[i].InodeTable = fs.bgs[i].InodeTable()
   900  				gotBgs[i].BlockBitmap = fs.bgs[i].BlockBitmap()
   901  				gotBgs[i].InodeBitmap = fs.bgs[i].InodeBitmap()
   902  				gotBgs[i].ExclusionBitmap = fs.bgs[i].ExclusionBitmap()
   903  				gotBgs[i].Flags = fs.bgs[i].Flags()
   904  
   905  				totalFreeInodes += fs.bgs[i].FreeInodesCount()
   906  				totalFreeBlocks += uint64(fs.bgs[i].FreeBlocksCount())
   907  			}
   908  
   909  			if diff := cmp.Diff(gotSb, test.wantSb); diff != "" {
   910  				t.Errorf("superblock mismatch (-want +got):\n%s", diff)
   911  			}
   912  
   913  			if diff := cmp.Diff(gotBgs, test.wantBgs); diff != "" {
   914  				t.Errorf("block group descriptors mismatch (-want +got):\n%s", diff)
   915  			}
   916  
   917  			if diff := cmp.Diff(totalFreeInodes, fs.sb.FreeInodesCount()); diff != "" {
   918  				t.Errorf("total free inodes mismatch (-want +got):\n%s", diff)
   919  			}
   920  
   921  			if diff := cmp.Diff(totalFreeBlocks, fs.sb.FreeBlocksCount()); diff != "" {
   922  				t.Errorf("total free blocks mismatch (-want +got):\n%s", diff)
   923  			}
   924  		})
   925  	}
   926  }