Skip to content

Commit 1b3a9b8

Browse files
committed
fix(tuner): re-validate config on the resume paths, not just start()
resume() and validate_profile() loaded config_json via TunerConfig.from_json (which rejects wrong types only) but never called config.validate(), so a corrupted or hand-edited DB row with a well-typed but out-of-range value (e.g. coarse_step=0) revived on the resume path the non-convergent-search class that start() already rejects. No unsafe CO write results -- every SMU write is range-checked -- but the tune could spin instead of failing closed. Both paths now validate and refuse, mirroring start(). Regression tests (TestResumePathsValidateConfig) are ablation-verified: they go red when the guard is removed. Eval: gate-green
1 parent f2b2f88 commit 1b3a9b8

2 files changed

Lines changed: 66 additions & 0 deletions

File tree

src/tuner/engine.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,15 @@ def resume(self, session_id: int) -> None:
352352
if self._smu is not None:
353353
self._config.clamp_max_offset(self._smu.commands.co_range)
354354

355+
# Fail closed on a corrupted/hand-edited config_json: from_json only
356+
# rejects wrong TYPES, so a well-typed but out-of-range value (e.g.
357+
# coarse_step=0, which makes the search non-convergent) survives it.
358+
# start() rejects those; resume must too, or the bug returns on this path.
359+
errors = self._config.validate()
360+
if errors:
361+
self.log_message.emit(f"Invalid tuner config: {'; '.join(errors)}")
362+
return
363+
355364
self._core_states = tp.load_core_states(self._db, session_id)
356365

357366
# Rebuild the proven-safe envelope from the CO journal: a value the machine
@@ -531,6 +540,11 @@ def validate_profile(self, session_id: int) -> None:
531540
self._config = TunerConfig.from_json(session.config_json)
532541
if self._smu is not None:
533542
self._config.clamp_max_offset(self._smu.commands.co_range)
543+
# Fail closed on a corrupted config_json, exactly as resume()/start().
544+
errors = self._config.validate()
545+
if errors:
546+
self.log_message.emit(f"Invalid tuner config: {'; '.join(errors)}")
547+
return
534548

535549
# Reset confirmed cores to "confirming" for re-validation
536550
self._core_states = tp.load_core_states(self._db, session_id)

tests/test_tuner_faults.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,3 +1112,55 @@ def test_validation_terminates_and_settles_safely(self, data):
11121112
)
11131113
finally:
11141114
db.close()
1115+
1116+
1117+
class TestResumePathsValidateConfig:
1118+
"""resume() and validate_profile() re-validate the loaded config_json and fail
1119+
closed, not only start(). from_json rejects wrong TYPES but passes a well-typed
1120+
out-of-range value (e.g. coarse_step=0, a non-convergent search), so a corrupted
1121+
or hand-edited DB row would otherwise revive on resume the exact 'looped forever'
1122+
class start() rejects. A resident CO is never unsafe here (every SMU write is
1123+
range-checked), but the tune must refuse rather than spin."""
1124+
1125+
def test_resume_fails_closed_on_out_of_range_config(self, db, topo, smu, mock_backend):
1126+
sid = tp.create_session(db, TunerConfig(cores_to_test=[0], coarse_step=0), "", "")
1127+
eng = make_engine(db, topo, smu, mock_backend)
1128+
logs: list[str] = []
1129+
eng.log_message.connect(logs.append)
1130+
with patch.object(eng, "_run_next") as run_next:
1131+
eng.resume(sid)
1132+
assert run_next.call_count == 0, "resume proceeded on an invalid config"
1133+
assert any("Invalid tuner config" in m for m in logs)
1134+
1135+
def test_validate_profile_fails_closed_on_out_of_range_config(
1136+
self, db, topo, smu, mock_backend
1137+
):
1138+
sid = tp.create_session(db, TunerConfig(cores_to_test=[0], coarse_step=0), "", "")
1139+
# A CONFIRMED core so validate_profile clears its empty-profile guard and
1140+
# reaches the config load/validate.
1141+
tp.save_core_state(db, sid, CoreState(
1142+
core_id=0, phase=TunerPhase.CONFIRMED,
1143+
current_offset=-10, best_offset=-10, baseline_offset=0,
1144+
))
1145+
eng = make_engine(db, topo, smu, mock_backend)
1146+
logs: list[str] = []
1147+
eng.log_message.connect(logs.append)
1148+
with patch.object(eng, "_run_next") as run_next:
1149+
eng.validate_profile(sid)
1150+
assert run_next.call_count == 0, "validate_profile proceeded on an invalid config"
1151+
assert any("Invalid tuner config" in m for m in logs)
1152+
1153+
def test_resume_still_proceeds_on_a_valid_config(self, db, topo, smu, mock_backend):
1154+
"""The guard is not over-eager: a valid config_json resumes normally."""
1155+
sid = tp.create_session(db, TunerConfig(cores_to_test=[0]), "", "")
1156+
tp.save_core_state(db, sid, CoreState(
1157+
core_id=0, phase=TunerPhase.FINE_SEARCH,
1158+
current_offset=-10, best_offset=-8, baseline_offset=0, in_test=False,
1159+
))
1160+
eng = make_engine(db, topo, smu, mock_backend)
1161+
logs: list[str] = []
1162+
eng.log_message.connect(logs.append)
1163+
with patch.object(eng, "_run_next") as run_next:
1164+
eng.resume(sid)
1165+
assert run_next.called, "resume bailed on a valid config"
1166+
assert not any("Invalid tuner config" in m for m in logs)

0 commit comments

Comments
 (0)