forward-port: write size cap + crypto/rand temp suffixes in fs sandbox

Re-applies two hardening changes from the deleted pkg/tools/filesystem.go
to upstream's pkg/tools/fs/filesystem.go (sandboxFs.WriteFile):

1. Reject writes larger than MaxWriteFileSize (20 MB) before opening any
   file. Prevents runaway writes from a misbehaving caller filling disk.
2. Replace time.Now().UnixNano() temp suffixes with crypto/rand-generated
   hex. Eliminates predictable temp paths under concurrent or hostile
   filesystem scenarios; the prior nanosecond stamp could collide on
   high-resolution clocks across goroutines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
admin-mf 2026-05-07 12:16:13 -05:00
parent 94d28c1b86
commit cd1720f40b

View file

@ -4,6 +4,8 @@ import (
"bufio"
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
@ -15,14 +17,16 @@ import (
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/sipeed/picoclaw/pkg/fileutil"
"github.com/sipeed/picoclaw/pkg/logger"
)
const MaxReadFileSize = 64 * 1024 // 64KB limit to avoid context overflow
const (
MaxReadFileSize = 64 * 1024 // 64KB limit to avoid context overflow
MaxWriteFileSize = 20 * 1024 * 1024 // 20MB cap on a single write to avoid runaway writes
)
func ValidatePathWithAllowPaths(
path, workspace string,
@ -1089,6 +1093,9 @@ func (r *sandboxFs) ReadFile(path string) ([]byte, error) {
}
func (r *sandboxFs) WriteFile(path string, data []byte) error {
if len(data) > MaxWriteFileSize {
return fmt.Errorf("write rejected: %d bytes exceeds %d-byte cap", len(data), MaxWriteFileSize)
}
return r.execute(path, func(root *os.Root, relPath string) error {
dir := filepath.Dir(relPath)
if dir != "." && dir != "/" {
@ -1099,7 +1106,13 @@ func (r *sandboxFs) WriteFile(path string, data []byte) error {
// Use atomic write pattern with explicit sync for flash storage reliability.
// Using 0o600 (owner read/write only) for secure default permissions.
tmpRelPath := fmt.Sprintf(".tmp-%d-%d", os.Getpid(), time.Now().UnixNano())
// Random suffix prevents collisions and avoids predictable temp names that
// could be exploited in concurrent or hostile filesystem scenarios.
var rnd [8]byte
if _, err := rand.Read(rnd[:]); err != nil {
return fmt.Errorf("failed to generate temp file suffix: %w", err)
}
tmpRelPath := fmt.Sprintf(".tmp-%d-%s", os.Getpid(), hex.EncodeToString(rnd[:]))
tmpFile, err := root.OpenFile(tmpRelPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {