Skip to content

Runner.access bypasses StatHandler for virtual filesystems #1318

Closed

Description

@amjadjibon

Summary

Runner.access() on Unix platforms directly calls unix.Access() against the host OS filesystem. When the interpreter is configured with custom handlers (StatHandler, OpenHandler, etc.) pointing to a virtual or in-memory filesystem, unix.Access() fails with ENOENT, which causes commands like cd to fail with "permission denied" even though the custom StatHandler resolves the path perfectly.
(Note: The non-Unix implementation in os_notunix.go avoids this by properly checking permission bits against r.lstat() instead of relying on OS syscalls, meaning Windows/Plan9 already work correctly with virtual filesystems).

Reproduction

package main

import (
	"context"
	"fmt"
	"io/fs"
	"os"
	"strings"
	"time"

	"mvdan.cc/sh/v3/interp"
	"mvdan.cc/sh/v3/syntax"
)

// Minimal virtual file info
type virtualDir struct {
	name string
}

func (v *virtualDir) Name() string       { return v.name }
func (v *virtualDir) Size() int64        { return 0 }
func (v *virtualDir) Mode() fs.FileMode  { return fs.ModeDir | 0o755 }
func (v *virtualDir) ModTime() time.Time { return time.Now() }
func (v *virtualDir) IsDir() bool        { return true }
func (v *virtualDir) Sys() interface{}   { return nil }

func main() {
	// Our virtual StatHandler pretends that "/hello123" exists with 0o755 permissions.
	virtualStatHandler := func(ctx context.Context, path string, followSymlinks bool) (fs.FileInfo, error) {
		if path == "/hello123" || path == "/" {
			return &virtualDir{name: path}, nil
		}
		// Fallback to real OS stat for any other files
		if !followSymlinks {
			return os.Lstat(path)
		}
		return os.Stat(path)
	}

	parser := syntax.NewParser()
	src := `
echo "Before cd: $PWD"
cd hello123 || exit 1
echo "After cd: $PWD"
`
	prog, err := parser.Parse(strings.NewReader(src), "")
	if err != nil {
		panic(err)
	}

	runner, err := interp.New(
		interp.Dir("/"),
		interp.StatHandler(virtualStatHandler),
		interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
	)
	if err != nil {
		panic(err)
	}

	err = runner.Run(context.Background(), prog)
	if err != nil {
		fmt.Printf("Run error: %v\n", err)
	}
}

Output:

❯ go run main.go
Before cd: /
cd: permission denied: "hello123"
Run error: exit status 1

Activity

  1. mvdan commented on Jun 20, 2026

    @mvdan
    Owner

    I am aware of the issue, hence the TODO in the code.

  2. added a commit that references this issue on Jul 23, 2026
    fe464d7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      ⌂ 换网址