From 87df09c00ca4b3425fa32128599a636a3fe5cfcc Mon Sep 17 00:00:00 2001 From: Cristi Date: Tue, 29 Sep 2026 20:16:22 +0300 Subject: [PATCH] fix(images): Fix symlinked image imports and add regression tests --- gns3server/utils/images.py | 2 +- tests/services/test_image_reconciliation.py | 34 ++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/gns3server/utils/images.py b/gns3server/utils/images.py index a1b208c5c..58acc8b1e 100644 --- a/gns3server/utils/images.py +++ b/gns3server/utils/images.py @@ -132,7 +132,7 @@ def get_builtin_disks() -> List[str]: def inspect_image_file(path, expected_image_type=None, allow_raw_image=False, stopped_event=None): """Read a stable regular file once, never trusting checksum sidecars.""" before = fingerprint(path) - flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0) + flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) with os.fdopen(os.open(path, flags), "rb") as f: info = os.fstat(f.fileno()) if not stat.S_ISREG(info.st_mode) or stat_fingerprint(info) != before: diff --git a/tests/services/test_image_reconciliation.py b/tests/services/test_image_reconciliation.py index cf0c41d6a..fa8e9beba 100644 --- a/tests/services/test_image_reconciliation.py +++ b/tests/services/test_image_reconciliation.py @@ -176,7 +176,39 @@ async def test_rename_retains_old_reference(inventory, config): assert next(row for row in await rows(inventory) if row["image_id"] == old["image_id"])["availability"] == "missing" -async def test_symlinks_hidden_files_and_libraries_are_not_imported(inventory, config, tmp_path): +async def test_symlink_images_are_imported_and_target_changes_detected(inventory, config): + target = image_file(config, ".storage/target.qcow2") + path = image_file(config) + path.unlink() + path.symlink_to(target) + assert fingerprint(path) == fingerprint(target) + result = await scan(inventory) + assert result["status"] == "completed", result + assert result["counts"]["added"] == 1 + original = (await rows(inventory))[0] + assert original["path"] == str(path) + assert original["checksum"] == hashlib.md5(QCOW).hexdigest() + target.write_bytes(QCOW + b"changed") + result = await scan(inventory) + assert result["counts"]["updated"] == 1 + updated = (await rows(inventory))[0] + assert updated["image_id"] == original["image_id"] + assert updated["checksum"] == hashlib.md5(target.read_bytes()).hexdigest() + assert path.is_symlink() + + +@pytest.mark.parametrize("target_kind", ["directory", "missing"]) +async def test_fingerprint_rejects_symlinks_without_regular_file_targets(tmp_path, target_kind): + target = tmp_path / "target" + if target_kind == "directory": + target.mkdir() + link = tmp_path / "image.qcow2" + link.symlink_to(target, target_is_directory=target_kind == "directory") + with pytest.raises(OSError): + fingerprint(link) + + +async def test_external_symlinks_hidden_files_and_libraries_are_not_imported(inventory, config, tmp_path): outside = tmp_path / "outside.qcow2" outside.write_bytes(QCOW) path = image_file(config)