blob: d30bdceeac5cd126d94fdae61066c14a975028a6 [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"
Lorenz Brun9956e722021-03-24 18:48:55 +010034
35 "golang.org/x/sys/unix"
36)
37
Serge Bazanski216fe7b2021-05-21 18:36:16 +020038// Lazily-initialized file descriptor for the control device /dev/loop-control
39// (singleton)
Lorenz Brun9956e722021-03-24 18:48:55 +010040var (
41 mutex sync.Mutex
42 loopControlFd *os.File
43)
44
45const (
Lorenz Brun9956e722021-03-24 18:48:55 +010046 // LOOP_MAJOR from @linux//include/uapi/linux:major.h
47 loopMajor = 7
48)
49
Lorenz Brun9956e722021-03-24 18:48:55 +010050type Config struct {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020051 // Block size of the loop device in bytes. Power of 2 between 512 and page
52 // size. Zero defaults to an reasonable block size.
Lorenz Brun9956e722021-03-24 18:48:55 +010053 BlockSize uint32
54 // Combination of flags from the Flag constants in this package.
55 Flags uint32
Serge Bazanski216fe7b2021-05-21 18:36:16 +020056 // Offset in bytes from the start of the file to the first byte of the
57 // device. Usually zero.
Lorenz Brun9956e722021-03-24 18:48:55 +010058 Offset uint64
Serge Bazanski216fe7b2021-05-21 18:36:16 +020059 // Maximum size of the loop device in bytes. Zero defaults to the whole
60 // file.
Lorenz Brun9956e722021-03-24 18:48:55 +010061 SizeLimit uint64
62}
63
64func (c *Config) validate() error {
65 // Additional validation because of inconsistent kernel-side enforcement
66 if c.BlockSize != 0 {
67 if c.BlockSize < 512 || c.BlockSize > uint32(os.Getpagesize()) || bits.OnesCount32(c.BlockSize) > 1 {
68 return errors.New("BlockSize needs to be a power of two between 512 bytes and the OS page size")
69 }
70 }
71 return nil
72}
73
74// ensureFds lazily initializes control devices
75func ensureFds() (err error) {
76 mutex.Lock()
77 defer mutex.Unlock()
78 if loopControlFd != nil {
79 return
80 }
81 loopControlFd, err = os.Open("/dev/loop-control")
82 return
83}
84
85// Device represents a loop device.
86type Device struct {
87 num uint32
88 dev *os.File
89
90 closed bool
91}
92
93// All from @linux//include/uapi/linux:loop.h
94const (
95 // Makes the loop device read-only even if the backing file is read-write.
96 FlagReadOnly = 1
Serge Bazanski216fe7b2021-05-21 18:36:16 +020097 // Unbinds the backing file as soon as the last user is gone. Useful for
98 // unbinding after unmount.
Lorenz Brun9956e722021-03-24 18:48:55 +010099 FlagAutoclear = 4
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200100 // Enables kernel-side partition scanning on the loop device. Needed if you
101 // want to access specific partitions on a loop device.
Lorenz Brun9956e722021-03-24 18:48:55 +0100102 FlagPartscan = 8
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200103 // Enables direct IO for the loop device, bypassing caches and buffer
104 // copying.
Lorenz Brun9956e722021-03-24 18:48:55 +0100105 FlagDirectIO = 16
106)
107
108// Create creates a new loop device backed with the given file.
109func Create(f *os.File, c Config) (*Device, error) {
110 if err := c.validate(); err != nil {
111 return nil, err
112 }
113 if err := ensureFds(); err != nil {
114 return nil, fmt.Errorf("failed to access loop control device: %w", err)
115 }
116 for {
Jan Schärd1683d22025-01-21 10:54:48 +0100117 devNum, err := unix.IoctlRetInt(int(loopControlFd.Fd()), unix.LOOP_CTL_GET_FREE)
118 if err != nil {
119 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 +0100120 }
121 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 +0100122 if errors.Is(err, unix.EBUSY) {
Tim Windelschmidtaf821c82024-04-23 15:03:52 +0200123 // We have lost the race, get a new device
124 continue
Lorenz Brun9956e722021-03-24 18:48:55 +0100125 }
126 if err != nil {
127 return nil, fmt.Errorf("failed to open newly-allocated loop device: %w", err)
128 }
129
Jan Schärd1683d22025-01-21 10:54:48 +0100130 var config unix.LoopConfig
131 config.Fd = uint32(f.Fd())
132 config.Size = c.BlockSize
133 config.Info.Flags = c.Flags
134 config.Info.Offset = c.Offset
135 config.Info.Sizelimit = c.SizeLimit
Lorenz Brun9956e722021-03-24 18:48:55 +0100136
Jan Schärd1683d22025-01-21 10:54:48 +0100137 err = unix.IoctlLoopConfigure(int(dev.Fd()), &config)
138 if errors.Is(err, unix.EBUSY) {
139 // We have lost the race, get a new device
140 continue
141 }
142 if err != nil {
Lorenz Brun9956e722021-03-24 18:48:55 +0100143 return nil, os.NewSyscallError("ioctl(LOOP_CONFIGURE)", err)
144 }
145 return &Device{dev: dev, num: uint32(devNum)}, nil
146 }
147}
148
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200149// Open opens a loop device at the given path. It returns an error if the path
150// is not a loop device.
Lorenz Brun9956e722021-03-24 18:48:55 +0100151func Open(path string) (*Device, error) {
152 potentialDevice, err := os.Open(path)
153 if err != nil {
154 return nil, fmt.Errorf("failed to open device: %w", err)
155 }
Jan Schärd1683d22025-01-21 10:54:48 +0100156 loopInfo, err := unix.IoctlLoopGetStatus64(int(potentialDevice.Fd()))
157 if err == nil {
158 return &Device{dev: potentialDevice, num: loopInfo.Number}, nil
Lorenz Brun9956e722021-03-24 18:48:55 +0100159 }
160 potentialDevice.Close()
Jan Schärd1683d22025-01-21 10:54:48 +0100161 if errors.Is(err, unix.EINVAL) {
Lorenz Brun9956e722021-03-24 18:48:55 +0100162 return nil, errors.New("not a loop device")
163 }
Jan Schärd1683d22025-01-21 10:54:48 +0100164 return nil, fmt.Errorf("failed to determine state of potential loop device: %w", err)
Lorenz Brun9956e722021-03-24 18:48:55 +0100165}
166
167func (d *Device) ensureOpen() error {
168 if d.closed {
169 return errors.New("device is closed")
170 }
171 return nil
172}
173
174// DevPath returns the canonical path of this loop device in /dev.
175func (d *Device) DevPath() (string, error) {
176 if err := d.ensureOpen(); err != nil {
177 return "", err
178 }
179 return fmt.Sprintf("/dev/loop%d", d.num), nil
180}
181
182// Dev returns the Linux device ID of the loop device.
183func (d *Device) Dev() (uint64, error) {
184 if err := d.ensureOpen(); err != nil {
185 return 0, err
186 }
187 return unix.Mkdev(loopMajor, d.num), nil
188}
189
190// BackingFilePath returns the path of the backing file
191func (d *Device) BackingFilePath() (string, error) {
Lorenz Brun764a2de2021-11-22 16:26:36 +0100192 backingFile, err := os.ReadFile(fmt.Sprintf("/sys/block/loop%d/loop/backing_file", d.num))
Lorenz Brun9956e722021-03-24 18:48:55 +0100193 if err != nil {
194 return "", fmt.Errorf("failed to get backing file path: %w", err)
195 }
196 return string(backingFile), err
197}
198
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200199// RefreshSize recalculates the size of the loop device based on the config and
200// the size of the backing file.
Lorenz Brun9956e722021-03-24 18:48:55 +0100201func (d *Device) RefreshSize() error {
202 if err := d.ensureOpen(); err != nil {
203 return err
204 }
205 return unix.IoctlSetInt(int(d.dev.Fd()), unix.LOOP_SET_CAPACITY, 0)
206}
207
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200208// Close closes all file descriptors open to the device. Does not remove the
209// device itself or alter its configuration.
Lorenz Brun9956e722021-03-24 18:48:55 +0100210func (d *Device) Close() error {
211 if err := d.ensureOpen(); err != nil {
212 return err
213 }
214 d.closed = true
215 return d.dev.Close()
216}
217
218// Remove removes the loop device.
219func (d *Device) Remove() error {
220 if err := d.ensureOpen(); err != nil {
221 return err
222 }
223 err := unix.IoctlSetInt(int(d.dev.Fd()), unix.LOOP_CLR_FD, 0)
224 if err != nil {
225 return err
226 }
227 if err := d.Close(); err != nil {
228 return fmt.Errorf("failed to close device: %w", err)
229 }
230 if err := unix.IoctlSetInt(int(loopControlFd.Fd()), unix.LOOP_CTL_REMOVE, int(d.num)); err != nil {
231 return err
232 }
233 return nil
234}