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.

Solutions

  1. Reopen the file after it is replaced and lock the new descriptor rather than reusing the old one.
  2. Avoid replacing files (rename/unlink) while a process holds a lock on them.
  3. 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

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


AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12). Data as JSON: /api/errors/4ad604441eb1c3e4. Report an issue: GitHub.

Appendix: 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)