Skip to content
Merged
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
122 changes: 121 additions & 1 deletion ext/src/ruby_api/wasi_config.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,51 @@
use super::root;
use crate::error;
use crate::helpers::OutputLimitedBuffer;
use crate::ruby_api::convert::ToValType;
use crate::{define_rb_intern, helpers::SymbolEnum};
use lazy_static::lazy_static;
use magnus::{
class, function, gc::Marker, method, typed_data::Obj, value::Opaque, DataTypeFunctions, Error,
Module, Object, RArray, RHash, RString, Ruby, TryConvert, TypedData,
IntoValue, Module, Object, RArray, RHash, RString, Ruby, Symbol, TryConvert, TypedData, Value,
};
use rb_sys::ruby_rarray_flags::RARRAY_EMBED_FLAG;
use std::cell::RefCell;
use std::convert::TryFrom;
use std::fs;
use std::path::Path;
use std::{fs::File, path::PathBuf};
use wasmtime_wasi::p2::pipe::MemoryInputPipe;
use wasmtime_wasi::p2::{OutputFile, WasiCtx, WasiCtxBuilder};
use wasmtime_wasi::preview1::WasiP1Ctx;
use wasmtime_wasi::{DirPerms, FilePerms};

define_rb_intern!(
READ => "read",
WRITE => "write",
MUTATE => "mutate",
ALL => "all",
);

lazy_static! {
static ref FILE_PERMS_MAPPING: SymbolEnum<'static, FilePerms> = {
let mapping = vec![
(*READ, FilePerms::READ),
(*WRITE, FilePerms::WRITE),
(*ALL, FilePerms::all()),
];

SymbolEnum::new(":file_perms", mapping)
};
static ref DIR_PERMS_MAPPING: SymbolEnum<'static, DirPerms> = {
let mapping = vec![
(*READ, DirPerms::READ),
(*MUTATE, DirPerms::MUTATE),
(*ALL, DirPerms::all()),
];

SymbolEnum::new(":dir_perms", mapping)
};
}

enum ReadStream {
Inherit,
Expand Down Expand Up @@ -43,6 +78,24 @@ impl WriteStream {
}
}

struct PermsSymbolEnum(Symbol);

#[derive(Clone)]
struct MappedDirectory {
host_path: Opaque<RString>,
guest_path: Opaque<RString>,
dir_perms: Opaque<Symbol>,
file_perms: Opaque<Symbol>,
}
impl MappedDirectory {
pub fn mark(&self, marker: &Marker) {
marker.mark(self.host_path);
marker.mark(self.guest_path);
marker.mark(self.dir_perms);
marker.mark(self.file_perms);
}
}

#[derive(Default)]
struct WasiConfigInner {
stdin: Option<ReadStream>,
Expand All @@ -51,6 +104,7 @@ struct WasiConfigInner {
env: Option<Opaque<RHash>>,
args: Option<Opaque<RArray>>,
deterministic: bool,
mapped_directories: Vec<MappedDirectory>,
}

impl WasiConfigInner {
Expand All @@ -70,6 +124,23 @@ impl WasiConfigInner {
if let Some(v) = self.args.as_ref() {
marker.mark(*v);
}
for v in &self.mapped_directories {
v.mark(marker);
}
}
}

impl TryFrom<PermsSymbolEnum> for DirPerms {
type Error = magnus::Error;
fn try_from(value: PermsSymbolEnum) -> Result<Self, Error> {
DIR_PERMS_MAPPING.get(value.0.into_value())
}
}

impl TryFrom<PermsSymbolEnum> for FilePerms {
type Error = magnus::Error;
fn try_from(value: PermsSymbolEnum) -> Result<Self, Error> {
FILE_PERMS_MAPPING.get(value.0.into_value())
}
}

Expand Down Expand Up @@ -233,6 +304,34 @@ impl WasiConfig {
rb_self
}

/// @yard
/// Set mapped directory for host path and guest path.
/// @param host_path [String]
/// @param guest_path [String]
/// @param dir_perms [Symbol] Directory permissions, one of :read, :mutate, or :all
/// @param file_perms [Symbol] File permissions, one of :read, :write, or :all
/// @def set_mapped_directory(host_path, guest_path, dir_perms, file_perms)
/// @return [WasiConfig] +self+
pub fn set_mapped_directory(
rb_self: RbSelf,
host_path: RString,
guest_path: RString,
dir_perms: Symbol,
file_perms: Symbol,
) -> RbSelf {
let mapped_dir = MappedDirectory {
host_path: host_path.into(),
guest_path: guest_path.into(),
dir_perms: dir_perms.into(),
file_perms: file_perms.into(),
};

let mut inner = rb_self.inner.borrow_mut();
inner.mapped_directories.push(mapped_dir);

rb_self
}

pub fn build_p1(&self, ruby: &Ruby) -> Result<WasiP1Ctx, Error> {
let mut builder = self.build_impl(ruby)?;
let ctx = builder.build_p1();
Expand Down Expand Up @@ -317,6 +416,22 @@ impl WasiConfig {
deterministic_wasi_ctx::add_determinism_to_wasi_ctx_builder(&mut builder);
}

for mapped_dir in &inner.mapped_directories {
let host_path = ruby.get_inner(mapped_dir.host_path).to_string()?;
let guest_path = ruby.get_inner(mapped_dir.guest_path).to_string()?;
let dir_perms = ruby.get_inner(mapped_dir.dir_perms);
let file_perms = ruby.get_inner(mapped_dir.file_perms);

builder
.preopened_dir(
Path::new(&host_path),
&guest_path,
PermsSymbolEnum(dir_perms).try_into()?,
PermsSymbolEnum(file_perms).try_into()?,
)
.map_err(|e| error!("{}", e))?;
}

Ok(builder)
}
}
Expand Down Expand Up @@ -355,5 +470,10 @@ pub fn init() -> Result<(), Error> {

class.define_method("set_argv", method!(WasiConfig::set_argv, 1))?;

class.define_method(
"set_mapped_directory",
method!(WasiConfig::set_mapped_directory, 4),
)?;

Ok(())
}
Binary file added spec/fixtures/wasi-fs-p2.wasm
Binary file not shown.
Binary file added spec/fixtures/wasi-fs.wasm
Binary file not shown.
2 changes: 2 additions & 0 deletions spec/fixtures/wasi-fs/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
target = "wasm32-wasip1"
10 changes: 10 additions & 0 deletions spec/fixtures/wasi-fs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "wasi-fs"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[workspace]

[dependencies]
14 changes: 14 additions & 0 deletions spec/fixtures/wasi-fs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Example WASI program used to test WASI preopened directories

To update:

```shell
cargo build --release && \
wasm-opt -O \
--enable-bulk-memory \
target/wasm32-wasip1/release/wasi-fs.wasm \
-o ../wasi-fs.wasm && \
cargo build --target=wasm32-wasip2 --release && \
cp target/wasm32-wasip2/release/wasi-fs.wasm \
../wasi-fs-p2.wasm
```
18 changes: 18 additions & 0 deletions spec/fixtures/wasi-fs/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use std::io::{Write, Read};
use std::fs::File;

fn main() {
let args: Vec<String> = std::env::args().collect();

let counter_file = File::open(&args[1]).expect("failed to open counter file");

let mut counter_str = String::new();
counter_file.take(100).read_to_string(&mut counter_str)
.expect("failed to read counter file");
let mut counter: u32 = counter_str.trim().parse().expect("failed to parse counter");

counter += 1;

let mut counter_file = File::create(&args[1]).expect("failed to create counter file");
write!(counter_file, "{}", counter).expect("failed to write counter file");
}
111 changes: 111 additions & 0 deletions spec/unit/wasi_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ module Wasmtime
# Compile module only once for speed
@compiled_wasi_module = @engine.precompile_module(IO.binread("spec/fixtures/wasi-debug.wasm"))
@compiled_wasi_deterministic_module = @engine.precompile_module(IO.binread("spec/fixtures/wasi-deterministic.wasm"))
@compiled_wasi_fs_module = @engine.precompile_module(IO.binread("spec/fixtures/wasi-fs.wasm"))

@compiled_wasi_component = @engine.precompile_component(IO.binread("spec/fixtures/wasi-debug-p2.wasm"))
@compiled_wasi_deterministic_component = @engine.precompile_component(IO.binread("spec/fixtures/wasi-deterministic-p2.wasm"))
@compiled_wasi_fs_component = @engine.precompile_component(IO.binread("spec/fixtures/wasi-fs-p2.wasm"))
end

describe "Linker.new" do
Expand Down Expand Up @@ -233,13 +235,103 @@ module Wasmtime
end
end
end

it "writes to mapped directory" do
Dir.mkdir(tempfile_path("tmp"))
File.write(tempfile_path(File.join("tmp", "counter")), "0")

wasi_config = WasiConfig.new
.set_argv(["wasi-fs", "/tmp/counter"])
.set_mapped_directory(tempfile_path("tmp"), "/tmp", :all, :all)

expect { run_fs.call(wasi_config) }.not_to raise_error

expect(File.read(tempfile_path(File.join("tmp", "counter")))).to eq("1")
end

