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 }