Skip to content

Commit 3bfaf5f

Browse files
committed
fix: fail closed on malformed observation/control JSON and purge stale plugin imports
Four fail-closed and correctness bugs identified during codebase audit: 1. _observation_pair_failures silently passed non-dict JSON by returning an empty failure list. Now reports a malformed-JSON failure. 2. _usage in heldout_runner accepted non-dict stats (e.g. null) without raising, potentially proceeding with incomplete result data. Now validates stats is a dict before access. 3. load_registry cache had no invalidation path, making test fixtures that mutate the registry unreliable. Added invalidate_registry_cache(). 4. install_source_only_importer inserted a SourceOnlyFinder but did not purge already-imported modules from sys.modules, so stale bytecode could shadow the finder. Now purges the target package from sys.modules first. Includes formal regression tests for all four fixes.
1 parent b0e186a commit 3bfaf5f

5 files changed

Lines changed: 133 additions & 3 deletions

File tree

benchmarks/tooling/benchmark_contracts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ def _observation_pair_failures() -> list[str]:
223223
treatment = _read_json(treatment_path)
224224
control = _read_json(control_path)
225225
if not isinstance(treatment, dict) or not isinstance(control, dict):
226-
return []
226+
return ["observation/control pair contains malformed (non-object) JSON"]
227227

228228
def normalized(value: dict[str, Any]) -> dict[str, Any]:
229229
copy: dict[str, Any] = json.loads(json.dumps(value))

benchmarks/tooling/harbor_suite.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,11 @@ def load_registry(path: Path = REGISTRY_PATH) -> tuple[Suite, ...]:
553553
return result
554554

555555

556+
def invalidate_registry_cache() -> None:
557+
"""Drop cached registry entries so subsequent load_registry calls re-parse."""
558+
_load_registry_cache.clear()
559+
560+
556561
def get_suite(dataset: str, *, path: Path = REGISTRY_PATH) -> Suite:
557562
short = dataset.removeprefix(DATASET_PREFIX)
558563
for suite in load_registry(path):
@@ -1085,6 +1090,7 @@ def report_ok(message: str) -> None:
10851090
"iter_task_dirs",
10861091
"load_environment_profiles",
10871092
"load_registry",
1093+
"invalidate_registry_cache",
10881094
"report_failures",
10891095
"report_ok",
10901096
"select_task_refs",

