| // 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 |
| } |