{"record":{"id":"915e1d937a34e31d","repo":"tinyhumansai/openhuman","slug":"workspace-lock-file-lock-path-is-a-symlink-re","errorCode":null,"errorMessage":"workspace lock file {lock_path:?} is a symlink; refusing to follow it","messagePattern":"workspace lock file (.+?) is a symlink; refusing to follow it","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/openhuman/tools/impl/filesystem/update_memory_md.rs","lineNumber":71,"sourceCode":"/// `MEMORY.md` mid read-modify-write. This takes an `fs2` exclusive `flock` on a\n/// sentinel `.memory-write.lock` file in the workspace; the returned `File`\n/// holds the lock until it is dropped (end of the write). `flock` acquisition\n/// blocks, so it runs on a blocking thread.\nasync fn acquire_cross_process_write_lock(workspace_dir: &Path) -> anyhow::Result<std::fs::File> {\n    let lock_path = workspace_dir.join(\".memory-write.lock\");\n    tokio::task::spawn_blocking(move || {\n        use fs2::FileExt;\n        if let Some(parent) = lock_path.parent() {\n            let _ = std::fs::create_dir_all(parent);\n        }\n        // Reject a symlinked lock file: a project-controlled `.memory-write.lock`\n        // symlink (including a dangling one) could otherwise redirect this\n        // create/open/lock to a path OUTSIDE the already-containment-checked\n        // workspace, bypassing the symlink hardening applied to MEMORY.md /\n        // SKILL.md. If it exists it must be a regular file.\n        if let Ok(meta) = std::fs::symlink_metadata(&lock_path) {\n            if meta.file_type().is_symlink() {\n                return Err(anyhow::anyhow!(\n                    \"workspace lock file {lock_path:?} is a symlink; refusing to follow it\"\n                ));\n            }\n        }\n        let mut opts = std::fs::OpenOptions::new();\n        opts.create(true).write(true).truncate(false);\n        #[cfg(unix)]\n        {\n            // O_NOFOLLOW closes the TOCTOU window: if a symlink is swapped in\n            // after the check above, the open fails (ELOOP) rather than follows.\n            use std::os::unix::fs::OpenOptionsExt;\n            opts.custom_flags(libc::O_NOFOLLOW);\n        }\n        let file = opts\n            .open(&lock_path)\n            .map_err(|e| anyhow::anyhow!(\"open workspace lock file {lock_path:?}: {e}\"))?;\n        file.lock_exclusive()\n            .map_err(|e| anyhow::anyhow!(\"acquire workspace write flock: {e}\"))?;","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/tools/impl/filesystem/update_memory_md.rs#L53-L89","documentation":"Security guard in the MEMORY.md cross-process lock acquisition: the sentinel .memory-write.lock file in the workspace is a symbolic link. Because flock would follow it, the tool refuses rather than lock an attacker-chosen target — a deliberate TOCTOU mitigation.","triggerScenarios":"Thrown at src/openhuman/tools/impl/filesystem/update_memory_md.rs:71 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Delete the symlinked .memory-write.lock from the workspace","Investigate how the symlink was created — it may indicate tampering","The next write attempt will recreate a regular lock file"],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}