fix(installer): resolve nested loop ancestry to a fixed point

This commit is contained in:
Codex
2026-10-13 05:40:36 +03:00
parent 55648c26e9
commit 5ea9da1162
2 changed files with 111 additions and 10 deletions
+34 -10
View File
@@ -74,6 +74,7 @@ class Topology:
def __init__(self):
self.nodes = {}
self.edges = {}
self.parents = {}
self.protected = set()
@classmethod
@@ -112,6 +113,7 @@ class Topology:
raise InstallError('Inconsistent duplicate lsblk node')
result.nodes[path] = node
result.edges.setdefault(path, set())
result.parents.setdefault(path, set())
if any(m for m in mounts):
result.protected.add(path)
pkname = node.get('pkname')
@@ -168,6 +170,17 @@ class Topology:
def link(self, child, parent):
if child == parent or child not in self.nodes or parent not in self.nodes:
raise InstallError('Unresolved or cyclic block-device parent')
# Backing links discovered after lsblk must obey the same directed
# ancestry rule. An undirected shared disk is not itself a cycle.
seen, pending = set(), [parent]
while pending:
current = pending.pop()
if current == child:
raise InstallError('Cyclic block-device backing ancestry')
if current not in seen:
seen.add(current)
pending.extend(self.parents[current] - seen)
self.parents[child].add(parent)
self.edges[child].add(parent)
self.edges[parent].add(child)
@@ -240,16 +253,6 @@ class SystemProbe:
source = self._block_source(mount, topo)
if source:
topo.protect(source)
# Mounted loops may hide the physical filesystem that stores their file.
for path, node in topo.nodes.items():
if node['type'] == 'loop' and path in topo.protected:
identity = self._loop_backing_identity(path, node['maj:min'])
sources = [p for p, n in topo.nodes.items() if n['maj:min'] == identity]
if len(sources) != 1:
raise InstallError(f'Unknown physical backing for mounted loop: {path} ({identity})')
source = sources[0]
topo.protect(source)
topo.link(path, source)
if not Path(payload).is_file():
raise InstallError(f'Prepared squashfs payload is not a regular file: {payload}')
source = self._source_for_path(self.resolve(payload), topo)
@@ -257,6 +260,27 @@ class SystemProbe:
# tmpfs/overlay payload location cannot establish live-medium ancestry.
raise InstallError('Cannot identify the payload backing block device; use --payload on the mounted live medium')
topo.protect(source)
# Loops can hide further loops, including ancestors of a mounted
# partition or device mapper. Resolve protected components to a fixed
# point, independent of lsblk ordering and payload discovery order.
inspected = set()
while True:
protected_devices = set()
for path in topo.protected:
protected_devices.update(topo.component(path))
pending = [path for path in topo.nodes if path in protected_devices
and topo.nodes[path]['type'] == 'loop' and path not in inspected]
if not pending:
break
for path in pending:
identity = self._loop_backing_identity(path, topo.nodes[path]['maj:min'])
sources = [p for p, n in topo.nodes.items() if n['maj:min'] == identity]
if len(sources) != 1:
raise InstallError(f'Unknown physical backing for mounted loop: {path} ({identity})')
source = sources[0]
topo.link(path, source)
topo.protect(source)
inspected.add(path)
return topo
@staticmethod