Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 150 additions & 22 deletions src/builtins/runtime/io.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::fs::OpenOptions;
use std::future::Future;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicU32, Ordering};
Expand Down Expand Up @@ -204,7 +205,16 @@ pub(super) fn builtin_io_open(
path: &str,
mode: &str,
) -> VmResult<HostCallResult<i64>> {
let path = path.to_string();
let writes = match mode {
"r" => false,
"w" | "a" | "r+" | "w+" | "a+" => true,
other => {
return Err(VmError::HostError(format!(
"unsupported io_open mode '{other}', expected r/w/a/r+/w+/a+"
)));
}
};
let path = authorize_io_path(vm, path, writes)?;
let mode = mode.to_string();
let op_id = schedule_io_task(vm, None, move || {
let mut options = OpenOptions::new();
Expand Down Expand Up @@ -260,6 +270,16 @@ pub(super) fn builtin_io_popen(
"unsupported io_popen mode '{mode}', expected r or w"
)));
}
if vm
.host
.io_policy
.as_ref()
.is_some_and(|policy| !policy.allow_process)
{
return Err(VmError::HostError(
"io_popen requires the process capability".to_string(),
));
}
let command = command.to_string();
let mode = mode.to_string();
let op_id = schedule_io_task(vm, None, move || {
Expand Down Expand Up @@ -298,23 +318,31 @@ pub(super) fn builtin_io_popen(
/// Reads all remaining text from an I/O handle.
#[pd_host_function(name = "io::read_all")]
pub(super) fn builtin_io_read_all(vm: &mut Vm, handle_id: i64) -> VmResult<HostCallResult<String>> {
let max_read_bytes = vm
.host
.io_policy
.as_ref()
.map(|policy| policy.max_read_bytes);
let handle = resource_handle(handle_id)?;
let resource = io_resource_for_handle(vm, handle)?;
let op_id = schedule_io_task(vm, Some(handle), move || {
let result = resource.with_handle_mut(|handle| {
let mut out = String::new();
match handle {
IoHandle::File(file) => file
.read_to_string(&mut out)
.map_err(|err| VmError::HostError(format!("io_read_all failed: {err}")))?,
IoHandle::PopenRead { child } => child
.stdout
.as_mut()
.ok_or_else(|| {
VmError::HostError("io_read_all popen handle missing stdout".to_string())
})?
.read_to_string(&mut out)
.map_err(|err| VmError::HostError(format!("io_read_all failed: {err}")))?,
IoHandle::File(file) => {
read_to_string_with_limit(file, max_read_bytes, &mut out)?;
}
IoHandle::PopenRead { child } => {
read_to_string_with_limit(
child.stdout.as_mut().ok_or_else(|| {
VmError::HostError(
"io_read_all popen handle missing stdout".to_string(),
)
})?,
max_read_bytes,
&mut out,
)?;
}
IoHandle::PopenWrite { .. } => {
return Err(VmError::HostError(
"io_read_all requires a readable handle".to_string(),
Expand All @@ -334,17 +362,23 @@ pub(super) fn builtin_io_read_line(
vm: &mut Vm,
handle_id: i64,
) -> VmResult<HostCallResult<String>> {
let max_read_bytes = vm
.host
.io_policy
.as_ref()
.map(|policy| policy.max_read_bytes);
let handle = resource_handle(handle_id)?;
let resource = io_resource_for_handle(vm, handle)?;
let op_id = schedule_io_task(vm, Some(handle), move || {
let result = resource.with_handle_mut(|handle| {
let line = match handle {
IoHandle::File(file) => read_line_from_reader(file)?,
IoHandle::PopenRead { child } => {
read_line_from_reader(child.stdout.as_mut().ok_or_else(|| {
IoHandle::File(file) => read_line_from_reader(file, max_read_bytes)?,
IoHandle::PopenRead { child } => read_line_from_reader(
child.stdout.as_mut().ok_or_else(|| {
VmError::HostError("io_read_line popen handle missing stdout".to_string())
})?)?
}
})?,
max_read_bytes,
)?,
IoHandle::PopenWrite { .. } => {
return Err(VmError::HostError(
"io_read_line requires a readable handle".to_string(),
Expand All @@ -365,6 +399,14 @@ pub(super) fn builtin_io_write(
handle_id: i64,
text: &str,
) -> VmResult<HostCallResult<i64>> {
if let Some(policy) = vm.host.io_policy.as_ref()
&& text.len() > policy.max_write_bytes
{
return Err(VmError::HostError(format!(
"io_write exceeds the configured write limit of {} bytes",
policy.max_write_bytes
)));
}
let bytes = text.as_bytes().to_vec();
let handle = resource_handle(handle_id)?;
let resource = io_resource_for_handle(vm, handle)?;
Expand Down Expand Up @@ -445,15 +487,65 @@ pub(super) fn builtin_io_close(vm: &mut Vm, handle_id: i64) -> VmResult<HostCall
/// Returns whether a file system path exists.
#[pd_host_function(name = "io::exists")]
pub(super) fn builtin_io_exists(vm: &mut Vm, path: &str) -> VmResult<HostCallResult<bool>> {
let path = path.to_string();
let path = authorize_io_path(vm, path, false)?;
let op_id = schedule_io_task(vm, None, move || {
IoAsyncCompletion::result(Ok(CallReturn::one(Value::Bool(
std::path::Path::new(path.as_str()).exists(),
))))
IoAsyncCompletion::result(Ok(CallReturn::one(Value::Bool(path.exists()))))
})?;
Ok(HostCallResult::Pending(op_id))
}

fn authorize_io_path(vm: &Vm, path: &str, writes: bool) -> VmResult<PathBuf> {
let requested = PathBuf::from(path);
let Some(policy) = vm.host.io_policy.as_ref() else {
return Ok(requested);
};
if writes && !policy.allow_write {
return Err(VmError::HostError(
"io path write requires the write capability".to_string(),
));
}
let absolute = if requested.is_absolute() {
requested
} else {
std::env::current_dir()
.map_err(|error| VmError::HostError(format!("io path resolution failed: {error}")))?
.join(requested)
};
let canonical = canonicalize_io_target(&absolute)?;
for root in &policy.allowed_roots {
let root = Path::new(root).canonicalize().map_err(|error| {
VmError::HostError(format!(
"io allowed root '{root}' cannot be resolved: {error}"
))
})?;
if canonical.starts_with(root) {
return Ok(canonical);
}
}
Err(VmError::HostError(format!(
"io path '{}' is outside the allowed roots",
canonical.display()
)))
}

fn canonicalize_io_target(path: &Path) -> VmResult<PathBuf> {
if path.exists() {
return path
.canonicalize()
.map_err(|error| VmError::HostError(format!("io path resolution failed: {error}")));
}
let parent = path
.parent()
.ok_or_else(|| VmError::HostError(format!("io path '{}' has no parent", path.display())))?;
let file_name = path.file_name().ok_or_else(|| {
VmError::HostError(format!("io path '{}' has no file name", path.display()))
})?;
parent
.canonicalize()
.map(|parent| parent.join(file_name))
.map_err(|error| VmError::HostError(format!("io path resolution failed: {error}")))
}

fn spawn_shell_command(command: &str, mode: &str) -> VmResult<Child> {
let mut process = if cfg!(windows) {
let mut cmd = Command::new("cmd");
Expand Down Expand Up @@ -882,7 +974,37 @@ fn terminate_process_tree(process_id: u32) -> VmResult<()> {
)))
}

fn read_line_from_reader(reader: &mut impl Read) -> VmResult<String> {
fn read_to_string_with_limit(
reader: &mut impl Read,
max_read_bytes: Option<usize>,
out: &mut String,
) -> VmResult<()> {
match max_read_bytes {
None => {
reader
.read_to_string(out)
.map_err(|err| VmError::HostError(format!("io_read_all failed: {err}")))?;
}
Some(limit) => {
let take_limit = u64::try_from(limit).unwrap_or(u64::MAX).saturating_add(1);
reader
.take(take_limit)
.read_to_string(out)
.map_err(|err| VmError::HostError(format!("io_read_all failed: {err}")))?;
if out.len() > limit {
return Err(VmError::HostError(format!(
"io_read_all exceeds the configured read limit of {limit} bytes"
)));
}
}
}
Ok(())
}

fn read_line_from_reader(
reader: &mut impl Read,
max_read_bytes: Option<usize>,
) -> VmResult<String> {
let mut bytes = Vec::new();
let mut one = [0u8; 1];
loop {
Expand All @@ -893,6 +1015,12 @@ fn read_line_from_reader(reader: &mut impl Read) -> VmResult<String> {
break;
}
bytes.push(one[0]);
if max_read_bytes.is_some_and(|limit| bytes.len() > limit) {
return Err(VmError::HostError(format!(
"io_read_line exceeds the configured read limit of {} bytes",
max_read_bytes.expect("read limit should be present")
)));
}
if one[0] == b'\n' {
break;
}
Expand Down
13 changes: 7 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,13 @@ pub use jit::{
pub use vm::diagnostics::render_vm_error;
#[cfg(feature = "runtime")]
pub use vm::{
AotArtifactError, CallOutcome, CallReturn, CancellationReason, DEFAULT_MAX_SCRIPT_CALL_DEPTH,
EpochCheckpoint, EpochHandle, FuelCheckpoint, HostArgsFunction, HostAsyncBridge,
HostBindingPlan, HostFunction, HostFunctionRegistry, HostOpId, HostStackFunction,
IntoScriptValue, QueuedScriptInvocation, ScriptArgs, ScriptCallback, ScriptResult,
StaticHostArgsFunction, StaticHostFunction, StaticHostStackFunction, Store, Vm, VmError,
VmResult, VmStatus, VmYieldReason,
AotArtifactError, CallOutcome, CallReturn, CancellationReason, CapabilityProfile,
CapabilityProfileBuilder, DEFAULT_MAX_SCRIPT_CALL_DEPTH, EpochCheckpoint, EpochHandle,
FuelCheckpoint, HostArgsFunction, HostAsyncBridge, HostBindingPlan, HostFunction,
HostFunctionRegistry, HostOpId, HostStackFunction, IntoScriptValue, IoPolicy,
QueuedScriptInvocation, ScriptArgs, ScriptCallback, ScriptResult, StaticHostArgsFunction,
StaticHostFunction, StaticHostStackFunction, Store, Vm, VmError, VmResult, VmStatus,
VmYieldReason,
};
#[cfg(feature = "sqlite")]
pub use vm::{SqliteLimits, SqlitePolicy};
Expand Down
Loading
Loading