Skip to content

os: make DirFS implement ReadFile, ReadDir #53761

@ivan4th

Description

@ivan4th

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

$ go version
go version go1.18.3 linux/amd64

Does this issue reproduce with the latest release?

Yes

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/root/.cache/go-build"
GOENV="/root/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/root/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/root/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/go/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.18.3"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/dev/null"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build3873644549=/tmp/go-build -gno-record-gcc-switches"

What did you do?

package main

import (
	"fmt"
	"io/fs"
	"io/ioutil"
	"os"
)

func verify(fpath string) {
	bs, err := fs.ReadFile(os.DirFS(""), fpath)
	if err != nil {
		panic(err)
	}
	fmt.Printf("fs.ReadFile %q: %q\n", fpath, bs)

	f, err := os.Open("/" + fpath)
	if err != nil {
		panic(err)
	}
	defer f.Close()

	bs, err = ioutil.ReadAll(f)
	if err != nil {
		panic(err)
	}
	fmt.Printf("ReadAll %q: %q\n", fpath, bs)
}

func main() {
	verify("sys/fs/cgroup/cpuset.cpus.effective")
	verify("sys/devices/system/cpu/cpu0/topology/thread_siblings_list")
}

What did you expect to see?

I'd expect thread_sibling_list file to be read correctly:

fs.ReadFile "sys/fs/cgroup/cpuset.cpus.effective": "0\n"
ReadAll "sys/fs/cgroup/cpuset.cpus.effective": "0\n"
fs.ReadFile "sys/devices/system/cpu/cpu0/topology/thread_siblings_list": "0\n"
ReadAll "sys/devices/system/cpu/cpu0/topology/thread_siblings_list": "0\n"

There are no problems with thread_sibling_list on Ubuntu 20.04 / kernel 5.4.0:

Linux i4u 5.4.0-73-generic #82-Ubuntu SMP Wed Apr 14 17:39:42 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

What did you see instead?

When code is run on Ubuntu 22.04 on Linux 5.15.0 the following output is produced:

fs.ReadFile "sys/fs/cgroup/cpuset.cpus.effective": "0\n"
ReadAll "sys/fs/cgroup/cpuset.cpus.effective": "0\n"
fs.ReadFile "sys/devices/system/cpu/cpu0/topology/thread_siblings_list": ""
ReadAll "sys/devices/system/cpu/cpu0/topology/thread_siblings_list": "0\n"

Note that for thread_siblings_list, the empty string is returned.
I tested it on my own machine with Ubuntu 22.04 and also on a Digital Ocean instance with Ubuntu 22.04:

Linux rmmenow 5.15.0-25-generic #25-Ubuntu SMP Wed Mar 30 15:54:22 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

Additional notes

The relevant part of strace looks like this:

