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{}))
+}