Skip to content

os: Root.OpenRoot sets incorrect name, losing prefix of original root #73868

Open
@nkraetzschmar

Description

@nkraetzschmar

Calling OpenRoot on a *os.Root sets the resulting root's name to the relative component only, rather than preserving the full path including the original root. This leads to incorrect behavior in downstream operations that rely on the Name() of the root, particularly when constructing paths for syscalls.

Minimal example

The following program fails despite the directory structure existing as expected:

package main

import (
	"fmt"
	"os"
)

func main() {
	rootDirA, err := os.OpenRoot("a")
	if err != nil {
		panic(err)
	}

	rootDirB, err := rootDirA.OpenRoot("b")
	if err != nil {
		panic(err)
	}

	dirC, err := rootDirB.Open("c")
	if err != nil {
		panic(err)
	}

	entries, err := dirC.ReadDir(-1)
	if err != nil {
		panic(err)
	}

	for _, entry := range entries {
		info, err := entry.Info()
		if err != nil {
			panic(err)
		}

		fmt.Printf("%s\n", info)
	}
}

Directory structure:

a/
└── b/
    └── c/
        └── test

Runtime panic:

panic: lstat b/c/test: no such file or directory

goroutine 1 [running]:
main.main()
	/mnt/main.go:32 +0x108
exit status 2

Root cause analysis

The function (d *unixDirent) Info() at os/file_unix.go:480 performs:

lstat(d.parent + "/" + d.name)

At this point, d.parent == "b/c", which is incorrect. It should be "a/b/c".

Tracing this back:

rootOpenFileNolog (at os/root_unix.go:92) sets the correct name by doing:

newFile(fd, joinPath(root.Name(), name), kindOpenFile, unix.HasNonblockFlag(flag))

But openRootInRoot (at os/root_unix.go:74) does:

newRoot(fd, name)

Where name is the name of the directory inside the root, so in this case name == "b".
The prefix "a" from the original root is dropped when opening "b" under it, and all nested paths become relative to a truncated root.

For most operations this issue remains unnoticed, since they operate on directory file descriptors instead of paths and are thus unaffected. Only once the unixDirent.Info() uses this path for the lstat syscall does this bug result in unexpected behaviour.

Impact

In this minimal example the potential impact is obviously relatively small since it's doing an lstat only, but depending on where else the name property of an os.Root is used this may even lead to unintended root path escapes.

Proposed Fix

In openRootInRoot, replace:

return newRoot(fd, name)

with:

return newRoot(fd, joinPath(r.Name(), name))

This ensures that Root.Name() always reflects the full path, and any derived File or DirEntry will operate on the correct base.


go version and go env:

go version go1.24.3 linux/arm64

AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/root/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/root/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2623320160=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='arm64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/root/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='local'
GOTOOLDIR='/usr/local/go/pkg/tool/linux_arm64'
GOVCS=''
GOVERSION='go1.24.3'
GOWORK=''
PKG_CONFIG='pkg-config'

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions