///|
/// A WASI errno that the sandbox gives.
#derive(Eq, Debug)
pub(all) enum Errno {
Success
/// 8: the fd is not open; the sandbox has only fds 0, 1 and 2.
Badf
/// 21: an address that the guest gave is outside its memory.
Fault
/// 28: an argument is not valid, for example an unknown clock id.
Inval
/// 76: the sandbox does not give the guest this capability.
Notcapable
}
///|
pub extend Errno with Eq::{equal, not_equal}
///|
pub extend Errno with @debug.Debug::{to_repr}
///|
/// The number of the errno.
pub fn Errno::code(self : Errno) -> Int32 {
match self {
Success => 0
Badf => 8
Fault => 21
Inval => 28
Notcapable => 76
}
}
///|
/// A WASI clock.
#derive(Eq, Debug)
pub(all) enum ClockId {
Realtime
Monotonic
ProcessCputime
ThreadCputime
}
///|
pub extend ClockId with Eq::{equal, not_equal}
///|
pub extend ClockId with @debug.Debug::{to_repr}
///|
fn ClockId::parse(id : Int64) -> ClockId? {
match id {
0L => Some(Realtime)
1L => Some(Monotonic)
2L => Some(ProcessCputime)
3L => Some(ThreadCputime)
_ => None
}
}
///|
/// A clock source of the embedder: the time and the resolution of each clock, in nanoseconds.
pub(all) struct Clock {
time : (ClockId) -> UInt64
resolution : (ClockId) -> UInt64
}
///|
/// The result of a sandbox call.
#derive(Eq, Debug)
pub(all) enum Outcome {
/// The function returns this errno to the guest.
Return(Errno)
/// `proc_exit` with this code: the call of the guest ends.
Exit(Int32)
}
///|
pub extend Outcome with Eq::{equal, not_equal}
///|
pub extend Outcome with @debug.Debug::{to_repr}
///|
/// A sink bound that breaks its rule: the bytes and the rule.
#derive(Eq, Debug)
pub(all) suberror SinkBoundError {
SinkBoundError(Int32, String)
}
///|
pub extend SinkBoundError with Eq::{equal, not_equal}
///|
pub extend SinkBoundError with @debug.Debug::{to_repr}
///|
/// The most bytes that one `fd_write` gives a sink: 1 byte to 16 MiB. The bound applies to each call;
/// a guest that writes in a loop makes many calls, so the embedder's sink bounds the total itself, for
/// example with a tail of the last bytes.
#derive(Eq, Debug)
pub struct SinkBound {
priv bytes : Int32
}
///|
pub extend SinkBound with Eq::{equal, not_equal}
///|
pub extend SinkBound with @debug.Debug::{to_repr}
///|
/// The largest sink bound: 16 MiB.
let max_sink_bound : Int32 = 16 * 1024 * 1024
///|
/// A sink bound of `bytes` bytes, from 1 to 16 MiB.
pub fn SinkBound::parse(bytes : Int32) -> SinkBound raise SinkBoundError {
guard bytes >= 1 && bytes <= max_sink_bound else {
raise SinkBoundError(
bytes,
"a sink bound is from 1 byte to \{max_sink_bound} bytes",
)
}
{ bytes, }
}
///|
/// The default sink bound: 1 MiB.
pub fn SinkBound::default() -> SinkBound {
{ bytes: 1024 * 1024, }
}
///|
pub fn SinkBound::bytes(self : SinkBound) -> Int32 {
self.bytes
}
///|
/// A sink of the embedder and its bound.
priv struct Sink {
write : (BytesView) -> Unit
bound : SinkBound
}
///|
/// The WASI preview 1 sandbox of a plugin: no preopens, empty arguments, environment and stdin, clocks
/// and random bytes from sources that the embedder gives, and stdout and stderr to sinks that the
/// embedder gives. Every other function returns `ENOTCAPABLE` or `EBADF`. The builder steps give a new
/// sandbox.
pub struct Sandbox {
priv clock : Clock?
priv random : ((Int32) -> Bytes)?
priv stdout : Sink?
priv stderr : Sink?
}
///|
/// A sandbox with no clock, no random source and no sinks.
pub fn Sandbox::new() -> Sandbox {
{ clock: None, random: None, stdout: None, stderr: None, }
}
///|
/// The sandbox with `clock` as its clock source.
pub fn Sandbox::clock(self : Sandbox, clock : Clock) -> Sandbox {
{ ..self, clock: Some(clock), }
}
///|
/// The sandbox with a random source, which gives the number of bytes that it is asked for.
pub fn Sandbox::random(self : Sandbox, source : (Int32) -> Bytes) -> Sandbox {
{ ..self, random: Some(source), }
}
///|
/// The sandbox with a sink for the bytes that the guest writes to fd 1. One `fd_write` gives the sink
/// at most `bound` bytes, one iovec at a time; it writes the first `bound` bytes and reports them, as a
/// POSIX `write` that writes part of its bytes.
pub fn Sandbox::stdout(
self : Sandbox,
sink : (BytesView) -> Unit,
bound~ : SinkBound = SinkBound::default(),
) -> Sandbox {
{ ..self, stdout: Some({ write: sink, bound, }), }
}
///|
/// The sandbox with a sink for the bytes that the guest writes to fd 2, with the rules of `stdout`.
pub fn Sandbox::stderr(
self : Sandbox,
sink : (BytesView) -> Unit,
bound~ : SinkBound = SinkBound::default(),
) -> Sandbox {
{ ..self, stderr: Some({ write: sink, bound, }), }
}
///|
/// A file type: a character device.
let character_device : Int32 = 2
///|
/// The `fd_read` right.
let right_fd_read : UInt64 = 1UL << 1
///|
/// The `fd_write` right.
let right_fd_write : UInt64 = 1UL << 6
///|
/// The indices of the fd arguments of a function, in its core signature.
fn fd_arguments(function : Function) -> Array[Int32] {
match function {
FdRenumber => [0, 1]
PathSymlink => [2]
PathLink => [0, 4]
PathRename => [0, 3]
_ =>
if function.name().has_prefix("fd_") ||
function.name().has_prefix("path_") ||
function.name().has_prefix("sock_") {
[0]
} else {
[]
}
}
}
///|
fn standard(fd : Int64) -> Bool {
fd >= 0L && fd <= 2L
}
///|
/// An argument as an unsigned 32-bit number: an address, a length or an fd.
fn unsigned(value : Int64) -> Int64 {
value & 0xffffffffL
}
///|
/// Runs the preview 1 function `function` with the arguments `args` (an `i32` as its value, an `i64` as
/// its value) on the guest memory `memory`.
pub fn[M : GuestMemory] Sandbox::call(
self : Sandbox,
function : Function,
memory : M,
args : Array[Int64],
) -> Outcome {
if args.length() != function.params().length() {
return Return(Inval)
}
let arg = (i : Int32) => unsigned(args[i])
if fd_arguments(function).iter().any(i => !standard(arg(i))) {
return Return(Badf)
}
let errno = self.answer(function, memory, arg) catch {
OutOfBounds(_, _) => Fault
}
match function {
ProcExit => Exit(args[0].to_int())
_ => Return(errno)
}
}
///|
fn[M : GuestMemory] Sandbox::answer(
self : Sandbox,
function : Function,
memory : M,
arg : (Int32) -> Int64,
) -> Errno raise GuestMemoryError {
match function {
ArgsSizesGet | EnvironSizesGet => {
write_u32(memory, arg(0), 0L)
write_u32(memory, arg(1), 0L)
Success
}
ArgsGet | EnvironGet => Success
FdRead =>
if arg(0) == 0L {
write_u32(memory, arg(3), 0L)
Success
} else {
Notcapable
}
FdWrite => {
let sink = match arg(0) {
1L => Some(self.stdout)
2L => Some(self.stderr)
_ => None
}
guard sink is Some(sink) else { return Notcapable }
// Every range is checked before any byte goes to the sink.
guard iovecs(memory, arg(1), arg(2)) is Some(pairs) else { return Inval }
guard inside(memory, arg(3), 4L) else { raise OutOfBounds(arg(3), 4) }
let bound = match sink {
Some(sink) => sink.bound.bytes().to_int64()
None => SinkBound::default().bytes().to_int64()
}
let mut written = 0L
for pair in pairs {
let (buffer, length) = pair
let part = @cmp.minimum(length, bound - written)
if part > 0L && sink is Some(sink) {
(sink.write)(memory.read(buffer, part.to_int()))
}
written = written + part
}
write_u32(memory, arg(3), written)
Success
}
FdFdstatGet => {
let rights = if arg(0) == 0L { right_fd_read } else { right_fd_write }
let stat = Bytes::makei(24, i => {
if i == 0 {
character_device.to_byte()
} else {
b'\x00'
}
})
memory.write(arg(1), stat)
write_u64(memory, arg(1) + 8L, rights)
write_u64(memory, arg(1) + 16L, 0UL)
Success
}
FdFilestatGet => {
let stat = Bytes::makei(64, i => {
if i == 16 {
character_device.to_byte()
} else {
b'\x00'
}
})
memory.write(arg(1), stat)
Success
}
ClockResGet | ClockTimeGet => {
guard self.clock is Some(clock) else { return Notcapable }
guard ClockId::parse(arg(0)) is Some(id) else { return Inval }
if function is ClockResGet {
write_u64(memory, arg(1), (clock.resolution)(id))
} else {
write_u64(memory, arg(2), (clock.time)(id))
}
Success
}
RandomGet => {
guard self.random is Some(source) else { return Notcapable }
// The range is checked before the embedder's source runs; a length of 2^31 or more is larger
// than a guest memory that the engines give.
guard arg(1) < 0x80000000L && inside(memory, arg(0), arg(1)) else {
raise OutOfBounds(arg(0), arg(1).to_int())
}
let length = arg(1).to_int()
let bytes = source(length)
guard bytes.length() == length else { return Notcapable }
memory.write(arg(0), bytes)
Success
}
ProcExit => Success
_ => Notcapable
}
}