{"record":{"id":"4ad604441eb1c3e4","repo":"golang/go","slug":"inode-for-file-changed-since-last-lock-or-rlock","errorCode":null,"errorMessage":"inode for file changed since last Lock or RLock","messagePattern":"inode for file changed since last Lock or RLock","errorType":"exception","errorClass":"fs.PathError","httpStatus":null,"severity":"error","filePath":"src/cmd/go/internal/lockedfile/internal/filelock/filelock_fcntl.go","lineNumber":65,"sourceCode":"\nfunc lock(f File, lt lockType) (err error) {\n\t// POSIX locks apply per inode and process, and the lock for an inode is\n\t// released when *any* descriptor for that inode is closed. So we need to\n\t// synchronize access to each inode internally, and must serialize lock and\n\t// unlock calls that refer to the same inode through different descriptors.\n\tfi, err := f.Stat()\n\tif err != nil {\n\t\treturn err\n\t}\n\tino := fi.Sys().(*syscall.Stat_t).Ino\n\n\tmu.Lock()\n\tif i, dup := inodes[f]; dup && i != ino {\n\t\tmu.Unlock()\n\t\treturn &fs.PathError{\n\t\t\tOp:   lt.String(),\n\t\t\tPath: f.Name(),\n\t\t\tErr:  errors.New(\"inode for file changed since last Lock or RLock\"),\n\t\t}\n\t}\n\tinodes[f] = ino\n\n\tvar wait chan File\n\tl := locks[ino]\n\tif l.owner == f {\n\t\t// This file already owns the lock, but the call may change its lock type.\n\t} else if l.owner == nil {\n\t\t// No owner: it's ours now.\n\t\tl.owner = f\n\t} else {\n\t\t// Already owned: add a channel to wait on.\n\t\twait = make(chan File)\n\t\tl.queue = append(l.queue, wait)\n\t}\n\tlocks[ino] = l\n\tmu.Unlock()","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/golang/go/blob/b6b368adc57c96c3151d224d172029f233ead2c3/src/cmd/go/internal/lockedfile/internal/filelock/filelock_fcntl.go#L47-L83","documentation":"The fcntl-based file lock tracks the inode associated with each locked *os.File in the inodes map. POSIX locks are per-inode-per-process, so if the inode for an already-tracked file descriptor changes between two lock calls, the recorded state is stale and unsafe; the lock refuses and returns a PathError wrapping this message.","triggerScenarios":"The same filelock.File is locked twice (Lock/RLock), but the underlying inode changed between calls because the file was replaced on disk (rename, unlink+recreate, truncate-and-rewrite by another process).","commonSituations":"go.sum/go.mod rewritten in place by another go command while a lock is held; log rotation replacing a locked file; /tmp files on overlay filesystems that remap inodes; tooling that truncates+rewrites instead of mutating in place.","solutions":["Reopen the file after it is replaced and lock the new descriptor rather than reusing the old one.","Avoid replacing files (rename/unlink) while a process holds a lock on them.","Use a dedicated lock file (e.g. foo.lock) that is never replaced, instead of locking the data file directly."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Before re-locking, detect an inode change and reopen:\n//   st, err := os.Stat(f.Name())\n//   prev, _ := f.Stat()\n//   if st.Sys().(*syscall.Stat_t).Ino != prev.Sys().(*syscall.Stat_t).Ino {\n//     f, _ = os.Open(f.Name()) // reopen the replaced file\n//   }","typeGuard":null,"tryCatchPattern":"// On *fs.PathError where Err.Error() contains\n// \"inode for file changed since last Lock or RLock\",\n// reopen the file and retry the lock once:\n//   var pathErr *fs.PathError\n//   if errors.As(err, &pathErr) && /* matches */ {\n//     f, _ = os.Open(path); return filelock.Lock(f)\n//   }","preventionTips":["Never rename, unlink, or rewrite a file while a process holds a lock on it.","Use a dedicated lock file that is created once and never replaced."],"tags":["go","filelock","concurrency","posix","filesystem"],"backgroundTag":null,"analyzedSha":"b6b368adc57c96c3151d224d172029f233ead2c3","analyzedAt":"2026-08-12T00:22:02.250Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}