it "fails to write to mapped directory if not permitted" do
Dir.mkdir(tempfile_path("tmp"))
File.write(tempfile_path(File.join("tmp", "counter")), "0")

stderr_str = ""
wasi_config = WasiConfig.new
.set_argv(["wasi-fs", "/tmp/counter"])
.set_stderr_buffer(stderr_str, 40000)
.set_mapped_directory(tempfile_path("tmp"), "/tmp", :read, :read)

expect { run_fs.call(wasi_config) }.to raise_error do |error|
expect(error).to be_a(Wasmtime::Error)
end

expect(stderr_str).to match(/failed to create counter file/)

expect(File.read(tempfile_path(File.join("tmp", "counter")))).to eq("0")
end

it "fails to read from mapped directory if not permitted" do
Dir.mkdir(tempfile_path("tmp"))
File.write(tempfile_path(File.join("tmp", "counter")), "0")

stderr_str = ""
wasi_config = WasiConfig.new
.set_argv(["wasi-fs", "/tmp/counter"])
.set_stderr_buffer(stderr_str, 40000)
.set_mapped_directory(tempfile_path("tmp"), "/tmp", :mutate, :write)

expect { run_fs.call(wasi_config) }.to raise_error do |error|
expect(error).to be_a(Wasmtime::Error)
end

expect(stderr_str).to match(/failed to open counter file/)

expect(File.read(tempfile_path(File.join("tmp", "counter")))).to eq("0")
end

it "fails to access non-mapped directories" do
Dir.mkdir(tempfile_path("tmp"))
File.write(tempfile_path(File.join("tmp", "counter")), "0")

stderr_str = ""
wasi_config = WasiConfig.new
.set_argv(["wasi-fs", File.join(tempfile_path("tmp"), "counter")])
.set_stderr_buffer(stderr_str, 40000)

expect { run_fs.call(wasi_config) }.to raise_error do |error|
expect(error).to be_a(Wasmtime::Error)
end

expect(stderr_str).to match(/failed to find a pre-opened file descriptor/)

expect(File.read(tempfile_path(File.join("tmp", "counter")))).to eq("0")
end

it "does not accept an invalid host path" do
wasi_config = WasiConfig.new
.set_mapped_directory(tempfile_path("tmp"), "/tmp", :all, :all)

expect { run_fs.call(wasi_config) }.to raise_error do |error|
expect(error).to be_a(Wasmtime::Error)
# error message is os-specific
end
end

it "does not accept invalid permissions" do
wasi_config = WasiConfig.new
.set_mapped_directory(tempfile_path("tmp"), "/tmp", :mutate, :invalid_permission)

expect { run_fs.call(wasi_config) }.to raise_error do |error|
expect(error).to be_a(ArgumentError)
expect(error.message).to match(/invalid :file_perms, expected one of \[:read, :write, :all\], got :invalid_permission/)
end
end
end

describe "WasiConfig preview 1" do
it_behaves_like WasiConfig do
let(:run) { method(:run_wasi_module) }
let(:wasi_env) { method(:wasi_module_env) }
let(:run_deterministic) { method(:run_wasi_module_deterministic) }
let(:run_fs) { method(:run_wasi_module_fs) }
end
end

Expand All @@ -248,6 +340,7 @@ module Wasmtime
let(:run) { method(:run_wasi_component) }
let(:wasi_env) { method(:wasi_component_env) }
let(:run_deterministic) { method(:run_wasi_component_deterministic) }
let(:run_fs) { method(:run_wasi_component_fs) }
end
end

Expand All @@ -272,6 +365,13 @@ def run_wasi_module_deterministic(wasi_config)
.invoke("_start")
end

def run_wasi_module_fs(wasi_config)
linker = Linker.new(@engine)
WASI::P1.add_to_linker_sync(linker)
store = Store.new(@engine, wasi_p1_config: wasi_config)
linker.instantiate(store, Module.deserialize(@engine, @compiled_wasi_fs_module)).invoke("_start")
end

def wasi_module_env
stdout_file = tempfile_path("stdout")

Expand Down Expand Up @@ -318,6 +418,17 @@ def run_wasi_component_deterministic(wasi_config)
).call_run(store)
end

def run_wasi_component_fs(wasi_config)
linker = Component::Linker.new(@engine)
WASI::P2.add_to_linker_sync(linker)
store = Store.new(@engine, wasi_config: wasi_config)
Component::WasiCommand.new(
store,
Component::Component.deserialize(@engine, @compiled_wasi_fs_component),
linker
).call_run(store)
end

def tempfile_path(name)
File.join(tmpdir, name)
end
Expand Down
Loading