From f70a1f835ef614f5e1ad1bde76d4629c650fd192 Mon Sep 17 00:00:00 2001 From: JJ Liebig Date: Tue, 15 Sep 2026 18:14:07 +0200 Subject: fix(windows): reject dynamic msvc runtime in packaging (#4200) The v0.8.2 Windows archive shipped an herdr.exe that imported VCRUNTIME140.dll and the api-ms-win-crt-* Universal CRT, none of which the archive provides. Static CRT linkage was restored for x86_64-pc-windows-msvc in .cargo/config.toml, but nothing verified the packaged executable, so the dependency could silently regress. Parse the PE import and delay-import tables and reject herdr.exe when it depends on the dynamic Microsoft C/C++ runtime, including release and debug variants. Check the input while staging and re-check the staged executable before archiving so a direct archive cannot bypass the guard. refs #3129 --- scripts/test_package_windows_conpty.py | 186 ++++++++++++++++++++++++++++++++- 1 file changed, 185 insertions(+), 1 deletion(-) (limited to 'scripts/test_package_windows_conpty.py') diff --git a/scripts/test_package_windows_conpty.py b/scripts/test_package_windows_conpty.py index b92edaf3..61aec9d0 100644 --- a/scripts/test_package_windows_conpty.py +++ b/scripts/test_package_windows_conpty.py @@ -129,11 +129,20 @@ class WindowsConptyPackageTests(unittest.TestCase): encoding="utf-8", ) herdr = root / "input-herdr.exe" - herdr.write_bytes(b"herdr") + herdr.write_bytes(self._pe_with_imports(0x8664, ["KERNEL32.dll"])) stage = root / "stage" package.stage_bundle(metadata_path, "x86_64", nupkg, herdr, stage) package.validate_stage(metadata_path, "x86_64", stage) + (stage / "herdr.exe").write_bytes( + self._pe_with_imports(0x8664, ["MSVCP140D.dll"]) + ) + with self.assertRaisesRegex(ValueError, "dynamic Microsoft"): + package.validate_stage(metadata_path, "x86_64", stage) + (stage / "herdr.exe").write_bytes( + self._pe_with_imports(0x8664, ["KERNEL32.dll"]) + ) + output = root / "herdr.zip" package.archive_bundle(metadata_path, "x86_64", stage, output) with zipfile.ZipFile(output) as archive: @@ -148,6 +157,107 @@ class WindowsConptyPackageTests(unittest.TestCase): with self.assertRaisesRegex(ValueError, "staged file hash mismatch"): package.validate_stage(metadata_path, "x86_64", stage) + def test_pe_imported_dlls_reads_import_and_delay_import_tables(self) -> None: + data = self._pe_with_imports( + 0x8664, + ["KERNEL32.dll", "api-ms-win-core-synch-l1-2-0.dll"], + ["USER32.dll"], + ) + self.assertEqual( + set(package.pe_imported_dlls(data)), + {"KERNEL32.dll", "api-ms-win-core-synch-l1-2-0.dll", "USER32.dll"}, + ) + pe32 = self._pe_with_imports(0x8664, ["VCRUNTIME140.dll"], magic=0x10B) + self.assertEqual(package.pe_imported_dlls(pe32), ["VCRUNTIME140.dll"]) + + def test_pe_imported_dlls_handles_va_based_delay_imports(self) -> None: + # VA-based delay-import descriptors predate the RVA flag and are only + # representable with a 32-bit image base. + data = self._pe_with_imports( + 0x8664, + ["KERNEL32.dll"], + ["MSVCP140.dll"], + magic=0x10B, + delay_imports_use_va=True, + ) + self.assertEqual( + set(package.pe_imported_dlls(data)), + {"KERNEL32.dll", "MSVCP140.dll"}, + ) + with self.assertRaisesRegex(ValueError, "dynamic Microsoft"): + package.validate_static_msvc_runtime(data, "herdr.exe") + + def test_pe_imported_dlls_respects_declared_directory_size(self) -> None: + # The second descriptor is outside the declared import-directory size, + # so it must not be treated as an import even though it names a + # dynamic runtime. + outside = self._pe_with_imports( + 0x8664, + ["KERNEL32.dll", "VCRUNTIME140.dll"], + import_directory_size=20, + ) + self.assertEqual(package.pe_imported_dlls(outside), ["KERNEL32.dll"]) + package.validate_static_msvc_runtime(outside, "herdr.exe") + + inside = self._pe_with_imports(0x8664, ["KERNEL32.dll", "VCRUNTIME140.dll"]) + with self.assertRaisesRegex(ValueError, "dynamic Microsoft"): + package.validate_static_msvc_runtime(inside, "herdr.exe") + + def test_dynamic_msvc_runtime_imports_are_rejected(self) -> None: + for dll in ( + "VCRUNTIME140.dll", + "vcruntime140_1.dll", + "vcruntime140d.dll", + "MSVCP140.dll", + "MSVCP140D.dll", + "MSVCR120.dll", + "MSVCP120.dll", + "concrt140d.dll", + "ucrtbase.dll", + "ucrtbased.dll", + "api-ms-win-crt-runtime-l1-1-0.dll", + ): + with self.subTest(dll=dll): + with self.assertRaisesRegex(ValueError, "dynamic Microsoft"): + package.validate_static_msvc_runtime( + self._pe_with_imports(0x8664, [dll]), "herdr.exe" + ) + + def test_static_crt_imports_are_accepted(self) -> None: + data = self._pe_with_imports( + 0x8664, + [ + "KERNEL32.dll", + "ntdll.dll", + "msvcrt.dll", + "api-ms-win-core-synch-l1-2-0.dll", + ], + ["USER32.dll"], + ) + package.validate_static_msvc_runtime(data, "herdr.exe") + self.assertFalse(package.is_dynamic_msvc_runtime("msvcrt.dll")) + + def test_stage_rejects_dynamic_crt_executable(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + metadata_path = root / "conpty.json" + metadata_path.write_text( + json.dumps( + {"schema_version": 1, "bundles": {"x86_64": {}}, "notices": []} + ), + encoding="utf-8", + ) + herdr = root / "herdr.exe" + herdr.write_bytes(self._pe_with_imports(0x8664, ["VCRUNTIME140.dll"])) + with self.assertRaisesRegex(ValueError, "dynamic Microsoft"): + package.stage_bundle( + metadata_path, + "x86_64", + root / "missing.nupkg", + herdr, + root / "stage", + ) + @staticmethod def _pe(machine: int, payload: bytes) -> bytes: data = bytearray(0x80) @@ -157,6 +267,80 @@ class WindowsConptyPackageTests(unittest.TestCase): struct.pack_into(" bytes: + delay_imports = delay_imports or [] + pe_offset = 0x40 + optional_size = 0xF0 if magic == 0x20B else 0xE0 + data_directory_offset = 112 if magic == 0x20B else 96 + image_base = 0x140000000 if magic == 0x20B else 0x400000 + section_rva = 0x1000 + section_raw = 0x400 + + import_size = 20 * (len(imports) + 1) + delay_size = 32 * (len(delay_imports) + 1) if delay_imports else 0 + section = bytearray(import_size + delay_size) + name_rvas: dict[str, int] = {} + for name in [*imports, *delay_imports]: + if name not in name_rvas: + name_rvas[name] = section_rva + len(section) + section += name.encode("ascii") + b"\0" + for index, name in enumerate(imports): + struct.pack_into(" dict[str, str]: return { -- cgit v1.2.3