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