mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Add macOS voice runtime projection (#42204)
## What changed - Add a shared runtime preparer that validates native build receipts, source and file digests, dependency closure, and output containment before copying the required voice plugins and libraries. - Add macOS-specific Mach-O inspection and relocation that removes build-time runpaths, rewrites non-system dependencies relative to each loader, applies development ad-hoc signatures, and records the resulting payload in `runtime.json`. - Close the state runtime explicitly in the clear-memories CLI test. ## Testing - Add macOS tests covering relocation after the source prefix is removed and fail-closed handling of invalid receipts, changed inputs, unsafe paths, dependency conflicts, malformed Mach-O metadata, and failed transforms. GitOrigin-RevId: 3be7a6fabf0135a57036cbb0a9a3e597096930fd
This commit is contained in:
committed by
copyberry
parent
9d57be71ba
commit
8813bd4b00
@@ -17,7 +17,7 @@ async fn debug_clear_memories_resets_state_and_removes_memory_dir() -> Result<()
|
||||
let codex_home = TempDir::new()?;
|
||||
let sqlite = codex_state::SqliteConfig::new_for_testing(codex_home.path().abs());
|
||||
let runtime = StateRuntime::init(sqlite.clone(), "test-provider".to_string()).await?;
|
||||
drop(runtime);
|
||||
runtime.close().await;
|
||||
|
||||
let thread_id = "00000000-0000-0000-0000-000000000123";
|
||||
let db_path = sqlite.state_db_path();
|
||||
|
||||
Vendored
+2
@@ -33,6 +33,8 @@ filegroup(
|
||||
srcs = [
|
||||
"assemble_package.py",
|
||||
"build_native.py",
|
||||
"macos_runtime.py",
|
||||
"runtime.py",
|
||||
":source_inputs",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
|
||||
Vendored
+24
@@ -86,3 +86,27 @@ may remain external; runtime closure inspection must verify that independently.
|
||||
`//third_party/voice:build_inputs` exposes the recipe and source inputs to Bazel.
|
||||
Neither this filegroup nor a successful prefix build proves final Cargo/Bazel
|
||||
linkage, safe private runtime loading, or an installed voice-capable Codex package.
|
||||
|
||||
## Private macOS runtime projection
|
||||
|
||||
`macos_runtime.py --prefix <extracted-prefix> --receipts <native-ci-receipts>
|
||||
--target <macOS-triple> --output <fresh-directory>` verifies the native receipt,
|
||||
source manifest, per-file digests and Mach-O architecture before projecting the
|
||||
seven explicit plugins and their declared library dependencies. It removes SDK
|
||||
aliases and build-machine runpaths, rewrites private imports relative to each
|
||||
loader, and regenerates only development ad-hoc signatures. Inputs are untouched.
|
||||
The output must be new and outside the input directories; failures remove only
|
||||
that new output. `runtime.json` records source and transformed file identities.
|
||||
Xcode's `xcrun llvm-objdump` inspects Mach-O headers and load commands; the
|
||||
preparer reads its output and enforces the package dependency policy rather than
|
||||
decoding binary structures itself. `install_name_tool` still rewrites paths.
|
||||
`runtime.py` owns shared receipt checks, dependency selection, verified copying,
|
||||
and cleanup; each platform owns its binary format and loader changes. Output
|
||||
containment uses filesystem identity, and copied bytes are checked again before
|
||||
transformation so input changes cannot silently invalidate the source receipt.
|
||||
|
||||
This is a development-only payload, not a signed distribution package or proof of
|
||||
audio behavior. Dynamic-only dependencies, native helper linkage, LGPL notices,
|
||||
production signing/notarization, Windows/Linux loading and security approval remain
|
||||
separate requirements. No microphone, device, plugin scanner or backend is started
|
||||
by projection. Run its native relocation tests on macOS with Python 3.12 or newer.
|
||||
|
||||
Vendored
+164
@@ -0,0 +1,164 @@
|
||||
"""Project a verified macOS prefix into a private, relocatable development runtime."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Import only this script's siblings, including under PYTHONSAFEPATH.
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from runtime import Binary as MachO
|
||||
from runtime import PLUGINS, RuntimeFormat, digest, prepare
|
||||
|
||||
SYSTEM_IMPORTS = frozenset(
|
||||
{
|
||||
"/usr/lib/libSystem.B.dylib",
|
||||
"/usr/lib/libobjc.A.dylib",
|
||||
"/usr/lib/libiconv.2.dylib",
|
||||
"/usr/lib/libresolv.9.dylib",
|
||||
"/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit",
|
||||
"/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation",
|
||||
"/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices",
|
||||
"/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def run(command):
|
||||
return subprocess.check_output(
|
||||
command, text=True, stderr=subprocess.STDOUT, timeout=30
|
||||
)
|
||||
|
||||
|
||||
def inspect(path, target):
|
||||
cpu = {"aarch64-apple-darwin": "ARM64", "x86_64-apple-darwin": "X86_64"}[target]
|
||||
try:
|
||||
output = run(
|
||||
[
|
||||
"/usr/bin/xcrun",
|
||||
"llvm-objdump",
|
||||
"--macho",
|
||||
"--universal-headers",
|
||||
"--private-headers",
|
||||
str(path),
|
||||
]
|
||||
)
|
||||
except subprocess.CalledProcessError as error:
|
||||
raise ValueError("LLVM rejected the Mach-O library") from error
|
||||
header, *commands = re.split(r"(?m)^Load command [0-9]+\n", output)
|
||||
match = re.fullmatch(
|
||||
r"Mach header\n[^\n]+\nMH_MAGIC_64 +"
|
||||
+ cpu
|
||||
+ r" +\S+ +\S+ +DYLIB +(\d+) +(\d+) +[^\n]+\n",
|
||||
header.removeprefix(str(path) + ":\n"),
|
||||
)
|
||||
if not match or len(commands) != int(match[1]) or int(match[2]) > 1024 * 1024:
|
||||
raise ValueError(f"expected a thin {target} dylib with bounded load commands")
|
||||
identities, imports, rpaths = [], [], []
|
||||
for block in commands:
|
||||
match = re.match(r" +cmd (\S+)\n +cmdsize ([0-9]+)\n", block)
|
||||
if not match:
|
||||
raise ValueError("unrecognized LLVM load-command output")
|
||||
command = match[1]
|
||||
if command in {
|
||||
"LC_PREBOUND_DYLIB",
|
||||
"LC_REEXPORT_DYLIB",
|
||||
"LC_LAZY_LOAD_DYLIB",
|
||||
"LC_LOAD_UPWARD_DYLIB",
|
||||
"LC_DYLD_ENVIRONMENT",
|
||||
"LC_DYLIB_CODE_SIGN_DRS",
|
||||
"LC_LAZY_LOAD_DYLIB_INFO",
|
||||
"?(0x0000003a)",
|
||||
}:
|
||||
raise ValueError(f"unsupported loader command: {command}")
|
||||
if command not in {
|
||||
"LC_ID_DYLIB",
|
||||
"LC_LOAD_DYLIB",
|
||||
"LC_LOAD_WEAK_DYLIB",
|
||||
"LC_RPATH",
|
||||
}:
|
||||
continue
|
||||
# Match the entire command to reject multiline names masquerading as metadata.
|
||||
pattern = r" +path ([^\r\n]+) \(offset [0-9]+\)\n"
|
||||
if command != "LC_RPATH":
|
||||
pattern = (
|
||||
r" +name ([^\r\n]+) \(offset [0-9]+\)\n"
|
||||
r" +time stamp [^\n]+\n +current version [^\n]+\ncompatibility version [^\n]+\n"
|
||||
)
|
||||
value = re.fullmatch(pattern, block[match.end() :])
|
||||
if not value:
|
||||
raise ValueError(
|
||||
"undeclared native dependency or unrecognized LLVM loader output"
|
||||
)
|
||||
if command == "LC_ID_DYLIB":
|
||||
identities.append(value[1])
|
||||
elif command == "LC_RPATH":
|
||||
rpaths.append(value[1])
|
||||
else:
|
||||
imports.append(value[1])
|
||||
if len(identities) != 1 or not re.fullmatch(
|
||||
r"[A-Za-z0-9_+.-]+\.dylib", Path(identities[0]).name
|
||||
):
|
||||
raise ValueError("expected one valid native library identity")
|
||||
return MachO(identities[0], tuple(imports), tuple(rpaths))
|
||||
|
||||
|
||||
def finalize_copy(destination, metadata, dependency_paths):
|
||||
identity = f"@rpath/{destination.name}"
|
||||
command = ["/usr/bin/install_name_tool", "-id", identity]
|
||||
imports = []
|
||||
for dependency in metadata.imports:
|
||||
rewritten = dependency
|
||||
if dependency not in SYSTEM_IMPORTS:
|
||||
rewritten = "@loader_path/" + os.path.relpath(
|
||||
dependency_paths[dependency], destination.parent
|
||||
)
|
||||
command.extend(["-change", dependency, rewritten])
|
||||
imports.append(rewritten)
|
||||
for rpath in metadata.rpaths:
|
||||
command.extend(["-delete_rpath", rpath])
|
||||
run([*command, str(destination)])
|
||||
# Development code signatures only; no identity, entitlement or trust-policy changes.
|
||||
run(
|
||||
[
|
||||
"/usr/bin/codesign",
|
||||
"--force",
|
||||
"--sign",
|
||||
"-",
|
||||
"--timestamp=none",
|
||||
str(destination),
|
||||
]
|
||||
)
|
||||
run(["/usr/bin/codesign", "--verify", "--strict", str(destination)])
|
||||
return MachO(identity, tuple(imports), ())
|
||||
|
||||
|
||||
def project(prefix, receipts, target, output):
|
||||
if sys.platform != "darwin" or target not in (
|
||||
"aarch64-apple-darwin",
|
||||
"x86_64-apple-darwin",
|
||||
):
|
||||
raise ValueError(
|
||||
"runtime projection requires macOS and an explicit macOS target"
|
||||
)
|
||||
format = RuntimeFormat(
|
||||
tuple(
|
||||
Path(f"lib/gstreamer-1.0/libgst{name}.dylib") for name in sorted(PLUGINS)
|
||||
),
|
||||
SYSTEM_IMPORTS,
|
||||
inspect,
|
||||
finalize_copy,
|
||||
)
|
||||
prepare(prefix, receipts, target, output, format)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--prefix", type=Path, required=True)
|
||||
parser.add_argument("--receipts", type=Path, required=True)
|
||||
parser.add_argument("--target", required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
project(args.prefix, args.receipts, args.target, args.output)
|
||||
Vendored
+181
@@ -0,0 +1,181 @@
|
||||
"""Prepare receipt-verified native libraries without changing their input trees."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
|
||||
PLUGINS = (
|
||||
"app",
|
||||
"audioconvert",
|
||||
"audioresample",
|
||||
"coreelements",
|
||||
"opus",
|
||||
"rtp",
|
||||
"rtpmanager",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Binary:
|
||||
identity: str
|
||||
imports: tuple[str, ...]
|
||||
rpaths: tuple[str, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuntimeFormat:
|
||||
"""Platform loader policy; inspect reads metadata, finalize_copy transforms or checks a verified copy."""
|
||||
|
||||
plugins: tuple[Path, ...]
|
||||
system_imports: frozenset[str]
|
||||
inspect: Callable
|
||||
finalize_copy: Callable
|
||||
library_dir: str = "lib"
|
||||
plugin_dir: str = "plugins"
|
||||
|
||||
|
||||
def digest(path):
|
||||
with path.open("rb") as source:
|
||||
return hashlib.file_digest(source, "sha256").hexdigest()
|
||||
|
||||
|
||||
def prepare(prefix, receipts, target, output, format):
|
||||
prefix, receipts = prefix.resolve(strict=True), receipts.resolve(strict=True)
|
||||
output = output.absolute()
|
||||
if (
|
||||
output.exists()
|
||||
or output.is_symlink()
|
||||
or any(
|
||||
parent.exists() and parent.samefile(source)
|
||||
for parent in output.resolve().parents
|
||||
for source in (prefix, receipts)
|
||||
)
|
||||
):
|
||||
raise ValueError("output must be fresh and outside the inputs")
|
||||
ci = json.loads((receipts / "ci.json").read_text())
|
||||
source_hash = digest(Path(__file__).with_name("sources.json"))
|
||||
if (
|
||||
ci.get("target") != target
|
||||
or ci.get("build_complete") is not True
|
||||
or ci.get("inspection_complete") is not True
|
||||
or ci.get("manifest_sha256") != source_hash
|
||||
or not re.fullmatch(r"[0-9a-f]{40}", ci.get("commit", ""))
|
||||
):
|
||||
raise ValueError(
|
||||
"native build receipt does not match the pinned source inputs and target"
|
||||
)
|
||||
inventory_path = receipts / "inspection/binaries.json"
|
||||
inventory = json.loads(inventory_path.read_text())
|
||||
if not 1 <= len(inventory) <= 128:
|
||||
raise ValueError("unexpected native inventory size")
|
||||
binaries, identities, destinations = {}, {}, {}
|
||||
for record in inventory:
|
||||
spelling = (
|
||||
record["path"].replace("\\", "/") if os.name == "nt" else record["path"]
|
||||
)
|
||||
relative = Path(spelling)
|
||||
if relative.anchor or ".." in relative.parts or relative.as_posix() != spelling:
|
||||
raise ValueError("native inventory path must be canonical and relative")
|
||||
path = prefix / relative
|
||||
if (
|
||||
path.is_symlink()
|
||||
or not path.is_file()
|
||||
or not path.resolve().is_relative_to(prefix)
|
||||
):
|
||||
raise ValueError(
|
||||
"native inventory entries must be regular files inside the prefix"
|
||||
)
|
||||
if record["target"] != target or digest(path) != record["sha256"]:
|
||||
raise ValueError(f"native input target/digest mismatch: {relative}")
|
||||
metadata = format.inspect(path, target)
|
||||
name = Path(metadata.identity).name
|
||||
if not re.fullmatch(r"[A-Za-z0-9_+.-]+", name):
|
||||
raise ValueError("invalid native library identity")
|
||||
if relative in format.plugins and name != relative.name:
|
||||
raise ValueError("explicit plugin identity must preserve its filename")
|
||||
if (
|
||||
metadata.identity in identities
|
||||
or metadata.identity in format.system_imports
|
||||
or relative in binaries
|
||||
):
|
||||
raise ValueError("duplicate or system native library identity")
|
||||
destination = (
|
||||
Path(
|
||||
format.plugin_dir if relative in format.plugins else format.library_dir
|
||||
)
|
||||
/ name
|
||||
)
|
||||
if any(
|
||||
destination.as_posix().casefold() == p.as_posix().casefold()
|
||||
for p in destinations.values()
|
||||
):
|
||||
raise ValueError("colliding runtime library filenames")
|
||||
binaries[relative] = (record, metadata)
|
||||
identities[metadata.identity] = relative
|
||||
destinations[relative] = destination
|
||||
pending, selected = list(format.plugins), set()
|
||||
while pending:
|
||||
relative = pending.pop()
|
||||
if relative in selected:
|
||||
continue
|
||||
if relative not in binaries:
|
||||
raise ValueError(f"missing explicit plugin: {relative}")
|
||||
selected.add(relative)
|
||||
for dependency in binaries[relative][1].imports:
|
||||
if dependency in format.system_imports:
|
||||
continue
|
||||
if dependency not in identities:
|
||||
raise ValueError(f"undeclared native dependency: {dependency}")
|
||||
pending.append(identities[dependency])
|
||||
output.mkdir() # Only this exclusively created output may be removed on failure.
|
||||
try:
|
||||
records = []
|
||||
dependency_paths = {
|
||||
identity: output / destinations[path]
|
||||
for identity, path in identities.items()
|
||||
}
|
||||
for relative in sorted(selected):
|
||||
record, metadata = binaries[relative]
|
||||
destination = output / destinations[relative]
|
||||
destination.parent.mkdir(exist_ok=True)
|
||||
shutil.copy2(prefix / relative, destination)
|
||||
if (
|
||||
digest(destination) != record["sha256"]
|
||||
or format.inspect(destination, target) != metadata
|
||||
):
|
||||
raise ValueError("copied native input changed after verification")
|
||||
expected = format.finalize_copy(destination, metadata, dependency_paths)
|
||||
if format.inspect(destination, target) != expected:
|
||||
raise ValueError(
|
||||
"finalized loader commands did not match the private runtime layout"
|
||||
)
|
||||
records.append(
|
||||
{
|
||||
"path": destinations[relative].as_posix(),
|
||||
"sourcePath": relative.as_posix(),
|
||||
"sourceSha256": record["sha256"],
|
||||
"sha256": digest(destination),
|
||||
"imports": list(expected.imports),
|
||||
}
|
||||
)
|
||||
manifest = {
|
||||
"schemaVersion": 1,
|
||||
"developmentOnly": True,
|
||||
"target": target,
|
||||
"sourceCommit": ci["commit"],
|
||||
"sourceManifestSha256": source_hash,
|
||||
"inventorySha256": digest(inventory_path),
|
||||
"libraries": records,
|
||||
"plugins": [destinations[path].as_posix() for path in format.plugins],
|
||||
}
|
||||
(output / "runtime.json").write_text(
|
||||
json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
except BaseException:
|
||||
shutil.rmtree(output)
|
||||
raise
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
"""Check real Mach-O relocation and fail-closed native input handling."""
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from macos_runtime import PLUGINS, digest, inspect, project
|
||||
|
||||
|
||||
@unittest.skipUnless(sys.platform == "darwin", "macOS native runtime projection")
|
||||
class RuntimeTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
temporary = tempfile.TemporaryDirectory(prefix="native voice ")
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self.root = Path(temporary.name)
|
||||
self.prefix = self.root / "input"
|
||||
self.receipts = self.root / "receipts"
|
||||
self.output = self.root / "runtime"
|
||||
self.target = (
|
||||
"aarch64" if platform.machine() == "arm64" else "x86_64"
|
||||
) + "-apple-darwin"
|
||||
(self.prefix / "lib/gstreamer-1.0").mkdir(parents=True)
|
||||
(self.receipts / "inspection").mkdir(parents=True)
|
||||
source = self.root / "fixture.c"
|
||||
source.write_text("int voice_fixture(void) { return 42; }\n")
|
||||
self.library = self.prefix / "lib/libfixture.1.2.dylib"
|
||||
subprocess.run(
|
||||
[
|
||||
"/usr/bin/cc",
|
||||
"-dynamiclib",
|
||||
str(source),
|
||||
"-o",
|
||||
str(self.library),
|
||||
"-Wl,-install_name,@rpath/libfixture.1.dylib",
|
||||
"-Wl,-headerpad_max_install_names",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
(self.prefix / "lib/libfixture.1.dylib").symlink_to(self.library.name)
|
||||
source.write_text(
|
||||
"extern int voice_fixture(void); int voice_plugin(void) { return voice_fixture(); }\n"
|
||||
)
|
||||
prototype = self.prefix / f"lib/gstreamer-1.0/libgst{PLUGINS[0]}.dylib"
|
||||
subprocess.run(
|
||||
[
|
||||
"/usr/bin/cc",
|
||||
"-dynamiclib",
|
||||
str(source),
|
||||
str(self.library),
|
||||
"-o",
|
||||
str(prototype),
|
||||
f"-Wl,-install_name,{prototype}",
|
||||
f"-Wl,-rpath,{self.prefix / 'lib'}",
|
||||
"-Wl,-headerpad_max_install_names",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
for name in PLUGINS[1:]:
|
||||
path = self.prefix / f"lib/gstreamer-1.0/libgst{name}.dylib"
|
||||
shutil.copy2(prototype, path)
|
||||
subprocess.run(
|
||||
["/usr/bin/install_name_tool", "-id", str(path), str(path)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
self.ci = {
|
||||
"commit": "a" * 40,
|
||||
"target": self.target,
|
||||
"build_complete": True,
|
||||
"inspection_complete": True,
|
||||
"manifest_sha256": digest(Path(__file__).with_name("sources.json")),
|
||||
}
|
||||
(self.receipts / "ci.json").write_text(json.dumps(self.ci))
|
||||
self.inventory = [
|
||||
{
|
||||
"path": p.relative_to(self.prefix).as_posix(),
|
||||
"target": self.target,
|
||||
"sha256": digest(p),
|
||||
}
|
||||
for p in sorted(self.prefix.rglob("*.dylib"))
|
||||
if not p.is_symlink()
|
||||
]
|
||||
self.inventory_path = self.receipts / "inspection/binaries.json"
|
||||
self.inventory_path.write_text(json.dumps(self.inventory))
|
||||
|
||||
def test_relocated_payload_loads_after_original_prefix_is_removed(self):
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(Path(__file__).with_name("macos_runtime.py").resolve()),
|
||||
"--prefix",
|
||||
str(self.prefix),
|
||||
"--receipts",
|
||||
str(self.receipts),
|
||||
"--target",
|
||||
self.target,
|
||||
"--output",
|
||||
str(self.output),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
cwd=self.root,
|
||||
env={**os.environ, "PYTHONSAFEPATH": "1"},
|
||||
)
|
||||
manifest = json.loads((self.output / "runtime.json").read_text())
|
||||
before = {r["path"]: digest(self.prefix / r["path"]) for r in self.inventory}
|
||||
self.assertEqual(before, {r["path"]: r["sha256"] for r in self.inventory})
|
||||
self.assertEqual(
|
||||
{r["path"] for r in manifest["libraries"]},
|
||||
{
|
||||
"lib/libfixture.1.dylib",
|
||||
*(f"plugins/libgst{name}.dylib" for name in PLUGINS),
|
||||
},
|
||||
)
|
||||
moved = self.root / "relocated package"
|
||||
self.output.rename(moved)
|
||||
shutil.rmtree(self.prefix)
|
||||
for relative in manifest["plugins"]:
|
||||
plugin = ctypes.CDLL(str(moved / relative))
|
||||
self.assertEqual(plugin.voice_plugin(), 42)
|
||||
for record in manifest["libraries"]:
|
||||
metadata = inspect(moved / record["path"], self.target)
|
||||
self.assertEqual(metadata.rpaths, ())
|
||||
self.assertEqual(metadata.imports, tuple(record["imports"]))
|
||||
self.assertEqual(digest(moved / record["path"]), record["sha256"])
|
||||
|
||||
def test_input_digest_target_and_receipt_must_match(self):
|
||||
for key, value in [
|
||||
("build_complete", False),
|
||||
("manifest_sha256", "b" * 64),
|
||||
("target", "wrong"),
|
||||
]:
|
||||
with self.subTest(key=key), self.assertRaises(ValueError):
|
||||
(self.receipts / "ci.json").write_text(
|
||||
json.dumps({**self.ci, key: value})
|
||||
)
|
||||
project(self.prefix, self.receipts, self.target, self.output)
|
||||
self.assertFalse(self.output.exists())
|
||||
(self.receipts / "ci.json").write_text(json.dumps(self.ci))
|
||||
self.library.write_bytes(b"tampered")
|
||||
with self.assertRaisesRegex(ValueError, "digest mismatch"):
|
||||
project(self.prefix, self.receipts, self.target, self.output)
|
||||
self.assertFalse(self.output.exists())
|
||||
|
||||
def test_case_alias_cannot_place_output_inside_inputs(self):
|
||||
for source in (self.prefix, self.receipts):
|
||||
alias = source.with_name(source.name.upper())
|
||||
if not alias.exists():
|
||||
self.skipTest("requires a case-insensitive filesystem")
|
||||
with (
|
||||
self.subTest(source=source),
|
||||
self.assertRaisesRegex(ValueError, "outside the inputs"),
|
||||
):
|
||||
project(self.prefix, self.receipts, self.target, alias / "runtime")
|
||||
self.assertFalse((source / "runtime").exists())
|
||||
|
||||
def test_case_colliding_library_destinations_are_rejected(self):
|
||||
duplicate = self.prefix / "lib/another.dylib"
|
||||
shutil.copy2(self.library, duplicate)
|
||||
subprocess.run(
|
||||
[
|
||||
"/usr/bin/install_name_tool",
|
||||
"-id",
|
||||
"@rpath/LIBFIXTURE.1.dylib",
|
||||
str(duplicate),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
self.inventory.append(
|
||||
{
|
||||
"path": duplicate.relative_to(self.prefix).as_posix(),
|
||||
"target": self.target,
|
||||
"sha256": digest(duplicate),
|
||||
}
|
||||
)
|
||||
self.inventory_path.write_text(json.dumps(self.inventory))
|
||||
with self.assertRaisesRegex(ValueError, "colliding runtime library filenames"):
|
||||
project(self.prefix, self.receipts, self.target, self.output)
|
||||
self.assertFalse(self.output.exists())
|
||||
|
||||
def test_changed_source_at_copy_is_rejected(self):
|
||||
copy = shutil.copy2
|
||||
|
||||
def change_then_copy(source, destination):
|
||||
source.write_bytes(source.read_bytes() + b"changed after verification")
|
||||
return copy(source, destination)
|
||||
|
||||
with patch("runtime.shutil.copy2", side_effect=change_then_copy):
|
||||
with self.assertRaisesRegex(ValueError, "copied native input changed"):
|
||||
project(self.prefix, self.receipts, self.target, self.output)
|
||||
self.assertFalse(self.output.exists())
|
||||
|
||||
def test_missing_dependency_and_duplicate_identity_fail_before_output_creation(
|
||||
self,
|
||||
):
|
||||
self.inventory_path.write_text(
|
||||
json.dumps([r for r in self.inventory if "libfixture" not in r["path"]])
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "undeclared native dependency"):
|
||||
project(self.prefix, self.receipts, self.target, self.output)
|
||||
self.inventory_path.write_text(json.dumps([*self.inventory, self.inventory[0]]))
|
||||
with self.assertRaisesRegex(ValueError, "duplicate"):
|
||||
project(self.prefix, self.receipts, self.target, self.output)
|
||||
self.assertFalse(self.output.exists())
|
||||
|
||||
def test_inventory_cannot_escape_prefix(self):
|
||||
for relative in (
|
||||
"../fixture.c",
|
||||
str(self.library),
|
||||
"lib/../lib/libfixture.1.2.dylib",
|
||||
):
|
||||
self.inventory_path.write_text(
|
||||
json.dumps([{**self.inventory[0], "path": relative}])
|
||||
)
|
||||
with self.subTest(path=relative), self.assertRaises(ValueError):
|
||||
project(self.prefix, self.receipts, self.target, self.output)
|
||||
self.assertFalse(self.output.exists())
|
||||
|
||||
def test_multiline_import_cannot_impersonate_a_system_library(self):
|
||||
external = self.root / "outside\n name /usr/lib/libSystem.B.dylib"
|
||||
external.parent.mkdir(parents=True)
|
||||
shutil.copy2(self.library, external)
|
||||
plugin = self.prefix / f"lib/gstreamer-1.0/libgst{PLUGINS[0]}.dylib"
|
||||
subprocess.run(
|
||||
[
|
||||
"/usr/bin/install_name_tool",
|
||||
"-change",
|
||||
"@rpath/libfixture.1.dylib",
|
||||
str(external),
|
||||
str(plugin),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
for record in self.inventory:
|
||||
record["sha256"] = digest(self.prefix / record["path"])
|
||||
self.inventory_path.write_text(json.dumps(self.inventory))
|
||||
with self.assertRaisesRegex(ValueError, "undeclared native dependency"):
|
||||
project(self.prefix, self.receipts, self.target, self.output)
|
||||
self.assertFalse(self.output.exists())
|
||||
|
||||
def test_malformed_load_command_bounds_and_strings_are_rejected(self):
|
||||
command = struct.pack("<6I", 0xD, 32, 24, 0, 0, 0) + b"test\0\0\0\0"
|
||||
for count, size, payload in (
|
||||
(1, 1024 * 1024 + 1, command),
|
||||
(2, 32, command),
|
||||
(1, 32, command[:-1]),
|
||||
(1, 32, struct.pack("<3I", 0xD, 40, 24) + command[12:]),
|
||||
(1, 32, struct.pack("<3I", 0xD, 32, 8) + command[12:]),
|
||||
(1, 32, command[:24] + b"no null!"),
|
||||
):
|
||||
with self.subTest(count=count, size=size, payload=payload):
|
||||
cpu = 0x100000C if self.target.startswith("aarch64") else 0x1000007
|
||||
self.library.write_bytes(
|
||||
struct.pack("<8I", 0xFEEDFACF, cpu, 0, 6, count, size, 0, 0)
|
||||
+ payload
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
inspect(self.library, self.target)
|
||||
|
||||
def test_tool_cannot_hide_other_architectures_or_loader_commands(self):
|
||||
cpu = 0x100000C if self.target.startswith("aarch64") else 0x1000007
|
||||
name = b"@rpath/libtest.dylib\0"
|
||||
command = (
|
||||
struct.pack("<6I", 0xD, 48, 24, 0, 0, 0) + name + bytes(24 - len(name))
|
||||
)
|
||||
thin = struct.pack("<8I", 0xFEEDFACF, cpu, 0, 6, 1, 48, 0, 0) + command
|
||||
for tag in (0x2B, 0x3A):
|
||||
self.library.write_bytes(
|
||||
struct.pack("<8I", 0xFEEDFACF, cpu, 0, 6, 2, 64, 0, 0)
|
||||
+ command
|
||||
+ struct.pack("<4I", tag, 16, 0, 0)
|
||||
)
|
||||
with self.subTest(tag=tag), self.assertRaises(ValueError):
|
||||
inspect(self.library, self.target)
|
||||
fat = struct.pack(">7I", 0xCAFEBABE, 1, cpu, 0, 4096, len(thin), 12)
|
||||
self.library.write_bytes(fat + bytes(4096 - len(fat)) + thin)
|
||||
with self.assertRaises(ValueError):
|
||||
inspect(self.library, self.target)
|
||||
fake_cpu = "X86_64" if cpu == 0x100000C else "ARM64"
|
||||
fake_target = (
|
||||
"x86_64-apple-darwin" if cpu == 0x100000C else "aarch64-apple-darwin"
|
||||
)
|
||||
path = (
|
||||
self.root
|
||||
/ f"fake\nMH_MAGIC_64 {fake_cpu} ALL 0x00 DYLIB 1 48 0x00000000\n\n.dylib"
|
||||
)
|
||||
path.write_bytes(thin)
|
||||
with self.assertRaises(ValueError):
|
||||
inspect(path, fake_target)
|
||||
|
||||
def test_existing_outputs_and_failed_transform_preserve_inputs(self):
|
||||
for output in (self.prefix, self.prefix / "nested", self.receipts / "nested"):
|
||||
with self.subTest(output=output), self.assertRaises(ValueError):
|
||||
project(self.prefix, self.receipts, self.target, output)
|
||||
import macos_runtime
|
||||
|
||||
original_run = macos_runtime.run
|
||||
|
||||
def fail_transform(command):
|
||||
if command[0].endswith("install_name_tool"):
|
||||
raise subprocess.CalledProcessError(1, command)
|
||||
return original_run(command)
|
||||
|
||||
with patch("macos_runtime.run", side_effect=fail_transform):
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
project(self.prefix, self.receipts, self.target, self.output)
|
||||
self.assertFalse(self.output.exists())
|
||||
self.assertEqual(
|
||||
{r["path"]: digest(self.prefix / r["path"]) for r in self.inventory},
|
||||
{r["path"]: r["sha256"] for r in self.inventory},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user