O_DIRECTORY|O_NOFOLLOW follows a symlink to a directory
- 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
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