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.go b/metropolis/pkg/loop/loop.go
new file mode 100644
index 0000000..64b533b
--- /dev/null
+++ b/metropolis/pkg/loop/loop.go
@@ -0,0 +1,255 @@
+// 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 implements an interface to configure Linux loop devices.
+//
+// This package requires Linux 5.8 or higher because it uses the newer LOOP_CONFIGURE ioctl, which is better-behaved
+// and twice as fast as the old approach. It doesn't support all of the cryptloop functionality as it has been
+// superseded by dm-crypt and has known vulnerabilities. It also doesn't support on-the-fly reconfiguration of loop
+// devices as this is rather unusual, works only under very specific circumstances and would make the API less clean.
+package loop
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"math/bits"
+	"os"
+	"sync"
+	"syscall"
+	"unsafe"
+
+	"golang.org/x/sys/unix"
+)
+
+// Lazily-initialized file descriptor for the control device /dev/loop-control (singleton)
+var (
+	mutex         sync.Mutex
+	loopControlFd *os.File
+)
+
+const (
+	// LOOP_CONFIGURE from @linux//include/uapi/linux:loop.h
+	loopConfigure = 0x4C0A
+	// LOOP_MAJOR from @linux//include/uapi/linux:major.h
+	loopMajor = 7
+)
+
+// struct loop_config from @linux//include/uapi/linux:loop.h
+type loopConfig struct {
+	fd        uint32
+	blockSize uint32 // Power of 2 between 512 and os.Getpagesize(), defaults reasonably
+	info      loopInfo64
+	_reserved [64]byte
+}
+
+// struct loop_info64 from @linux//include/uapi/linux:loop.h
+type loopInfo64 struct {
+	device         uint64
+	inode          uint64
+	rdevice        uint64
+	offset         uint64 // used
+	sizeLimit      uint64 // used
+	number         uint32
+	encryptType    uint32
+	encryptKeySize uint32
+	flags          uint32   // Flags from Flag constant
+	filename       [64]byte // used
+	cryptname      [64]byte
+	encryptkey     [32]byte
+	init           [2]uint64
+}
+
+type Config struct {
+	// Block size of the loop device in bytes. Power of 2 between 512 and page size.
+	// Zero defaults to an reasonable block size.
+	BlockSize uint32
+	// Combination of flags from the Flag constants in this package.
+	Flags uint32
+	// Offset in bytes from the start of the file to the first byte of the device. Usually zero.
+	Offset uint64
+	// Maximum size of the loop device in bytes. Zero defaults to the whole file.
+	SizeLimit uint64
+}
+
+func (c *Config) validate() error {
+	// Additional validation because of inconsistent kernel-side enforcement
+	if c.BlockSize != 0 {
+		if c.BlockSize < 512 || c.BlockSize > uint32(os.Getpagesize()) || bits.OnesCount32(c.BlockSize) > 1 {
+			return errors.New("BlockSize needs to be a power of two between 512 bytes and the OS page size")
+		}
+	}
+	return nil
+}
+
+// ensureFds lazily initializes control devices
+func ensureFds() (err error) {
+	mutex.Lock()
+	defer mutex.Unlock()
+	if loopControlFd != nil {
+		return
+	}
+	loopControlFd, err = os.Open("/dev/loop-control")
+	return
+}
+
+// Device represents a loop device.
+type Device struct {
+	num uint32
+	dev *os.File
+
+	closed bool
+}
+
+// All from @linux//include/uapi/linux:loop.h
+const (
+	// Makes the loop device read-only even if the backing file is read-write.
+	FlagReadOnly = 1
+	// Unbinds the backing file as soon as the last user is gone. Useful for unbinding after unmount.
+	FlagAutoclear = 4
+	// Enables kernel-side partition scanning on the loop device. Needed if you want to access specific partitions on
+	// a loop device.
+	FlagPartscan = 8
+	// Enables direct IO for the loop device, bypassing caches and buffer copying.
+	FlagDirectIO = 16
+)
+
+// Create creates a new loop device backed with the given file.
+func Create(f *os.File, c Config) (*Device, error) {
+	if err := c.validate(); err != nil {
+		return nil, err
+	}
+	if err := ensureFds(); err != nil {
+		return nil, fmt.Errorf("failed to access loop control device: %w", err)
+	}
+	for {
+		devNum, _, errno := syscall.Syscall(unix.SYS_IOCTL, loopControlFd.Fd(), unix.LOOP_CTL_GET_FREE, 0)
+		if errno != unix.Errno(0) {
+			return nil, fmt.Errorf("failed to allocate loop device: %w", os.NewSyscallError("ioctl(LOOP_CTL_GET_FREE)", errno))
+		}
+		dev, err := os.OpenFile(fmt.Sprintf("/dev/loop%v", devNum), os.O_RDWR|os.O_EXCL, 0)
+		if pe, ok := err.(*os.PathError); ok {
+			if pe.Err == unix.EBUSY {
+				// We have lost the race, get a new device
+				continue
+			}
+		}
+		if err != nil {
+			return nil, fmt.Errorf("failed to open newly-allocated loop device: %w", err)
+		}
+
+		var config loopConfig
+		config.fd = uint32(f.Fd())
+		config.blockSize = c.BlockSize
+		config.info.flags = c.Flags
+		config.info.offset = c.Offset
+		config.info.sizeLimit = c.SizeLimit
+
+		if _, _, err := syscall.Syscall(unix.SYS_IOCTL, dev.Fd(), loopConfigure, uintptr(unsafe.Pointer(&config))); err != 0 {
+			if err == unix.EBUSY {
+				// We have lost the race, get a new device
+				continue
+			}
+			return nil, os.NewSyscallError("ioctl(LOOP_CONFIGURE)", err)
+		}
+		return &Device{dev: dev, num: uint32(devNum)}, nil
+	}
+}
+
+// Open opens a loop device at the given path. It returns an error if the path is not a loop device.
+func Open(path string) (*Device, error) {
+	potentialDevice, err := os.Open(path)
+	if err != nil {
+		return nil, fmt.Errorf("failed to open device: %w", err)
+	}
+	var loopInfo loopInfo64
+	_, _, err = syscall.Syscall(unix.SYS_IOCTL, potentialDevice.Fd(), unix.LOOP_GET_STATUS64, uintptr(unsafe.Pointer(&loopInfo)))
+	if err == syscall.Errno(0) {
+		return &Device{dev: potentialDevice, num: loopInfo.number}, nil
+	}
+	potentialDevice.Close()
+	if err == syscall.EINVAL {
+		return nil, errors.New("not a loop device")
+	}
+	return nil, fmt.Errorf("failed to determine state of potential loop device: %w", err)
+}
+
+func (d *Device) ensureOpen() error {
+	if d.closed {
+		return errors.New("device is closed")
+	}
+	return nil
+}
+
+// DevPath returns the canonical path of this loop device in /dev.
+func (d *Device) DevPath() (string, error) {
+	if err := d.ensureOpen(); err != nil {
+		return "", err
+	}
+	return fmt.Sprintf("/dev/loop%d", d.num), nil
+}
+
+// Dev returns the Linux device ID of the loop device.
+func (d *Device) Dev() (uint64, error) {
+	if err := d.ensureOpen(); err != nil {
+		return 0, err
+	}
+	return unix.Mkdev(loopMajor, d.num), nil
+}
+
+// BackingFilePath returns the path of the backing file
+func (d *Device) BackingFilePath() (string, error) {
+	backingFile, err := ioutil.ReadFile(fmt.Sprintf("/sys/block/loop%d/loop/backing_file", d.num))
+	if err != nil {
+		return "", fmt.Errorf("failed to get backing file path: %w", err)
+	}
+	return string(backingFile), err
+}
+
+// RefreshSize recalculates the size of the loop device based on the config and the size of the backing file.
+func (d *Device) RefreshSize() error {
+	if err := d.ensureOpen(); err != nil {
+		return err
+	}
+	return unix.IoctlSetInt(int(d.dev.Fd()), unix.LOOP_SET_CAPACITY, 0)
+}
+
+// Close closes all file descriptors open to the device. Does not remove the device itself or alter its configuration.
+func (d *Device) Close() error {
+	if err := d.ensureOpen(); err != nil {
+		return err
+	}
+	d.closed = true
+	return d.dev.Close()
+}
+
+// Remove removes the loop device.
+func (d *Device) Remove() error {
+	if err := d.ensureOpen(); err != nil {
+		return err
+	}
+	err := unix.IoctlSetInt(int(d.dev.Fd()), unix.LOOP_CLR_FD, 0)
+	if err != nil {
+		return err
+	}
+	if err := d.Close(); err != nil {
+		return fmt.Errorf("failed to close device: %w", err)
+	}
+	if err := unix.IoctlSetInt(int(loopControlFd.Fd()), unix.LOOP_CTL_REMOVE, int(d.num)); err != nil {
+		return err
+	}
+	return nil
+}