{"record":{"id":"24ad9eb909c6e8da","repo":"abhigyanpatwari/GitNexus","slug":"filename-moved-or-was-replaced-while-being-open","errorCode":null,"errorMessage":"${filename} moved or was replaced while being opened","messagePattern":"(.+?) moved or was replaced while being opened","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gitnexus/src/config/repo-control-file.ts","lineNumber":68,"sourceCode":"      stream.pause();\n      stream.once('open', (fd) => {\n        try {\n          const opened = fs.fstatSync(fd);\n          if (!opened.isFile()) throw new Error(`${filename} must be a regular file`);\n          if (opened.nlink !== 1) throw new Error(`${filename} must not be a hard link`);\n          if (opened.size > MAX_REPO_CONTROL_FILE_BYTES) {\n            throw new Error(`${filename} exceeds ${MAX_REPO_CONTROL_FILE_BYTES} bytes`);\n          }\n\n          const entry = fs.lstatSync(requested);\n          if (entry.isSymbolicLink()) throw new Error(`${filename} must not be a symbolic link`);\n          if (\n            !entry.isFile() ||\n            entry.nlink !== 1 ||\n            entry.dev !== opened.dev ||\n            entry.ino !== opened.ino\n          ) {\n            throw new Error(`${filename} moved or was replaced while being opened`);\n          }\n          const canonicalFile = fs.realpathSync(requested);\n          const canonicalRelative = path.relative(canonicalRoot, canonicalFile);\n          if (canonicalRelative.startsWith('..') || path.isAbsolute(canonicalRelative)) {\n            throw new Error(`${filename} resolves outside the repository root`);\n          }\n          const canonical = fs.statSync(canonicalFile);\n          if (\n            canonical.nlink !== 1 ||\n            canonical.dev !== opened.dev ||\n            canonical.ino !== opened.ino\n          ) {\n            throw new Error(`${filename} moved or was replaced while being opened`);\n          }\n\n          validated = true;\n          stream.resume();\n        } catch (error) {","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/52924ef12c2290ceee4612526a828ec4cdf2047f/gitnexus/src/config/repo-control-file.ts#L50-L86","documentation":"readRepoControlFile compares the identity (dev/ino) of the opened fd with a fresh lstat of the path; if they differ, or the entry is no longer a single-link regular file, the file changed identity while being opened and this error is thrown. It guarantees the content streamed comes from exactly the file that was validated.","triggerScenarios":"Between createReadStream opening the fd and the follow-up lstatSync(requested), the path is renamed, deleted-and-recreated, or swapped, so entry.dev/entry.ino no longer match opened.dev/opened.ino (or entry fails isFile()/nlink===1).","commonSituations":"Editor save-via-rename (write temp + rename) racing the read; `git checkout`/branch switch replacing the file mid-run; a build script regenerating the config concurrently; malicious file-swap during CI.","solutions":["Serialize access: ensure no editor, git operation, or generator rewrites the control file during analysis.","Re-run the command after concurrent writes settle — the race is transient.","Use atomic-in-place writes (write to temp, then rename only when idle) or stop rewriting files the reader watches.","Investigate unexpected writers (`lsof <file>`, audit logs) if the race recurs without obvious cause."],"exampleFix":"// before: editor races the reader via rename\nmv .gitnexusrc.tmp .gitnexusrc  # during read\n// after: finalize writes before starting analysis\n# wait for editor/git to finish, then run the command","handlingStrategy":"retry","validationCode":"// No pure pre-check can prevent a mid-open swap; minimize the window by\n// ensuring no concurrent writers, then read:\nimport fs from 'node:fs';\nconst a = fs.lstatSync(controlFilePath);\n// ...proceed only when no git/editor/generator process is active on the repo","typeGuard":null,"tryCatchPattern":"async function readStable(root: string, filename: string, tries = 3): Promise<string> {\n  for (let i = 0; ; i++) {\n    try { return await readRepoControlFile(root, filename); }\n    catch (err) {\n      if (i < tries && (err as Error).message.includes('moved or was replaced')) {\n        await new Promise(r => setTimeout(r, 200 * (i + 1)));\n        continue;\n      }\n      throw err;\n    }\n  }\n}","preventionTips":["Serialize writes: no git operations or editors touching the file during analysis.","Use atomic single-rename writes and avoid rewriting files that are read frequently.","Back off and retry — the race window is tiny, so a short delay usually succeeds.","Investigate persistent offenders with lsof/audit tooling."],"tags":["filesystem","security","race-condition","toctou"],"backgroundTag":"file-changed-while-reading","analyzedSha":"52924ef12c2290ceee4612526a828ec4cdf2047f","analyzedAt":"2026-09-01T13:15:02.810Z","contentChangedAt":"2026-09-01T13:15:02.810Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}