O_DIRECTORY|O_NOFOLLOW follows a symlink to a directory
Author: dany74qCreated Sep 16, 2026Updated Sep 16, 2026
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// 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)
}
}Source: google/gvisor