blob: a4974a8413c4ecf70533ea965a0d944e37095b70 [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)
153 if pe, ok := err.(*os.PathError); ok {
154 if pe.Err == unix.EBUSY {
155 // We have lost the race, get a new device
156 continue
157 }
158 }
159 if err != nil {
160 return nil, fmt.Errorf("failed to open newly-allocated loop device: %w", err)
161 }
162
163 var config loopConfig
164 config.fd = uint32(f.Fd())
165 config.blockSize = c.BlockSize
166 config.info.flags = c.Flags
167 config.info.offset = c.Offset
168 config.info.sizeLimit = c.SizeLimit
169
170 if _, _, err := syscall.Syscall(unix.SYS_IOCTL, dev.Fd(), loopConfigure, uintptr(unsafe.Pointer(&config))); err != 0 {
171 if err == unix.EBUSY {
172 // We have lost the race, get a new device
173 continue
174 }
175 return nil, os.NewSyscallError("ioctl(LOOP_CONFIGURE)", err)
176 }
177 return &Device{dev: dev, num: uint32(devNum)}, nil
178 }
179}
180
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200181// Open opens a loop device at the given path. It returns an error if the path
182// is not a loop device.
Lorenz Brun9956e722021-03-24 18:48:55 +0100183func Open(path string) (*Device, error) {
184 potentialDevice, err := os.Open(path)
185 if err != nil {
186 return nil, fmt.Errorf("failed to open device: %w", err)
187 }
188 var loopInfo loopInfo64
189 _, _, err = syscall.Syscall(unix.SYS_IOCTL, potentialDevice.Fd(), unix.LOOP_GET_STATUS64, uintptr(unsafe.Pointer(&loopInfo)))
190 if err == syscall.Errno(0) {
191 return &Device{dev: potentialDevice, num: loopInfo.number}, nil
192 }
193 potentialDevice.Close()
194 if err == syscall.EINVAL {
195 return nil, errors.New("not a loop device")
196 }
197 return nil, fmt.Errorf("failed to determine state of potential loop device: %w", err)
198}
199
200func (d *Device) ensureOpen() error {
201 if d.closed {
202 return errors.New("device is closed")
203 }
204 return nil
205}
206
207// DevPath returns the canonical path of this loop device in /dev.
208func (d *Device) DevPath() (string, error) {
209 if err := d.ensureOpen(); err != nil {
210 return "", err
211 }
212 return fmt.Sprintf("/dev/loop%d", d.num), nil
213}
214
215// Dev returns the Linux device ID of the loop device.
216func (d *Device) Dev() (uint64, error) {
217 if err := d.ensureOpen(); err != nil {
218 return 0, err
219 }
220 return unix.Mkdev(loopMajor, d.num), nil
221}
222
223// BackingFilePath returns the path of the backing file
224func (d *Device) BackingFilePath() (string, error) {
Lorenz Brun764a2de2021-11-22 16:26:36 +0100225 backingFile, err := os.ReadFile(fmt.Sprintf("/sys/block/loop%d/loop/backing_file", d.num))
Lorenz Brun9956e722021-03-24 18:48:55 +0100226 if err != nil {
227 return "", fmt.Errorf("failed to get backing file path: %w", err)
228 }
229 return string(backingFile), err
230}
231
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200232// RefreshSize recalculates the size of the loop device based on the config and
233// the size of the backing file.
Lorenz Brun9956e722021-03-24 18:48:55 +0100234func (d *Device) RefreshSize() error {
235 if err := d.ensureOpen(); err != nil {
236 return err
237 }
238 return unix.IoctlSetInt(int(d.dev.Fd()), unix.LOOP_SET_CAPACITY, 0)
239}
240
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200241// Close closes all file descriptors open to the device. Does not remove the
242// device itself or alter its configuration.
Lorenz Brun9956e722021-03-24 18:48:55 +0100243func (d *Device) Close() error {
244 if err := d.ensureOpen(); err != nil {
245 return err
246 }
247 d.closed = true
248 return d.dev.Close()
249}
250
251// Remove removes the loop device.
252func (d *Device) Remove() error {
253 if err := d.ensureOpen(); err != nil {
254 return err
255 }
256 err := unix.IoctlSetInt(int(d.dev.Fd()), unix.LOOP_CLR_FD, 0)
257 if err != nil {
258 return err
259 }
260 if err := d.Close(); err != nil {
261 return fmt.Errorf("failed to close device: %w", err)
262 }
263 if err := unix.IoctlSetInt(int(loopControlFd.Fd()), unix.LOOP_CTL_REMOVE, int(d.num)); err != nil {
264 return err
265 }
266 return nil
267}