The virtual filesystem
Every read and write a session performs goes through one interface. That is what makes "no ambient filesystem access" true rather than aspirational, and it is where a host decides what the sandbox can see.
IFileSystem
The interface covers what a shell needs: ReadFileAsync, WriteFileAsync, AppendFileAsync,
CreateDirectoryAsync, ReadDirectoryAsync, StatAsync, ExistsAsync, RenameAsync,
CopyAsync, RemoveAsync, symlinks, mode and modified time. A host either uses the bundled in-memory backend or
implements the interface over whatever it actually has: a database, an object store, a set of
generated documents, or a projection of its own configuration.
var bash = Bash.CreateBuilder()
.WithFileSystem(new InMemoryFileSystem())
.WithFileSystemLimits(new FsLimits { MaxTotalBytes = 32 * 1024 * 1024 })
.Build();
With no WithFileSystem call the session gets an empty in-memory tree, so it can read nothing until
you put something in it.
Seeding it
The simplest case is to write the files from the host before handing the session over:
var fs = new InMemoryFileSystem();
await fs.CreateDirectoryAsync(VPath.Parse("/data"), recursive: true);
await fs.WriteFileAsync(VPath.Parse("/data/orders.csv"), Encoding.UTF8.GetBytes(csv));
var bash = Bash.CreateBuilder().WithFileSystem(fs).Build();
The other case, and the more interesting one, is a filesystem whose contents are generated on
read. Nothing requires the tree to be stored: an implementation may synthesise a directory listing
from a query and produce a file's bytes when it is opened. That is how Curiosity Workspace mounts a
workspace's configuration for Sudo:
/proc there is regenerated on every read, so cat run twice reports two different moments.
POSIX paths
Paths are POSIX on every host, handled by VPath rather than System.IO.Path:
/is the separator, on Windows too;- there is no drive letter and no UNC path to resolve;
.and..are normalised inside the virtual root, so..cannot climb out of it;- a path is a value, not a host resource, so nothing about it depends on the machine.
Quotas
The filesystem carries its own limits, separate from the execution budget:
| Limit | Guards against |
|---|---|
MaxTotalBytes (100 MB) |
a script filling memory by writing |
MaxFileBytes (50 MB) |
one enormous file |
MaxFiles (10,000) |
a script filling memory by creating |
MaxDepth (64) |
a nesting bomb, and an unbounded tree walk |
MaxSymlinkHops (40) |
a symlink cycle |
MaxNameLength (255) |
a pathological file name |
Depth is capped where paths are created as well as where they are walked, so a host-supplied filesystem that is deeper than this sandbox would have allowed still cannot make a walk run forever.
Bytes, not strings
The unit of shell data is bytes. StreamData holds them, and UTF-8 decoding happens only at the
edges, so binary content survives a pipeline intact instead of being mangled by a round trip through
string.
That matters when a script does something like xxd, tar or a checksum, and it is why a custom
filesystem backend should hand back bytes rather than decoded text.
Sharing it with Python
When the joined Computerwelt package is used, the Python side's open, os and os.path are
backed by the same IFileSystem. A file a shell command wrote is a file the Python program reads,
under the same quotas, with the same paths.
A host function that needs storage where the host configured none says so in the program's own
terms: it raises a Python OSError the script can catch, rather than letting a host exception
escape into it.
Read next
- The sandbox model — why this interface is the boundary.
- Python — how the same filesystem reaches
openandos. - Limits — the execution budget beside these quotas.