Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ jobs:
# ping_test.go. Go test helpers are only in scope within the same directory,
# so both files must reside in builtins/ping/.
corpus_path: builtins/ping
- pkg: ./builtins/pwd/
name: pwd
# pwd fuzz tests live in builtins/pwd/ alongside the pwd_test.go
# helpers (pwdRun) — same rationale as ping above.
corpus_path: builtins/pwd
- pkg: ./interp/tests/
name: interp
corpus_path: interp/tests
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
**/.claude/settings.local.json
.claude/scheduled_tasks.lock
.claude/worktrees/
.superset/

Expand Down
1 change: 1 addition & 0 deletions SHELL_FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Blocked features are rejected before execution with exit code 2.
- ✅ `ping [-c N] [-W DURATION] [-i DURATION] [-q] [-4|-6] [-h] HOST` — send ICMP echo requests to a network host and report round-trip statistics; `-f` (flood), `-b` (broadcast), `-s` (packet size), `-I` (interface), `-p` (pattern), and `-R` (record route) are blocked; count/wait/interval are clamped to safe ranges with a warning; multicast, unspecified (`0.0.0.0`/`::`), and broadcast addresses (IPv4 last-octet `.255`) are rejected — note: directed broadcasts on non-standard subnets (e.g. `.127` on a `/25`) are not blocked without subnet-mask knowledge
- ✅ `ps [-e|-A] [-f] [-p PIDLIST]` — report process status; default shows current-session processes; `-e`/`-A` shows all; `-f` adds UID/PPID/STIME columns; `-p` selects by PID list
- ✅ `printf FORMAT [ARGUMENT]...` — format and print data to stdout; supports `%s`, `%b`, `%c`, `%d`, `%i`, `%o`, `%u`, `%x`, `%X`, `%e`, `%E`, `%f`, `%F`, `%g`, `%G`, `%%`; format reuse for excess arguments; `%n` rejected (security risk); `-v` rejected
- ✅ `pwd [-LP]` — print the absolute pathname of the current working directory; `-L` (default) prints the shell's tracked logical path, `-P` resolves all symlinks; `-P` is best-effort within the sandbox (path components above `AllowedPaths` pass through unresolved); `--version` rejected
- ✅ `sed [-n] [-e SCRIPT] [-E|-r] [SCRIPT] [FILE]...` — stream editor for filtering and transforming text; uses RE2 regex engine; `-i`/`-f` rejected; `e`/`w`/`W`/`r`/`R` commands blocked
- ✅ `strings [-a] [-n MIN] [-t o|d|x] [-o] [-f] [-s SEP] [FILE]...` — print printable character sequences in files (default min length 4); offsets via `-t`/`-o`; filename prefix via `-f`; custom separator via `-s`
- ✅ `tail [-n N|-c N] [-q|-v] [-z] [FILE]...` — output the last part of files (default: last 10 lines); supports `+N` offset mode; `-f`/`--follow` is rejected
Expand Down
61 changes: 58 additions & 3 deletions allowedpaths/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,17 @@ const (
const MaxGlobEntries = 10_000

// root pairs an absolute directory path with its opened os.Root handle.
//
// canonicalAbsPath is the symlink-resolved form of absPath (computed
// via filepath.EvalSymlinks at sandbox-setup time). It equals absPath
// when absPath is not a symlink. Builtins that compute canonical
// paths (e.g. pwd -P) use this to translate the configured-root
// prefix back to its on-disk canonical form, which os.Root has
// already followed implicitly when opening the root.
type root struct {
absPath string
root *os.Root
absPath string
canonicalAbsPath string
root *os.Root
}

// Sandbox restricts filesystem access to a set of allowed directories.
Expand Down Expand Up @@ -68,7 +76,18 @@ func New(paths []string) (sb *Sandbox, warnings []byte, err error) {
fmt.Fprintf(&buf, "AllowedPaths: skipping %q: %v\n", abs, err)
continue
}
roots = append(roots, root{absPath: abs, root: r})
// Record the canonical (symlink-resolved) form of the configured
// root. os.OpenRoot already follows symlinks at the path itself,
// so the opened handle observes the target directory; we capture
// that resolution here so builtins like `pwd -P` can translate
// the configured-root prefix back to its canonical form.
// EvalSymlinks failure is not fatal — fall back to the configured
// path, matching prior behavior.
canonical, evalErr := filepath.EvalSymlinks(abs)
if evalErr != nil {
canonical = abs
}
roots = append(roots, root{absPath: abs, canonicalAbsPath: canonical, root: r})
}
return &Sandbox{roots: roots}, buf.Bytes(), nil
}
Expand Down Expand Up @@ -623,6 +642,42 @@ func (s *Sandbox) HostPrefix() string {
return s.hostPrefix
}

