175 lines
4.9 KiB
Go
175 lines
4.9 KiB
Go
//go:build linux
|
|
|
|
package loopback
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"syscall"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// Loopback related errors
|
|
var (
|
|
ErrAttachLoopbackDevice = errors.New("loopback attach failed")
|
|
ErrGetLoopbackBackingFile = errors.New("unable to get loopback backing file")
|
|
ErrSetCapacity = errors.New("unable set loopback capacity")
|
|
)
|
|
|
|
func stringToLoopName(src string) [LoNameSize]uint8 {
|
|
var dst [LoNameSize]uint8
|
|
copy(dst[:], src[:])
|
|
return dst
|
|
}
|
|
|
|
func getNextFreeLoopbackIndex() (int, error) {
|
|
f, err := os.OpenFile("/dev/loop-control", os.O_RDONLY, 0o644)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer f.Close()
|
|
|
|
index, err := ioctlLoopCtlGetFree(f.Fd())
|
|
if index < 0 {
|
|
index = 0
|
|
}
|
|
return index, err
|
|
}
|
|
|
|
func openNextAvailableLoopback(sparseName string, sparseFile *os.File) (*os.File, error) {
|
|
// Read information about the loopback file.
|
|
var st syscall.Stat_t
|
|
if err := syscall.Fstat(int(sparseFile.Fd()), &st); err != nil {
|
|
logrus.Errorf("Reading information about loopback file %s: %v", sparseName, err)
|
|
return nil, ErrAttachLoopbackDevice
|
|
}
|
|
|
|
// upper bound to avoid infinite loop
|
|
remaining := 1000
|
|
|
|
// Start looking for a free /dev/loop
|
|
for {
|
|
if remaining == 0 {
|
|
logrus.Errorf("No free loopback devices available")
|
|
return nil, ErrAttachLoopbackDevice
|
|
}
|
|
remaining--
|
|
|
|
index, err := getNextFreeLoopbackIndex()
|
|
if err != nil {
|
|
logrus.Debugf("Error retrieving the next available loopback: %s", err)
|
|
return nil, err
|
|
}
|
|
|
|
target := fmt.Sprintf("/dev/loop%d", index)
|
|
|
|
// OpenFile adds O_CLOEXEC
|
|
loopFile, err := os.OpenFile(target, os.O_RDWR, 0o644)
|
|
if err != nil {
|
|
// The kernel returns ENXIO when opening a device that is in the "deleting" or "rundown" state, so
|
|
// just treat ENXIO as if the device does not exist.
|
|
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, unix.ENXIO) {
|
|
// Another process could have taken the loopback device in the meantime. So repeat
|
|
// the process with the next loopback device.
|
|
continue
|
|
}
|
|
logrus.Errorf("Opening loopback device: %s", err)
|
|
return nil, ErrAttachLoopbackDevice
|
|
}
|
|
|
|
fi, err := loopFile.Stat()
|
|
if err != nil {
|
|
loopFile.Close()
|
|
logrus.Errorf("Stat loopback device: %s", err)
|
|
return nil, ErrAttachLoopbackDevice
|
|
}
|
|
if fi.Mode()&os.ModeDevice != os.ModeDevice {
|
|
loopFile.Close()
|
|
logrus.Errorf("Loopback device %s is not a block device.", target)
|
|
continue
|
|
}
|
|
|
|
// Try to attach to the loop file
|
|
if err := ioctlLoopSetFd(loopFile.Fd(), sparseFile.Fd()); err != nil {
|
|
loopFile.Close()
|
|
|
|
// If the error is EBUSY, then try the next loopback
|
|
if err == syscall.EBUSY {
|
|
continue
|
|
}
|
|
|
|
logrus.Errorf("Cannot set up loopback device %s: %s", target, err)
|
|
return nil, ErrAttachLoopbackDevice
|
|
}
|
|
|
|
// Check if the loopback driver and underlying filesystem agree on the loopback file's
|
|
// device and inode numbers.
|
|
dev, ino, err := getLoopbackBackingFile(loopFile)
|
|
if err != nil {
|
|
logrus.Errorf("Getting loopback backing file: %s", err)
|
|
return nil, ErrGetLoopbackBackingFile
|
|
}
|
|
if dev != uint64(st.Dev) || ino != st.Ino { //nolint:unconvert
|
|
logrus.Errorf("Loopback device and filesystem disagree on device/inode for %q: %#x(%d):%#x(%d) vs %#x(%d):%#x(%d)", sparseName, dev, dev, ino, ino, st.Dev, st.Dev, st.Ino, st.Ino)
|
|
}
|
|
return loopFile, nil
|
|
}
|
|
}
|
|
|
|
// AttachLoopDevice attaches the given sparse file to the next
|
|
// available loopback device. It returns an opened *os.File.
|
|
func AttachLoopDevice(sparseName string) (loop *os.File, err error) {
|
|
return attachLoopDevice(sparseName, false)
|
|
}
|
|
|
|
// AttachLoopDeviceRO attaches the given sparse file opened read-only to
|
|
// the next available loopback device. It returns an opened *os.File.
|
|
func AttachLoopDeviceRO(sparseName string) (loop *os.File, err error) {
|
|
return attachLoopDevice(sparseName, true)
|
|
}
|
|
|
|
func attachLoopDevice(sparseName string, readonly bool) (loop *os.File, err error) {
|
|
var sparseFile *os.File
|
|
|
|
// OpenFile adds O_CLOEXEC
|
|
if readonly {
|
|
sparseFile, err = os.OpenFile(sparseName, os.O_RDONLY, 0o644)
|
|
} else {
|
|
sparseFile, err = os.OpenFile(sparseName, os.O_RDWR, 0o644)
|
|
}
|
|
if err != nil {
|
|
logrus.Errorf("Opening sparse file: %v", err)
|
|
return nil, ErrAttachLoopbackDevice
|
|
}
|
|
defer sparseFile.Close()
|
|
|
|
loopFile, err := openNextAvailableLoopback(sparseName, sparseFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set the status of the loopback device
|
|
loopInfo := &loopInfo64{
|
|
loFileName: stringToLoopName(loopFile.Name()),
|
|
loOffset: 0,
|
|
loFlags: LoFlagsAutoClear,
|
|
}
|
|
|
|
if err := ioctlLoopSetStatus64(loopFile.Fd(), loopInfo); err != nil {
|
|
logrus.Errorf("Cannot set up loopback device info: %s", err)
|
|
|
|
// If the call failed, then free the loopback device
|
|
if err := ioctlLoopClrFd(loopFile.Fd()); err != nil {
|
|
logrus.Error("While cleaning up the loopback device")
|
|
}
|
|
loopFile.Close()
|
|
return nil, ErrAttachLoopbackDevice
|
|
}
|
|
|
|
return loopFile, nil
|
|
}
|