monty-fs
Host-side filesystem mounts: a MountTable maps real directories into the sandbox at virtual paths and services the sandbox’s OS calls.
Filesystem mounting system for sandboxed execution.
Provides MountTable, which maps virtual paths to real host directories
with configurable access modes. When sandbox code calls filesystem methods
like Path.read_text(), the mount table intercepts the operation, resolves
the virtual path, and executes it according to the mount mode.
This crate is HOST-side code: it performs real std::fs I/O and is linked
only by host/parent crates (monty-pool, the CLI, bindings). The monty
interpreter crate deliberately does not depend on it — sandboxed code can
only request filesystem operations by suspending with an
OsFunctionCall, which a host holding a
MountTable services via MountTable::handle_os_call.
The monty runtime MUST NEVER read, write, or obtain any information about any file or directory outside the specific directory that is mounted.
Enforced by the operating system, not by path arithmetic: each mount holds
a cap_std::fs::Dir opened once at mount time, and every operation is
performed relative to that descriptor, which refuses to resolve past its
own root. path_security only normalizes the virtual path and strips the
mount prefix — it is path policy, not the boundary.
Each mount has an aggregate memory budget, defaulting to
DEFAULT_MEMORY_USAGE_LIMIT, for retained overlay data and results.
MountMode::ReadWrite— full read/write access to the host directoryMountMode::ReadOnly— reads work, writes raisePermissionErrorMountMode::OverlayMemory— reads fall through to host; writes stored in memory
pub enum MountError {
/// The virtual path does not fall under any configured mount point.
NoMountPoint(String),
/// Path traversal or symlink escape detected. The resolved host path is
/// intentionally NOT included to avoid leaking host filesystem information.
PathEscape { virtual_path: String },
/// A path contained an embedded null byte, which no filesystem name may.
///
/// Carries CPython's wording for the operation that saw it rather than a
/// path: CPython raises this from argument parsing, before the path is
/// echoed anywhere. See `OsFunctionCall::embedded_null_message`.
///
EmbeddedNullByte(&'static str),
/// A write operation was attempted on a read-only mount.
ReadOnly(String),
/// A rename was attempted across different mount points (EXDEV).
CrossMountRename { src: String, dst: String },
/// An I/O error from the host filesystem.
Io(io::Error, String),
/// A file contained bytes that could not be decoded as UTF-8. Carries the
/// details needed to reproduce CPython's `UnicodeDecodeError` wording
/// exactly (see `monty_types::unicode_decode_error_msg`).
InvalidUtf8 { start: usize, end: usize, first_byte: u8, reason: &'static str, data: monty_types::ExcData },
/// Invalid mount configuration (e.g., host path doesn't exist or isn't a directory).
InvalidMount(String),
/// Cumulative write bytes exceeded the configured per-mount limit.
/// The configured byte limit that was exceeded.
WriteLimitExceeded(u64),
/// An operation would exceed the mount's aggregate memory budget.
/// The configured byte limit that was exceeded.
MemoryUsageLimitExceeded(u64),
}
Errors from mount configuration or filesystem operations.
pub fn into_exception(self) -> MontyException
Converts this error into a MontyException for returning to the sandbox.
Implements: Debug, Display, Error.
pub enum MountMode {
/// Full read and write access to the host directory.
///
/// Files written by sandboxed code persist on the host and are untrusted;
/// the host must not execute them. That includes indirect execution: a
/// Python `import` when the directory is on `sys.path`, or a tool reading
/// `conftest.py` or `.git/hooks/*`. `Self::OverlayMemory` behaves the
/// same inside the sandbox but keeps writes in memory.
ReadWrite,
/// Read-only access. Write operations raise `PermissionError`.
ReadOnly,
/// Copy-on-write overlay backed by in-memory storage.
///
/// Reads fall through to the host directory. Writes are captured in the
/// contained `OverlayState`. Deletions insert `OverlayEntry::Deleted`
/// tombstones that hide real files from subsequent reads. Directory listings
/// merge real and overlay entries, with overlay taking precedence.
OverlayMemory(OverlayState),
}
Access policy for a mount point.
Controls what operations sandbox code can perform on files within the mounted directory. The overlay modes provide copy-on-write semantics where reads fall through to the real directory but writes are captured separately.
Regardless of mode, path traversal and symlink escape protection is always enforced.
pub fn from_mode_str(mode: &str) -> Result<Self, String>
Parses a mode string into a MountMode.
Accepted values: "read-only", "read-write", "overlay".
Returns a descriptive error string on invalid input.
pub fn as_str(&self) -> &'static str
Returns a short string label for this mode ("read-write", "read-only",
or "overlay").
Implements: Debug.
pub const DEFAULT_MEMORY_USAGE_LIMIT: u64 = 100_000_000;
Default aggregate memory budget for one mount: 100 MB in decimal bytes.
pub struct Mount { /* private fields */ }
A single mount point mapping a virtual path to a host directory.
Owns the MountMode which includes overlay state for
MountMode::OverlayMemory mounts. It can be constructed before its table
and transferred into it with MountTable::push_mount.
pub fn new(
virtual_path: &str,
host_path: impl AsRef<Path>,
mode: MountMode,
write_bytes_limit: Option<u64>,
) -> Result<Self, MountError>
Creates a new mount point, opening a descriptor on the host directory.
Mount memory defaults to DEFAULT_MEMORY_USAGE_LIMIT.
A host mounting the same directory repeatedly should open a MountRoot
once and use Mount::with_root, resolving the name only once.
Returns MountError::InvalidMount if the virtual path is not absolute,
or the host path cannot be opened as a directory or canonicalized.
pub fn with_root(root: MountRoot, mode: MountMode, write_bytes_limit: Option<u64>) -> Self
Mounts an already-opened MountRoot, touching no filesystem at all.
Mount memory defaults to DEFAULT_MEMORY_USAGE_LIMIT.
pub fn root(&self) -> &MountRoot
Returns the opened root, to clone into a later mount of the same directory.
pub fn virtual_path(&self) -> &str
Returns the normalized virtual path prefix for this mount.
pub fn host_path(&self) -> &Path
Returns the canonical host directory path. Diagnostics only.
pub fn mode(&self) -> &MountMode
Returns the access mode for this mount.
pub fn write_bytes_limit(&self) -> Option<u64>
Returns the optional write bytes limit for this mount.
pub fn memory_usage_limit(&self) -> u64
Returns the aggregate mount memory budget.
pub fn with_memory_usage_limit(self, limit: u64) -> Self
Overrides the aggregate mount memory budget.
pub fn memory_usage(&self) -> u64
Returns memory currently retained by this mount’s overlay.
pub fn write_bytes_used(&self) -> u64
Returns the cumulative number of bytes written through this mount.
Implements: Debug.
pub enum MountCallOutcome {
/// A mount covered the call and serviced it (successfully or not).
Handled(Result<monty_types::MontyObject, MountError>),
/// Non-filesystem op or no matching mount — the call, returned unchanged.
NotHandled(monty_types::OsFunctionCall),
}
Outcome of MountTable::handle_os_call.
The call is consumed so write payloads can be moved into overlay storage;
when no mount covers it, ownership is handed back so the caller can
surface the call to its fallback handler (host callback, on_no_handler).
Implements: Debug.
pub struct MountRoot { /* private fields */ }
A host directory opened once, mountable as often as the host likes; cloning shares the descriptor.
Reuse one instead of re-deriving a mount from its path: sandbox code that can rename inside a parent mount redirects that name between rebuilds, and an open descriptor cannot be redirected.
pub fn open(virtual_path: &str, host_path: impl AsRef<Path>) -> Result<Self, MountError>
Opens host_path, pinning the root to the directory that is there now.
Returns MountError::InvalidMount if the virtual path is not absolute,
or the host path cannot be opened as a directory or canonicalized.
pub fn virtual_path(&self) -> &str
Returns the normalized virtual path prefix this root answers on.
pub fn host_path(&self) -> &Path
Returns the canonical host directory path. Diagnostics only.
Implements: Clone, Debug.
pub struct MountTable { /* private fields */ }
A collection of mount points mapping virtual paths to host directories.
Mounts are checked in longest-prefix-first order so that more specific mounts take precedence.
pub fn new() -> Self
Creates a new empty mount table.
pub fn mount(
&mut self,
virtual_path: &str,
host_path: impl AsRef<Path>,
mode: MountMode,
write_bytes_limit: Option<u64>,
) -> Result<(), MountError>
Adds a mount point mapping a virtual path to a host directory.
The host directory is opened once here, and every later operation runs
relative to that descriptor — so the mount stays attached to the
directory that was named, whatever the host does to the path afterwards.
Mount memory uses DEFAULT_MEMORY_USAGE_LIMIT unless a pre-built
Mount overrides it.
With MountMode::ReadWrite, files written by sandboxed code persist
on the host. Read that variant’s warning before choosing it.
Returns MountError::InvalidMount if the virtual path is not absolute,
the host path doesn’t exist or isn’t a directory, or it cannot be opened
— on macOS/BSD that includes a search-only (0o111) directory, which
Linux accepts because it opens directories with O_PATH.
pub fn push_mount(&mut self, mount: Mount)
Adds a pre-built Mount to the table.
Use this when a mount was validated before the table was assembled.
pub fn handle_os_call(&mut self, call: OsFunctionCall) -> MountCallOutcome
Handles an OS call using the mount table.
Consumes the call so a covered write’s payload is moved into the
backend (overlay storage retains it without a copy). Routing happens
on a borrow first, so MountCallOutcome::NotHandled hands the call
back untouched for the caller’s fallback handler (a host callback or
OsFunctionCall::on_no_handler).
Path length and null bytes are checked before anything else touches the path, so both apply whether or not a mount covers it — as in CPython, where neither reaches a syscall.
pub fn is_empty(&self) -> bool
Returns true if no mount points are configured.
pub fn len(&self) -> usize
Returns the number of configured mount points.
Implements: Debug, Default.
pub struct OverlayState { /* private fields */ }
In-memory overlay state for MountMode::OverlayMemory.
A single BTreeMap stores relative mount paths and the overlay entry that
currently shadows or extends the underlying real filesystem.
pub fn new() -> Self
Creates a new empty overlay state.
Implements: Debug, Default.