test_ffi_provenance.py (11247B)
1 from __future__ import annotations 2 3 import copy 4 import os 5 import subprocess 6 import sys 7 import tempfile 8 import unittest 9 from pathlib import Path 10 from unittest.mock import patch 11 12 SCRIPTS = Path(__file__).resolve().parent 13 if str(SCRIPTS) not in sys.path: 14 sys.path.insert(0, str(SCRIPTS)) 15 16 import ffi_build as builder # noqa: E402 17 import ffi_provenance as provenance # noqa: E402 18 import ffi_source as source # noqa: E402 19 import package_contract as contract # noqa: E402 20 21 22 class ProducerSourceTests(unittest.TestCase): 23 def setUp(self) -> None: 24 self.temporary = tempfile.TemporaryDirectory() 25 self.addCleanup(self.temporary.cleanup) 26 self.root = Path(self.temporary.name).resolve() 27 self.git("init", "--quiet") 28 (self.root / "core").mkdir() 29 (self.root / "core/lib.rs").write_text("pub fn value() -> u8 { 1 }\n") 30 (self.root / "Cargo.lock").write_text("# synthetic source-capture fixture\n") 31 self.git("add", "core", "Cargo.lock") 32 self.inputs = ["core", "Cargo.lock"] 33 34 def git(self, *args: str) -> str: 35 return subprocess.check_output( 36 ["git", *args], cwd=self.root, text=True, stderr=subprocess.DEVNULL 37 ).strip() 38 39 def snapshot(self) -> dict: 40 return source.source_snapshot(self.root, self.inputs) 41 42 def test_tree_is_real_and_deterministic_without_an_introducing_commit(self) -> None: 43 first = self.snapshot() 44 self.assertEqual(first, self.snapshot()) 45 self.assertEqual(self.git("write-tree"), first["tree"]) 46 self.assertEqual(self.git("cat-file", "-t", first["tree"]), "tree") 47 # Generated evidence never changes its own source tree identity. 48 (self.root / "provenance.json").write_bytes(provenance.encoded(first)) 49 self.git("add", "provenance.json") 50 self.assertEqual(first, self.snapshot()) 51 52 def test_worktree_change_requires_an_explicit_staged_transaction(self) -> None: 53 first = self.snapshot() 54 (self.root / "core/lib.rs").write_text("pub fn value() -> u8 { 2 }\n") 55 with self.assertRaisesRegex(source.ProvenanceError, "differs from staged"): 56 self.snapshot() 57 self.git("add", "core/lib.rs") 58 self.assertNotEqual(first["tree"], self.snapshot()["tree"]) 59 60 def test_untracked_and_missing_source_fail_closed(self) -> None: 61 (self.root / "core/extra.rs").write_text("// untracked input\n") 62 with self.assertRaisesRegex(source.ProvenanceError, "untracked"): 63 self.snapshot() 64 (self.root / "core/extra.rs").unlink() 65 (self.root / "Cargo.lock").unlink() 66 self.git("add", "-u", "Cargo.lock") 67 with self.assertRaisesRegex(source.ProvenanceError, "missing from index"): 68 self.snapshot() 69 70 def test_symlink_and_file_mode_changes_are_rejected(self) -> None: 71 path = self.root / "core/lib.rs" 72 path.chmod(0o755) 73 with self.assertRaisesRegex(source.ProvenanceError, "differs from staged"): 74 self.snapshot() 75 path.unlink() 76 path.symlink_to("../Cargo.lock") 77 with self.assertRaisesRegex(source.ProvenanceError, "symlink"): 78 self.snapshot() 79 self.git("add", "core/lib.rs") 80 with self.assertRaisesRegex(source.ProvenanceError, "unsupported mode"): 81 self.snapshot() 82 83 def test_source_byte_bound_prevents_unbounded_capture(self) -> None: 84 (self.root / "core/lib.rs").write_bytes(b"x" * (source.MAX_BYTES + 1)) 85 with self.assertRaisesRegex(contract.PackageContractError, "byte limit"): 86 self.snapshot() 87 88 def test_gitignore_cannot_hide_a_producer_input(self) -> None: 89 (self.root / ".gitignore").write_text("core/hidden.rs\n") 90 (self.root / "core/hidden.rs").write_text("// ignored source input\n") 91 with self.assertRaisesRegex(source.ProvenanceError, "ignored source"): 92 self.snapshot() 93 94 def test_provenance_rejects_every_source_or_build_tuple_mutation(self) -> None: 95 expected = { 96 "source": self.snapshot(), 97 "cargo_lock_sha256": "a" * 64, 98 "foundation": {"revision": "b" * 40}, 99 "build": { 100 "target": "aarch64-apple-ios", 101 "rustc": "rustc 1.97.1", 102 "features": [], 103 "package_rust_flags": [], 104 "feature_graph": ["tera_core|mobile-social"], 105 }, 106 } 107 provenance.verify_record(provenance.encoded(expected), expected) 108 for section, key, value in ( 109 ("source", "tree", "c" * 40), 110 ("foundation", "revision", "d" * 40), 111 ("build", "target", "aarch64-apple-ios-sim"), 112 ("build", "rustc", "rustc 1.96.0"), 113 ("build", "features", ["extra"]), 114 ("build", "package_rust_flags", ["-Copt-level=0"]), 115 ("build", "feature_graph", []), 116 ): 117 with self.subTest(section=section, key=key): 118 changed = copy.deepcopy(expected) 119 changed[section][key] = value 120 with self.assertRaisesRegex( 121 source.ProvenanceError, "exact source/build tuple" 122 ): 123 provenance.verify_record(provenance.encoded(changed), expected) 124 changed = {**expected, "cargo_lock_sha256": "f" * 64} 125 with self.assertRaises(source.ProvenanceError): 126 provenance.verify_record(provenance.encoded(changed), expected) 127 128 def test_output_rejects_repository_paths_escapes_and_symlinks(self) -> None: 129 external = self.root / "external" 130 external.mkdir() 131 repository = self.root / "repository" 132 repository.mkdir() 133 with patch.dict( 134 os.environ, 135 { 136 "EXT_BUILD_RUN_ACTIVE": "1", 137 "EXT_BUILD_PROJECT_DIR": str(external), 138 }, 139 ): 140 for output in (repository / "evidence.json", external / "../escape.json"): 141 with self.assertRaises(source.ProvenanceError): 142 provenance.output_path(repository, "aarch64-apple-ios", str(output)) 143 (external / "link").symlink_to(repository, target_is_directory=True) 144 with self.assertRaises(source.ProvenanceError): 145 provenance.output_path( 146 repository, "aarch64-apple-ios", str(external / "link/value.json") 147 ) 148 149 def test_ungoverned_build_flags_are_rejected(self) -> None: 150 for key in ( 151 "RUSTFLAGS", 152 "RUSTC_WRAPPER", 153 "CARGO_BUILD_RUSTFLAGS", 154 "CARGO_TARGET_AARCH64_APPLE_IOS_LINKER", 155 ): 156 with self.subTest(key=key), patch.dict(os.environ, {key: "synthetic"}): 157 with self.assertRaisesRegex(source.ProvenanceError, "ungoverned"): 158 source.reject_build_overrides() 159 160 def test_local_cargo_build_configuration_cannot_escape_source_identity( 161 self, 162 ) -> None: 163 (self.root / ".cargo").mkdir() 164 config = self.root / ".cargo/config.toml" 165 config.write_text('[build]\nrustflags = ["--cfg", "unrecorded"]\n') 166 with self.assertRaisesRegex(source.ProvenanceError, "Cargo configuration"): 167 source.reject_cargo_configuration(self.root) 168 169 def test_current_contract_separates_foundation_and_application(self) -> None: 170 config = source.producer_contract(SCRIPTS.parent) 171 self.assertEqual(config["ffi"]["package"], "tera_ffi") 172 self.assertEqual(config["repository"], "https://github.com/radrootslabs/tera") 173 cargo = contract._read_toml(SCRIPTS.parent / "Cargo.toml") 174 lock = contract._read_toml(SCRIPTS.parent / config["foundation_lock"]) 175 changed = copy.deepcopy(cargo) 176 changed["workspace"]["dependencies"]["radroots_sdk"]["rev"] = "f" * 40 177 with self.assertRaisesRegex( 178 contract.PackageContractError, "foundation Cargo revision" 179 ): 180 source.validate_foundation(changed, lock) 181 182 def test_mismatched_foundation_consumer_lock_fails_before_source_capture( 183 self, 184 ) -> None: 185 read_toml = contract._read_toml 186 187 def mismatched(path: Path) -> dict: 188 value = read_toml(path) 189 if path.name == "radroots.lib.source-lock.v1.toml": 190 value["lockfile_sha256"] = "0" * 64 191 return value 192 193 with patch.object(contract, "_read_toml", side_effect=mismatched): 194 with self.assertRaisesRegex( 195 contract.PackageContractError, "foundation consumer Cargo lock digest" 196 ): 197 source.producer_contract(SCRIPTS.parent) 198 199 def test_host_install_name_is_fixed_and_scoped_to_the_ffi_library(self) -> None: 200 config = source.producer_contract(SCRIPTS.parent) 201 build = config["build"] 202 flags = [ 203 "-Clink-arg=-Wl,-install_name,@rpath/libtera_ffi.dylib", 204 "-Clink-arg=-Wl,-reproducible", 205 "-Clink-arg=-Wl,-oso_prefix,{extbuild_root}", 206 ] 207 project = Path("/external/test-producer") 208 for target in build["targets"]: 209 with self.subTest(target=target): 210 command = builder.library_command(SCRIPTS.parent, target, project) 211 self.assertIn("--lib", command) 212 self.assertEqual(command[command.index("-p") + 1], "tera_ffi") 213 if target == build["host"]: 214 self.assertEqual(command[1], "rustc") 215 self.assertEqual( 216 command[-4:], 217 ["--", *(flag.format(extbuild_root=project) for flag in flags)], 218 ) 219 self.assertEqual(source.library_rust_flags(build, target), flags) 220 else: 221 self.assertEqual(command[1], "build") 222 self.assertNotIn("--", command) 223 self.assertEqual(source.library_rust_flags(build, target), []) 224 self.assertFalse(any("install_name" in flag for flag in build["rust_flags"])) 225 for install_name in ( 226 "/tmp/libtera_ffi.dylib", 227 "libtera_ffi.dylib", 228 "@rpath/other.dylib", 229 ): 230 changed = copy.deepcopy(build) 231 changed["host_dylib_install_name"] = install_name 232 with ( 233 self.subTest(install_name=install_name), 234 self.assertRaises(contract.PackageContractError), 235 ): 236 source.validate_build(changed) 237 for prefix in ("/tmp", "{producer_root}", ""): 238 changed = copy.deepcopy(build) 239 changed["host_oso_prefix"] = prefix 240 with ( 241 self.subTest(prefix=prefix), 242 self.assertRaises(contract.PackageContractError), 243 ): 244 source.validate_build(changed) 245 for reproducible in (False, 1, "true"): 246 changed = copy.deepcopy(build) 247 changed["host_linker_reproducible"] = reproducible 248 with ( 249 self.subTest(reproducible=reproducible), 250 self.assertRaises(contract.PackageContractError), 251 ): 252 source.validate_build(changed) 253 254 255 if __name__ == "__main__": 256 unittest.main()