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

  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.