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

  1. Inspect the panic value in the wrapped error to find the panicking fs.FS implementation and fix its bug
  2. Guard plugin-provided FS state for concurrent access (mutex) or stop sharing it across goroutines
  3. Ensure the source FS passed to the plugin filesystem is non-nil and properly initialized
  4. 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

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


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/333c4e85eff865fd. Report an issue: GitHub.