benchmarks/tooling/heldout_runner.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ def _usage(result_path: Path) -> tuple[int, float]:
9797
if not isinstance(result, dict):
9898
raise HarborSuiteError("Harbor result must be an object")
9999
stats = result.get("stats")
100-
if isinstance(stats, dict) and any(
100+
if not isinstance(stats, dict):
101+
raise HarborSuiteError("Harbor result stats must be an object")
102+
if any(
101103
stats.get(key, 0)
102104
for key in (
103105
"n_errored_trials",

src/jacobian/implementation.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,18 @@ def install_source_only_importer(entrypoint: str) -> None:
7676
"""Force the entrypoint package's future imports to compile measured source."""
7777

7878
module_name, _ = split_entrypoint(entrypoint)
79-
sys.meta_path.insert(0, _SourceOnlyFinder(module_name.split(".", 1)[0]))
79+
top_level = module_name.split(".", 1)[0]
80+
# Purge any pre-imported modules from the target package so the
81+
# SourceOnlyFinder recompiles them from measured source. Without
82+
# this, already-imported modules in sys.modules would shadow the
83+
# finder and serve potentially stale bytecode.
84+
stale = [
85+
name for name in list(sys.modules)
86+
if name == top_level or name.startswith(top_level + ".")
87+
]
88+
for name in stale:
89+
del sys.modules[name]
90+
sys.meta_path.insert(0, _SourceOnlyFinder(top_level))
8091

8192

8293
def split_entrypoint(entrypoint: str) -> tuple[str, str]:
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""Formal tests for the audit-confirmed bugs and their fixes.
2+
3+
These tests serve as formal validation that:
4+
1. _observation_pair_failures fails closed on malformed JSON (Bug 1a)
5+
2. _usage rejects non-dict stats (Bug 1b)
6+
3. _load_registry_cache provides invalidation (Bug 3a)
7+
4. install_source_only_importer purges sys.modules (Bug TOCTOU)
8+
"""
9+
import json
10+
import sys
11+
from pathlib import Path
12+
from unittest.mock import patch
13+
14+
import pytest
15+
16+
17+
# Bug 1a: _observation_pair_failures should fail closed on non-dict JSON
18+
def test_observation_pair_failures_fails_closed_on_non_dict(tmp_path):
19+
"""Formally prove that malformed observation JSON is not silently accepted."""
20+
from benchmarks.tooling import benchmark_contracts
21+
22+
# When treatment/control JSON is a valid JSON but not a dict (e.g., a list),
23+
# _observation_pair_failures should return failures, not empty list.
24+
treatment_path = Path(benchmark_contracts.BENCHMARKS) / "datasets" / "mathematical-benchmarks-v1" / "jobs" / "jacobian-observation.json"
25+
control_path = Path(benchmark_contracts.BENCHMARKS) / "config" / "mathematical-benchmarks-v1-control.json"
26+
27+
# Mock _read_json to return non-dict values
28+
original_read_json = benchmark_contracts._read_json
29+
30+
try:
31+
# Test: JSON array instead of object
32+
def mock_read_json_array(path):
33+
return []
34+
benchmark_contracts._read_json = mock_read_json_array
35+
failures = benchmark_contracts._observation_pair_failures()
36+
assert len(failures) > 0, "Bug 1a: malformed JSON should not silently pass"
37+
assert "malformed" in failures[0].lower()
38+
39+
# Test: JSON null
40+
def mock_read_json_null(path):
41+
return None
42+
benchmark_contracts._read_json = mock_read_json_null
43+
failures = benchmark_contracts._observation_pair_failures()
44+
assert len(failures) > 0, "Bug 1a: null JSON should not silently pass"
45+
finally:
46+
benchmark_contracts._read_json = original_read_json
47+
48+
49+
# Bug 1b: _usage should reject non-dict stats
50+
def test_usage_rejects_non_dict_stats():
51+
"""Formally prove that non-dict stats is rejected."""
52+
from benchmarks.tooling import heldout_runner
53+
54+
# Create a temporary result file with stats as null
55+
import tempfile
56+
with tempfile.NamedTemporaryFile(suffix=".json", mode="w", delete=False) as f:
57+
json.dump({"stats": None}, f)
58+
path = Path(f.name)
59+
60+
try:
61+
with pytest.raises(Exception) as exc_info:
62+
heldout_runner._usage(path)
63+
assert "stats" in str(exc_info.value).lower()
64+
finally:
65+
path.unlink(missing_ok=True)
66+
67+
68+
# Bug 3a: Registry cache should be invalidatable
69+
def test_registry_cache_invalidation():
70+
"""Formally prove that cache invalidation works."""
71+
from benchmarks.tooling import harbor_suite
72+
73+
assert hasattr(harbor_suite, "invalidate_registry_cache"), "invalidate_registry_cache should exist"
74+
harbor_suite.invalidate_registry_cache()
75+
assert harbor_suite._load_registry_cache == {}, "Cache should be empty after invalidation"
76+
77+
78+
# Bug TOCTOU: install_source_only_importer should purge sys.modules
79+
def test_source_only_importer_purges_sys_modules():
80+
"""Formally prove that pre-imported modules are purged."""
81+
import importlib
82+
from jacobian.implementation import install_source_only_importer
83+
84+
# Create a fake module in sys.modules that looks like the target package
85+
fake_module = type(sys)("fake_test_package")
86+
fake_module.some_value = "stale"
87+
sys.modules["fake_test_package"] = fake_module
88+
sys.modules["fake_test_package.helper"] = type(sys)("fake_test_package.helper")
89+
90+
try:
91+
install_source_only_importer("fake_test_package:main")
92+
assert "fake_test_package" not in sys.modules, "Pre-imported module should be purged"
93+
assert "fake_test_package.helper" not in sys.modules, "Pre-imported submodule should be purged"
94+
finally:
95+
sys.modules.pop("fake_test_package", None)
96+
sys.modules.pop("fake_test_package.helper", None)
97+
# Clean up meta_path
98+
from jacobian.implementation import _SourceOnlyFinder
99+
sys.meta_path = [f for f in sys.meta_path if not isinstance(f, _SourceOnlyFinder)]
100+
101+
102+
if __name__ == "__main__":
103+
test_observation_pair_failures_fails_closed_on_non_dict(None)
104+
print("Bug 1a test passed")
105+
test_usage_rejects_non_dict_stats()
106+
print("Bug 1b test passed")
107+
test_registry_cache_invalidation()
108+
print("Bug 3a test passed")
109+
test_source_only_importer_purges_sys_modules()
110+
print("TOCTOU test passed")
111+
print("\nAll audit fix tests passed!")

0 commit comments

Comments
 (0)