Add Loop Device package
This adds Loop device support in our Linux kernel and adds a Go package for working with them.
It also drive-by adds a pre-mounted tmpfs to ktest as that is quite useful in a lot of situations.
Test Plan: Comes with ktests.
X-Origin-Diff: phab/D745
GitOrigin-RevId: fa06bcdddc033efb136f56da3b4a91159273bf88
diff --git a/metropolis/pkg/loop/loop_test.go b/metropolis/pkg/loop/loop_test.go
new file mode 100644
index 0000000..1ddb34f
--- /dev/null
+++ b/metropolis/pkg/loop/loop_test.go
@@ -0,0 +1,208 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package loop
+
+import (
+ "encoding/binary"
+ "io"
+ "io/ioutil"
+ "math"
+ "os"
+ "runtime"
+ "syscall"
+ "testing"
+ "unsafe"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/sys/unix"
+)
+
+// Write a test file with a very specific pattern (increasing little-endian 16 bit unsigned integers) to detect offset
+// correctness. File is always 128KiB large (2^16 * 2 bytes).
+func makeTestFile() *os.File {
+ f, err := ioutil.TempFile("/tmp", "")
+ if err != nil {
+ panic(err)
+ }
+ for i := 0; i <= math.MaxUint16; i++ {
+ if err := binary.Write(f, binary.LittleEndian, uint16(i)); err != nil {
+ panic(err)
+ }
+ }
+ if _, err := f.Seek(0, io.SeekStart); err != nil {
+ panic(err)
+ }
+ return f
+}
+
+func getBlkdevSize(f *os.File) (size uint64) {
+ if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), unix.BLKGETSIZE64, uintptr(unsafe.Pointer(&size))); err != 0 {
+ panic(err)
+ }
+ return
+}
+
+func getOffsetFromContent(dev *Device) (firstIndex uint16) {
+ if err := binary.Read(dev.dev, binary.LittleEndian, &firstIndex); err != nil {
+ panic(err)
+ }
+ firstIndex *= 2 // 2 bytes per index
+ return
+}
+
+func setupCreate(t *testing.T, config Config) *Device {
+ f := makeTestFile()
+ dev, err := Create(f, config)
+ defer f.Close()
+ assert.NoError(t, err)
+ t.Cleanup(func() {
+ if dev != nil {
+ dev.Remove()
+ }
+ os.Remove(f.Name())
+ })
+ if dev == nil {
+ t.FailNow()
+ }
+ return dev
+}
+
+func TestDeviceAccessors(t *testing.T) {
+ if os.Getenv("IN_KTEST") != "true" {
+ t.Skip("Not in ktest")
+ }
+ dev := setupCreate(t, Config{})
+
+ devPath, err := dev.DevPath()
+ assert.NoError(t, err)
+ require.Equal(t, "/dev/loop0", devPath)
+
+ var stat unix.Stat_t
+ assert.NoError(t, unix.Stat("/dev/loop0", &stat))
+ devNum, err := dev.Dev()
+ assert.NoError(t, err)
+ require.Equal(t, stat.Rdev, devNum)
+
+ backingFile, err := dev.BackingFilePath()
+ assert.NoError(t, err)
+ // The filename of the temporary file is not available in this context, but we know that the file
+ // needs to be in /tmp, which should be a good-enough test.
+ assert.Contains(t, backingFile, "/tmp/")
+}
+
+func TestCreate(t *testing.T) {
+ if os.Getenv("IN_KTEST") != "true" {
+ t.Skip("Not in ktest")
+ }
+ t.Parallel()
+ tests := []struct {
+ name string
+ config Config
+ validate func(t *testing.T, dev *Device)
+ }{
+ {"NoOpts", Config{}, func(t *testing.T, dev *Device) {
+ require.Equal(t, uint64(128*1024), getBlkdevSize(dev.dev))
+ require.Equal(t, uint16(0), getOffsetFromContent(dev))
+
+ _, err := dev.dev.WriteString("test")
+ assert.NoError(t, err)
+ }},
+ {"DirectIO", Config{Flags: FlagDirectIO}, func(t *testing.T, dev *Device) {
+ require.Equal(t, uint64(128*1024), getBlkdevSize(dev.dev))
+
+ _, err := dev.dev.WriteString("test")
+ assert.NoError(t, err)
+ }},
+ {"ReadOnly", Config{Flags: FlagReadOnly}, func(t *testing.T, dev *Device) {
+ _, err := dev.dev.WriteString("test")
+ assert.Error(t, err)
+ }},
+ {"Mapping", Config{BlockSize: 512, SizeLimit: 2048, Offset: 4096}, func(t *testing.T, dev *Device) {
+ assert.Equal(t, uint16(4096), getOffsetFromContent(dev))
+ assert.Equal(t, uint64(2048), getBlkdevSize(dev.dev))
+ }},
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ dev := setupCreate(t, test.config)
+ test.validate(t, dev)
+ assert.NoError(t, dev.Remove())
+ })
+ }
+}
+
+func TestOpenBadDevice(t *testing.T) {
+ if os.Getenv("IN_KTEST") != "true" {
+ t.Skip("Not in ktest")
+ }
+ dev, err := Open("/dev/null")
+ require.Error(t, err)
+ if dev != nil { // Prevent leaks in case this test fails
+ dev.Close()
+ }
+}
+
+func TestOpen(t *testing.T) {
+ if os.Getenv("IN_KTEST") != "true" {
+ t.Skip("Not in ktest")
+ }
+ f := makeTestFile()
+ defer os.Remove(f.Name())
+ defer f.Close()
+ dev, err := Create(f, Config{})
+ assert.NoError(t, err)
+ path, err := dev.DevPath()
+ assert.NoError(t, err)
+ assert.NoError(t, dev.Close())
+ reopenedDev, err := Open(path)
+ assert.NoError(t, err)
+ defer reopenedDev.Remove()
+ reopenedDevPath, err := reopenedDev.DevPath()
+ assert.NoError(t, err)
+ require.Equal(t, path, reopenedDevPath) // Still needs to be the same device
+}
+
+func TestResize(t *testing.T) {
+ if os.Getenv("IN_KTEST") != "true" {
+ t.Skip("Not in ktest")
+ }
+ f, err := ioutil.TempFile("/tmp", "")
+ assert.NoError(t, err)
+ empty1K := make([]byte, 1024)
+ for i := 0; i < 64; i++ {
+ _, err := f.Write(empty1K)
+ assert.NoError(t, err)
+ }
+ dev, err := Create(f, Config{})
+ assert.NoError(t, err)
+ require.Equal(t, uint64(64*1024), getBlkdevSize(dev.dev))
+ for i := 0; i < 32; i++ {
+ _, err := f.Write(empty1K)
+ assert.NoError(t, err)
+ }
+ assert.NoError(t, f.Sync())
+ assert.NoError(t, dev.RefreshSize())
+ require.Equal(t, uint64(96*1024), getBlkdevSize(dev.dev))
+}
+
+func TestStructSize(t *testing.T) {
+ if runtime.GOOS != "linux" && runtime.GOARCH != "amd64" {
+ t.Skip("Reference value not available")
+ }
+ require.Equal(t, uintptr(304), unsafe.Sizeof(loopConfig{}))
+}