blob: f51c0f00144579d76bd92d2541934168702edee2 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Lorenz Brun9956e722021-03-24 18:48:55 +01002// SPDX-License-Identifier: Apache-2.0
Lorenz Brun9956e722021-03-24 18:48:55 +01003
4// Package loop implements an interface to configure Linux loop devices.
5//
Serge Bazanski216fe7b2021-05-21 18:36:16 +02006// This package requires Linux 5.8 or higher because it uses the newer
7// LOOP_CONFIGURE ioctl, which is better-behaved and twice as fast as the old
8// approach. It doesn't support all of the cryptloop functionality as it has
9// been superseded by dm-crypt and has known vulnerabilities. It also doesn't
10// support on-the-fly reconfiguration of loop devices as this is rather
11// unusual, works only under very specific circumstances and would make the API
12// less clean.
Lorenz Brun9956e722021-03-24 18:48:55 +010013package loop
14
15import (
16 "errors"
17 "fmt"
Lorenz Brun9956e722021-03-24 18:48:55 +010018 "math/bits"
19 "os"
20 "sync"
Lorenz Brun9956e722021-03-24 18:48:55 +010021
22 "golang.org/x/sys/unix"
23)
24
Serge Bazanski216fe7b2021-05-21 18:36:16 +020025// Lazily-initialized file descriptor for the control device /dev/loop-control
26// (singleton)
Lorenz Brun9956e722021-03-24 18:48:55 +010027var (
28 mutex sync.Mutex
29 loopControlFd *os.File
30)
31
32const (
Lorenz Brun9956e722021-03-24 18:48:55 +010033 // LOOP_MAJOR from @linux//include/uapi/linux:major.h
34 loopMajor = 7
35)
36
Lorenz Brun9956e722021-03-24 18:48:55 +010037type Config struct {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020038 // Block size of the loop device in bytes. Power of 2 between 512 and page
39 // size. Zero defaults to an reasonable block size.
Lorenz Brun9956e722021-03-24 18:48:55 +010040 BlockSize uint32
41 // Combination of flags from the Flag constants in this package.
42 Flags uint32
Serge Bazanski216fe7b2021-05-21 18:36:16 +020043 // Offset in bytes from the start of the file to the first byte of the
44 // device. Usually zero.
Lorenz Brun9956e722021-03-24 18:48:55 +010045 Offset uint64
Serge Bazanski216fe7b2021-05-21 18:36:16 +020046 // Maximum size of the loop device in bytes. Zero defaults to the whole
47 // file.
Lorenz Brun9956e722021-03-24 18:48:55 +010048 SizeLimit uint64
49}
50
51func (c *Config) validate() error {
52 // Additional validation because of inconsistent kernel-side enforcement
53 if c.BlockSize != 0 {
54 if c.BlockSize < 512 || c.BlockSize > uint32(os.Getpagesize()) || bits.OnesCount32(c.BlockSize) > 1 {
55 return errors.New("BlockSize needs to be a power of two between 512 bytes and the OS page size")
56 }
57 }
58 return nil
59}
60
61// ensureFds lazily initializes control devices
62func ensureFds() (err error) {
63 mutex.Lock()
64 defer mutex.Unlock()
65 if loopControlFd != nil {
66 return
67 }
68 loopControlFd, err = os.Open("/dev/loop-control")
69 return
70}
71
72// Device represents a loop device.
73type Device struct {
74 num uint32
75 dev *os.File
76
77 closed bool
78}
79
80// All from @linux//include/uapi/linux:loop.h
81const (
82 // Makes the loop device read-only even if the backing file is read-write.
83 FlagReadOnly = 1
Serge Bazanski216fe7b2021-05-21 18:36:16 +020084 // Unbinds the backing file as soon as the last user is gone. Useful for
85 // unbinding after unmount.
Lorenz Brun9956e722021-03-24 18:48:55 +010086 FlagAutoclear = 4
Serge Bazanski216fe7b2021-05-21 18:36:16 +020087 // Enables kernel-side partition scanning on the loop device. Needed if you
88 // want to access specific partitions on a loop device.
Lorenz Brun9956e722021-03-24 18:48:55 +010089 FlagPartscan = 8
Serge Bazanski216fe7b2021-05-21 18:36:16 +020090 // Enables direct IO for the loop device, bypassing caches and buffer
91 // copying.
Lorenz Brun9956e722021-03-24 18:48:55 +010092 FlagDirectIO = 16
93)
94
95// Create creates a new loop device backed with the given file.
96func Create(f *os.File, c Config) (*Device, error) {
97 if err := c.validate(); err != nil {
98 return nil, err
99 }
100 if err := ensureFds(); err != nil {
101 return nil, fmt.Errorf("failed to access loop control device: %w", err)
102 }
103 for {
Jan Schärd1683d22025-01-21 10:54:48 +0100104 devNum, err := unix.IoctlRetInt(int(loopControlFd.Fd()), unix.LOOP_CTL_GET_FREE)
105 if err != nil {
106 return nil, fmt.Errorf("failed to allocate loop device: %w", os.NewSyscallError("ioctl(LOOP_CTL_GET_FREE)", err))
Lorenz Brun9956e722021-03-24 18:48:55 +0100107 }
108 dev, err := os.OpenFile(fmt.Sprintf("/dev/loop%v", devNum), os.O_RDWR|os.O_EXCL, 0)
Jan Schärd1683d22025-01-21 10:54:48 +0100109 if errors.Is(err, unix.EBUSY) {
Tim Windelschmidtaf821c82024-04-23 15:03:52 +0200110 // We have lost the race, get a new device
111 continue
Lorenz Brun9956e722021-03-24 18:48:55 +0100112 }
113 if err != nil {
114 return nil, fmt.Errorf("failed to open newly-allocated loop device: %w", err)
115 }
116
Jan Schärd1683d22025-01-21 10:54:48 +0100117 var config unix.LoopConfig
118 config.Fd = uint32(f.Fd())
119 config.Size = c.BlockSize
120 config.Info.Flags = c.Flags
121 config.Info.Offset = c.Offset
122 config.Info.Sizelimit = c.SizeLimit
Lorenz Brun9956e722021-03-24 18:48:55 +0100123
Jan Schärd1683d22025-01-21 10:54:48 +0100124 err = unix.IoctlLoopConfigure(int(dev.Fd()), &config)
125 if errors.Is(err, unix.EBUSY) {
126 // We have lost the race, get a new device
127 continue
128 }
129 if err != nil {
Lorenz Brun9956e722021-03-24 18:48:55 +0100130 return nil, os.NewSyscallError("ioctl(LOOP_CONFIGURE)", err)
131 }
132 return &Device{dev: dev, num: uint32(devNum)}, nil
133 }
134}
135
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200136// Open opens a loop device at the given path. It returns an error if the path
137// is not a loop device.
Lorenz Brun9956e722021-03-24 18:48:55 +0100138func Open(path string) (*Device, error) {
139 potentialDevice, err := os.Open(path)
140 if err != nil {
141 return nil, fmt.Errorf("failed to open device: %w", err)
142 }
Jan Schärd1683d22025-01-21 10:54:48 +0100143 loopInfo, err := unix.IoctlLoopGetStatus64(int(potentialDevice.Fd()))
144 if err == nil {
145 return &Device{dev: potentialDevice, num: loopInfo.Number}, nil
Lorenz Brun9956e722021-03-24 18:48:55 +0100146 }
147 potentialDevice.Close()
Jan Schärd1683d22025-01-21 10:54:48 +0100148 if errors.Is(err, unix.EINVAL) {
Lorenz Brun9956e722021-03-24 18:48:55 +0100149 return nil, errors.New("not a loop device")
150 }
Jan Schärd1683d22025-01-21 10:54:48 +0100151 return nil, fmt.Errorf("failed to determine state of potential loop device: %w", err)
Lorenz Brun9956e722021-03-24 18:48:55 +0100152}
153
154func (d *Device) ensureOpen() error {
155 if d.closed {
156 return errors.New("device is closed")
157 }
158 return nil
159}
160
161// DevPath returns the canonical path of this loop device in /dev.
162func (d *Device) DevPath() (string, error) {
163 if err := d.ensureOpen(); err != nil {
164 return "", err
165 }
166 return fmt.Sprintf("/dev/loop%d", d.num), nil
167}
168
169// Dev returns the Linux device ID of the loop device.
170func (d *Device) Dev() (uint64, error) {
171 if err := d.ensureOpen(); err != nil {
172 return 0, err
173 }
174 return unix.Mkdev(loopMajor, d.num), nil
175}
176
177// BackingFilePath returns the path of the backing file
178func (d *Device) BackingFilePath() (string, error) {
Lorenz Brun764a2de2021-11-22 16:26:36 +0100179 backingFile, err := os.ReadFile(fmt.Sprintf("/sys/block/loop%d/loop/backing_file", d.num))
Lorenz Brun9956e722021-03-24 18:48:55 +0100180 if err != nil {
181 return "", fmt.Errorf("failed to get backing file path: %w", err)
182 }
183 return string(backingFile), err
184}
185
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200186// RefreshSize recalculates the size of the loop device based on the config and
187// the size of the backing file.
Lorenz Brun9956e722021-03-24 18:48:55 +0100188func (d *Device) RefreshSize() error {
189 if err := d.ensureOpen(); err != nil {
190 return err
191 }
192 return unix.IoctlSetInt(int(d.dev.Fd()), unix.LOOP_SET_CAPACITY, 0)
193}
194
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200195// Close closes all file descriptors open to the device. Does not remove the
196// device itself or alter its configuration.
Lorenz Brun9956e722021-03-24 18:48:55 +0100197func (d *Device) Close() error {
198 if err := d.ensureOpen(); err != nil {
199 return err
200 }
201 d.closed = true
202 return d.dev.Close()
203}
204
205// Remove removes the loop device.
206func (d *Device) Remove() error {
207 if err := d.ensureOpen(); err != nil {
208 return err
209 }
210 err := unix.IoctlSetInt(int(d.dev.Fd()), unix.LOOP_CLR_FD, 0)
211 if err != nil {
212 return err
213 }
214 if err := d.Close(); err != nil {
215 return fmt.Errorf("failed to close device: %w", err)
216 }
217 if err := unix.IoctlSetInt(int(loopControlFd.Fd()), unix.LOOP_CTL_REMOVE, int(d.num)); err != nil {
218 return err
219 }
220 return nil
221}