google / google/gvisor

O_DIRECTORY|O_NOFOLLOW follows a symlink to a directory

Open
#14,786 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
19.3k
Forks
2k
Avg merge
3d 5h
Merged PRs (30d)
264

Description

ShouldFollowSymlink follows the final symlink whenever mustBeDir is set (resolving_path.go:461), so O_DIRECTORY|O_NOFOLLOW on a symlink to a directory opens the target where Linux fails with ENOTDIR.

Go's os.Root walks with that flag pair, so an os.Root inside the sandbox can be walked out of.

reproducer

```go
// Run as root, once per runtime:
//
// go build -o odirnofollow odirnofollow.go
// sudo runsc --network=none --ignore-cgroups --platform=systrap do $PWD/odirnofollow
// ./odirnofollow
package main

import (
"fmt"
"os"
"path/filepath"
"syscall"
)

const call = `openat(dirfd, "link", O_RDONLY|O_DIRECTORY|O_NOFOLLOW)`

func main() {
root, err := os.MkdirTemp("", "odirnofollow-")
must(err)
defer os.RemoveAll(root)
target := filepath.Join(root, "target")
must(os.Mkdir(target, 0o755))
must(os.Symlink(target, filepath.Join(root, "link")))
dirfd, err := syscall.Open(root, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_CLOEXEC, 0)
must(err)
var targetStat syscall.Stat_t
must(syscall.Stat(target, &targetStat))

flags := syscall.O_RDONLY | syscall.O_DIRECTORY | syscall.O_NOFOLLOW | syscall.O_CLOEXEC
fd, err := syscall.Openat(dirfd, "link", flags, 0)
if err != nil {
fmt.Printf("%s: refused with %s (errno %d)\n", call, errnoName(err), int(err.(syscall.Errno)))
return
}
var st syscall.Stat_t
must(syscall.Fstat(fd, &st))
sameInode := st.Dev == targetStat.Dev && st.Ino == targetStat.Ino
fmt.Printf("%s: opened fd %d, fstat ino=%d, it is the symlink target directory: %t\n", call, fd, st.Ino, sameInode)
}

func errnoName(err error) string {
switch err {
case syscall.ENOTDIR:
return "ENOTDIR"
case syscall.ELOOP:
return "ELOOP"
}
return err.Error()
}

func must(err error) {
if err != nil {
fmt.Println("setup failed:", err)
os.Exit(1)
}
}
```

Contributor guide

Open the contributing guide

Research direction

Start at ShouldFollowSymlink in resolving_path.go:461 and compare its handling of mustBeDir with Linux's O_DIRECTORY|O_NOFOLLOW behavior. Run the supplied Go reproducer under runsc to confirm the symlink-to-directory case. Done means the flag combination refuses the final symlink with the expected failure instead of opening its target, preserving os.Root sandbox boundaries.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, linux
Domain
operating-systems, security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.