blob: 64b533b96ce386855980e685083c964e20f181a0 [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//
19// This package requires Linux 5.8 or higher because it uses the newer LOOP_CONFIGURE ioctl, which is better-behaved
20// and twice as fast as the old approach. It doesn't support all of the cryptloop functionality as it has been
21// superseded by dm-crypt and has known vulnerabilities. It also doesn't support on-the-fly reconfiguration of loop
22// devices as this is rather unusual, works only under very specific circumstances and would make the API less clean.
23package loop
24
25import (
26 "errors"
27 "fmt"
28 "io/ioutil"
29 "math/bits"
30 "os"
31 "sync"
32 "syscall"
33 "unsafe"
34
35 "golang.org/x/sys/unix"
36)
37
38// Lazily-initialized file descriptor for the control device /dev/loop-control (singleton)
39var (
40 mutex sync.Mutex
41 loopControlFd *os.File
42)
43
44const (
45 // LOOP_CONFIGURE from @linux//include/uapi/linux:loop.h
46 loopConfigure = 0x4C0A
47 // LOOP_MAJOR from @linux//include/uapi/linux:major.h
48 loopMajor = 7
49)
50
51// struct loop_config from @linux//include/uapi/linux:loop.h
52type loopConfig struct {
53 fd uint32
54 blockSize uint32 // Power of 2 between 512 and os.Getpagesize(), defaults reasonably
55 info loopInfo64
56 _reserved [64]byte
57}
58
59// struct loop_info64 from @linux//include/uapi/linux:loop.h
60type loopInfo64 struct {
61 device uint64
62 inode uint64
63 rdevice uint64
64 offset uint64 // used
65 sizeLimit uint64 // used
66 number uint32
67 encryptType uint32
68 encryptKeySize uint32
69 flags uint32 // Flags from Flag constant
70 filename [64]byte // used
71 cryptname [64]byte
72 encryptkey [32]byte
73 init [2]uint64
74}
75
76type Config struct {
77 // Block size of the loop device in bytes. Power of 2 between 512 and page size.
78 // Zero defaults to an reasonable block size.
79 BlockSize uint32
80 // Combination of flags from the Flag constants in this package.
81 Flags uint32
82 // Offset in bytes from the start of the file to the first byte of the device. Usually zero.
83 Offset uint64
84 // Maximum size of the loop device in bytes. Zero defaults to the whole file.
85 SizeLimit uint64
86}
87
88func (c *Config) validate() error {
89 // Additional validation because of inconsistent kernel-side enforcement
90 if c.BlockSize != 0 {
91 if c.BlockSize < 512 || c.BlockSize > uint32(os.Getpagesize()) || bits.OnesCount32(c.BlockSize) > 1 {
92 return errors.New("BlockSize needs to be a power of two between 512 bytes and the OS page size")
93 }
94 }
95 return nil
96}
97
98// ensureFds lazily initializes control devices
99func ensureFds() (err error) {
100 mutex.Lock()
101 defer mutex.Unlock()
102 if loopControlFd != nil {
103 return
104 }
105 loopControlFd, err = os.Open("/dev/loop-control")
106 return
107}
108
109// Device represents a loop device.
110type Device struct {
111 num uint32
112 dev *os.File
113
114 closed bool
115}
116
117// All from @linux//include/uapi/linux:loop.h
118const (
119 // Makes the loop device read-only even if the backing file is read-write.
120 FlagReadOnly = 1
121 // Unbinds the backing file as soon as the last user is gone. Useful for unbinding after unmount.
122 FlagAutoclear = 4
123 // Enables kernel-side partition scanning on the loop device. Needed if you want to access specific partitions on
124 // a loop device.
125 FlagPartscan = 8
126 // Enables direct IO for the loop device, bypassing caches and buffer copying.
127 FlagDirectIO = 16
128)
129
130// Create creates a new loop device backed with the given file.
131func Create(f *os.File, c Config) (*Device, error) {
132 if err := c.validate(); err != nil {
133 return nil, err
134 }
135 if err := ensureFds(); err != nil {
136 return nil, fmt.Errorf("failed to access loop control device: %w", err)
137 }
138 for {
139 devNum, _, errno := syscall.Syscall(unix.SYS_IOCTL, loopControlFd.Fd(), unix.LOOP_CTL_GET_FREE, 0)
140 if errno != unix.Errno(0) {
141 return nil, fmt.Errorf("failed to allocate loop device: %w", os.NewSyscallError("ioctl(LOOP_CTL_GET_FREE)", errno))
142 }
143 dev, err := os.OpenFile(fmt.Sprintf("/dev/loop%v", devNum), os.O_RDWR|os.O_EXCL, 0)
144 if pe, ok := err.(*os.PathError); ok {
145 if pe.Err == unix.EBUSY {
146 // We have lost the race, get a new device
147 continue
148 }
149 }
150 if err != nil {
151 return nil, fmt.Errorf("failed to open newly-allocated loop device: %w", err)
152 }
153
154 var config loopConfig
155 config.fd = uint32(f.Fd())
156 config.blockSize = c.BlockSize
157 config.info.flags = c.Flags
158 config.info.offset = c.Offset
159 config.info.sizeLimit = c.SizeLimit
160
161 if _, _, err := syscall.Syscall(unix.SYS_IOCTL, dev.Fd(), loopConfigure, uintptr(unsafe.Pointer(&config))); err != 0 {
162 if err == unix.EBUSY {
163 // We have lost the race, get a new device
164 continue
165 }
166 return nil, os.NewSyscallError("ioctl(LOOP_CONFIGURE)", err)
167 }
168 return &Device{dev: dev, num: uint32(devNum)}, nil
169 }
170}
171
172// Open opens a loop device at the given path. It returns an error if the path is not a loop device.
173func Open(path string) (*Device, error) {
174 potentialDevice, err := os.Open(path)
175 if err != nil {
176 return nil, fmt.Errorf("failed to open device: %w", err)
177 }
178 var loopInfo loopInfo64
179 _, _, err = syscall.Syscall(unix.SYS_IOCTL, potentialDevice.Fd(), unix.LOOP_GET_STATUS64, uintptr(unsafe.Pointer(&loopInfo)))
180 if err == syscall.Errno(0) {
181 return &Device{dev: potentialDevice, num: loopInfo.number}, nil
182 }
183 potentialDevice.Close()
184 if err == syscall.EINVAL {
185 return nil, errors.New("not a loop device")
186 }
187 return nil, fmt.Errorf("failed to determine state of potential loop device: %w", err)
188}
189
190func (d *Device) ensureOpen() error {
191 if d.closed {
192 return errors.New("device is closed")
193 }
194 return nil
195}
196
197// DevPath returns the canonical path of this loop device in /dev.
198func (d *Device) DevPath() (string, error) {
199 if err := d.ensureOpen(); err != nil {
200 return "", err
201 }
202 return fmt.Sprintf("/dev/loop%d", d.num), nil
203}
204
205// Dev returns the Linux device ID of the loop device.
206func (d *Device) Dev() (uint64, error) {
207 if err := d.ensureOpen(); err != nil {
208 return 0, err
209 }
210 return unix.Mkdev(loopMajor, d.num), nil
211}
212
213// BackingFilePath returns the path of the backing file
214func (d *Device) BackingFilePath() (string, error) {
215 backingFile, err := ioutil.ReadFile(fmt.Sprintf("/sys/block/loop%d/loop/backing_file", d.num))
216 if err != nil {
217 return "", fmt.Errorf("failed to get backing file path: %w", err)
218 }
219 return string(backingFile), err
220}
221
222// RefreshSize recalculates the size of the loop device based on the config and the size of the backing file.
223func (d *Device) RefreshSize() error {
224 if err := d.ensureOpen(); err != nil {
225 return err
226 }
227 return unix.IoctlSetInt(int(d.dev.Fd()), unix.LOOP_SET_CAPACITY, 0)
228}
229
230// Close closes all file descriptors open to the device. Does not remove the device itself or alter its configuration.
231func (d *Device) Close() error {
232 if err := d.ensureOpen(); err != nil {
233 return err
234 }
235 d.closed = true
236 return d.dev.Close()
237}
238
239// Remove removes the loop device.
240func (d *Device) Remove() error {
241 if err := d.ensureOpen(); err != nil {
242 return err
243 }
244 err := unix.IoctlSetInt(int(d.dev.Fd()), unix.LOOP_CLR_FD, 0)
245 if err != nil {
246 return err
247 }
248 if err := d.Close(); err != nil {
249 return fmt.Errorf("failed to close device: %w", err)
250 }
251 if err := unix.IoctlSetInt(int(loopControlFd.Fd()), unix.LOOP_CTL_REMOVE, int(d.num)); err != nil {
252 return err
253 }
254 return nil
255}