From 3c3be920166a035329f64c06849dd514ca56aa81 Mon Sep 17 00:00:00 2001 From: Jonathan Lebon Date: Tue, 13 Aug 2024 11:57:59 -0400 Subject: [PATCH] devices/loopback: add read-only option It's sometimes useful to set up a loop device for an already formatted disk/filesystem image to derive new artifacts from it. In that case, we want to make sure it's impossible to modify its contents in any way in that process, both for our own purposes and for other stages operating on it. Notably, mounting some filesystems read-only still seem to touch the disk (like XFS). --- devices/org.osbuild.loopback | 10 ++++++++-- osbuild/loop.py | 22 +++++++++++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/devices/org.osbuild.loopback b/devices/org.osbuild.loopback index b98ddfea..b19f147d 100755 --- a/devices/org.osbuild.loopback +++ b/devices/org.osbuild.loopback @@ -58,6 +58,10 @@ SCHEMA = """ "lock": { "type": "boolean", "description": "Lock the device after opening it" + }, + "read-only": { + "type": "boolean", + "description": "Set up the device as read-only" } } """ @@ -77,7 +81,7 @@ class LoopbackService(devices.DeviceService): lock = UdevInhibitor.for_device(lo.LOOP_MAJOR, lo.minor) lo.on_close = lambda _l: lock.release() - def make_loop(self, fd: int, offset, sizelimit, lock, partscan): + def make_loop(self, fd: int, offset, sizelimit, lock, partscan, read_only): if not sizelimit: sizelimit = os.fstat(fd).st_size - offset else: @@ -89,6 +93,7 @@ class LoopbackService(devices.DeviceService): sizelimit=sizelimit, blocksize=self.sector_size, partscan=partscan, + read_only=read_only, autoclear=True) return lo @@ -100,12 +105,13 @@ class LoopbackService(devices.DeviceService): size = options.get("size") lock = options.get("lock", False) partscan = options.get("partscan", False) + read_only = options.get("read-only", False) path = os.path.join(tree, filename.lstrip("/")) self.fd = os.open(path, os.O_RDWR | os.O_CLOEXEC) try: - self.lo = self.make_loop(self.fd, start, size, lock, partscan) + self.lo = self.make_loop(self.fd, start, size, lock, partscan, read_only) except Exception as error: # pylint: disable: broad-except self.close() raise error from None diff --git a/osbuild/loop.py b/osbuild/loop.py index 2e05638e..ec6d3619 100644 --- a/osbuild/loop.py +++ b/osbuild/loop.py @@ -317,7 +317,7 @@ class Loop: # it is bound, check if it is bound by `fd` return loop_info.is_bound_to(file_info) - def _config_info(self, info, offset, sizelimit, autoclear, partscan): + def _config_info(self, info, offset, sizelimit, autoclear, partscan, read_only): # pylint: disable=attribute-defined-outside-init if offset: info.lo_offset = offset @@ -333,9 +333,14 @@ class Loop: info.lo_flags |= self.LO_FLAGS_PARTSCAN else: info.lo_flags &= ~self.LO_FLAGS_PARTSCAN + if read_only is not None: + if read_only: + info.lo_flags |= self.LO_FLAGS_READ_ONLY + else: + info.lo_flags &= ~self.LO_FLAGS_READ_ONLY return info - def set_status(self, offset=None, sizelimit=None, autoclear=None, partscan=None): + def set_status(self, offset=None, sizelimit=None, autoclear=None, partscan=None, read_only=None): """Set properties of the loopback device The loopback device must be bound, and the properties will be @@ -371,12 +376,16 @@ class Loop: partscan : bool, optional Whether or not to enable partition scanning, or None to leave unchanged (default is None) + read_only : bool, optional + Whether or not to setup the loopback device as read-only (default + is None). """ - info = self._config_info(self.get_status(), offset, sizelimit, autoclear, partscan) + info = self._config_info(self.get_status(), offset, sizelimit, autoclear, partscan, read_only) fcntl.ioctl(self.fd, self.LOOP_SET_STATUS64, info) - def configure(self, fd: int, offset=None, sizelimit=None, blocksize=0, autoclear=None, partscan=None): + def configure(self, fd: int, offset=None, sizelimit=None, blocksize=0, autoclear=None, partscan=None, + read_only=None): """ Configure the loopback device Bind and configure in a single operation a file descriptor to the @@ -426,12 +435,15 @@ class Loop: partscan : bool, optional Whether or not to enable partition scanning, or None to leave unchanged (default is None) + read_only : bool, optional + Whether or not to setup the loopback device as read-only (default + is None). """ # pylint: disable=attribute-defined-outside-init config = LoopConfig() config.fd = fd config.block_size = int(blocksize) - config.info = self._config_info(LoopInfo(), offset, sizelimit, autoclear, partscan) + config.info = self._config_info(LoopInfo(), offset, sizelimit, autoclear, partscan, read_only) try: fcntl.ioctl(self.fd, self.LOOP_CONFIGURE, config) except OSError as e: