bashkit

Virtual filesystem

Every Bashkit script runs against an in-memory virtual filesystem (VFS), not the host disk. cat, ls, cp, redirections, mkdir — they all work exactly as a script expects, but the bytes live in memory and disappear when the interpreter is dropped. Path traversal like ../../../etc/passwd is normalised away, and symlinks are stored but never followed. The host is invisible by default; you grant access deliberately, never by accident.

This is the foundation of the security model: there is no real filesystem to escape to unless you mount one.

The layering stack

A Bash instance composes its filesystem from layers. Each layer wraps the one below it, so you can stack read-only enforcement, text mounts, and host mounts over an in-memory base — and swap mounts at runtime.

MountableFs · live mounts ReadOnlyFs · optional OverlayFs · text mounts MountableFs · real mounts base · InMemoryFs or custom
<g font-size="11" fill="#404040">
  <text x="338" y="42">Bash::mount() / unmount()</text>
  <text x="338" y="94">readonly_filesystem()</text>
  <text x="338" y="146">mount_text()</text>
  <text x="338" y="198">mount_real_*_at()</text>
  <text x="338" y="250">Bash::new()</text>
</g>
<g stroke="#0a1636" stroke-opacity="0.25">
  <line x1="180" y1="60" x2="180" y2="68"/>
  <line x1="180" y1="112" x2="180" y2="120"/>
  <line x1="180" y1="164" x2="180" y2="172"/>
  <line x1="180" y1="216" x2="180" y2="224"/>
</g>

Two-layer trait model

Internally the VFS splits raw storage from POSIX semantics:

LayerTraitResponsibility
BackendFsBackendRaw storage operations (minimal contract)
POSIXFileSystem / PosixFsPOSIX-like semantics (no duplicate names, type-safe ops, parent-dir rules)

If you want a custom backend (a database, object store, key-value store), implement the small FsBackend and wrap it in PosixFs — the POSIX checks come for free. Implement FileSystem directly only when you need full control over semantics.

Built-in implementations

ImplementationPurpose
InMemoryFsDefault (Bash::new()). HashMap-backed, thread-safe, no persistence. Seeds /, /tmp, /home, /home/user, /dev.
OverlayFsCopy-on-write over another filesystem, with whiteout tracking for deletes.
MountableFsMount multiple filesystems at different paths (longest-prefix match). Always the outermost layer, enabling live mounts.
ReadOnlyFsDelegates reads, denies every mutation with PermissionDenied — even writes to /tmp, cp, mv, rm, chmod. For inspection-only sessions.
RealFs (realfs feature)Direct access to a host directory. Read-only (safe) or read-write (dangerous); path traversal blocked by canonicalisation + root-prefix checks.

Mounting host directories

Real host access is opt-in and read-only by default. The realfs feature adds builder and CLI entry points:

use bashkit::Bash;

let mut bash = Bash::builder()
    .mount_real_readonly("/host/data")        // visible read-only inside the VFS
    .build();
bashkit --mount-ro /host/data:/data -c 'ls /data'   # read-only
bashkit --mount-rw /host/out:/out  -c 'echo hi > /out/f'  # writable (dangerous)

To freeze a session — including in-memory writes — wrap it with readonly_filesystem().

  • /dev/null is handled at the interpreter level (not the filesystem), so a custom backend can’t intercept it. /dev/urandom / /dev/random return bounded random data.
  • Symlinks are stored but never followed — this closes symlink-escape (TM-ESC-002) and symlink-loop DoS (TM-DOS-011).

Binding parity

Every language binding exposes the same concepts, so the model is identical from Rust, Python, and Node:

files:  { "/path": "content" }                 # writable in-memory text files
mounts: [{ host_path, vfs_path?, writable? }]  # real FS (read-only by default)
readonly_filesystem: bool                       # deny all VFS mutations after setup

See also