What version of Go are you using (go version)?

$ go version
devel go1.22-98bacdeafe Mon Aug 14 12:51:16 2023 +0000

Does this issue reproduce with the latest release?

Yes.

What did you do?

  1. Use os.Open to open an os.File for a relative directory.
  2. Call os.Chdir to change the current working directory.
  3. Call ReadDir or Readdir on the File opened in step (1).
  4. Inspect the resulting FileInfos.

(https://go.dev/play/p/bLLMvUJGu_u?v=gotip)

What did you expect to see?

The FileInfos should correspond to the directory entries relative to the File.

What did you see instead?

On Linux: - ReadDir produces the directory contents relative to the current directory instead of the opened file. - Readdir produces DirEntry contents relative to the opened file, but calling the Info method on those entries attempts to open the wrong paths (compare #52747).

It isn't clear to me how this can be fixed in general — it is possible to use fdopendir to read the correct entries while the parent file is still open, but if the parent file is closed before the Info method is called on the returned DirEntry, I don't see a standard API to reliably open the correct path.

Comment From: rsc

In general Chdir breaks plenty of invariants, and we're not likely to try to work around that fact, simply because it's impossible to catch them all and just makes things more brittle. The only safe Chdirs are (1) at quiet moments when nothing else is going on and you're not going to try to reuse FS state from before the Chdir afterward, which typically reduces to (1a) at startup, and (2) during os/exec by setting cmd.Dir, which doesn't affect your process.

If there was some compelling use case where Open(dir), Chdir, ReadDir+Info, dir.Close was common, then I think the fix would be optimistic fstatat in the implementation of Readdir and the implementation of DirEntry.Info, with the latter falling back to name-based stat if the directory fd has been closed. But I'd want to see that use case before complicating this code. A racing Chdir in another goroutine would be the most common way to get that sequence, but in that case the Chdir is racing with Open and so you have bigger problems.

Leaving this open but probably we won't fix this unless there is a more compelling demonstration of this arising in actual practice.

Comment From: bcmills

A racing Chdir in another goroutine would be the most common way to get that sequence, but in that case the Chdir is racing with Open and so you have bigger problems.

Note that the Chdir need not race with Open: it only needs to race with the Info method of the DirEntry structs returned by File.ReadDir.

But I agree that this doesn't seem to arise in real-world use. (I noticed it in the context of thinking about edge-cases while reviewing https://go.dev/cl/518195.)

Comment From: neild

This isn't just a problem with Chdir.

  1. link is a symlink to dir, dir is a directory.
  2. os.Open("link").
  3. Remove link.
  4. Call ReadDir on the File obtained in step 2.
  5. Call Info on the resulting FileInfo.

This returns an error, because the Info call tries to stat a file via the now-deleted symlink.