// CanonicalizeRootPrefix returns absPath with any matching sandbox-root
// prefix replaced by that root's canonical (symlink-resolved) form. If
// absPath is outside every root, or its containing root is not a
// symlink, the input is returned unchanged.
//
// Use case: builtins like `pwd -P` walk symlinks within the sandbox
// via callCtx.LstatFile/ReadlinkFile, but the *root itself* may be a
// symlink (e.g. AllowedPaths=/tmp/link with /tmp/link -> /tmp/real).
// os.OpenRoot follows the root symlink at open time, so per-component
// LstatFile cannot detect it. This helper applies the missing
// translation by mapping the configured-root prefix to the canonical
// one captured at New() time.
func (s *Sandbox) CanonicalizeRootPrefix(absPath string) string {
if s == nil {
return absPath
}
for i := range s.roots {
r := &s.roots[i]
if r.canonicalAbsPath == "" || r.canonicalAbsPath == r.absPath {
continue
}
rel, err := filepath.Rel(r.absPath, absPath)
if err != nil {
continue
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
continue
}
if rel == "." {
return r.canonicalAbsPath
}
return filepath.Join(r.canonicalAbsPath, rel)
}
return absPath
}

// Paths returns the resolved absolute paths of all allowed directories.
func (s *Sandbox) Paths() []string {
if s == nil {
Expand Down
1 change: 1 addition & 0 deletions analysis/symbols_allowedpaths.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ var allowedpathsAllowedSymbols = []string{
"path/filepath.Abs", // 🟢 returns absolute path; pure path computation.
"path/filepath.Clean", // 🟢 normalizes a path; pure function, no I/O.
"path/filepath.Dir", // 🟢 returns directory portion of a path; pure function, no I/O.
"path/filepath.EvalSymlinks", // 🟠 resolves symlinks via os.Lstat; the sandbox uses this at setup time to record canonical root paths so builtins like `pwd -P` can reflect the symlink resolution that os.Root has implicitly followed.
"path/filepath.IsAbs", // 🟢 checks if path is absolute; pure function, no I/O.
"path/filepath.Join", // 🟢 joins path elements; pure function, no I/O.
"path/filepath.Rel", // 🟢 returns relative path; pure path computation.
Expand Down
21 changes: 21 additions & 0 deletions analysis/symbols_builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,22 @@ var builtinPerCommandSymbols = map[string][]string{
"strings.ReplaceAll", // 🟢 replaces all occurrences of a substring; pure function, no I/O.
"strings.ToLower", // 🟢 converts string to lowercase; pure function, no I/O.
},
"pwd": {
"context.Context", // 🟢 deadline/cancellation plumbing; pure interface, no side effects.
"errors.Is", // 🟢 error comparison; pure function, no I/O.
"errors.New", // 🟢 creates a simple error value; pure function, no I/O.
"fmt.Errorf", // 🟢 error formatting; pure function, no I/O.
"io/fs.ModeSymlink", // 🟢 file mode bit constant for symlinks; pure constant.
"path/filepath.Clean", // 🟢 normalizes a path lexically (collapses ".", "..", duplicate separators); pure function, no I/O.
"path/filepath.Dir", // 🟢 returns the directory component of a path; pure function, no I/O.
"path/filepath.IsAbs", // 🟢 reports whether a path is absolute; pure function, no I/O.
"path/filepath.Join", // 🟢 lexically joins path components with the OS separator; pure function, no I/O.
"path/filepath.Separator", // 🟢 OS path separator constant ('/' or '\\'); pure constant, no I/O.
"path/filepath.VolumeName", // 🟢 returns the volume prefix of a path (e.g. "C:" on Windows, "" on Unix); pure function, no I/O.
"strings.HasPrefix", // 🟢 pure function for prefix matching; no I/O.
"strings.IndexByte", // 🟢 finds byte in string; pure function, no I/O.
"strings.TrimPrefix", // 🟢 removes a leading prefix from a string; pure function, no I/O.
},
"sort": {
"bufio.NewScanner", // 🟢 line-by-line input reading (e.g. head, cat); no write or exec capability.
"context.Context", // 🟢 deadline/cancellation plumbing; pure interface, no side effects.
Expand Down Expand Up @@ -472,9 +488,13 @@ var builtinAllowedSymbols = []string{
"os.IsNotExist", // 🟢 checks if error is "not exist"; pure function, no I/O.
"os.O_RDONLY", // 🟢 read-only file flag constant; cannot open files by itself.
"os.PathError", // 🟢 error type for filesystem path errors; pure type, no I/O.
"path/filepath.Clean", // 🟢 normalizes a path lexically (collapses ".", "..", duplicate separators); pure function, no I/O.
"path/filepath.Dir", // 🟢 returns the directory component of a path; pure function, no I/O.
"path/filepath.IsAbs", // 🟢 reports whether a path is absolute; pure function, no I/O.
"path/filepath.Join", // 🟢 lexically joins path components with the OS separator; pure function, no I/O.
"path/filepath.Separator", // 🟢 OS path separator constant ('/' or '\\'); pure constant, no I/O.
"path/filepath.ToSlash", // 🟢 converts OS path separators to forward slashes; pure function, no I/O.
"path/filepath.VolumeName", // 🟢 returns the volume prefix of a path (e.g. "C:" on Windows, "" on Unix); pure function, no I/O.
"regexp.Compile", // 🟢 compiles a regular expression; pure function, no I/O. Uses RE2 engine (linear-time, no backtracking).
"regexp.QuoteMeta", // 🟢 escapes all special regex characters in a string; pure function, no I/O.
"regexp.Regexp", // 🟢 compiled regular expression type; no I/O side effects. All matching methods are linear-time (RE2).
Expand Down Expand Up @@ -503,6 +523,7 @@ var builtinAllowedSymbols = []string{
"strings.ReplaceAll", // 🟢 replaces all occurrences of a substring; pure function, no I/O.
"strings.Split", // 🟢 splits a string by separator into a slice; pure function, no I/O.
"strings.ToLower", // 🟢 converts string to lowercase; pure function, no I/O.
"strings.TrimPrefix", // 🟢 removes a leading prefix from a string; pure function, no I/O.
"strings.TrimSpace", // 🟢 removes leading/trailing whitespace; pure function.
"syscall.ByHandleFileInformation", // 🟢 Windows file info struct for extracting nlink; read-only type, no I/O.
"syscall.EACCES", // 🟢 POSIX errno constant for permission denied; pure constant, no I/O.
Expand Down
18 changes: 18 additions & 0 deletions builtins/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,24 @@ type CallContext struct {
// Used by builtins that need to compute absolute paths for sub-operations.
WorkDir func() string

// HostPrefix returns the configured host-mount prefix used by
// container-style sandboxes to translate host-absolute paths
// (e.g. /var/log/pods/...) into the prefixed paths the sandbox can
// open (e.g. /mnt/host/var/log/pods/...). Returns "" when no prefix
// is configured. Builtins that resolve absolute symlink targets
// (e.g. pwd -P) use this to keep their output consistent with what
// the sandbox itself accepts.
HostPrefix func() string

// CanonicalizeRootPrefix translates a configured AllowedPaths root
// prefix in absPath to that root's canonical (symlink-resolved)
// form. Used by `pwd -P` so that when the sandbox root is itself a
// symlink (e.g. /tmp/link -> /tmp/real), the printed path reflects
// the resolution that os.Root has already followed implicitly. If
// absPath is outside every root or the matching root is not a
// symlink, the input is returned unchanged.
CanonicalizeRootPrefix func(absPath string) string

// RunCommand executes a builtin command within the shell's sandbox.
// dir overrides the working directory for path resolution.
// Returns the command's exit code.
Expand Down
Loading
Loading