|  | // 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" | 
|  | "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 is a power of 2 between 512 and os.Getpagesize(), defaults | 
|  | // reasonably | 
|  | blockSize uint32 | 
|  | 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 := os.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 | 
|  | } |