Skip to content
Draft
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
13 changes: 13 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ jobs:
- pkg: ./builtins/tests/ps/
name: ps
corpus_path: builtins/tests/ps
- pkg: ./builtins/df/
name: df
# df fuzz tests live in builtins/df/ (not builtins/tests/df/)
# because the test-helper functions (firstLine, requireSupported)
# are defined in df_test.go and only visible to files in the
# same directory.
corpus_path: builtins/df
- pkg: ./builtins/internal/diskstats/
name: diskstats
# The mountinfo parser is the most security-sensitive parser
# in df. Fuzzing it directly is much faster than going
# through the shell runner.
corpus_path: builtins/internal/diskstats
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ The shell is supported on Linux, Windows and macOS.

- **`ss` and `ip route` bypass `AllowedPaths` for `/proc/net/*` reads.** Both builtins delegate `/proc/net/` I/O to internal packages (`builtins/internal/procnetsocket` for `ss`, `builtins/internal/procnetroute` for `ip route`) that call `os.Open` directly on kernel pseudo-filesystem paths (e.g. `/proc/net/tcp`, `/proc/net/route`). These paths are hardcoded in the implementation and are never derived from user input, so `AllowedPaths` restrictions do not apply to them. As a consequence, operators cannot use `AllowedPaths` to block `ss` from enumerating local sockets or `ip route` from reading the routing table. This is an intentional trade-off: the paths are non-user-controllable, so there is no sandbox-escape risk, but the operator loses the ability to deny these reads via sandbox configuration.

- **`df` bypasses `AllowedPaths` for mount-table enumeration.** `df` delegates filesystem listing to `builtins/internal/diskstats`, which on Linux reads `/proc/self/mountinfo` directly via `os.Open` and then calls `unix.Statfs(2)` on every mount point returned by the kernel. On macOS it calls `unix.Getfsstat(2)`. The mount-point paths are kernel-controlled β€” never derived from user input β€” so the same trade-off as `ss` / `ip route` applies: operators cannot use `AllowedPaths` to hide individual mounts from `df`. `Statfs` returns metadata only (block / inode counts, filesystem type, block size); no file content is read.
Comment thread
julesmcrt marked this conversation as resolved.

## CRITICAL: Bug Fixes and Bash Compatibility

- **ALWAYS prioritise fixing the shell implementation to match bash behaviour over changing tests to match the current (incorrect) shell output.** Never "fix" a failing test by updating its expected output to match broken shell behaviour β€” fix the shell instead.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Every access path is default-deny:

**AllowedPaths** restricts all file operations to specified directories using Go's `os.Root` API (`openat` syscalls), making it immune to symlink traversal, TOCTOU races, and `..` escape attacks. Configured directories that cannot be opened (missing, not a directory, no permission) are skipped with a diagnostic message; by default these messages are flushed once to the runner's stderr at construction time. Callers that need to keep stderr clean of sandbox diagnostics can route them to a dedicated sink with `WarningsWriter(io.Writer)` or retrieve them programmatically via `Runner.Warnings()`.

> **Note:** The `ss` and `ip route` builtins bypass `AllowedPaths` for their `/proc/net/*` reads. Both builtins open kernel pseudo-filesystem paths (e.g. `/proc/net/tcp`, `/proc/net/route`) directly with `os.Open` rather than going through the sandboxed opener. These paths are hardcoded in the implementation and are never derived from user input, so there is no sandbox-escape risk. However, operators cannot use `AllowedPaths` to block `ss` from enumerating local sockets or `ip route` from reading the routing table β€” these reads succeed regardless of the configured path policy.
> **Note:** The `ss`, `ip route`, and `df` builtins bypass `AllowedPaths` for their kernel-state reads. `ss` and `ip route` open `/proc/net/*` paths directly; `df` reads `/proc/self/mountinfo` (Linux) or calls `getfsstat(2)` (macOS), then issues `unix.Statfs(2)` against every kernel-reported mount point. These paths are hardcoded β€” never derived from user input β€” and `Statfs` returns metadata only (block / inode counts, filesystem type, block size). There is no sandbox-escape risk, but operators cannot use `AllowedPaths` to block `ss` from enumerating local sockets, `ip route` from reading the routing table, or `df` from reporting mount-table capacity β€” these reads succeed regardless of the configured path policy.

