larksuite/cli · error
panic: %v
Error message
panic: %v
What it means
pluginFS.Stat wraps the underlying fs.Stat in a recover() so a panic inside the source filesystem (e.g. a plugin-provided fs.FS misbehaving) is converted into a normal error via p.pathError("stat", name, ...) with message "panic: %v". The library throws/captures this to keep a panicking plugin filesystem from crashing the host process, surfacing it as a path-scoped error instead.
Source
Thrown at internal/skillpolicy/pluginfs.go:58
file, err = p.source.Open(name)
if err != nil {
return nil, p.pathError("open", name, err)
}
if file == nil {
return nil, p.pathError("open", name, errorsNilResult)
}
safe := &pluginFile{fsys: p, path: name, source: file}
if dir, ok := file.(fs.ReadDirFile); ok {
return &pluginReadDirFile{pluginFile: safe, dir: dir}, nil
}
return safe, nil
}
func (p *pluginFS) Stat(name string) (info fs.FileInfo, err error) {
defer func() {
if value := recover(); value != nil {
info = nil
err = p.pathError("stat", name, fmt.Errorf("panic: %v", value))
}
}()
info, err = fs.Stat(p.source, name)
if err != nil {
return nil, p.pathError("stat", name, err)
}
if info == nil {
return nil, p.pathError("stat", name, errorsNilResult)
}
return snapshotFileInfo(info), nil
}
func (p *pluginFS) ReadFile(name string) (data []byte, err error) {
defer p.recoverPath("readfile", name, &err)
data, err = fs.ReadFile(p.source, name)
if err != nil {
return nil, p.pathError("readfile", name, err)
}View on GitHub (pinned to 7fd6ef3c07)
Solutions
- Inspect the panic value in the wrapped error to find the panicking fs.FS implementation and fix its bug
- Guard plugin-provided FS state for concurrent access (mutex) or stop sharing it across goroutines
- Ensure the source FS passed to the plugin filesystem is non-nil and properly initialized
- Reproduce the Stat call against the raw source FS to capture the full panic stack
Example fix
// before (plugin FS with data race panics on Stat)
func (f *myFS) Stat(name string) (fs.FileInfo, error) { return f.cache[name].info, nil } // nil map entry panic
// after
func (f *myFS) Stat(name string) (fs.FileInfo, error) {
f.mu.Lock(); defer f.mu.Unlock()
e, ok := f.cache[name]
if !ok { return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrNotExist} }
return e.info, nil
} Defensive patterns
Strategy: try-catch
Type guard
// narrow the wrapped path error before handling
func asPathError(err error) (*fs.PathError, bool) {
var pe *fs.PathError
if errors.As(err, &pe) { return pe, true }
return nil, false
} Try / catch
info, err := pfs.Stat(name)
if err != nil {
if strings.HasPrefix(errors.Unwrap(err).Error(), "panic:") {
log.Printf("plugin FS panicked on stat %s: %v — failing over to direct source FS", name, err)
return fallbackFS.Stat(name)
}
return info, err
} Prevention
- Fuzz/panic-test custom fs.FS implementations before shipping them as plugins
- Protect shared FS state with mutexes; never share unsynchronized caches across goroutines
- Initialize the source FS eagerly so Stat cannot hit nil state
- Keep the recover-and-wrap pattern in plugin boundary code and log the panic value with a stack trace where possible
When it happens
Trigger: The underlying fs.FS implementation passed to pluginFS panics during Stat — typically a nil-map or index-out-of-range bug, or nil receiver, inside a plugin-supplied or faulty fstab/embed implementation, for the requested path name.
Common situations: A third-party or hand-written fs.FS plugin with an internal bug; concurrency unsafety where two goroutines mutate shared state during Stat; a nil source FS wired into the plugin filesystem; corrupted lazy-initialized caches in a virtual FS.
Related errors
- cannot read SKILL.md: %w
- invalid chart size: {size!r}
- No visible worksheet matched
- +csv-get truncated the scan range at {actual_range}; narrow
- --max-sheets must be at least 1
AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04).
Data as JSON: /api/errors/333c4e85eff865fd.
Report an issue: GitHub.