Skip to content

Commit ba3c57f

Browse files
committed
os: guarantee min buffer size for ReadFile reads on /proc-like files
For instance, this fixes os.ReadFile on plan9's /net/iproute file. But it's not necessarily plan9-specific; Linux /proc and /sys filesystems can exhibit the same problems. Fixes #72080 Change-Id: I60b035913f583a91c6d84df95a6ea7b7ec2b3c92 Reviewed-on: https://go-review.googlesource.com/c/go/+/654315 Reviewed-by: Ian Lance Taylor <[email protected]> Reviewed-by: Damien Neil <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent fd8938c commit ba3c57f

File tree

4 files changed

+135
-18
lines changed

4 files changed

+135
-18
lines changed

Diff for: src/os/export_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ var ErrPatternHasSeparator = errPatternHasSeparator
1515
func init() {
1616
checkWrapErr = true
1717
}
18+
19+
var ExportReadFileContents = readFileContents

Diff for: src/os/file.go

+36-17
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import (
5151
"io"
5252
"io/fs"
5353
"runtime"
54+
"slices"
5455
"syscall"
5556
"time"
5657
"unsafe"
@@ -846,30 +847,45 @@ func ReadFile(name string) ([]byte, error) {
846847
return nil, err
847848
}
848849
defer f.Close()
849-
return readFileContents(f)
850+
851+
return readFileContents(statOrZero(f), f.Read)
852+
}
853+
854+
func statOrZero(f *File) int64 {
855+
if fi, err := f.Stat(); err == nil {
856+
return fi.Size()
857+
}
858+
return 0
850859
}
851860

852-
func readFileContents(f *File) ([]byte, error) {
861+
// readFileContents reads the contents of a file using the provided read function
862+
// (*os.File.Read, except in tests) one or more times, until an error is seen.
863+
//
864+
// The provided size is the stat size of the file, which might be 0 for a
865+
// /proc-like file that doesn't report a size.
866+
func readFileContents(statSize int64, read func([]byte) (int, error)) ([]byte, error) {
867+
zeroSize := statSize == 0
868+
869+
// Figure out how big to make the initial slice. For files with known size
870+
// that fit in memory, use that size + 1. Otherwise, use a small buffer and
871+
// we'll grow.
853872
var size int
854-
if info, err := f.Stat(); err == nil {
855-
size64 := info.Size()
856-
if int64(int(size64)) == size64 {
857-
size = int(size64)
858-
}
873+
if int64(int(statSize)) == statSize {
874+
size = int(statSize)
859875
}
860876
size++ // one byte for final read at EOF
861877

862-
// If a file claims a small size, read at least 512 bytes.
863-
// In particular, files in Linux's /proc claim size 0 but
864-
// then do not work right if read in small pieces,
865-
// so an initial read of 1 byte would not work correctly.
866-
if size < 512 {
867-
size = 512
878+
const minBuf = 512
879+
// If a file claims a small size, read at least 512 bytes. In particular,
880+
// files in Linux's /proc claim size 0 but then do not work right if read in
881+
// small pieces, so an initial read of 1 byte would not work correctly.
882+
if size < minBuf {
883+
size = minBuf
868884
}
869885

870886
data := make([]byte, 0, size)
871887
for {
872-
n, err := f.Read(data[len(data):cap(data)])
888+
n, err := read(data[len(data):cap(data)])
873889
data = data[:len(data)+n]
874890
if err != nil {
875891
if err == io.EOF {
@@ -878,9 +894,12 @@ func readFileContents(f *File) ([]byte, error) {
878894
return data, err
879895
}
880896

881-
if len(data) >= cap(data) {
882-
d := append(data[:cap(data)], 0)
883-
data = d[:len(data)]
897+
// If we're either out of capacity or if the file was a /proc-like zero
898+
// sized file, grow the buffer. Per Issue 72080, we always want to issue
899+
// Read calls on zero-length files with a non-tiny buffer size.
900+
capRemain := cap(data) - len(data)
901+
if capRemain == 0 || (zeroSize && capRemain < minBuf) {
902+
data = slices.Grow(data, minBuf)
884903
}
885904
}
886905
}

Diff for: src/os/os_test.go

+96
Original file line numberDiff line numberDiff line change
@@ -3855,3 +3855,99 @@ func TestOpenFileDevNull(t *testing.T) {
38553855
}
38563856
f.Close()
38573857
}
3858+
3859+
func TestReadFileContents(t *testing.T) {
3860+
type readStep struct {
3861+
bufSize int // non-zero to check length of buf to Read
3862+
retN int // result of Read call
3863+
retErr error // error result of Read call
3864+
}
3865+
errFoo := errors.New("foo")
3866+
tests := []struct {
3867+
name string
3868+
statSize int64 // size of file to read, per stat (may be 0 for /proc files)
3869+
wantSize int // wanted length of []byte from readFileContents
3870+
wantErr error // wanted error from readFileContents
3871+
reads []readStep
3872+
}{
3873+
{
3874+
name: "big-file",
3875+
statSize: 2000,
3876+
wantSize: 2000,
3877+
reads: []readStep{
3878+
{bufSize: 2001, retN: 21, retErr: nil},
3879+
{bufSize: 1980, retN: 1979, retErr: io.EOF},
3880+
},
3881+
},
3882+
{
3883+
name: "small-file",
3884+
statSize: 100,
3885+
wantSize: 100,
3886+
reads: []readStep{
3887+
{bufSize: 512, retN: 100, retErr: io.EOF},
3888+
},
3889+
},
3890+
{
3891+
name: "returning-error",
3892+
statSize: 1000,
3893+
wantSize: 50,
3894+
wantErr: errFoo,
3895+
reads: []readStep{
3896+
{bufSize: 1001, retN: 25, retErr: nil},
3897+
{retN: 25, retErr: errFoo},
3898+
},
3899+
},
3900+
{
3901+
name: "proc-file",
3902+
statSize: 0,
3903+
wantSize: 1023,
3904+
reads: []readStep{
3905+
{bufSize: 512, retN: 512, retErr: nil},
3906+
{retN: 511, retErr: io.EOF},
3907+
},
3908+
},
3909+
{
3910+
name: "plan9-iproute-file", // Issue 72080
3911+
statSize: 0,
3912+
wantSize: 1032,
3913+
reads: []readStep{
3914+
{bufSize: 512, retN: 511, retErr: nil},
3915+
{retN: 511, retErr: nil},
3916+
{retN: 10, retErr: io.EOF},
3917+
},
3918+
},
3919+
}
3920+
for _, tt := range tests {
3921+
t.Run(tt.name, func(t *testing.T) {
3922+
remain := tt.reads
3923+
i := -1
3924+
got, err := ExportReadFileContents(tt.statSize, func(buf []byte) (int, error) {
3925+
i++
3926+
t.Logf("read[%d] with buf size %d", i, len(buf))
3927+
if len(remain) == 0 {
3928+
t.Fatalf("unexpected read of length %d after %d expected reads", len(buf), len(tt.reads))
3929+
}
3930+
if tt.statSize == 0 && len(buf) < 512 {
3931+
// Issue 72080: readFileContents should not do /proc reads with buffers
3932+
// smaller than 512.
3933+
t.Fatalf("read[%d] with buf size %d; want at least 512 for 0-sized file", i, len(buf))
3934+
}
3935+
step := remain[0]
3936+
remain = remain[1:]
3937+
if step.bufSize != 0 && len(buf) != step.bufSize {
3938+
t.Fatalf("read[%d] has buffer size %d; want %d", i, len(buf), step.bufSize)
3939+
}
3940+
return step.retN, step.retErr
3941+
})
3942+
if len(remain) > 0 {
3943+
t.Fatalf("expected %d reads, got %d", len(tt.reads), i+1)
3944+
}
3945+
if fmt.Sprint(err) != fmt.Sprint(tt.wantErr) {
3946+
t.Errorf("got error %v; want %v", err, tt.wantErr)
3947+
}
3948+
if len(got) != tt.wantSize {
3949+
t.Errorf("got size %d; want %d", len(got), tt.wantSize)
3950+
}
3951+
})
3952+
}
3953+
}

Diff for: src/os/root.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ func (rfs *rootFS) ReadFile(name string) ([]byte, error) {
312312
return nil, err
313313
}
314314
defer f.Close()
315-
return readFileContents(f)
315+
return readFileContents(statOrZero(f), f.Read)
316316
}
317317

318318
func (rfs *rootFS) Stat(name string) (FileInfo, error) {

0 commit comments

Comments
 (0)