Implement sandboxed execution: restricted mode + pluggable virtual filesystem#4
Open
lostbean wants to merge 5 commits intotv-labs:mainfrom
Open
Implement sandboxed execution: restricted mode + pluggable virtual filesystem#4lostbean wants to merge 5 commits intotv-labs:mainfrom
lostbean wants to merge 5 commits intotv-labs:mainfrom
Conversation
Add Bash.ExternalProcess as a centralized gateway wrapping all user-facing
OS process spawning (ExCmd.Process.start_link, ExCmd.stream, System.cmd).
When restricted mode is active, all three return {:error, :restricted}.
Thread the restricted flag from Session options through CommandPort,
AST.Command, Pipeline, JobProcess, and the exec/command/coproc builtins.
Guard Pipeline.external_command?/2 to force sequential dispatch in
restricted mode. Protect the restricted flag from mutation in set.ex and
wire shopt restricted_shell as a read-only query reflecting actual state.
Add Bash.ExternalProcess module with defguard-based pattern matching.
Add restricted: false to session default options and all three job_opts
construction sites in session.ex. Wrap JobProcess.await_start with
try/catch for the case where the GenServer exits on restricted failure.
Use Map.get/3 for options access to handle bare state maps in tests.
Forward stderr from temp collectors in pipeline and command substitution
to parent session sinks. Add {:exec, result} handling to SessionCase.
Add a `Bash.Filesystem` behaviour with 16 callbacks (exists?, dir?,
regular?, stat, lstat, read, write, mkdir_p, rm, open, handle_write,
handle_close, ls, wildcard, read_link, read_link_all) and a dispatcher
that unwraps {module, config} tuples from session state.
`Bash.Filesystem.LocalDisk` is the default adapter, delegating to
Elixir's `File` and Erlang's `:file` modules — preserving identical
behavior for sessions created without a `filesystem:` option.
The `filesystem` field is added to session state, initialized from
opts in `init/1`, and inherited by child sessions in `new_child/2`.
All 57+ filesystem access points across 14 files are refactored to
dispatch through `Bash.Filesystem` instead of calling `File.*` or
`:file.*` directly:
- builtin/test.ex: all 17+ file test operators (-e, -f, -d, -r, -w,
-x, -s, -L, -b, -c, -p, -S, -g, -k, -u, -N, -nt, -ot, -ef)
- builtin/cd.ex: resolve_path, search_cdpath, validate_directory,
resolve_symlinks
- builtin/pwd.ex: resolve_path_components symlink resolution
- builtin/source.ex: file existence checks and file reading
- builtin/pushd.ex, popd.ex: validate_directory (signature changed
to accept session_state)
- builtin/command.ex, type.ex, hash.ex: find_in_path PATH lookup
- ast/command.ex: noclobber check, mkdir_p, input redirect read,
PATH lookup in resolve_via_hash_or_path and find_command_in_path
- ast/helpers.ex: glob expansion (wildcard, ls, dir?)
- ast/while_loop.ex: input redirect file reading
- session.ex: chdir validation, open_fd, close_fd,
close_all_file_descriptors
- sink.ex: file/2 accepts filesystem: option for open/write/close
Test suite includes 49 new tests:
- 18 functional VFS tests with an InMemory adapter exercising test
operators, source, redirections, globs, cd/pwd, child context
propagation, and combined restricted+VFS mode
- 28 enforcement tests using poison-path technique (working dir at
/nonexistent_vfs_enforcement_path/workspace) that fail if any code
path leaks to direct File.* calls, covering all categories above
plus noclobber, while-loop redirects, and command/type/hash lookup
- 3 existing test files updated to include filesystem key in mock
session state maps
Add Bash.Filesystem.local_disk?/1 to distinguish LocalDisk from custom VFS implementations. In Session.init/1, automatically set restricted: true when a non-LocalDisk filesystem is provided, eliminating the split-brain state where builtins see the VFS but external commands hit the real OS. Narrow the restricted-mode PATH lookup guard in Builtin.Command to only block find_in_path/2 for LocalDisk sessions. VFS sessions still resolve commands via the virtual filesystem since ExternalProcess independently blocks execution. Add tests verifying auto-enforcement sets the restricted flag, reports restricted_shell as on via shopt, blocks external commands, and leaves LocalDisk sessions unrestricted by default.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
When a command is not a builtin, a user-defined function, or an Interop binding, the interpreter falls through to spawning a real OS process via
ExCmd. Commands likels,cat,grep,curl,rmexecute against the host with full access to the filesystem, network, and process table. There is no option to disable this fallback, and all filesystem access goes throughFile.*directly. This means there is no way to use the library as a sandboxed scripting environment.Use Case
curl,rm -rf /, or any installed binary, and builtins liketest -forsourcewould hit the real filesystem.Requirements
command,exec,coproc, pipelines, command substitution, background jobs).test,cd,source,pwd, redirections, glob expansion) dispatches through a pluggable filesystem behaviour instead of callingFile.*directly.Proposed Solution
The solution has two layers:
1. Restricted Mode (
Bash.ExternalProcess)A new
Bash.ExternalProcessmodule acts as a centralized gateway wrapping all user-facing OS process spawning (ExCmd.Process.start_link,ExCmd.stream,System.cmd). When restricted mode is active, all calls return{:error, :restricted}.graph TD CP[CommandPort] --> EP[ExternalProcess] JP[JobProcess] --> EP CO[Coproc] --> EP PL[Pipeline] --> EP CM[Command builtin] --> EP EP -->|restricted?| ERR["{:error, :restricted}"] EP -->|allowed| EX[ExCmd / System.cmd]The session carries the flag in
options.restricted, defaulting tofalse. Thesetbuiltin filters:restrictedfrom option changes, andshopttreatsrestricted_shellas read-only.2. Pluggable Filesystem (
Bash.Filesystem)A behaviour with 16 callbacks covering all filesystem operations used by the interpreter. Stored in session state as a
{module, config}tuple. All filesystem access (57+ call sites across 14 files) dispatches throughBash.Filesysteminstead of callingFile.*directly.graph TD subgraph Callers T[Test builtin] CD[cd/pwd] SRC[source] CMD[Command/Pipeline] R[Redirections] G[Glob expansion] end Callers --> FS[Bash.Filesystem] FS --> LD[LocalDisk — default] FS --> VFS[Custom VFS adapter]The default
Bash.Filesystem.LocalDiskadapter delegates to Elixir'sFileand Erlang's:filemodules. When a non-LocalDisk filesystem is provided,Session.init/1automatically enables restricted mode to prevent a split-brain state where builtins use the VFS but external commands hit the real OS.Usage