diff --git a/hydra/install_storage.py b/hydra/install_storage.py index c35d5e3..f22f5dd 100644 --- a/hydra/install_storage.py +++ b/hydra/install_storage.py @@ -118,6 +118,14 @@ def partition_plan(request, probe, topo): fail('ESP must be an existing FAT GPT EFI System Partition') if not UUID.fullmatch(esp_node.get('uuid') or ''): fail('ESP filesystem UUID missing') + # fstab resolves the ESP by filesystem UUID. Copies on any surviving + # device are ambiguous even when their GPT partition identities differ. + # Root's old filesystem is replaced before fstab is written. + copies = [path for path, node in topo.nodes.items() + if path not in {root, esp} + and (node.get('uuid') or '').lower() == esp_node['uuid'].lower()] + if copies: + fail('ESP filesystem UUID is ambiguous on surviving devices: ' + ', '.join(sorted(copies))) # Equal GPT IDs anywhere would make later PARTUUID recovery ambiguous. ids = [n.get('partuuid', '').lower() for n in topo.nodes.values() if n.get('partuuid')] if len(ids) != len(set(ids)): diff --git a/tests/test_installer.py b/tests/test_installer.py index 14c7ecf..847033c 100644 --- a/tests/test_installer.py +++ b/tests/test_installer.py @@ -108,6 +108,23 @@ class PartitionGuardTests(unittest.TestCase): def test_explicit_partitions_allow_unrelated_mounted_sibling(self): self.assertEqual(i.validate_request(partition_request(), FakeProbe(partition_topology(sibling_mounted=True))), '/dev/vda2') + def test_duplicate_esp_uuid_on_surviving_devices_is_refused(self): + for location in ('sibling', 'other-disk'): + for copied_uuid in ('ABCD-1234', 'abcd-1234'): + topo = partition_topology() + if location == 'sibling': + copied = topo.nodes['/dev/vda3'] + else: + copied = disk('/dev/vdb1', kind='part', pkname='/dev/vdb') + copied.update(start=2048, size=1024**3, + partuuid='bbbbbbbb-cccc-dddd-eeee-000000000001') + topo = topology(topo.nodes['/dev/vda'], disk('/dev/vdb', children=[copied])) + copied = topo.nodes['/dev/vdb1'] + copied.update(fstype='vfat', uuid=copied_uuid, + parttype='c12a7328-f81f-11d2-ba4b-00a0c93ec93b') + with self.subTest(location=location, uuid=copied_uuid), self.assertRaisesRegex(i.InstallError, 'ESP filesystem UUID'): + i.validate_request(partition_request(), FakeProbe(topo)) + def test_modes_and_confirmations_are_unambiguous(self): for changes in ({'disk': '/dev/vda'}, {'esp_partition': None}, {'root_fs': 'xfs'}, {'confirm_erase': '/dev/vda'}, {'confirm_format': '/dev/vda3', 'execute': True}): @@ -251,6 +268,17 @@ class PartitionPipelineTests(unittest.TestCase): def install(self, runner): i.install(self.request, runner, self.probe, self.root, 'test-password', io.StringIO()) + def test_duplicate_fat_uuid_with_distinct_partuuid_stops_before_root_format(self): + copied = self.probe.topo.nodes['/dev/vda3'] + copied.update(fstype='vfat', uuid='ABCD-1234', + parttype='c12a7328-f81f-11d2-ba4b-00a0c93ec93b') + self.assertNotEqual(copied['partuuid'], self.probe.topo.nodes['/dev/vda1']['partuuid']) + runner = FakeRunner(self.root) + with self.assertRaisesRegex(i.InstallError, 'ESP filesystem UUID'): + self.install(runner) + self.assertEqual(runner.calls, []) + self.assertFalse((self.root/'etc/fstab').exists()) + def test_existing_mode_formats_root_only_and_records_owned_layout(self): runner = FakeRunner(self.root) self.install(runner)