{"record":{"id":"88257bb4bf2e3ce6","repo":"BigPizzaV3/CodexPlusPlus","slug":"invalid-candidate-hash","errorCode":null,"errorMessage":"Invalid candidate hash","messagePattern":"Invalid candidate hash","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/native_browser.rs","lineNumber":470,"sourceCode":"    );\n    atomic_write(&target, &candidate)?;\n    ensure!(\n        read_regular(&target, MAX_SERVICE)? == candidate,\n        \"Runtime write verification failed\"\n    );\n    Ok(())\n}\n\nfn recovery_material(\n    paths: &BrowserPaths,\n    key: &str,\n    contract: &RuntimeContract,\n) -> Result<(Journal, Vec<u8>, Vec<u8>)> {\n    ensure!(key_valid(key), \"Invalid recovery key\");\n    let dir = paths.state_root.join(key);\n    let journal: Journal = serde_json::from_slice(&read_regular(&dir.join(\"journal.json\"), 4096)?)?;\n    let original = read_regular(&dir.join(\"original.mjs\"), MAX_SERVICE)?;\n    ensure!(\n        journal.candidate_sha.len() == 64\n            && journal.candidate_sha.bytes().all(|b| b.is_ascii_hexdigit()),\n        \"Invalid candidate hash\"\n    );\n    let candidate = read_regular(\n        &dir.join(format!(\"candidate-{}.mjs\", journal.candidate_sha)),\n        MAX_SERVICE,\n    )?;\n    ensure!(\n        journal.schema == 1\n            && (journal.original_sha == contract.service_sha\n                || journal.original_sha == ORIGINAL_SHA)\n            && sha(&original) == journal.original_sha\n            && journal.candidate_sha == sha(&candidate)\n            && journal.modified_nanos < 1_000_000_000,\n        \"Recovery journal conflicts with verified content\"\n    );\n    Ok((journal, original, candidate))","sourceCodeStart":452,"sourceCodeEnd":488,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/native_browser.rs#L452-L488","documentation":"`recovery_material` reads `journal.json` and requires `candidate_sha` to be exactly 64 ASCII hex characters (a SHA-256 hex string), because that value is interpolated into the candidate filename `candidate-<sha>.mjs`. A malformed hash means the journal is corrupt, hand-edited, or from an incompatible schema, and using it could point at a wrong or attacker-controlled filename.","triggerScenarios":"Any caller of `recovery_material` (`prepare`, `restore_all`, `verify_restored_state`) loads a `journal.json` whose `candidate_sha` fails the length/hex check — e.g. truncated file, JSON edited manually, or a journal written by a different tool.","commonSituations":"Journal partially written/corrupted by a crash or disk issue; user edited `journal.json` while debugging; state directory synced between machines with divergent journal versions.","solutions":["Delete `state_root/<key>/journal.json` (and the whole `<key>` state dir) and rerun reconcile to rebuild the journal from a fresh snapshot.","Restore the journal from a known-good backup or re-sync the state directory.","Never hand-edit `journal.json`; regenerate it via reconcile.","Check disk health if journal files are repeatedly truncated."],"exampleFix":"// before (journal.json)\n{\"schema\":1,\"candidate_sha\":\"abc123\",...}\n// after\n# delete corrupt journal and regenerate\nrm -rf ~/.codex/plugins/state/<key> && codex reconcile --browser-enabled","handlingStrategy":"validation","validationCode":"fn valid_sha_hex(s: &str) -> bool {\n    s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit())\n}\nlet journal: serde_json::Value = serde_json::from_slice(&std::fs::read(state_root.join(&key).join(\"journal.json\"))?)?;\nif !valid_sha_hex(journal[\"candidate_sha\"].as_str().unwrap_or(\"\")) {\n    // journal corrupt: reset state_root/<key> before reconcile\n}","typeGuard":"fn journal_candidate_sha(j: &serde_json::Value) -> Option<&str> {\n    let s = j.get(\"candidate_sha\")?.as_str()?;\n    if s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit()) { Some(s) } else { None }\n}","tryCatchPattern":"match reconcile(&paths, true) {\n    Err(e) if e.to_string().contains(\"Invalid candidate hash\") => {\n        std::fs::remove_dir_all(state_root.join(&key))?;\n        reconcile(&paths, true)?;\n    }\n    other => other?,\n}","preventionTips":["Never hand-edit journal.json","Treat crash-truncated journals as corrupt: reset the state dir","Verify disk health if journal files are repeatedly damaged","Avoid syncing state_root between machines"],"tags":["schema-validation","journal","recovery"],"backgroundTag":"schema-validation-failed","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}