fastshell security architecture and best practices.
All file operations go through a Virtual File System (VFS) rooted at the configured sandbox_path. Two layers of path escape prevention:
-
Component normalization —
..components are resolved by popping from a path stack. Paths like../../../etc/passwdresolve to the sandbox root (cannot go above). -
Symlink canonicalization —
fs::canonicalize()resolves real symlinks. If the resolved target lies outside the sandbox root, the operation is rejected withPathEscapeerror.
Operations protected: read_file, write_file, list_dir, exists, is_dir, copy, rename, remove_file, remove_dir.
fastshell uses a delegated permission model — it never makes authorization decisions. Instead, it returns special exit codes that the host app handles:
Exit code 100 → Permission needed
Exit code 126 → Feature not supported (plugin not registered)
The host app is responsible for:
- Detecting exit_code=100
- Showing a native permission dialog (with platform-native UI)
- Calling
set_permission(resource, allowed)to record the decision - Retrying the command
fastshell itself never displays any UI, never requests OS permissions, and never makes policy decisions.
| Platform | Default allow_subprocess |
Reason |
|---|---|---|
| Android | false | Phantom Process Killer (>32 processes → SIGKILL) |
| iOS | false | fork() is forbidden by iOS |
| macOS/Linux | true | Standard desktop environment |
When disabled, unknown commands return "command not found (subprocess disabled)" with exit_code=127. All 180+ built-in commands work regardless.
Embedded CPython executes within fastshell's process. Several sandbox measures:
- Shell redirection —
subprocess.run,Popen,os.system,asyncio.create_subprocess_shellare all redirected through fastshell's shell dispatcher - File path sandbox —
builtins.open()andos.open()are hooked to resolve paths within the VFS sandbox root...components are stripped before path construction - os.listdir / os.chdir — also hooked for sandbox path resolution
Not sandboxed: ctypes.CDLL (loading native libraries), low-level file descriptors. These are inherent limitations of in-process CPython. For stricter isolation, run Python outside fastshell in a separate process.
Pipeline stages (cmd1 | cmd2 | cmd3) run in parallel threads with mpsc channels for streaming data. Each stage gets its own Shell clone:
- VFS root is shared (PathBuf clone)
- cwd is cloned —
cdin a pipeline stage does not affect the parent shell - Permissions map is Arc-shared across all stages
If a pipeline thread panics, the panic is caught via std::panic::catch_unwind and all threads are joined before returning an error. No threads are leaked.
FastshellusesArc<Mutex<Runtime>>internally- All public methods take
&self(immutable reference), enabling concurrent access - Permission map uses
Arc<Mutex<HashMap>>for concurrent reads/writes - Device plugin uses
Arc<Mutex<Option<Box<dyn DevicePlugin>>>> - Mutex poisoning: if a plugin method panics, the poisoned mutex is recovered via
into_inner()
- Set sandbox_path to a dedicated directory — not shared with other app data
- Set command_timeout_ms — prevent infinite loops from consuming resources
- Implement Foreground Service (Android) — prevent OS from killing the app
- Bundle Python libraries at build time — avoid runtime downloads (iOS review)
- Configure cleartext traffic (Android) and ATS exceptions (iOS) — enable HTTP
- Run with
allow_subprocess = trueon desktop for maximum compatibility - Test with
allow_subprocess = falsebefore deploying to mobile - Implement DevicePlugin methods incrementally — start with the essentials
- Monitor exit_code=100 — ensure your permission dialog works end-to-end
- Python
ctypes.CDLLcan load system libraries and bypass sandbox - Python
os.openwith raw file descriptors bypasses path hooks (os-levelopenIS hooked, but low-levelfcntl/ioctlare not) - Subprocess fallthrough (when enabled) runs commands outside the VFS sandbox
kill/psoperate on real host processes (no PID namespace)- No resource quotas — CPU, memory, disk usage are not limited beyond timeout
- No audit logging — command execution history is not recorded internally