{"record":{"id":"ffdb08027da4d4ba","repo":"zed-industries/zed","slug":"revision-spec-revision-contains-a-newline-and","errorCode":null,"errorMessage":"revision spec {revision:?} contains a newline and cannot be passed to git cat-file --batch","messagePattern":"revision spec (.+?) contains a newline and cannot be passed to git cat-file --batch","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/git/src/repository.rs","lineNumber":1765,"sourceCode":"                        output.status.success(),\n                        \"Failed to unstage:\\n{}\",\n                        String::from_utf8_lossy(&output.stderr)\n                    );\n                }\n\n                Ok(())\n            })\n            .boxed()\n    }\n\n    fn remote_urls(&self) -> BoxFuture<'_, HashMap<String, String>> {\n        let git = self.git_binary();\n        self.executor\n            .spawn(async move {\n                if let Ok(stdout) = git.run(&[\"remote\", \"-v\"]).await {\n                    parse_remote_urls(&stdout)\n                } else {\n                    HashMap::default()\n                }\n            })\n            .boxed()\n    }\n\n    fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {\n        let git = self.git_binary();\n        self.executor\n            .spawn(async move {\n                let mut process = git\n                    .build_command(&[\"cat-file\", \"--batch-check=%(objectname)\"])\n                    .stdin(Stdio::piped())\n                    .stdout(Stdio::piped())\n                    .stderr(Stdio::piped())\n                    .spawn()?;\n\n                let stdin = process\n                    .stdin","sourceCodeStart":1747,"sourceCodeEnd":1783,"githubUrl":"https://github.com/zed-industries/zed/blob/5a9b9558db01a6b906cec2fb70a797affdc58cdd/crates/git/src/repository.rs#L1747-L1783","documentation":"load_revisions() batches revision lookups by writing every spec to the stdin of one `git cat-file --batch` process. The batch protocol delimits requests by newline, so a revision spec that itself contains a newline would be split into extra requests and desynchronize the responses. The library therefore rejects the whole batch before spawning git when any revision contains a newline.","triggerScenarios":"Calling a repository API that ends in load_revisions (batch-loading blob contents for multiple revisions, e.g. blame or project diff reading old file versions) where at least one revision string contains a newline. The offending spec is printed with Debug formatting ({revision:?}) so the message shows the embedded \\n.","commonSituations":"Revision strings assembled from untrusted or loosely validated input: text pasted by a user (trailing newline never trimmed), refs parsed out of commit messages, or scripting bugs that embed newlines into ref names. Cheap sanitization is applied to URLs and paths but not to revision specs.","solutions":["Trim and validate revision strings before passing them in: reject any spec containing a newline (and ideally enforce a strict revision charset such as A-Za-z0-9._/^~@:%-).","Sanitize at the input boundary (trim user-pasted text) instead of at the git call site.","Resolve each spec to a full SHA individually with `git rev-parse --verify <spec>` first, then batch the resolved SHAs, which are guaranteed newline-free.","If the newline arrived via a ref you created, fix the script that created the malformed ref."],"exampleFix":"// before\nlet contents = repo.load_revisions(vec![revision_from_user.clone()]).await?;\n// after\nlet revision_from_user = revision_from_user.trim();\nanyhow::ensure!(!revision_from_user.contains('\\n'), \"invalid revision spec: {revision_from_user:?}\");\nlet contents = repo.load_revisions(vec![revision_from_user.to_owned()]).await?;","handlingStrategy":"validation","validationCode":"fn sanitize_revision(spec: &str) -> anyhow::Result<&str> {\n    let trimmed = spec.trim();\n    anyhow::ensure!(\n        !trimmed.is_empty() && !trimmed.contains('\\n') && !trimmed.contains('\\0'),\n        \"revision spec contains control characters: {spec:?}\"\n    );\n    Ok(trimmed)\n}\n\nlet revisions = revisions\n    .iter()\n    .map(|r| sanitize_revision(r).map(str::to_owned))\n    .collect::<Result<Vec<_>>>()?;\nlet contents = repo.load_revisions(revisions).await?;","typeGuard":null,"tryCatchPattern":"match repo.load_revisions(revisions).await {\n    Err(e) if e.to_string().contains(\"cannot be passed to git cat-file\") => {\n        // bad input: sanitize the specs instead of retrying\n    }\n    other => other?,\n}","preventionTips":["Trim all user-supplied text at the input boundary before it can become a revision spec.","Validate revision specs against a strict charset (no control characters) in one shared helper used by every call site.","Pre-resolve specs to full SHAs with `git rev-parse --verify` before batching them into cat-file."],"tags":["git","input-validation","injection","cat-file"],"backgroundTag":"command-injection","analyzedSha":"5a9b9558db01a6b906cec2fb70a797affdc58cdd","analyzedAt":"2026-08-20T19:29:52.058Z","contentChangedAt":"2026-08-20T19:29:52.058Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}