From 80f2872c35e43b31c27206d172af18eb49129277 Mon Sep 17 00:00:00 2001 From: jay23606 <4602056+jay23606@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:24:16 -0400 Subject: [PATCH] build: reject Ubuntu initramfs source edits made during packing The Ubuntu manifest recorded the customization digest computed after the archive was packed, so an edit to a customization source during the build produced an artifact whose helpers came from the old source while the manifest advertised the new digest. The release guest-input gate accepted it. Snapshot the digest before the root is prepared and compare it again after packing. On a mismatch, remove the artifact and its manifest and fail, as the Alpine and kernel builds already do. Fixes #506 Co-Authored-By: Claude Haiku 5.5 Signed-off-by: jay23606 <4602056+jay23606@users.noreply.github.com> --- scripts/nvx_tools/build.py | 23 +++++++++---- scripts/test_nvx_tools.py | 69 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/scripts/nvx_tools/build.py b/scripts/nvx_tools/build.py index 6dfd9cb72..045938981 100644 --- a/scripts/nvx_tools/build.py +++ b/scripts/nvx_tools/build.py @@ -915,6 +915,13 @@ def build_initramfs(config: InitramfsBuildConfig) -> None: provenance_path = output.with_name(InitramfsBuildConstants.PROVENANCE_NAME) if provenance_inputs is not None: provenance_path.unlink(missing_ok=True) + # Snapshot before the root is prepared so the manifest describes the sources + # this build consumed; the post-pack comparison catches any edit in between. + ubuntu_input_sha256 = ( + ubuntu.converter_input_sha256(ubuntu.customization_files()) + if descriptor.name == "ubuntu" + else None + ) root = _prepare_guest_root(config, descriptor) helpers = _install_guest_files(config, root, descriptor) if descriptor.name == "ubuntu": @@ -922,13 +929,15 @@ def build_initramfs(config: InitramfsBuildConfig) -> None: else: _write_apk_manifest(root, output, helpers) _pack_initramfs(root, output) - if descriptor.name == "ubuntu": - _write_ubuntu_manifest( - root, - output, - helpers, - ubuntu.converter_input_sha256(ubuntu.customization_files()), - ) + if ubuntu_input_sha256 is not None: + _write_ubuntu_manifest(root, output, helpers, ubuntu_input_sha256) + if ( + ubuntu.converter_input_sha256(ubuntu.customization_files()) + != ubuntu_input_sha256 + ): + output.unlink(missing_ok=True) + package_manifest.unlink(missing_ok=True) + raise ScriptError("initramfs source inputs changed during the build") if ( provenance_inputs is not None and initramfs_provenance_inputs() != provenance_inputs diff --git a/scripts/test_nvx_tools.py b/scripts/test_nvx_tools.py index 961e7a0ee..d23019983 100644 --- a/scripts/test_nvx_tools.py +++ b/scripts/test_nvx_tools.py @@ -9208,6 +9208,75 @@ def test_ubuntu_initramfs_manifest_binds_artifact_digest(self): ) self.assertEqual(manifest["input_sha256"], "a" * 64) + def test_ubuntu_initramfs_rejects_source_mutation_during_pack(self): + # The manifest must describe the sources the build actually consumed, + # so an edit made while the root is packed must fail the build. + for mutate in (False, True): + with ( + self.subTest(mutate=mutate), + tempfile.TemporaryDirectory() as temporary, + ): + root = Path(temporary) + source = root / "nvx-bashrc" + source.write_bytes(b"old\n") + output = root / "output" / "initramfs-ubuntu.cpio.gz" + output.parent.mkdir() + + def digest(files: tuple[Path, ...]) -> str: + content = b"".join(Path(path).read_bytes() for path in files) + return hashlib.sha256(content).hexdigest() + + def pack( + _root: Path, + archive: Path, + mutate: bool = mutate, + source: Path = source, + ) -> None: + archive.write_bytes(b"old initramfs") + if mutate: + source.write_bytes(b"new\n") + + with ( + patch.object(build, "_require_linux"), + patch.object(build, "_prepare_guest_root", return_value=root), + patch.object(build, "_install_guest_files", return_value={}), + patch.object(build, "_pack_initramfs", side_effect=pack), + patch.object(ubuntu, "customization_files", return_value=(source,)), + patch.object(ubuntu, "converter_input_sha256", side_effect=digest), + patch.object(ubuntu, "apply_metadata_policy"), + patch.object( + ubuntu, + "package_manifest", + return_value={"format": 1, "guest": "ubuntu"}, + ), + ): + config = build_config.InitramfsBuildConfig( + guest="ubuntu", + work=root / "work", + output=output, + ) + if mutate: + with self.assertRaisesRegex( + common.ScriptError, + "inputs changed during the build", + ): + build.build_initramfs(config) + self.assertFalse(output.exists()) + self.assertFalse( + output.with_name(f"{output.name}.packages.json").exists() + ) + else: + build.build_initramfs(config) + manifest = json.loads( + output.with_name(f"{output.name}.packages.json").read_text( + encoding="utf-8" + ) + ) + self.assertEqual( + manifest["input_sha256"], + digest((source,)), + ) + def test_ubuntu_source_requirements_deduplicate_binary_manifests(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary)