debian-forge/osbuild/remoteloop.py
Thomas Lavocat da11ef4eb0 loop: use LOOP_CONFIGURE instead of LOOP_SET_FD
LOOP_CONFIGURE allows to atomically configure the decive when opening
it. This avoid the possibility of a race condition where between set_fd
and set_status some operations are already accepted by the loopback
device. See https://lwn.net/Articles/820408/

This feature was included in the linux kernel 5.8 however it is safe to
not include any kind of fallback to the previous method as @obudai
points out that:

LOOP_CONFIGURE was backported into RHEL 8 kernel in RHEL 8.4 as a part
of https://bugzilla.redhat.com/show_bug.cgi?id=1881760 (block layer:
update to upstream v5.8).

Since RHEL 8.4 is currently the oldest supported release that we support
running osbuild on, it might be just fine implementing this without the
fallback.

From a centos stream 8 container:
kernel-4.18.0-448.el8.x86_64
- loop: Fix missing discard support when using LOOP_CONFIGURE (Ming Lei) [1997338]
- [block] loop: Set correct device size when using LOOP_CONFIGURE (Ming Lei) [1881760]
- [block] loop: unset GENHD_FL_NO_PART_SCAN on LOOP_CONFIGURE (Ming Lei) [1881760]
- [block] loop: Add LOOP_CONFIGURE ioctl (Ming Lei) [1881760]
2023-05-05 15:42:47 +02:00

123 lines
3.5 KiB
Python

import contextlib
import errno
import os
from . import api, loop
from .util import jsoncomm
__all__ = [
"LoopClient",
"LoopServer"
]
class LoopServer(api.BaseAPI):
"""Server for creating loopback devices
The server listens for requests on a AF_UNIX/SOCK_DRGAM sockets.
A request should contain SCM_RIGHTS of two filedescriptors, one
that sholud be the backing file for the new loopdevice, and a
second that should be a directory file descriptor where the new
device node will be created.
The payload should be a JSON object with the mandatory arguments
@fd which is the offset in the SCM_RIGHTS array for the backing
file descriptor and @dir_fd which is the offset for the output
directory. Optionally, @offset and @sizelimit in bytes may also
be specified.
The server respods with a JSON object containing the device name
of the new device node created in the output directory.
The created loopback device is guaranteed to be bound to the
given backing file descriptor for the lifetime of the LoopServer
object.
"""
endpoint = "remoteloop"
def __init__(self, *, socket_address=None):
super().__init__(socket_address)
self.devs = []
self.ctl = loop.LoopControl()
def _create_device(self, fd, dir_fd, offset=None, sizelimit=None):
while True:
# Getting an unbound loopback device and attaching a backing
# file descriptor to it is racy, so we must use a retry loop
lo = loop.Loop(self.ctl.get_unbound())
try:
lo.configure(fd, offset=offset, sizelimit=sizelimit, autoclear=True)
except BlockingIOError:
lo.clear_fd()
lo.close()
continue
except OSError as e:
lo.close()
# `configure` returns EBUSY when the pages from the previously
# bound file have not been fully cleared yet.
if e.errno == errno.EBUSY:
continue
raise e
break
lo.mknod(dir_fd)
# Pin the Loop objects so they are only released when the LoopServer
# is destroyed.
self.devs.append(lo)
return lo.devname
def _message(self, msg, fds, sock):
fd = fds[msg["fd"]]
dir_fd = fds[msg["dir_fd"]]
offset = msg.get("offset")
sizelimit = msg.get("sizelimit")
devname = self._create_device(fd, dir_fd, offset, sizelimit)
sock.send({"devname": devname})
def _cleanup(self):
for lo in self.devs:
lo.close()
self.ctl.close()
class LoopClient:
client = None
def __init__(self, connect_to):
self.client = jsoncomm.Socket.new_client(connect_to)
def __del__(self):
if self.client is not None:
self.client.close()
@contextlib.contextmanager
def device(self, filename, offset=None, sizelimit=None):
req = {}
fds = []
fd = os.open(filename, os.O_RDWR)
dir_fd = os.open("/dev", os.O_DIRECTORY)
fds.append(fd)
req["fd"] = 0
fds.append(dir_fd)
req["dir_fd"] = 1
if offset:
req["offset"] = offset
if sizelimit:
req["sizelimit"] = sizelimit
self.client.send(req, fds=fds)
os.close(dir_fd)
os.close(fd)
payload, _, _ = self.client.recv()
path = os.path.join("/dev", payload["devname"])
try:
yield path
finally:
os.unlink(path)