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
Summary
Runner.access()on Unix platforms directly callsunix.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 withENOENT, which causes commands likecdto fail with "permission denied" even though the customStatHandlerresolves the path perfectly.(Note: The non-Unix implementation in
os_notunix.goavoids this by properly checking permission bits againstr.lstat()instead of relying on OS syscalls, meaning Windows/Plan9 already work correctly with virtual filesystems).Reproduction
Output: