Skip to content

Runner.access bypasses StatHandler for virtual filesystems #1318

@amjadjibon

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

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