**ProcPath** (Linux-only) overrides the proc filesystem root used by the `ps` builtin (default `/proc`). This is a privileged option set at runner construction time by trusted caller code β€” scripts cannot influence it. Access to the proc path is intentionally not subject to `AllowedPaths` restrictions, since proc is a read-only virtual filesystem that does not expose host data under the normal file hierarchy.

Expand Down
1 change: 1 addition & 0 deletions SHELL_FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Blocked features are rejected before execution with exit code 2.
- βœ… `cat [-AbeEnstTuv] [FILE]...` β€” concatenate files to stdout; supports line numbering, blank squeezing, and non-printing character display
- βœ… `continue` β€” skip to the next iteration of the innermost `for` loop
- βœ… `cut [-b LIST|-c LIST|-f LIST] [-d DELIM] [-s] [-n] [--complement] [--output-delimiter=STRING] [FILE]...` β€” remove sections from each line of files
- βœ… `df [-hHkPTialx] [-t TYPE] [-x TYPE] [--total] [--no-sync]` β€” report file system disk space usage (Linux/macOS; Linux reads `/proc/self/mountinfo` directly via `os.Open`, bypassing `AllowedPaths`); positional `FILE` operands and `--sync`, `-B`, `--output` are not supported; mount table capped at 100 000 entries
- βœ… `echo [-neE] [ARG]...` β€” write arguments to stdout; `-n` suppresses trailing newline, `-e` enables backslash escapes, `-E` disables them (default)
- βœ… `exit [N]` β€” exit the shell with status N (default 0)
- βœ… `false` β€” return exit code 1
Expand Down
16 changes: 16 additions & 0 deletions analysis/symbols_builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ var builtinPerCommandSymbols = map[string][]string{
"strings.IndexByte", // 🟒 finds byte in string; pure function, no I/O.
"strings.Split", // 🟒 splits a string by separator into a slice; pure function, no I/O.
},
"df": {
"context.Context", // 🟒 deadline/cancellation plumbing; pure interface, no side effects.
"errors.Is", // 🟒 error comparison via chain; pure function, no I/O.
"fmt.Sprintf", // 🟒 string formatting; pure function, no I/O.
"math.Ceil", // 🟒 ceiling of a float64; pure function, no I/O. Used for GNU-compatible round-up of human-readable sizes.
"sort.Slice", // 🟒 in-place slice sort with comparison func; pure function, no I/O.
"strconv.FormatUint", // 🟒 uint-to-string conversion; pure function, no I/O.
"strings.Builder", // 🟒 efficient string concatenation; pure in-memory buffer, no I/O.
"strings.Join", // 🟒 joins string slices; pure function, no I/O.
"strings.Repeat", // 🟒 returns a string of n repetitions; pure function, no I/O.
// Note: builtins/internal/diskstats symbols are exempt from this
// allowlist (internal packages are not checked by the
// builtinAllowedSymbols test).
},
"echo": {
"context.Context", // 🟒 deadline/cancellation plumbing; pure interface, no side effects.
"strings.Builder", // 🟒 efficient string concatenation; pure in-memory buffer, no I/O.
Expand Down Expand Up @@ -482,6 +496,8 @@ var builtinAllowedSymbols = []string{
"slices.Reverse", // 🟒 reverses a slice in-place; pure function, no I/O.
"slices.SortFunc", // 🟒 sorts a slice with a comparison function; pure function, no I/O.
"slices.SortStableFunc", // 🟒 stable sort with a comparison function; pure function, no I/O.
"sort.Slice", // 🟒 in-place slice sort with a comparison function; pure function, no I/O.
"strings.Repeat", // 🟒 returns a string of n repetitions; pure function, no I/O.
"strconv.Atoi", // 🟒 string-to-int conversion; pure function, no I/O.
"strconv.ErrRange", // 🟒 sentinel error value for overflow; pure constant.
"strconv.FormatInt", // 🟒 int-to-string conversion; pure function, no I/O.
Expand Down
Loading
Loading