{"record":{"id":"dc08a65fd103e240","repo":"Hmbown/CodeWhale","slug":"legacy-spillover-ownership-sidecar-must-not-be-a-s","errorCode":null,"errorMessage":"legacy spillover ownership sidecar must not be a symlink","messagePattern":"legacy spillover ownership sidecar must not be a symlink","errorType":"exception","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/truncate.rs","lineNumber":166,"sourceCode":"        digest: crate::hashing::sha256_hex(bytes),\n        size_bytes: bytes.len().try_into().unwrap_or(u64::MAX),\n    };\n    let sidecar = legacy_spillover_ownership_path(payload_path);\n    let encoded = serde_json::to_vec_pretty(&ownership)\n        .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;\n    crate::utils::write_atomic(&sidecar, &encoded)?;\n    Ok(sidecar)\n}\n\npub(crate) fn read_legacy_spillover_ownership(\n    payload_path: &Path,\n) -> io::Result<LegacySpilloverOwnership> {\n    let sidecar = legacy_spillover_ownership_path(payload_path);\n    if std::fs::symlink_metadata(&sidecar)?\n        .file_type()\n        .is_symlink()\n    {\n        return Err(io::Error::new(\n            io::ErrorKind::PermissionDenied,\n            \"legacy spillover ownership sidecar must not be a symlink\",\n        ));\n    }\n    let ownership = serde_json::from_slice::<LegacySpilloverOwnership>(&std::fs::read(sidecar)?)\n        .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;\n    if ownership.schema_version != LEGACY_SPILLOVER_OWNER_SCHEMA_VERSION {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            \"unsupported legacy spillover ownership schema\",\n        ));\n    }\n    Ok(ownership)\n}\n\n/// Resolve the spillover-file path for a SHA256 content hash. Separate\n/// namespace (`sha_<hex>.txt`) from the tool-call-id files so legacy\n/// SHA-addressed evidence can be recognized without colliding with","sourceCodeStart":148,"sourceCodeEnd":184,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/tools/truncate.rs#L148-L184","documentation":"PermissionDenied returned by read_legacy_spillover_ownership when the ownership sidecar file is a symbolic link. Spillover payloads are content-addressed and ownership sidecars are expected to be regular files written atomically; a symlink could redirect the trusted read to arbitrary targets, so the reader refuses to follow it. This is a tamper/safety guard, not a normal runtime condition.","triggerScenarios":"Something replaced <payload>.ownership (the sidecar next to the spillover file) with a symlink — an attacker preparing a redirection, or an over-eager dotfile/sync manager converting files to symlinks into cloud storage.","commonSituations":"Home-directory sync tools (dotfile managers, network-home setups) that symlink cache/state directories; security tooling flagging the behavior after the fact; deliberately constructed repro attempts; malicious local user tampering with another user's spillover dir.","solutions":["Inspect the symlink target: `ls -l <sidecar>` and decide whether the target is trusted","Delete the symlink and re-publish ownership (or delete the spillover payload) so a regular sidecar file is rewritten","If you did not create the symlink, treat the machine's local state as suspect and audit who wrote it","Exclude the Codewhale state/spillover directory from symlink-making dotfile managers"],"exampleFix":"# before\n$ ls -l ~/.codewhale/spillover/sha_abc.ownership\n... -> /etc/passwd    # read -> PermissionDenied\n\n# after\n$ rm ~/.codewhale/spillover/sha_abc.ownership\n# re-trigger the tool output write so the sidecar is recreated as a regular file","handlingStrategy":"try-catch","validationCode":"// Pre-check the sidecar with symlink_metadata (never follows links) before reading.\nlet meta = std::fs::symlink_metadata(&sidecar)?;\nif meta.file_type().is_symlink() {\n    // refuse + alert; do not follow or delete automatically in security-sensitive contexts\n}","typeGuard":"fn is_symlink_sidecar(e: &std::io::Error) -> bool {\n    e.kind() == std::io::ErrorKind::PermissionDenied && e.to_string().contains(\"must not be a symlink\")\n}","tryCatchPattern":"match read_legacy_spillover_ownership(&payload) {\n    Ok(o) => Ok(Some(o)),\n    Err(e) if is_symlink_sidecar(&e) => {\n        security_log(\"spillover sidecar replaced by symlink\", payload);\n        Ok(None) // treat artifact as unavailable; do not silently recover\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Exclude Codewhale state/spillover directories from dotfile managers and sync tools that create symlinks","Audit unexpected PermissionDenied reads from state dirs as potential tampering","Never 'fix' this by replacing the symlink target read — republish the sidecar instead"],"tags":["spillover","symlink","security","tamper-detection","rust"],"backgroundTag":"symlink-attack-guard","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}