#14786·gvisor

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
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)
	}
}