{"record":{"id":"4014805507a24561","repo":"Hmbown/CodeWhale","slug":"refusing-a-symlinked-runtime-chat-owner-lock","errorCode":null,"errorMessage":"refusing a symlinked Runtime Chat owner lock","messagePattern":"refusing a symlinked Runtime Chat owner lock","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_chat_relay.rs","lineNumber":1564,"sourceCode":"            .into_owned(),\n    );\n    execution.context.project_pack = Some(false);\n    execution\n        .skills\n        .get_or_insert_with(SkillsConfig::default)\n        .scan_codewhale_only = Some(true);\n    Ok((execution, workspace))\n}\n\n#[derive(Debug)]\nstruct RelayScopeLock {\n    _file: File,\n}\n\nimpl RelayScopeLock {\n    fn acquire(path: &Path) -> Result<Self> {\n        if fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink()) {\n            bail!(\"refusing a symlinked Runtime Chat owner lock\");\n        }\n        let mut options = fs::OpenOptions::new();\n        options.read(true).write(true).create(true);\n        #[cfg(unix)]\n        {\n            use std::os::unix::fs::OpenOptionsExt as _;\n            options.mode(0o600).custom_flags(libc::O_NOFOLLOW);\n        }\n        #[cfg(windows)]\n        {\n            use std::os::windows::fs::OpenOptionsExt as _;\n            use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;\n            options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);\n        }\n        let file = options.open(path).context(\"open Runtime Chat owner lock\")?;\n        if !file\n            .metadata()\n            .context(\"inspect Runtime Chat owner lock\")?","sourceCodeStart":1546,"sourceCodeEnd":1582,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/runtime_chat_relay.rs#L1546-L1582","documentation":"RelayScopeLock::acquire refuses to open the Runtime Chat owner lock file if the filesystem path is a symlink. Symlinked lock files are a classic privilege-escalation vector (an attacker points the lock path at a sensitive file the process would then open read/write), so the code checks symlink_metadata first and bails.","triggerScenarios":"Acquiring the owner scope lock when something (another process, a user, an attacker) has replaced the expected regular lock file at the state path with a symlink pointing elsewhere.","commonSituations":"Tampered or shared state directories (e.g. world-writable tmp dirs); restoring state via a symlink farm or dotfile manager that symlinks files; copying state with tools that create symlinks instead of copies.","solutions":["Remove the symlink at the lock path and let the application recreate a regular lock file","Restore the lock path as a regular file from a known-good backup","Check who/what created the symlink — on a shared machine treat it as potential tampering","Run the app against a state directory that is not managed by symlink-based dotfile tooling"],"exampleFix":"// before\nln -s /etc/passwd ~/.local/state/codewhale/runtime-chat-owner.lock\n// after\nrm ~/.local/state/codewhale/runtime-chat-owner.lock  # let the app recreate it as a regular file","handlingStrategy":"validation","validationCode":"fn lock_path_is_safe(path: &std::path::Path) -> bool {\n    !matches!(std::fs::symlink_metadata(path), Ok(m) if m.file_type().is_symlink())\n}","typeGuard":null,"tryCatchPattern":"match RelayScopeLock::acquire(&lock_path) {\n    Err(e) if e.to_string().contains(\"symlinked\") => {\n        eprintln!(\"lock path is a symlink; removing and retrying with a clean path\");\n        std::fs::remove_file(&lock_path)?;\n        RelayScopeLock::acquire(&lock_path)?\n    }\n    other => other?,\n}","preventionTips":["Keep state directories owned by the running user and not world-writable","Do not symlink individual state files (dotfile managers should manage directories, not lock files)","After backup restores, verify lock paths are regular files"],"tags":["security","symlink","filesystem","lock"],"backgroundTag":"path-traversal-blocked","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}