mod file_handle;
mod resolve;
use wasmtime::component::Resource;
use crate::engine::wasm::bindings::astrid::fs::host::{
self as fs, Datetime, ErrorCode, FileHandle, FileStat, FileType, OpenMode,
};
use crate::engine::wasm::host::util;
use crate::engine::wasm::host_state::HostState;
use resolve::{resolve_path, resolve_vfs};
fn audit_fs<T, E: std::fmt::Debug>(
state: &HostState,
op: &'static str,
path: &str,
result: &Result<T, E>,
) {
let capsule_id = state.capsule_id.as_str();
let principal = state.effective_principal();
match result {
Ok(_) => tracing::debug!(
target: "astrid.audit.fs",
%capsule_id,
%principal,
fn = op,
path,
"audit",
),
Err(e) => tracing::debug!(
target: "astrid.audit.fs",
%capsule_id,
%principal,
fn = op,
path,
error = ?e,
"audit",
),
}
}
fn map_resolve_err(e: String) -> ErrorCode {
if e.contains("escapes root boundary") || e.contains("escaped canonical root") {
ErrorCode::BoundaryEscape
} else if e.contains("not available") || e.contains("not mounted") {
ErrorCode::CapabilityDenied
} else {
ErrorCode::InvalidPath
}
}
fn map_vfs_err(e: astrid_vfs::VfsError) -> ErrorCode {
use astrid_vfs::VfsError;
match e {
VfsError::NotFound(_) => ErrorCode::NotFound,
VfsError::PermissionDenied(_) => ErrorCode::Access,
VfsError::SandboxViolation(_) => ErrorCode::BoundaryEscape,
VfsError::InvalidHandle => ErrorCode::InvalidPath,
VfsError::NotSupported(msg) => ErrorCode::Unknown(format!("not supported: {msg}")),
VfsError::Io(io) => ErrorCode::Unknown(io.to_string()),
}
}
fn gate_read(state: &HostState, physical: &std::path::Path) -> Result<(), ErrorCode> {
if let Some(gate) = state.security.clone() {
let capsule_id = state.capsule_id.as_str().to_owned();
let p = physical.to_string_lossy().to_string();
let home = state.effective_home_root_buf();
let check = util::bounded_block_on(
&state.runtime_handle,
&state.blocking_semaphore,
async move { gate.check_file_read(&capsule_id, &p, home.as_deref()).await },
);
if check.is_err() {
return Err(ErrorCode::CapabilityDenied);
}
}
Ok(())
}
fn gate_write(state: &HostState, physical: &std::path::Path) -> Result<(), ErrorCode> {
if let Some(gate) = state.security.clone() {
let capsule_id = state.capsule_id.as_str().to_owned();
let p = physical.to_string_lossy().to_string();
let home = state.effective_home_root_buf();
let check = util::bounded_block_on(
&state.runtime_handle,
&state.blocking_semaphore,
async move {
gate.check_file_write(&capsule_id, &p, home.as_deref())
.await
},
);
if check.is_err() {
return Err(ErrorCode::CapabilityDenied);
}
}
Ok(())
}
fn to_file_stat(meta: &astrid_vfs::VfsMetadata) -> FileStat {
let kind = if meta.is_dir {
FileType::Directory
} else if meta.is_file {
FileType::Regular
} else {
FileType::TypeUnknown
};
let modified = Some(Datetime {
seconds: meta.mtime as i64,
nanoseconds: 0,
});
FileStat {
size: meta.size,
kind,
mode: 0,
modified,
created: None,
accessed: None,
}
}
impl fs::Host for HostState {
fn fs_open(
&mut self,
_path: String,
_mode: OpenMode,
) -> Result<Resource<FileHandle>, ErrorCode> {
Err(ErrorCode::Unknown(
"fs-open: FileHandle resource port pending".to_string(),
))
}
fn fs_exists(&mut self, path: String) -> Result<bool, ErrorCode> {
let resolved = resolve_path(self, &path).map_err(map_resolve_err)?;
gate_read(self, &resolved.physical)?;
let vfs_path = resolve_vfs(self, &resolved).map_err(map_resolve_err)?;
let exists =
util::bounded_block_on(&self.runtime_handle, &self.blocking_semaphore, async {
vfs_path
.vfs
.exists(
&vfs_path.handle,
vfs_path.relative.to_string_lossy().as_ref(),
)
.await
})
.unwrap_or(false);
let result: Result<bool, ErrorCode> = Ok(exists);
audit_fs(self, "astrid:fs/host.fs-exists", &path, &result);
result
}
fn fs_mkdir(&mut self, path: String) -> Result<(), ErrorCode> {
let resolved = resolve_path(self, &path).map_err(map_resolve_err)?;
gate_write(self, &resolved.physical)?;
let vfs_path = resolve_vfs(self, &resolved).map_err(map_resolve_err)?;
let relative = vfs_path.relative.to_string_lossy().to_string();
if let Some(parent) = vfs_path.relative.parent()
&& !parent.as_os_str().is_empty()
{
let parent_rel = parent.to_string_lossy().to_string();
let parent_exists =
util::bounded_block_on(&self.runtime_handle, &self.blocking_semaphore, async {
vfs_path.vfs.exists(&vfs_path.handle, &parent_rel).await
})
.unwrap_or(false);
if !parent_exists {
let result: Result<(), ErrorCode> = Err(ErrorCode::NotFound);
audit_fs(self, "astrid:fs/host.fs-mkdir", &path, &result);
return result;
}
}
let result =
util::bounded_block_on(&self.runtime_handle, &self.blocking_semaphore, async {
vfs_path.vfs.mkdir(&vfs_path.handle, &relative).await
})
.map_err(map_vfs_err);
audit_fs(self, "astrid:fs/host.fs-mkdir", &path, &result);
result
}
fn fs_mkdir_all(&mut self, path: String) -> Result<(), ErrorCode> {
let resolved = resolve_path(self, &path).map_err(map_resolve_err)?;
gate_write(self, &resolved.physical)?;
let vfs_path = resolve_vfs(self, &resolved).map_err(map_resolve_err)?;
let result =
util::bounded_block_on(&self.runtime_handle, &self.blocking_semaphore, async {
vfs_path
.vfs
.mkdir(
&vfs_path.handle,
vfs_path.relative.to_string_lossy().as_ref(),
)
.await
})
.map_err(map_vfs_err);
audit_fs(self, "astrid:fs/host.fs-mkdir-all", &path, &result);
result
}
fn fs_readdir(&mut self, path: String) -> Result<Vec<String>, ErrorCode> {
let resolved = resolve_path(self, &path).map_err(map_resolve_err)?;
gate_read(self, &resolved.physical)?;
let vfs_path = resolve_vfs(self, &resolved).map_err(map_resolve_err)?;
let result =
util::bounded_block_on(&self.runtime_handle, &self.blocking_semaphore, async {
vfs_path
.vfs
.readdir(
&vfs_path.handle,
vfs_path.relative.to_string_lossy().as_ref(),
)
.await
})
.map(|entries| entries.into_iter().map(|e| e.name).collect::<Vec<_>>())
.map_err(map_vfs_err);
audit_fs(self, "astrid:fs/host.fs-readdir", &path, &result);
result
}
fn fs_stat(&mut self, path: String) -> Result<FileStat, ErrorCode> {
let resolved = resolve_path(self, &path).map_err(map_resolve_err)?;
gate_read(self, &resolved.physical)?;
let vfs_path = resolve_vfs(self, &resolved).map_err(map_resolve_err)?;
let result =
util::bounded_block_on(&self.runtime_handle, &self.blocking_semaphore, async {
vfs_path
.vfs
.stat(
&vfs_path.handle,
vfs_path.relative.to_string_lossy().as_ref(),
)
.await
})
.map(|m| to_file_stat(&m))
.map_err(map_vfs_err);
audit_fs(self, "astrid:fs/host.fs-stat", &path, &result);
result
}
fn fs_stat_symlink(&mut self, _path: String) -> Result<FileStat, ErrorCode> {
Err(ErrorCode::Unknown(
"fs-stat-symlink: lstat port pending".to_string(),
))
}
fn fs_unlink(&mut self, path: String) -> Result<(), ErrorCode> {
let resolved = resolve_path(self, &path).map_err(map_resolve_err)?;
gate_write(self, &resolved.physical)?;
let vfs_path = resolve_vfs(self, &resolved).map_err(map_resolve_err)?;
let result =
util::bounded_block_on(&self.runtime_handle, &self.blocking_semaphore, async {
vfs_path
.vfs
.unlink(
&vfs_path.handle,
vfs_path.relative.to_string_lossy().as_ref(),
)
.await
})
.map_err(map_vfs_err);
audit_fs(self, "astrid:fs/host.fs-unlink", &path, &result);
result
}
fn read_file(&mut self, path: String) -> Result<Vec<u8>, ErrorCode> {
let resolved = resolve_path(self, &path).map_err(map_resolve_err)?;
gate_read(self, &resolved.physical)?;
let vfs_path = resolve_vfs(self, &resolved).map_err(map_resolve_err)?;
const TOO_LARGE_TAG: &str = "astrid-read-file:too-large";
let result =
util::bounded_block_on(&self.runtime_handle, &self.blocking_semaphore, async {
let metadata = vfs_path
.vfs
.stat(
&vfs_path.handle,
vfs_path.relative.to_string_lossy().as_ref(),
)
.await?;
if metadata.size > util::MAX_GUEST_PAYLOAD_LEN {
return Err(astrid_vfs::VfsError::PermissionDenied(
TOO_LARGE_TAG.to_string(),
));
}
let handle = vfs_path
.vfs
.open(
&vfs_path.handle,
vfs_path.relative.to_string_lossy().as_ref(),
false,
false,
)
.await?;
let data = vfs_path.vfs.read(&handle).await;
let _ = vfs_path.vfs.close(&handle).await;
data
})
.map_err(|e| {
if matches!(&e, astrid_vfs::VfsError::PermissionDenied(msg) if msg == TOO_LARGE_TAG)
{
ErrorCode::TooLarge
} else {
map_vfs_err(e)
}
});
let result = result.and_then(|data| {
if data.len() as u64 > util::MAX_GUEST_PAYLOAD_LEN {
Err(ErrorCode::TooLarge)
} else {
Ok(data)
}
});
audit_fs(self, "astrid:fs/host.read-file", &path, &result);
result
}
fn write_file(&mut self, path: String, content: Vec<u8>) -> Result<(), ErrorCode> {
if content.len() as u64 > util::MAX_GUEST_PAYLOAD_LEN {
return Err(ErrorCode::TooLarge);
}
let resolved = resolve_path(self, &path).map_err(map_resolve_err)?;
gate_write(self, &resolved.physical)?;
let vfs_path = resolve_vfs(self, &resolved).map_err(map_resolve_err)?;
let result =
util::bounded_block_on(&self.runtime_handle, &self.blocking_semaphore, async {
let handle = vfs_path
.vfs
.open(
&vfs_path.handle,
vfs_path.relative.to_string_lossy().as_ref(),
true,
true,
)
.await?;
let res = vfs_path.vfs.write(&handle, &content).await;
let _ = vfs_path.vfs.close(&handle).await;
res
})
.map_err(map_vfs_err);
audit_fs(self, "astrid:fs/host.write-file", &path, &result);
result
}
fn fs_append(&mut self, _path: String, _content: Vec<u8>) -> Result<(), ErrorCode> {
Err(ErrorCode::Unknown(
"fs-append: append-mode port pending".to_string(),
))
}
fn fs_copy(&mut self, _src: String, _dst: String) -> Result<(), ErrorCode> {
Err(ErrorCode::Unknown(
"fs-copy: VFS copy port pending".to_string(),
))
}
fn fs_rename(&mut self, _src: String, _dst: String) -> Result<(), ErrorCode> {
Err(ErrorCode::Unknown(
"fs-rename: VFS rename port pending".to_string(),
))
}
fn fs_remove_dir_all(&mut self, _path: String) -> Result<u64, ErrorCode> {
Err(ErrorCode::Unknown(
"fs-remove-dir-all: recursive remove port pending".to_string(),
))
}
fn fs_canonicalize(&mut self, _path: String) -> Result<String, ErrorCode> {
Err(ErrorCode::Unknown(
"fs-canonicalize: VFS-scheme canonicalization port pending".to_string(),
))
}
fn fs_read_link(&mut self, _path: String) -> Result<String, ErrorCode> {
Err(ErrorCode::Unknown(
"fs-read-link: readlink port pending".to_string(),
))
}
fn fs_hard_link(&mut self, _src: String, _link_path: String) -> Result<(), ErrorCode> {
Err(ErrorCode::Unknown(
"fs-hard-link: cross-scheme guard + hard-link port pending".to_string(),
))
}
}