Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

os: File.ReadDir is not concurrency safe on some platforms #66498

Closed
neild opened this issue Mar 23, 2024 · 15 comments
Closed

os: File.ReadDir is not concurrency safe on some platforms #66498

neild opened this issue Mar 23, 2024 · 15 comments
Assignees
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Milestone

Comments

@neild
Copy link
Contributor

neild commented Mar 23, 2024

The Unix implementation of File.readdir accesses an internal buffer without synchronization. Simultaneous ReadDir calls on the same *os.File will race and misbehave.

@panjf2000
Copy link
Member

The docs of File.Readdir, File.ReadDir, and File.Readdirnames make no such guarantee of that. Any particular reasons why we should make it concurrent-safe? Calling ReadDir on the same *os.File simultaneously doesn't seem like a typical scenario.

@neild
Copy link
Contributor Author

neild commented Mar 23, 2024

The docs don't mention concurrency safety, but (and someone who was around when the os package was created can confirm or deny this) I suspect that's because the assumption was that *os.File methods are obviously supposed to be safe to call from multiple goroutines. An *os.File, per its documentation, represents an open file descriptor, and Unix FD operations are always concurrency-safe.

If ReadDir, or any other method, is not supposed to be safe to call concurrently, the docs definitely should mention that.

(Of course, methods which modify file state are inherently unsafe--Read and Write modify the current file position, for example. But I'd expect ReadAt to be safe.)

@dr2chase dr2chase added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Mar 25, 2024
@qmuntal
Copy link
Contributor

qmuntal commented Mar 25, 2024

File.ReadDir can't be concurrent safe when doing partial reads, i.e. calling File.ReadDir(n) on a directory with more than n entries. It uses an internal state to keep track of the current entry position so the next File.ReadDir(n) call returns the following n entries.

On the other hand, we could special the n <= 0 case, which is documented to return all the remaining entries, to make it concurrent-safe, as it doesn't need to keep track of read file entries between calls.

@adonovan
Copy link
Member

In general, unless otherwise documented, standalone functions should be designed to be concurrency-safe, but methods of a type should not be assumed to be concurrency safe. So os.Open must be safe, but Read or ReadDir need not be. I don't think there's a bug or even a doc issue here.

@ianlancetaylor
Copy link
Contributor

We do go to some trouble to ensure that concurrent calls to ReadAt and WriteAt will work. It is of course impossible to support concurrent calls to Read and Write. So I think the only question is whether we want Readdir(-1) to be concurrency-safe. I don't have a strong opinion myself.

@neild
Copy link
Contributor Author

neild commented Mar 25, 2024

Concurrent calls to Write in append mode might work? I don't recall what, if any, guarantees the OS offers there.

I think Readdir(-1) should either be concurrency-safe or explicitly documented as unsafe. I lean towards safe, but not strongly.

Should there be a version of File.ReadDir that returns an iterator? #61897 states that os.ReadDir won't get an iterator variant because it sorts its result, but File.ReadDir returns unsorted results.

@panjf2000 panjf2000 added this to the Backlog milestone Mar 25, 2024
@abursavich
Copy link

I was coming here to file a similar issue, but found this first. My real-world use case is concurrent calls to File.Readdirnames and File.Close. There's a goroutine polling for updates to a directory and the graceful shutdown code calls Close on the file from a different goroutine. I expected the same semantics as a concurrent Unix FD read and close.

[signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0x4e18ea]

goroutine 5044070 [running]:
os.(*File).readdir(0xc00039a0e0, 0xffffffffffffffff?, 0x0)
        /usr/local/go/src/os/dir_unix.go:81 +0x24a
os.(*File).Readdirnames(0x8a68ed?, 0xbdd000?)
        /usr/local/go/src/os/dir.go:70 +0x1a
<my code ommitted>

@adonovan
Copy link
Member

adonovan commented Apr 3, 2024

[signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0x4e18ea]

For the record: assuming you're using go1.22.0, this crash indicates a race on the File.dirInfo.buf buffer.

@abursavich
Copy link

There are races on File.dirinfo and dirInfo.buf.

https://github.com/golang/go/blob/go1.22.1/src/os/file_unix.go#L310-L313

func (file *file) close() error {
	// ...
	if file.dirinfo != nil {
		file.dirinfo.close()
		file.dirinfo = nil
	}
	// ...
}

https://github.com/golang/go/blob/go1.22.1/src/os/dir_unix.go#L37-L42

func (d *dirInfo) close() {
	if d.buf != nil {
		dirBufPool.Put(d.buf)
		d.buf = nil
	}
}

https://github.com/golang/go/blob/go1.22.1/src/os/dir_unix.go#L46-L50

func (f *File) readdir(n int, mode readdirMode) (names []string, dirents []DirEntry, infos []FileInfo, err error) {
	if f.dirinfo == nil {
		f.dirinfo = new(dirInfo)
		f.dirinfo.buf = dirBufPool.Get().(*[]byte)
	}
	d := f.dirinfo
	// ... many uses of d.buf ...
}

@jfrech
Copy link

jfrech commented Apr 4, 2024

Leaning towards os.(*File).ReadDir's behaviour being correctly not specified:

By default, programmers should expect that a type is safe for use only by a single goroutine at a time.

Cf. https://go.dev/doc/comment#type [2024-04-04]

By default, programmers can assume that a top-level function is safe to call from multiple goroutines; this fact need not be stated explicitly.

On the other hand, as noted in the previous section, using an instance of a type in any way, including calling a method, is typically assumed to be restricted to a single goroutine at a time.

Cf. https://go.dev/doc/comment#func [2024-04-04]

@adonovan
Copy link
Member

adonovan commented Apr 4, 2024

It might not hurt to make an exception to the rule here and redundantly document the lack of concurrency safety, because many readers may wrongly assume that Read, Write, ReadDir, and other methods named for UNIX system calls are nothing more than wrappers around those system calls.

@jfrech
Copy link

jfrech commented Apr 4, 2024

@neild

(Of course, methods which modify file state are inherently unsafe--Read and Write modify the current file position, for example. But I'd expect ReadAt to be safe.)

Concurrency is built into io.ReaderAt's contract, not a property of some external Unix-y interpretation of how a file ought to behave:

Clients of ReadAt can execute parallel ReadAt calls on the same input source.

Cf. https://pkg.go.dev/io@go1.22.2#ReaderAt [2024-04-04]

@abursavich
Copy link

@jfrech

Concurrency is built into io.ReaderAt's contract, not a property of some external Unix-y interpretation of how a file ought to behave:

The documentation for the os.File struct is "File represents an open file descriptor." I think that carries a bit of Unix-y interpretations with it.

@adonovan adonovan self-assigned this Apr 12, 2024
@gopherbot
Copy link

Change https://go.dev/cl/578322 mentions this issue: os: document concurrency unsafety of File, Readdir et al

@adonovan
Copy link
Member

Note: the subject of the fix as merged was "os: make File.Readdir et al concurrency-safe".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
None yet
Development

No branches or pull requests

9 participants