Hmbown/CodeWhale · error · std::io::Error

legacy spillover ownership sidecar must not be a symlink

Error message

legacy spillover ownership sidecar must not be a symlink

What it means

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.

Source

Thrown at crates/tui/src/tools/truncate.rs:166

        digest: crate::hashing::sha256_hex(bytes),
        size_bytes: bytes.len().try_into().unwrap_or(u64::MAX),
    };
    let sidecar = legacy_spillover_ownership_path(payload_path);
    let encoded = serde_json::to_vec_pretty(&ownership)
        .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
    crate::utils::write_atomic(&sidecar, &encoded)?;
    Ok(sidecar)
}

pub(crate) fn read_legacy_spillover_ownership(
    payload_path: &Path,
) -> io::Result<LegacySpilloverOwnership> {
    let sidecar = legacy_spillover_ownership_path(payload_path);
    if std::fs::symlink_metadata(&sidecar)?
        .file_type()
        .is_symlink()
    {
        return Err(io::Error::new(
            io::ErrorKind::PermissionDenied,
            "legacy spillover ownership sidecar must not be a symlink",
        ));
    }
    let ownership = serde_json::from_slice::<LegacySpilloverOwnership>(&std::fs::read(sidecar)?)
        .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
    if ownership.schema_version != LEGACY_SPILLOVER_OWNER_SCHEMA_VERSION {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "unsupported legacy spillover ownership schema",
        ));
    }
    Ok(ownership)
}

/// Resolve the spillover-file path for a SHA256 content hash. Separate
/// namespace (`sha_<hex>.txt`) from the tool-call-id files so legacy
/// SHA-addressed evidence can be recognized without colliding with

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Inspect the symlink target: `ls -l <sidecar>` and decide whether the target is trusted
  2. Delete the symlink and re-publish ownership (or delete the spillover payload) so a regular sidecar file is rewritten
  3. If you did not create the symlink, treat the machine's local state as suspect and audit who wrote it
  4. Exclude the Codewhale state/spillover directory from symlink-making dotfile managers

Example fix

# before
$ ls -l ~/.codewhale/spillover/sha_abc.ownership
... -> /etc/passwd    # read -> PermissionDenied

# after
$ rm ~/.codewhale/spillover/sha_abc.ownership
# re-trigger the tool output write so the sidecar is recreated as a regular file
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check the sidecar with symlink_metadata (never follows links) before reading.
let meta = std::fs::symlink_metadata(&sidecar)?;
if meta.file_type().is_symlink() {
    // refuse + alert; do not follow or delete automatically in security-sensitive contexts
}

Type guard

fn is_symlink_sidecar(e: &std::io::Error) -> bool {
    e.kind() == std::io::ErrorKind::PermissionDenied && e.to_string().contains("must not be a symlink")
}

Try / catch

match read_legacy_spillover_ownership(&payload) {
    Ok(o) => Ok(Some(o)),
    Err(e) if is_symlink_sidecar(&e) => {
        security_log("spillover sidecar replaced by symlink", payload);
        Ok(None) // treat artifact as unavailable; do not silently recover
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/dc08a65fd103e240. Report an issue: GitHub.