[pid  3185] openat(AT_FDCWD, "/sys/fs/cgroup/cpuset.cpus.effective", O_RDONLY|O_CLOEXEC) = 3
[pid  3185] epoll_create1(EPOLL_CLOEXEC) = 4
[pid  3185] pipe2([5, 6], O_NONBLOCK|O_CLOEXEC) = 0
[pid  3185] epoll_ctl(4, EPOLL_CTL_ADD, 5, {events=EPOLLIN, data={u32=5665744, u64=5665744}}) = 0
[pid  3185] epoll_ctl(4, EPOLL_CTL_ADD, 3, {events=EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, data={u32=3003887392, u64=140633118060320}}) = 0
[pid  3185] fcntl(3, F_GETFL)           = 0x8000 (flags O_RDONLY|O_LARGEFILE)
[pid  3185] fcntl(3, F_SETFL, O_RDONLY|O_NONBLOCK|O_LARGEFILE) = 0
[pid  3185] fstat(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
[pid  3185] read(3, "0", 1)             = 1
[pid  3185] read(3, "\n", 7)            = 1
[pid  3185] read(3, "", 6)              = 0
[pid  3185] epoll_ctl(4, EPOLL_CTL_DEL, 3, 0xc000066ca4) = 0
[pid  3185] close(3)                    = 0
[pid  3185] write(1, "fs.ReadFile \"sys/fs/cgroup/cpuse"..., 57fs.ReadFile "sys/fs/cgroup/cpuset.cpus.effective": "0\n"
) = 57
[pid  3185] openat(AT_FDCWD, "/sys/fs/cgroup/cpuset.cpus.effective", O_RDONLY|O_CLOEXEC) = 3
[pid  3185] epoll_ctl(4, EPOLL_CTL_ADD, 3, {events=EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, data={u32=3003887392, u64=140633118060320}}) = 0
[pid  3188] futex(0xc000038948, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...>
[pid  3186] <... nanosleep resumed>NULL) = 0
[pid  3185] fcntl(3, F_GETFL)           = 0x8000 (flags O_RDONLY|O_LARGEFILE)
[pid  3185] fcntl(3, F_SETFL, O_RDONLY|O_NONBLOCK|O_LARGEFILE) = 0
[pid  3185] read(3, "0\n", 512)         = 2
[pid  3185] read(3, "", 510)            = 0
[pid  3185] write(1, "ReadAll \"sys/fs/cgroup/cpuset.cp"..., 53ReadAll "sys/fs/cgroup/cpuset.cpus.effective": "0\n"
) = 53
[pid  3185] epoll_ctl(4, EPOLL_CTL_DEL, 3, 0xc000066d8c) = 0
[pid  3185] close(3)                    = 0
[pid  3185] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/topology/thread_siblings_list", O_RDONLY|O_CLOEXEC) = 3
[pid  3185] epoll_ctl(4, EPOLL_CTL_ADD, 3, {events=EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, data={u32=3003887392, u64=140633118060320}}) = 0
[pid  3185] fcntl(3, F_GETFL)           = 0x8000 (flags O_RDONLY|O_LARGEFILE)
[pid  3185] fcntl(3, F_SETFL, O_RDONLY|O_NONBLOCK|O_LARGEFILE) = 0
[pid  3185] fstat(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
[pid  3185] read(3, "", 1)              = 0
[pid  3185] epoll_ctl(4, EPOLL_CTL_DEL, 3, 0xc000066ca4) = 0
[pid  3185] close(3)                    = 0
[pid  3185] write(1, "fs.ReadFile \"sys/devices/system/"..., 76fs.ReadFile "sys/devices/system/cpu/cpu0/topology/thread_siblings_list": ""
) = 76
[pid  3185] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/topology/thread_siblings_list", O_RDONLY|O_CLOEXEC) = 3
[pid  3185] epoll_ctl(4, EPOLL_CTL_ADD, 3, {events=EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, data={u32=3003887392, u64=140633118060320}}) = 0
[pid  3185] fcntl(3, F_GETFL)           = 0x8000 (flags O_RDONLY|O_LARGEFILE)
[pid  3185] fcntl(3, F_SETFL, O_RDONLY|O_NONBLOCK|O_LARGEFILE) = 0
[pid  3185] read(3, "0\n", 512)         = 2
[pid  3185] read(3, "", 510)            = 0
[pid  3185] write(1, "ReadAll \"sys/devices/system/cpu/"..., 75ReadAll "sys/devices/system/cpu/cpu0/topology/thread_siblings_list": "0\n"
) = 75
[pid  3185] epoll_ctl(4, EPOLL_CTL_DEL, 3, 0xc000066d8c) = 0
[pid  3185] close(3)                    = 0
[pid  3185] exit_group(0)               = ?
[pid  3187] <... futex resumed>)        = ?
[pid  3187] +++ exited with 0 +++
[pid  3186] +++ exited with 0 +++
[pid  3188] <... futex resumed>)        = ?
[pid  3188] +++ exited with 0 +++
+++ exited with 0 +++

I suspect that the culprit is probably O_NONBLOCK combined that the fact that seeing the file size of 0 in sysfs

go/src/io/fs/readfile.go

Lines 43 to 49 in 5c1a13e

var size int
if info, err := file.Stat(); err == nil {
size64 := info.Size()
if int64(int(size64)) == size64 {
size = int(size64)
}
}

fs.ReadFile tries to read just one byte from it

go/src/io/fs/readfile.go

Lines 51 to 57 in 5c1a13e

data := make([]byte, 0, size+1)
for {
if len(data) >= cap(data) {
d := append(data[:cap(data)], 0)
data = d[:len(data)]
}
n, err := file.Read(data[len(data):cap(data)])

The resulting read() syscall returns 0 (perhaps due to O_NONBLOCK?)

read(3, "", 1)                          = 0

and file.Read() returns EOF error.

This is quite possibly a Linux kernel bug, nevertheless, it may affect many Go programs, as it's quite tempting to use io/fs to make sysfs-dependent code testable.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions