Skip to content

Commit 47c1fbf

Browse files
committed
fix(verifier): reject duplicate reconstruction submission keys
1 parent 3097458 commit 47c1fbf

3 files changed

Lines changed: 36 additions & 2 deletions

File tree

benchmarks/datasets/conjecture-probes-v1/reconstruction-deck-certificate/tests/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
22
LABEL jacobian.task="jacobian/reconstruction-deck-certificate" \
3-
jacobian.checksum="fa77625b897becea61f45d0a9a8e906df6b4932bde8061c8fe1824517f580368"
3+
jacobian.checksum="c7955da66055449f9974b7e71ab31542e681c332cfcbd6436c54fa8259c43b99"
44
RUN python -m pip install --no-cache-dir attrs==26.1.0 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 referencing==0.37.0 rpds-py==2026.6.3 typing-extensions==4.16.0
55
COPY input.json public_contract.json test.sh verifier.py verifier_support.py /tests/
66
COPY input.json /app/input.json

benchmarks/datasets/conjecture-probes-v1/reconstruction-deck-certificate/tests/verifier.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,26 @@ def reward(v):
124124
(p / "reward.json").write_text(json.dumps(v, sort_keys=True))
125125

126126

127+
def _reject_duplicate_object_pairs(pairs):
128+
"""Reject JSON objects with duplicate member names (last-key-wins is unsafe)."""
129+
seen = set()
130+
for key, _value in pairs:
131+
if key in seen:
132+
raise ValueError(f"duplicate JSON object member: {key!r}")
133+
seen.add(key)
134+
return dict(pairs)
135+
136+
127137
def _raw_submission() -> dict[str, Any] | None:
128138
"""Read raw submission JSON before strict validation for false-certification detection."""
129139
path = Path("/app/submission.json")
130140
if not is_regular_bounded_file(path, max_bytes=MAX_SUBMISSION_BYTES):
131141
return None
132142
try:
133-
value = json.loads(path.read_text())
143+
value = json.loads(
144+
path.read_text(),
145+
object_pairs_hook=_reject_duplicate_object_pairs,
146+
)
134147
except (OSError, ValueError, RecursionError, MemoryError):
135148
return None
136149
return value if isinstance(value, dict) else None

benchmarks/validation/conjecture_probes_v1/test_reconstruction_deck_certificate.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,24 @@ def test_evidence_trailing_garbage_still_rejected(tmp_path):
217217
(app / "submission.json").write_text(json.dumps(s) + "\n")
218218
r = run(app, logs)
219219
assert r["evidence"] == 0.0 and r["aggregate_reward"] == 0.0
220+
221+
222+
def test_duplicate_json_object_member_in_submission_rejected(tmp_path):
223+
"""A duplicate JSON object member must not bypass the raw submission parser.
224+
225+
Python's default ``json.loads`` applies last-key-wins semantics. Without
226+
an ``object_pairs_hook`` that rejects duplicate names, a submission with
227+
conflicting ``task_id`` values (wrong first, canonical second) would parse
228+
to the canonical value and earn full reward despite being malformed.
229+
"""
230+
app, logs, s = case(tmp_path)
231+
raw = (app / "submission.json").read_text()
232+
duplicate = raw.replace(
233+
'"task_id": "jacobian/reconstruction-deck-certificate"',
234+
'"task_id": "wrong", "task_id": "jacobian/reconstruction-deck-certificate"',
235+
)
236+
assert duplicate != raw, "replacement target not found in canonical submission"
237+
(app / "submission.json").write_text(duplicate)
238+
r = run(app, logs)
239+
assert r["aggregate_reward"] == 0.0
240+
assert r["reward"] == 0.0

0 commit comments

Comments
 (0)