golang/go · error · fs.PathError
inode for file changed since last Lock or RLock
Error message
inode for file changed since last Lock or RLock
What it means
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.
Source
Thrown at src/cmd/go/internal/lockedfile/internal/filelock/filelock_fcntl.go:65
func lock(f File, lt lockType) (err error) {
// POSIX locks apply per inode and process, and the lock for an inode is
// released when *any* descriptor for that inode is closed. So we need to
// synchronize access to each inode internally, and must serialize lock and
// unlock calls that refer to the same inode through different descriptors.
fi, err := f.Stat()
if err != nil {
return err
}
ino := fi.Sys().(*syscall.Stat_t).Ino
mu.Lock()
if i, dup := inodes[f]; dup && i != ino {
mu.Unlock()
return &fs.PathError{
Op: lt.String(),
Path: f.Name(),
Err: errors.New("inode for file changed since last Lock or RLock"),
}
}
inodes[f] = ino
var wait chan File
l := locks[ino]
if l.owner == f {
// This file already owns the lock, but the call may change its lock type.
} else if l.owner == nil {
// No owner: it's ours now.
l.owner = f
} else {
// Already owned: add a channel to wait on.
wait = make(chan File)
l.queue = append(l.queue, wait)
}
locks[ino] = l
mu.Unlock()View on GitHub (pinned to b6b368adc5)
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.
Defensive patterns
Strategy: retry
Validate before calling
// Before re-locking, detect an inode change and reopen:
// st, err := os.Stat(f.Name())
// prev, _ := f.Stat()
// if st.Sys().(*syscall.Stat_t).Ino != prev.Sys().(*syscall.Stat_t).Ino {
// f, _ = os.Open(f.Name()) // reopen the replaced file
// } Try / catch
// On *fs.PathError where Err.Error() contains
// "inode for file changed since last Lock or RLock",
// reopen the file and retry the lock once:
// var pathErr *fs.PathError
// if errors.As(err, &pathErr) && /* matches */ {
// f, _ = os.Open(path); return filelock.Lock(f)
// } Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- file is empty
- deleted in overlay
- cannot open directory in overlay
- file content changed underfoot
- reading %s: %v
AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12).
Data as JSON: /api/errors/4ad604441eb1c3e4.
Report an issue: GitHub.