blob: a8be9076712ea176490dfeac4aa777216587b1a5 [file] [log] [blame]
Lorenz Brun9956e722021-03-24 18:48:55 +01001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17// Package loop implements an interface to configure Linux loop devices.
18//
Serge Bazanski216fe7b2021-05-21 18:36:16 +020019// This package requires Linux 5.8 or higher because it uses the newer
20// LOOP_CONFIGURE ioctl, which is better-behaved and twice as fast as the old
21// approach. It doesn't support all of the cryptloop functionality as it has
22// been superseded by dm-crypt and has known vulnerabilities. It also doesn't
23// support on-the-fly reconfiguration of loop devices as this is rather
24// unusual, works only under very specific circumstances and would make the API
25// less clean.
Lorenz Brun9956e722021-03-24 18:48:55 +010026package loop
27
28import (
29 "errors"
30 "fmt"
Lorenz Brun9956e722021-03-24 18:48:55 +010031 "math/bits"
32 "os"
33 "sync"
34 "syscall"
35 "unsafe"
36
37 "golang.org/x/sys/unix"
38)
39
Serge Bazanski216fe7b2021-05-21 18:36:16 +020040// Lazily-initialized file descriptor for the control device /dev/loop-control
41// (singleton)
Lorenz Brun9956e722021-03-24 18:48:55 +010042var (
43 mutex sync.Mutex
44 loopControlFd *os.File
45)
46
47const (
48 // LOOP_CONFIGURE from @linux//include/uapi/linux:loop.h
49 loopConfigure = 0x4C0A
50 // LOOP_MAJOR from @linux//include/uapi/linux:major.h
51 loopMajor = 7
52)
53
54// struct loop_config from @linux//include/uapi/linux:loop.h
55type loopConfig struct {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020056 fd uint32
57 // blockSize is a power of 2 between 512 and os.Getpagesize(), defaults
58 // reasonably
59 blockSize uint32
Lorenz Brun9956e722021-03-24 18:48:55 +010060 info loopInfo64
61 _reserved [64]byte
62}
63
64// struct loop_info64 from @linux//include/uapi/linux:loop.h
65type loopInfo64 struct {
66 device uint64
67 inode uint64
68 rdevice uint64
69 offset uint64 // used
70 sizeLimit uint64 // used
71 number uint32
72 encryptType uint32
73 encryptKeySize uint32
74 flags uint32 // Flags from Flag constant
75 filename [64]byte // used
76 cryptname [64]byte
77 encryptkey [32]byte
78 init [2]uint64
79}
80
81type Config struct {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020082 // Block size of the loop device in bytes. Power of 2 between 512 and page
83 // size. Zero defaults to an reasonable block size.
Lorenz Brun9956e722021-03-24 18:48:55 +010084 BlockSize uint32
85 // Combination of flags from the Flag constants in this package.
86 Flags uint32
Serge Bazanski216fe7b2021-05-21 18:36:16 +020087 // Offset in bytes from the start of the file to the first byte of the
88 // device. Usually zero.
Lorenz Brun9956e722021-03-24 18:48:55 +010089 Offset uint64
Serge Bazanski216fe7b2021-05-21 18:36:16 +020090 // Maximum size of the loop device in bytes. Zero defaults to the whole
91 // file.
Lorenz Brun9956e722021-03-24 18:48:55 +010092 SizeLimit uint64
93}
94
95func (c *Config) validate() error {
96 // Additional validation because of inconsistent kernel-side enforcement
97 if c.BlockSize != 0 {
98 if c.BlockSize < 512 || c.BlockSize > uint32(os.Getpagesize()) || bits.OnesCount32(c.BlockSize) > 1 {
99 return errors.New("BlockSize needs to be a power of two between 512 bytes and the OS page size")
100 }
101 }
102 return nil
103}
104
105// ensureFds lazily initializes control devices
106func ensureFds() (err error) {
107 mutex.Lock()
108 defer mutex.Unlock()
109 if loopControlFd != nil {
110 return
111 }
112 loopControlFd, err = os.Open("/dev/loop-control")
113 return
114}
115
116// Device represents a loop device.
117type Device struct {
118 num uint32
119 dev *os.File
120
121 closed bool
122}
123
124// All from @linux//include/uapi/linux:loop.h
125const (
126 // Makes the loop device read-only even if the backing file is read-write.
127 FlagReadOnly = 1
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200128 // Unbinds the backing file as soon as the last user is gone. Useful for
129 // unbinding after unmount.
Lorenz Brun9956e722021-03-24 18:48:55 +0100130 FlagAutoclear = 4
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200131 // Enables kernel-side partition scanning on the loop device. Needed if you
132 // want to access specific partitions on a loop device.
Lorenz Brun9956e722021-03-24 18:48:55 +0100133 FlagPartscan = 8
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200134 // Enables direct IO for the loop device, bypassing caches and buffer
135 // copying.
Lorenz Brun9956e722021-03-24 18:48:55 +0100136 FlagDirectIO = 16
137)
138
139// Create creates a new loop device backed with the given file.
140func Create(f *os.File, c Config) (*Device, error) {
141 if err := c.validate(); err != nil {
142 return nil, err
143 }
144 if err := ensureFds(); err != nil {
145 return nil, fmt.Errorf("failed to access loop control device: %w", err)
146 }
147 for {
148 devNum, _, errno := syscall.Syscall(unix.SYS_IOCTL, loopControlFd.Fd(), unix.LOOP_CTL_GET_FREE, 0)
149 if errno != unix.Errno(0) {
150 return nil, fmt.Errorf("failed to allocate loop device: %w", os.NewSyscallError("ioctl(LOOP_CTL_GET_FREE)", errno))
151 }
152 dev, err := os.OpenFile(fmt.Sprintf("/dev/loop%v", devNum), os.O_RDWR|os.O_EXCL, 0)
Tim Windelschmidtaf821c82024-04-23 15:03:52 +0200153 var pe *os.PathError
154 if errors.As(err, &pe) && errors.Is(pe.Err, unix.EBUSY) {
155 // We have lost the race, get a new device
156 continue
Lorenz Brun9956e722021-03-24 18:48:55 +0100157 }
158 if err != nil {
159 return nil, fmt.Errorf("failed to open newly-allocated loop device: %w", err)
160 }
161
162 var config loopConfig
163 config.fd = uint32(f.Fd())
164 config.blockSize = c.BlockSize
165 config.info.flags = c.Flags
166 config.info.offset = c.Offset
167 config.info.sizeLimit = c.SizeLimit
168
169 if _, _, err := syscall.Syscall(unix.SYS_IOCTL, dev.Fd(), loopConfigure, uintptr(unsafe.Pointer(&config))); err != 0 {
170 if err == unix.EBUSY {
171 // We have lost the race, get a new device
172 continue
173 }
174 return nil, os.NewSyscallError("ioctl(LOOP_CONFIGURE)", err)
175 }
176 return &Device{dev: dev, num: uint32(devNum)}, nil
177 }
178}
179
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200180// Open opens a loop device at the given path. It returns an error if the path
181// is not a loop device.
Lorenz Brun9956e722021-03-24 18:48:55 +0100182func Open(path string) (*Device, error) {
183 potentialDevice, err := os.Open(path)
184 if err != nil {
185 return nil, fmt.Errorf("failed to open device: %w", err)
186 }
187 var loopInfo loopInfo64
188 _, _, err = syscall.Syscall(unix.SYS_IOCTL, potentialDevice.Fd(), unix.LOOP_GET_STATUS64, uintptr(unsafe.Pointer(&loopInfo)))
189 if err == syscall.Errno(0) {
190 return &Device{dev: potentialDevice, num: loopInfo.number}, nil
191 }
192 potentialDevice.Close()
193 if err == syscall.EINVAL {
194 return nil, errors.New("not a loop device")
195 }
196 return nil, fmt.Errorf("failed to determine state of potential loop device: %w", err)
197}
198
199func (d *Device) ensureOpen() error {
200 if d.closed {
201 return errors.New("device is closed")
202 }
203 return nil
204}
205
206// DevPath returns the canonical path of this loop device in /dev.
207func (d *Device) DevPath() (string, error) {
208 if err := d.ensureOpen(); err != nil {
209 return "", err
210 }
211 return fmt.Sprintf("/dev/loop%d", d.num), nil
212}
213
214// Dev returns the Linux device ID of the loop device.
215func (d *Device) Dev() (uint64, error) {
216 if err := d.ensureOpen(); err != nil {
217 return 0, err
218 }
219 return unix.Mkdev(loopMajor, d.num), nil
220}
221
222// BackingFilePath returns the path of the backing file
223func (d *Device) BackingFilePath() (string, error) {
Lorenz Brun764a2de2021-11-22 16:26:36 +0100224 backingFile, err := os.ReadFile(fmt.Sprintf("/sys/block/loop%d/loop/backing_file", d.num))
Lorenz Brun9956e722021-03-24 18:48:55 +0100225 if err != nil {
226 return "", fmt.Errorf("failed to get backing file path: %w", err)
227 }
228 return string(backingFile), err
229}
230
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200231// RefreshSize recalculates the size of the loop device based on the config and
232// the size of the backing file.
Lorenz Brun9956e722021-03-24 18:48:55 +0100233func (d *Device) RefreshSize() error {
234 if err := d.ensureOpen(); err != nil {
235 return err
236 }
237 return unix.IoctlSetInt(int(d.dev.Fd()), unix.LOOP_SET_CAPACITY, 0)
238}
239
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200240// Close closes all file descriptors open to the device. Does not remove the
241// device itself or alter its configuration.
Lorenz Brun9956e722021-03-24 18:48:55 +0100242func (d *Device) Close() error {
243 if err := d.ensureOpen(); err != nil {
244 return err
245 }
246 d.closed = true
247 return d.dev.Close()
248}
249
250// Remove removes the loop device.
251func (d *Device) Remove() error {
252 if err := d.ensureOpen(); err != nil {
253 return err
254 }
255 err := unix.IoctlSetInt(int(d.dev.Fd()), unix.LOOP_CLR_FD, 0)
256 if err != nil {
257 return err
258 }
259 if err := d.Close(); err != nil {
260 return fmt.Errorf("failed to close device: %w", err)
261 }
262 if err := unix.IoctlSetInt(int(loopControlFd.Fd()), unix.LOOP_CTL_REMOVE, int(d.num)); err != nil {
263 return err
264 }
265 return nil
266}