100 lines
2.6 KiB
Go
100 lines
2.6 KiB
Go
package exec
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"sync"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
|
)
|
|
|
|
// FileSandbox confines file read, write, and list access to a set of allowed
|
|
// root directories (DevDocs/design/04-运行时.md §8, design/11 §5). Every path is
|
|
// validated by [Resolve] before use, which rejects absolute paths outside the
|
|
// roots, relative paths that escape them, and symlinks that resolve outside
|
|
// them.
|
|
type FileSandbox struct {
|
|
mu sync.RWMutex
|
|
roots []string
|
|
log *slog.Logger
|
|
}
|
|
|
|
// NewFileSandbox builds a FileSandbox confined to roots. Raw roots are made
|
|
// absolute and cleaned. The logger is required.
|
|
func NewFileSandbox(roots []string, log *slog.Logger) (*FileSandbox, error) {
|
|
if log == nil {
|
|
return nil, ErrNilLogger
|
|
}
|
|
return &FileSandbox{
|
|
roots: absCleaners(roots),
|
|
log: logger.WithSource(log, "runtime/exec"),
|
|
}, nil
|
|
}
|
|
|
|
// SetRoots atomically replaces the allowed roots. It enables a live config
|
|
// reload without racing in-flight Resolve calls.
|
|
func (s *FileSandbox) SetRoots(roots []string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.roots = absCleaners(roots)
|
|
}
|
|
|
|
// Resolve validates path against the current allowed roots and returns its
|
|
// absolute, symlink-resolved location, or an error if it would escape. A
|
|
// relative path is resolved against the first allowed root.
|
|
func (s *FileSandbox) Resolve(path string) (string, error) {
|
|
roots := s.rootsLocked()
|
|
if len(roots) == 0 {
|
|
return "", ErrNoAllowedRoot
|
|
}
|
|
return resolveInsideRoot(path, roots)
|
|
}
|
|
|
|
// ReadFile reads a file confined to the allowed roots.
|
|
func (s *FileSandbox) ReadFile(path string) ([]byte, error) {
|
|
abs, err := s.Resolve(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := os.ReadFile(abs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("exec: read %s: %w", abs, err)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// WriteFile writes data to a path confined to the allowed roots.
|
|
func (s *FileSandbox) WriteFile(path string, data []byte, perm os.FileMode) error {
|
|
abs, err := s.Resolve(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(abs, data, perm); err != nil {
|
|
return fmt.Errorf("exec: write %s: %w", abs, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// List returns the directory entries of a directory confined to the allowed
|
|
// roots.
|
|
func (s *FileSandbox) List(dir string) ([]os.DirEntry, error) {
|
|
abs, err := s.Resolve(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entries, err := os.ReadDir(abs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("exec: list %s: %w", abs, err)
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
func (s *FileSandbox) rootsLocked() []string {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
roots := make([]string, len(s.roots))
|
|
copy(roots, s.roots)
|
|
return roots
|
|
}
|