{"record":{"id":"ee00d977635fa91d","repo":"Hmbown/CodeWhale","slug":"invalid-evidence-owner","errorCode":null,"errorMessage":"invalid evidence owner","messagePattern":"invalid evidence owner","errorType":"exception","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/large_output_router.rs","lineNumber":389,"sourceCode":"}\n\npub fn publish_evidence_metadata(\n    session_id: &str,\n    artifact: &EvidenceArtifact,\n) -> io::Result<PathBuf> {\n    let bytes = serde_json::to_vec_pretty(artifact)\n        .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;\n    crate::artifacts::write_session_relative_immutable(\n        session_id,\n        &evidence_metadata_relative_path(&artifact.handle),\n        &bytes,\n    )\n}\n\npub fn read_evidence_metadata(session_id: &str, handle: &str) -> io::Result<EvidenceArtifact> {\n    let relative = evidence_metadata_relative_path(handle);\n    let path = crate::artifacts::session_artifact_absolute_path(session_id, &relative)\n        .ok_or_else(|| io::Error::new(io::ErrorKind::PermissionDenied, \"invalid evidence owner\"))?;\n    let raw = std::fs::read(path)?;\n    serde_json::from_slice(&raw).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))\n}\n\n#[must_use]\npub fn unix_millis_now() -> u64 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .unwrap_or_default()\n        .as_millis()\n        .try_into()\n        .unwrap_or(u64::MAX)\n}\n\n#[must_use]\npub fn evidence_is_expired(artifact: &EvidenceArtifact, now_ms: u64) -> bool {\n    artifact.retention_state == EvidenceRetentionState::Expired\n        || now_ms > artifact.retain_until_unix_ms","sourceCodeStart":371,"sourceCodeEnd":407,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/tools/large_output_router.rs#L371-L407","documentation":"PermissionDenied ('invalid evidence owner') returned by read_evidence_metadata when session_artifact_absolute_path(session_id, relative) yields None — i.e. the evidence handle does not resolve to a path inside that session's artifact directory. The guard is an ownership check: artifacts written under one session cannot be read back through another session id or a hand-constructed handle.","triggerScenarios":"Calling read_evidence_metadata with a handle obtained from a different session, a truncated/edited handle string, or a handle whose relative path would escape the session artifact root (path traversal) so the absolute-path resolver refuses it.","commonSituations":"Restoring/replaying a transcript in a new session and reusing old artifact handles; copy-pasting a handle between sessions; clients that synthesize handle strings instead of using the one returned by the write side; tampered artifact metadata.","solutions":["Use the exact handle string returned by write_evidence_metadata (the artifact's own handle field), unmodified","Make sure the session_id you pass is the same session that owns the artifact — re-publish the artifact into the current session if you need it there","Never build handle/relative paths by string concatenation; treat them as opaque tokens"],"exampleFix":"// before\nlet artifact = read_evidence_metadata(&new_session_id, &old_handle)?; // PermissionDenied\n\n// after\nlet artifact = read_evidence_metadata(&owning_session_id, &old_handle)?; // or re-write the artifact under new_session_id first","handlingStrategy":"validation","validationCode":"// Only attempt reads with handles minted by the write side, and only under the owning session.\nfn valid_handle(h: &str) -> bool {\n    !h.trim().is_empty() && !h.contains(\"..\") && !h.contains('/') && !h.contains('\\\\')\n}\nif !valid_handle(&handle) { return Err(own_error(\"invalid artifact handle\")); }","typeGuard":"fn is_invalid_owner(e: &std::io::Error) -> bool {\n    e.kind() == std::io::ErrorKind::PermissionDenied && e.to_string().contains(\"invalid evidence owner\")\n}","tryCatchPattern":"match read_evidence_metadata(session_id, &handle) {\n    Ok(a) => Ok(a),\n    Err(e) if is_invalid_owner(&e) => Err(explain(\"artifact not owned by this session; re-publish it here first\")),\n    Err(e) => Err(e),\n}","preventionTips":["Treat evidence handles as opaque tokens: store the pair (session_id, handle) together","When replaying a transcript in a new session, re-publish artifacts instead of reusing old handles","Never construct handle strings by concatenation"],"tags":["artifacts","evidence","session-ownership","security","rust"],"backgroundTag":"artifact-ownership-mismatch","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}