diff options
Diffstat (limited to 'filesystem/mountpoint_test.go')
| -rw-r--r-- | filesystem/mountpoint_test.go | 525 |
1 files changed, 525 insertions, 0 deletions
diff --git a/filesystem/mountpoint_test.go b/filesystem/mountpoint_test.go index 73904a2..fd7e05d 100644 --- a/filesystem/mountpoint_test.go +++ b/filesystem/mountpoint_test.go @@ -17,9 +17,18 @@ * the License. */ +// Note: these tests assume the existence of some well-known directories: /mnt, +// /home, and /tmp. This is because the mountpoint loading code only retains +// mountpoints on valid directories. + package filesystem import ( + "fmt" + "os" + "path/filepath" + "reflect" + "strings" "testing" ) @@ -29,6 +38,504 @@ func TestLoadMountInfo(t *testing.T) { } } +// Lock the mount maps so that concurrent tests don't interfere with each other. +func beginLoadMountInfoTest() { + mountMutex.Lock() +} + +func endLoadMountInfoTest() { + // Invalidate the fake mount information in case a test runs later which + // needs the real mount information. + mountsInitialized = false + mountMutex.Unlock() +} + +func loadMountInfoFromString(str string) { + readMountInfo(strings.NewReader(str)) +} + +func mountForDevice(deviceNumberStr string) *Mount { + deviceNumber, _ := newDeviceNumberFromString(deviceNumberStr) + return mountsByDevice[deviceNumber] +} + +// Test basic loading of a single mountpoint. +func TestLoadMountInfoBasic(t *testing.T) { + var mountinfo = ` +15 0 259:3 / / rw,relatime shared:1 - ext4 /dev/root rw,data=ordered +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + if len(mountsByDevice) != 1 { + t.Error("Loaded wrong number of mounts") + } + mnt := mountForDevice("259:3") + if mnt == nil { + t.Fatal("Failed to load mount") + } + if mnt.Path != "/" { + t.Error("Wrong path") + } + if mnt.FilesystemType != "ext4" { + t.Error("Wrong filesystem type") + } + if mnt.DeviceNumber.String() != "259:3" { + t.Error("Wrong device number") + } + if mnt.Subtree != "/" { + t.Error("Wrong subtree") + } + if mnt.ReadOnly { + t.Error("Wrong readonly flag") + } + if len(mountsByPath) != 1 { + t.Error("mountsByPath doesn't contain exactly one entry") + } + if mountsByPath[mnt.Path] != mnt { + t.Error("mountsByPath doesn't contain the correct entry") + } +} + +// Test that Mount.Device is set to the mountpoint's source device if +// applicable, otherwise it is set to the empty string. +func TestLoadSourceDevice(t *testing.T) { + // The mountinfo parser ignores devices that don't exist. For the valid + // device, try /dev/loop0. If it doesn't exist, skip the test. + if _, err := os.Stat("/dev/loop0"); err != nil { + t.Skip("/dev/loop0 does not exist, skipping test") + } + var mountinfo = ` +15 0 7:0 / / rw shared:1 - foo /dev/loop0 rw,data=ordered +31 15 0:27 / /tmp rw,nosuid,nodev shared:17 - tmpfs tmpfs rw +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + mnt := mountForDevice("7:0") + if mnt.Device != "/dev/loop0" { + t.Error("mnt.Device wasn't set to source device") + } + mnt = mountForDevice("0:27") + if mnt.Device != "" { + t.Error("mnt.Device wasn't set to empty string for an invalid device") + } +} + +// Test that non-directory mounts are ignored. +func TestNondirectoryMountsIgnored(t *testing.T) { + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + file, err := os.CreateTemp("", "fscrypt_regfile") + if err != nil { + t.Fatal(err) + } + file.Close() + defer os.Remove(file.Name()) + + mountinfo := fmt.Sprintf("15 0 259:3 /foo %s rw,relatime shared:1 - ext4 /dev/root rw", file.Name()) + loadMountInfoFromString(mountinfo) + if len(mountsByDevice) != 0 { + t.Error("Non-directory mount wasn't ignored") + } +} + +// Test that when multiple mounts are on one directory, the last is the one +// which is kept. +func TestNonLatestMountsIgnored(t *testing.T) { + mountinfo := ` +15 0 259:3 / / rw shared:1 - ext4 /dev/root rw +15 0 259:3 / / rw shared:1 - f2fs /dev/root rw +15 0 259:3 / / rw shared:1 - ubifs /dev/root rw +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + mnt := mountForDevice("259:3") + if mnt.FilesystemType != "ubifs" { + t.Error("Last mount didn't supersede previous ones") + } +} + +// Test that escape sequences in the mountinfo file are unescaped correctly. +func TestLoadMountWithSpecialCharacters(t *testing.T) { + tempDir, err := os.MkdirTemp("", "fscrypt") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + tempDir, err = filepath.Abs(tempDir) + if err != nil { + t.Fatal(err) + } + mountpoint := filepath.Join(tempDir, "/My Directory\t\n\\") + if err := os.Mkdir(mountpoint, 0700); err != nil { + t.Fatal(err) + } + mountinfo := fmt.Sprintf("15 0 259:3 / %s/My\\040Directory\\011\\012\\134 rw shared:1 - ext4 /dev/root rw", tempDir) + + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + mnt := mountForDevice("259:3") + if mnt.Path != mountpoint { + t.Fatal("Wrong mountpoint") + } +} + +// Tests the EscapeString() and unescapeString() functions. +func TestStringEscaping(t *testing.T) { + charsNeedEscaping := " \t\n\\" + charsDontNeedEscaping := "ABCDEF\u2603\xff\xff\v" + + orig := charsNeedEscaping + charsDontNeedEscaping + escaped := `\040\011\012\134` + charsDontNeedEscaping + if EscapeString(orig) != escaped { + t.Fatal("EscapeString gave wrong result") + } + if unescapeString(escaped) != orig { + t.Fatal("unescapeString gave wrong result") + } +} + +// Test parsing some invalid mountinfo lines. +func TestLoadBadMountInfo(t *testing.T) { + mountinfos := []string{"a", + "a a a a a a a a a a a a a a a", + "a a a a a a a a a a a a - a a", + "15 0 BAD:3 / / rw,relatime shared:1 - ext4 /dev/root rw,data=ordered"} + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + for _, mountinfo := range mountinfos { + loadMountInfoFromString(mountinfo) + if len(mountsByDevice) != 0 { + t.Error("Loaded mount from invalid mountinfo line") + } + } +} + +// Test that the ReadOnly flag is set if the mount is readonly, even if the +// filesystem is read-write. +func TestLoadReadOnlyMount(t *testing.T) { + mountinfo := ` +222 15 259:3 / /mnt ro,relatime shared:1 - ext4 /dev/root rw,data=ordered +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + mnt := mountForDevice("259:3") + if !mnt.ReadOnly { + t.Error("Wrong readonly flag") + } +} + +// Test that a read-write mount is preferred over a read-only mount. +func TestReadWriteMountIsPreferredOverReadOnlyMount(t *testing.T) { + mountinfo := ` +222 15 259:3 / /home ro shared:1 - ext4 /dev/root rw +222 15 259:3 / /mnt rw shared:1 - ext4 /dev/root rw +222 15 259:3 / /tmp ro shared:1 - ext4 /dev/root rw +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + mnt := mountForDevice("259:3") + if mnt.Path != "/mnt" { + t.Error("Wrong mount was chosen") + } +} + +// Test that a mount of the full filesystem is preferred over mounts of non-root +// subtrees, given independent mountpoints. +func TestRootSubtreeIsPreferred(t *testing.T) { + mountinfo := ` +222 15 259:3 /subtree1 /home rw shared:1 - ext4 /dev/root rw +222 15 259:3 / /mnt rw shared:1 - ext4 /dev/root rw +222 15 259:3 /subtree2 /tmp rw shared:1 - ext4 /dev/root rw +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + mnt := mountForDevice("259:3") + if mnt.Subtree != "/" { + t.Error("Wrong mount was chosen") + } +} + +// Test that a mount that is not of the full filesystem but still contains all +// other mounted subtrees is preferred, given independent mountpoints. +func TestHighestSubtreeIsPreferred(t *testing.T) { + mountinfo := ` +222 15 259:3 /foo/bar /mnt rw shared:1 - ext4 /dev/root rw +222 15 259:3 /foo /tmp rw shared:1 - ext4 /dev/root rw +222 15 259:3 /foo/baz /home rw shared:1 - ext4 /dev/root rw +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + deviceNumber, _ := newDeviceNumberFromString("259:3") + mnt := mountsByDevice[deviceNumber] + if mnt.Subtree != "/foo" { + t.Error("Wrong mount was chosen") + } +} + +// Test that mountpoint "/" is preferred, given independent subtrees. +func TestRootMountpointIsPreferred(t *testing.T) { + mountinfo := ` +222 15 259:3 /var/cache/pacman/pkg /mnt rw shared:1 - ext4 /dev/root rw +222 15 259:3 /var/lib/lxc/base/rootfs / rw shared:1 - ext4 /dev/root rw +222 15 259:3 /srv/repo/x86_64 /home rw shared:1 - ext4 /dev/root rw +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + deviceNumber, _ := newDeviceNumberFromString("259:3") + mnt := mountsByDevice[deviceNumber] + if mnt.Subtree != "/var/lib/lxc/base/rootfs" { + t.Error("Wrong mount was chosen") + } +} + +// Test that a mountpoint that is not "/" but still contains all other +// mountpoints is preferred, given independent subtrees. +func TestHighestMountpointIsPreferred(t *testing.T) { + tempDir, err := os.MkdirTemp("", "fscrypt") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + tempDir, err = filepath.Abs(tempDir) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(tempDir+"/a/b", 0700); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(tempDir+"/a/c", 0700); err != nil { + t.Fatal(err) + } + mountinfo := fmt.Sprintf(` +222 15 259:3 /0 %s rw shared:1 - ext4 /dev/root rw +222 15 259:3 /1 %s rw shared:1 - ext4 /dev/root rw +222 15 259:3 /2 %s rw shared:1 - ext4 /dev/root rw +`, tempDir+"/a/b", tempDir+"/a", tempDir+"/a/c") + + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + deviceNumber, _ := newDeviceNumberFromString("259:3") + mnt := mountsByDevice[deviceNumber] + if mnt.Subtree != "/1" { + t.Error("Wrong mount was chosen") + } +} + +// Test that if some subtrees are contained in other subtrees, *and* some +// mountpoints are contained in other mountpoints, the chosen Mount is the root +// of a tree of mountpoints whose mounted subtrees contain all mounted subtrees. +func TestLoadContainedSubtreesAndMountpoints(t *testing.T) { + tempDir, err := os.MkdirTemp("", "fscrypt") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + tempDir, err = filepath.Abs(tempDir) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(tempDir+"/a/b", 0700); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(tempDir+"/a/c", 0700); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(tempDir+"/d", 0700); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(tempDir+"/e", 0700); err != nil { + t.Fatal(err) + } + // The first three mounts form a tree of mountpoints. The rest have + // independent mountpoints but have mounted subtrees contained in the + // mounted subtrees of the first mountpoint tree. + mountinfo := fmt.Sprintf(` +222 15 259:3 /0 %s rw shared:1 - ext4 /dev/root rw +222 15 259:3 /1 %s rw shared:1 - ext4 /dev/root rw +222 15 259:3 /2 %s rw shared:1 - ext4 /dev/root rw +222 15 259:3 /1/3 %s rw shared:1 - ext4 /dev/root rw +222 15 259:3 /2/4 %s rw shared:1 - ext4 /dev/root rw +`, tempDir+"/a/b", tempDir+"/a", tempDir+"/a/c", + tempDir+"/d", tempDir+"/e") + + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + deviceNumber, _ := newDeviceNumberFromString("259:3") + mnt := mountsByDevice[deviceNumber] + if mnt.Subtree != "/1" { + t.Error("Wrong mount was chosen") + } +} + +// Test loading mounts with independent subtrees *and* independent mountpoints. +// This case is ambiguous, so an explicit nil entry should be stored. +func TestLoadAmbiguousMounts(t *testing.T) { + mountinfo := ` +222 15 259:3 /foo /mnt rw shared:1 - ext4 /dev/root rw +222 15 259:3 /bar /tmp rw shared:1 - ext4 /dev/root rw +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + deviceNumber, _ := newDeviceNumberFromString("259:3") + mnt, ok := mountsByDevice[deviceNumber] + if !ok { + t.Error("Entry should exist") + } + if mnt != nil { + t.Error("Entry should be nil") + } +} + +// Test making a filesystem link and following it, and test that leading and +// trailing whitespace in the link is ignored. +func TestGetMountFromLink(t *testing.T) { + mnt, err := getTestMount(t) + if err != nil { + t.Skip(err) + } + link, err := makeLink(mnt) + if err != nil { + t.Fatal(err) + } + linkedMnt, err := getMountFromLink(link) + if err != nil { + t.Fatal(err) + } + if linkedMnt != mnt { + t.Fatal("Link doesn't point to the same Mount") + } + if linkedMnt, err = getMountFromLink(link + "\n"); err != nil { + t.Fatal(err) + } + if linkedMnt != mnt { + t.Fatal("Link doesn't point to the same Mount") + } + if linkedMnt, err = getMountFromLink(" " + link + " \r\n"); err != nil { + t.Fatal(err) + } + if linkedMnt != mnt { + t.Fatal("Link doesn't point to the same Mount") + } +} + +// Test that makeLink() is including the expected information in links. +func TestMakeLink(t *testing.T) { + mnt, err := getTestMount(t) + if err != nil { + t.Skip(err) + } + link, err := makeLink(mnt) + if err != nil { + t.Fatal(err) + } + + // Normally, both UUID and PATH should be included. + if !strings.Contains(link, "UUID=") { + t.Fatal("Link doesn't contain UUID") + } + if !strings.Contains(link, "PATH=") { + t.Fatal("Link doesn't contain PATH") + } + + // Without a valid device number, only PATH should be included. + mntCopy := *mnt + mntCopy.DeviceNumber = 0 + link, err = makeLink(&mntCopy) + if err != nil { + t.Fatal(err) + } + if strings.Contains(link, "UUID=") { + t.Fatal("Link shouldn't contain UUID") + } + if !strings.Contains(link, "PATH=") { + t.Fatal("Link doesn't contain PATH") + } +} + +// Test that old filesystem links that contain a UUID only still work. +func TestGetMountFromLegacyLink(t *testing.T) { + mnt, err := getTestMount(t) + if err != nil { + t.Skip(err) + } + uuid, err := mnt.getFilesystemUUID() + if uuid == "" || err != nil { + t.Fatal("Can't get UUID of test filesystem") + } + + link := fmt.Sprintf("UUID=%s", uuid) + linkedMnt, err := getMountFromLink(link) + if err != nil { + t.Fatal(err) + } + if linkedMnt != mnt { + t.Fatal("Link doesn't point to the same Mount") + } +} + +// Test that if the UUID in a filesystem link doesn't work, then the PATH is +// used instead, and vice versa. +func TestGetMountFromLinkFallback(t *testing.T) { + mnt, err := getTestMount(t) + if err != nil { + t.Skip(err) + } + badUUID := "00000000-0000-0000-0000-000000000000" + badPath := "/NONEXISTENT_MOUNT" + goodUUID, err := mnt.getFilesystemUUID() + if goodUUID == "" || err != nil { + t.Fatal("Can't get UUID of test filesystem") + } + + // only PATH valid (should succeed) + link := fmt.Sprintf("UUID=%s\nPATH=%s\n", badUUID, mnt.Path) + linkedMnt, err := getMountFromLink(link) + if err != nil { + t.Fatal(err) + } + if linkedMnt != mnt { + t.Fatal("Link doesn't point to the same Mount") + } + + // only PATH given at all (should succeed) + link = fmt.Sprintf("PATH=%s\n", mnt.Path) + linkedMnt, err = getMountFromLink(link) + if err != nil { + t.Fatal(err) + } + if linkedMnt != mnt { + t.Fatal("Link doesn't point to the same Mount") + } + + // only UUID valid (should succeed) + link = fmt.Sprintf("UUID=%s\nPATH=%s\n", goodUUID, badPath) + if linkedMnt, err = getMountFromLink(link); err != nil { + t.Fatal(err) + } + if linkedMnt != mnt { + t.Fatal("Link doesn't point to the same Mount") + } + + // neither valid (should fail) + link = fmt.Sprintf("UUID=%s\nPATH=%s\n", badUUID, badPath) + linkedMnt, err = getMountFromLink(link) + if linkedMnt != nil || err == nil { + t.Fatal("Following a bad link succeeded") + } +} + // Benchmarks how long it takes to update the mountpoint data func BenchmarkLoadFirst(b *testing.B) { for n := 0; n < b.N; n++ { @@ -38,3 +545,21 @@ func BenchmarkLoadFirst(b *testing.B) { } } } + +// Test mount comparison by values instead of by reference, +// as the map mountsByDevice gets recreated on each load. +// This ensures that concurrent mounts work properly. +func TestMountComparison(t *testing.T) { + var mountinfo = ` +15 0 259:3 / /home rw,relatime shared:1 - ext4 /dev/root rw,data=ordered +` + beginLoadMountInfoTest() + defer endLoadMountInfoTest() + loadMountInfoFromString(mountinfo) + firstMnt := mountForDevice("259:3") + loadMountInfoFromString(mountinfo) + secondMnt := mountForDevice("259:3") + if !reflect.DeepEqual(firstMnt, secondMnt) { + t.Errorf("Mount comparison does not work") + } +} |