Documentation
¶
Overview ¶
Package interp implements a restricted shell interpreter designed for safe, sandboxed execution. It supports a subset of Bash syntax with many features intentionally blocked (see [validateNode]).
The interpreter behaves like a non-interactive shell. External command execution and filesystem access are denied by default and must be explicitly enabled via RunnerOption functions.
Index ¶
Constants ¶
const MaxGlobReadDirCalls = 10_000
MaxGlobReadDirCalls is the maximum number of ReadDirForGlob invocations allowed per Run() call. This prevents memory exhaustion from scripts that trigger an excessive number of glob expansions (e.g. millions of unquoted * tokens, or deeply nested glob patterns in loops).
const MaxHeredocBytes = 10 << 20 // 10 MiB
MaxHeredocBytes is the maximum size of a heredoc body in bytes. Heredocs exceeding this limit are rejected to prevent memory exhaustion.
const MaxScriptBytes = 5 * 1024 * 1024 // 5 MiB
MaxScriptBytes is the maximum allowed byte length of a shell script passed to ParseScript. Scripts larger than this are rejected before parsing to prevent the parser from allocating unbounded memory. Unlike other per-input limits (variables, command substitution, per-line builtins) this cap is enforced at the API boundary rather than inside the interpreter, because the interpreter only receives the pre-parsed AST.
const MaxTotalVarsBytes = 1 << 20 // 1 MiB
MaxTotalVarsBytes is the maximum total size in bytes of all variable values combined. Assignments that would push the total over this limit are rejected.
const MaxVarBytes = 1 << 20 // 1 MiB
MaxVarBytes is the maximum size in bytes of a single variable value. Assignments that exceed this limit are rejected with an error.
Variables ¶
var ErrOutputLimitExceeded = errors.New(fmt.Sprintf(
"stdout limit exceeded: script produced more than %d MiB of output",
maxStdoutBytes/(1024*1024),
))
ErrOutputLimitExceeded is returned by Run when a script produces more stdout than maxStdoutBytes. Partial output up to the limit is still delivered to the caller's writer. Use errors.Is to check for this condition.
Functions ¶
func ParseScript ¶ added in v0.0.8
ParseScript parses script as a shell program and returns the resulting AST. It enforces MaxScriptBytes: if len(script) exceeds that limit the call returns an error immediately, before the parser allocates any memory.
name is an optional filename used in parse-error messages (pass "" if there is no associated file).
Library callers should use ParseScript rather than calling the underlying syntax parser directly so that the size limit is consistently enforced.
Types ¶
type ExecHandlerFunc ¶
ExecHandlerFunc is a handler which executes simple commands. It is called for all syntax.CallExpr nodes where the first argument is not a builtin.
Returning a nil error means a zero exit status. Other exit statuses can be set by returning or wrapping an ExitStatus error, and such an error is returned via the API if it is the last statement executed. Any other error will halt the Runner and will be returned via the API.
type ExitStatus ¶
type ExitStatus uint8
ExitStatus is a non-zero status code resulting from running a shell node.
func (ExitStatus) Error ¶
func (s ExitStatus) Error() string
type HandlerContext ¶
type HandlerContext struct {
// Env is a read-only version of the interpreter's environment,
// including environment variables and global variables.
Env expand.Environ
// Dir is the interpreter's current directory.
Dir string
// Pos is the source position which relates to the operation,
// such as a [syntax.CallExpr] when calling an [ExecHandlerFunc].
// It may be invalid if the operation has no relevant position information.
Pos syntax.Pos
// Stdin is the interpreter's current standard input reader.
// It is always an [*os.File], but the type here remains an [io.Reader]
// due to backwards compatibility.
Stdin io.Reader
// Stdout is the interpreter's current standard output writer.
Stdout io.Writer
// Stderr is the interpreter's current standard error writer.
Stderr io.Writer
}
HandlerContext is the data passed to all the handler functions via context.WithValue. It contains some of the current state of the Runner.
func HandlerCtx ¶
func HandlerCtx(ctx context.Context) HandlerContext
HandlerCtx returns HandlerContext value stored in ctx. It panics if ctx has no HandlerContext stored; this indicates a programming error by the caller (e.g. passing a context that did not originate from the interpreter).
type OpenHandlerFunc ¶
type OpenHandlerFunc func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error)
OpenHandlerFunc is a handler which opens files. It is called for all files that are opened directly by the shell, such as in redirects. Files opened by executed programs are not included.
The path parameter may be relative to the current directory, which can be fetched via HandlerCtx.
Use a return error of type *os.PathError to have the error printed to stderr and the exit status set to 1. Any other error will halt the Runner and will be returned via the API.
Note that implementations which do not return os.File will cause extra files and goroutines for input redirections; see StdIO.
type ReadDirHandlerFunc ¶
ReadDirHandlerFunc is a handler which reads directories. It is called during shell globbing, if enabled.
type Runner ¶
type Runner struct {
// contains filtered or unexported fields
}
A Runner interprets shell programs. It can be reused, but it is not safe for concurrent use. Use New to build a new Runner.
Runner's exported fields are meant to be configured via RunnerOption; once a Runner has been created, the fields should be treated as read-only.
func New ¶
func New(opts ...RunnerOption) (*Runner, error)
New creates a new Runner, applying a number of options. If applying any of the options results in an error, it is returned.
Any unset options fall back to their defaults. For example, not supplying the environment defaults to an empty environment (no host env inherited), and not supplying the standard output writer means that the output will be discarded.
func (*Runner) Close ¶
Close releases resources held by the Runner, such as os.Root file descriptors opened by AllowedPaths. It is safe to call Close multiple times.
func (*Runner) Reset ¶
func (r *Runner) Reset()
Reset returns a runner to its initial state, right before the first call to Run or Reset.
Typically, this function only needs to be called if a runner is reused to run multiple programs non-incrementally. Not calling Reset between each run will mean that the shell state will be kept, including variables, options, and the current directory.
func (*Runner) Run ¶
Run interprets a node, which can be a [*File], [*Stmt], or [Command]. If a non-nil error is returned, it will typically contain a command's exit status, which can be retrieved with errors.As and ExitStatus.
Run can be called multiple times synchronously to interpret programs incrementally. To reuse a Runner without keeping the internal shell state, call Reset.
type RunnerOption ¶
RunnerOption can be passed to New to alter a Runner's behaviour.
func AllowedCommands ¶ added in v0.0.3
func AllowedCommands(names []string) RunnerOption
AllowedCommands restricts command execution to the specified command names. Names must use the "rshell:" namespace prefix (e.g. "rshell:cat", "rshell:find"). Names without a colon separator or with an unknown namespace are rejected. The bare command name (after the prefix) is stored internally and matched exactly against the command name (args[0]) at execution time.
Only commands whose name appears in the list may be executed; all others are rejected with "<cmd>: command not allowed".
After prefix stripping, path-containing names (e.g. "rshell:/bin/bash") will not match bare command names and vice versa. Empty strings and empty command names are rejected.
When not set (default), no commands are allowed.
func AllowedPaths ¶
func AllowedPaths(paths []string) RunnerOption
AllowedPaths restricts file and directory access to the specified directories. Paths must be absolute directories that exist. When set, only files within these directories can be opened, read, or executed.
When not set (default), all file access is blocked. An empty slice also blocks all file access.
func Env ¶
func Env(pairs ...string) RunnerOption
Env sets the initial environment for the interpreter. Each pair must be in "KEY=value" format. If this option is not used, the interpreter starts with an empty environment (no host environment variables are inherited).
func MaxExecutionTime ¶ added in v0.0.7
func MaxExecutionTime(d time.Duration) RunnerOption
MaxExecutionTime bounds the total execution time of each Runner.Run call.
When d is zero, no timeout is applied. Negative values are rejected.
The timeout is applied per Run call rather than when the Runner is created, so reusing a Runner across multiple runs yields a fresh deadline each time.
func ProcPath ¶ added in v0.0.6
func ProcPath(path string) RunnerOption
ProcPath sets the path to the proc filesystem used by the ps builtin. When not set (default), ps uses "/proc". This option has no effect on non-Linux platforms.
Note: bare ps (session mode) uses the host process's PID to walk the PPID chain. If path points to a proc filesystem from a different PID namespace, the host PID will likely not be found there and session output will be empty. ps -e and ps -p work correctly against any proc tree.
func StdIO ¶
func StdIO(in io.Reader, out, err io.Writer) RunnerOption
StdIO configures an interpreter's standard input, standard output, and standard error. If out or err are nil, they default to a writer that discards the output.
Note that providing a non-nil standard input other than *os.File will require an os.Pipe and spawning a goroutine to copy into it, as an os.File is the only way to share a reader with subprocesses. This may cause the interpreter to consume the entire reader. See os/exec.Cmd.Stdin.
When providing an *os.File as standard input, consider using an os.Pipe as it has the best chance to support cancellable reads via os.File.SetReadDeadline, so that cancelling the runner's context can stop a blocked standard input read.
When a non-*os.File reader is provided, the background copy goroutine uses context.Background because StdIO is a RunnerOption executed before any Runner.Run call, so no run-scoped context is available at this point. The goroutine terminates as soon as the reader returns io.EOF or the pipe's write end is closed, so it is bounded by the reader's lifetime. Callers that require context-cancellable stdin should provide an *os.File (e.g. via os.Pipe) directly, or use a redirect inside the script. If the provided reader's Read blocks indefinitely (for example a net.Conn without a deadline), the goroutine may outlive the script; callers in that situation should wrap the reader with a deadline-aware adapter before passing it to StdIO.