{"record":{"id":"a6d02ead62d73cd0","repo":"Hmbown/CodeWhale","slug":"reviewed-plugin-executable-bytes-changed-before-sp","errorCode":null,"errorMessage":"reviewed plugin executable bytes changed before spawn","messagePattern":"reviewed plugin executable bytes changed before spawn","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/tui/src/mcp.rs","lineNumber":912,"sourceCode":"        let mut hasher = sha2::Sha256::new();\n        hasher.update(b\"codewhale-plugin-file-bytes-v1\\0\");\n        let mut buffer = [0_u8; 64 * 1024];\n        loop {\n            let read = file\n                .read(&mut buffer)\n                .context(\"read reviewed launch file\")?;\n            if read == 0 {\n                break;\n            }\n            hasher.update(&buffer[..read]);\n        }\n        let actual = hasher\n            .finalize()\n            .iter()\n            .map(|byte| format!(\"{byte:02x}\"))\n            .collect::<String>();\n        if &actual != expected {\n            anyhow::bail!(\"reviewed plugin executable bytes changed before spawn\");\n        }\n        file.seek(std::io::SeekFrom::Start(0))\n            .context(\"rewind reviewed launch file after verification\")?;\n\n        #[cfg(unix)]\n        let launch_path = {\n            use std::os::fd::AsRawFd as _;\n            let fd = file.as_raw_fd();\n            // SAFETY: `fd` is owned by `file`; clearing only FD_CLOEXEC keeps\n            // that same descriptor available across the imminent exec.\n            let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };\n            if flags < 0 || unsafe { libc::fcntl(fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC) } < 0\n            {\n                anyhow::bail!(\"failed to inherit reviewed plugin executable descriptor\");\n            }\n            #[cfg(target_os = \"linux\")]\n            let prefix = \"/proc/self/fd\";\n            #[cfg(not(target_os = \"linux\"))]","sourceCodeStart":894,"sourceCodeEnd":930,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/mcp.rs#L894-L930","documentation":"Thrown by the reviewed-plugin launch guard in bind_file (crates/tui/src/mcp.rs:912). Before spawning a trusted plugin's executable, Codewhale re-hashes the file (SHA-256 over the domain tag \"codewhale-plugin-file-bytes-v1\\0\" plus the bytes) and compares it with the hash recorded in the byte inventory captured at /plugin trust time. A mismatch means the exact bytes that were reviewed are no longer on disk, so launch fails closed. This is a deliberate tamper/TOCTOU check, not an I/O malfunction.","triggerScenarios":"Spawning an MCP server from a plugin whose executable or bound bundle file was modified after /plugin trust recorded its hash: rebuilding the plugin, a package manager / sync client rewriting the file, an editor touching the script, or /plugin update landing without re-trusting.","commonSituations":"Plugin authors iterating on a bundle and re-running the TUI without re-trusting; build watchers or CI rewriting plugin outputs between trust and use; marketplace updates arriving mid-session; the rare case of actual tampering, which this guard exists to catch.","solutions":["Re-trust the plugin: run /plugin reload, inspect /plugin show <name>, repeat the displayed /plugin trust <name> <token> command, then /plugin enable <name> (the exact steps the adjacent validate_before_use message prescribes at mcp.rs:719-721).","If the change was intentional, go through the update path (/plugin update) so the byte inventory is regenerated instead of bypassed.","Find what rewrote the file: compare mtimes (stat on the bundle files), disable sync clients (Dropbox/OneDrive) over the plugins directory, stop build watchers that rewrite outputs.","If nothing you did explains the change, treat it as tampering: verify the file's provenance out-of-band before re-trusting."],"exampleFix":"// before: plugin binary rebuilt after trust -> spawn aborts:\n//   reviewed plugin executable bytes changed before spawn\n// after: refresh the trust receipt after every intentional byte change\n//   /plugin reload\n//   /plugin show my-plugin\n//   /plugin trust my-plugin <token-shown-by-show>\n//   /plugin enable my-plugin","handlingStrategy":"validation","validationCode":"use sha2::{Digest, Sha256};\nuse std::{fs::File, io::Read, path::Path};\n\n/// Mirrors the launcher's check: SHA-256 over the domain tag + file bytes.\nfn launch_bytes_match(path: &Path, expected_hex: &str) -> anyhow::Result<bool> {\n    let mut file = File::open(path)?;\n    let mut hasher = Sha256::new();\n    hasher.update(b\"codewhale-plugin-file-bytes-v1\\0\");\n    let mut buf = [0u8; 64 * 1024];\n    loop {\n        let n = file.read(&mut buf)?;\n        if n == 0 { break; }\n        hasher.update(&buf[..n]);\n    }\n    let actual: String = hasher.finalize().iter().map(|b| format!(\"{b:02x}\")).collect();\n    Ok(actual == expected_hex)\n}","typeGuard":null,"tryCatchPattern":"match McpConnection::connect_with_policy(name, config, &timeouts, policy).await {\n    Ok(conn) => conn,\n    Err(err) if err.to_string().contains(\"reviewed plugin executable bytes changed\") => {\n        // Fail-closed by design: surface the re-trust steps, never retry unchanged.\n        Err(err.context(\"plugin bytes drifted from review; re-run /plugin trust\"))?\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Re-run /plugin trust after every intentional change to plugin bytes.","Keep reviewed plugin bundles in a directory no build step or sync client writes to.","Treat any unexplained occurrence as tampering and investigate before re-trusting."],"tags":["security","plugin","mcp","hash","tamper-detection"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}