Skip to content

Commit 5f4de3f

Browse files
Implement preopened_dir using set_mapped_directory setting (#498)
* Implement preopened_dir using set_mapped_directory setting Add support for directory and file permissions * Add tests for mapped directories * Use rust struct instead of nested RArrays * Model enums as SymbolEnums * Lazily convert ruby MappedDirectory values to rust --------- Co-authored-by: Ahmed Al Hafoudh <[email protected]> Co-authored-by: william-stacken <[email protected]>
1 parent a36c0a5 commit 5f4de3f

File tree

8 files changed

+276
-1
lines changed

8 files changed

+276
-1
lines changed

ext/src/ruby_api/wasi_config.rs

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,51 @@
11
use super::root;
22
use crate::error;
33
use crate::helpers::OutputLimitedBuffer;
4+
use crate::ruby_api::convert::ToValType;
5+
use crate::{define_rb_intern, helpers::SymbolEnum};
6+
use lazy_static::lazy_static;
47
use magnus::{
58
class, function, gc::Marker, method, typed_data::Obj, value::Opaque, DataTypeFunctions, Error,
6-
Module, Object, RArray, RHash, RString, Ruby, TryConvert, TypedData,
9+
IntoValue, Module, Object, RArray, RHash, RString, Ruby, Symbol, TryConvert, TypedData, Value,
710
};
11+
use rb_sys::ruby_rarray_flags::RARRAY_EMBED_FLAG;
812
use std::cell::RefCell;
13+
use std::convert::TryFrom;
914
use std::fs;
15+
use std::path::Path;
1016
use std::{fs::File, path::PathBuf};
1117
use wasmtime_wasi::p2::pipe::MemoryInputPipe;
1218
use wasmtime_wasi::p2::{OutputFile, WasiCtx, WasiCtxBuilder};
1319
use wasmtime_wasi::preview1::WasiP1Ctx;
20+
use wasmtime_wasi::{DirPerms, FilePerms};
21+
22+
define_rb_intern!(
23+
READ => "read",
24+
WRITE => "write",
25+
MUTATE => "mutate",
26+
ALL => "all",
27+
);
28+
29+
lazy_static! {
30+
static ref FILE_PERMS_MAPPING: SymbolEnum<'static, FilePerms> = {
31+
let mapping = vec![
32+
(*READ, FilePerms::READ),
33+
(*WRITE, FilePerms::WRITE),
34+
(*ALL, FilePerms::all()),
35+
];
36+
37+
SymbolEnum::new(":file_perms", mapping)
38+
};
39+
static ref DIR_PERMS_MAPPING: SymbolEnum<'static, DirPerms> = {
40+
let mapping = vec![
41+
(*READ, DirPerms::READ),
42+
(*MUTATE, DirPerms::MUTATE),
43+
(*ALL, DirPerms::all()),
44+
];
45+
46+
SymbolEnum::new(":dir_perms", mapping)
47+
};
48+
}
1449

1550
enum ReadStream {
1651
Inherit,
@@ -43,6 +78,24 @@ impl WriteStream {
4378
}
4479
}
4580

81+
struct PermsSymbolEnum(Symbol);
82+
83+
#[derive(Clone)]
84+
struct MappedDirectory {
85+
host_path: Opaque<RString>,
86+
guest_path: Opaque<RString>,
87+
dir_perms: Opaque<Symbol>,
88+
file_perms: Opaque<Symbol>,
89+
}
90+
impl MappedDirectory {
91+
pub fn mark(&self, marker: &Marker) {
92+
marker.mark(self.host_path);
93+
marker.mark(self.guest_path);
94+
marker.mark(self.dir_perms);
95+
marker.mark(self.file_perms);
96+
}
97+
}
98+
4699
#[derive(Default)]
47100
struct WasiConfigInner {
48101
stdin: Option<ReadStream>,
@@ -51,6 +104,7 @@ struct WasiConfigInner {
51104
env: Option<Opaque<RHash>>,
52105
args: Option<Opaque<RArray>>,
53106
deterministic: bool,
107+
mapped_directories: Vec<MappedDirectory>,
54108
}
55109

56110
impl WasiConfigInner {
@@ -70,6 +124,23 @@ impl WasiConfigInner {
70124
if let Some(v) = self.args.as_ref() {
71125
marker.mark(*v);
72126
}
127+
for v in &self.mapped_directories {
128+
v.mark(marker);
129+
}
130+
}
131+
}
132+
133+
impl TryFrom<PermsSymbolEnum> for DirPerms {
134+
type Error = magnus::Error;
135+
fn try_from(value: PermsSymbolEnum) -> Result<Self, Error> {
136+
DIR_PERMS_MAPPING.get(value.0.into_value())
137+
}
138+
}
139+
140+
impl TryFrom<PermsSymbolEnum> for FilePerms {
141+
type Error = magnus::Error;
142+
fn try_from(value: PermsSymbolEnum) -> Result<Self, Error> {
143+
FILE_PERMS_MAPPING.get(value.0.into_value())
73144
}
74145
}
75146

@@ -233,6 +304,34 @@ impl WasiConfig {
233304
rb_self
234305
}
235306

307+
/// @yard
308+
/// Set mapped directory for host path and guest path.
309+
/// @param host_path [String]
310+
/// @param guest_path [String]
311+
/// @param dir_perms [Symbol] Directory permissions, one of :read, :mutate, or :all
312+
/// @param file_perms [Symbol] File permissions, one of :read, :write, or :all
313+
/// @def set_mapped_directory(host_path, guest_path, dir_perms, file_perms)
314+
/// @return [WasiConfig] +self+
315+
pub fn set_mapped_directory(
316+
rb_self: RbSelf,
317+
host_path: RString,
318+
guest_path: RString,
319+
dir_perms: Symbol,
320+
file_perms: Symbol,
321+
) -> RbSelf {
322+
let mapped_dir = MappedDirectory {
323+
host_path: host_path.into(),
324+
guest_path: guest_path.into(),
325+
dir_perms: dir_perms.into(),
326+
file_perms: file_perms.into(),
327+
};
328+
329+
let mut inner = rb_self.inner.borrow_mut();
330+
inner.mapped_directories.push(mapped_dir);
331+
332+
rb_self
333+
}
334+
236335
pub fn build_p1(&self, ruby: &Ruby) -> Result<WasiP1Ctx, Error> {
237336
let mut builder = self.build_impl(ruby)?;
238337
let ctx = builder.build_p1();
@@ -317,6 +416,22 @@ impl WasiConfig {
317416
deterministic_wasi_ctx::add_determinism_to_wasi_ctx_builder(&mut builder);
318417
}
319418

419+
for mapped_dir in &inner.mapped_directories {
420+
let host_path = ruby.get_inner(mapped_dir.host_path).to_string()?;
421+
let guest_path = ruby.get_inner(mapped_dir.guest_path).to_string()?;
422+
let dir_perms = ruby.get_inner(mapped_dir.dir_perms);
423+
let file_perms = ruby.get_inner(mapped_dir.file_perms);
424+
425+
builder
426+
.preopened_dir(
427+
Path::new(&host_path),
428+
&guest_path,
429+
PermsSymbolEnum(dir_perms).try_into()?,
430+
PermsSymbolEnum(file_perms).try_into()?,
431+
)
432+
.map_err(|e| error!("{}", e))?;
433+
}
434+
320435
Ok(builder)
321436
}
322437
}
@@ -355,5 +470,10 @@ pub fn init() -> Result<(), Error> {
355470

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

473+
class.define_method(
474+
"set_mapped_directory",
475+
method!(WasiConfig::set_mapped_directory, 4),
476+
)?;
477+
358478
Ok(())
359479
}

spec/fixtures/wasi-fs-p2.wasm

129 KB
Binary file not shown.

spec/fixtures/wasi-fs.wasm

64.4 KB
Binary file not shown.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[build]
2+
target = "wasm32-wasip1"

spec/fixtures/wasi-fs/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "wasi-fs"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[workspace]
9+
10+
[dependencies]

spec/fixtures/wasi-fs/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Example WASI program used to test WASI preopened directories
2+
3+
To update:
4+
5+
```shell
6+
cargo build --release && \
7+
wasm-opt -O \
8+
--enable-bulk-memory \
9+
target/wasm32-wasip1/release/wasi-fs.wasm \
10+
-o ../wasi-fs.wasm && \
11+
cargo build --target=wasm32-wasip2 --release && \
12+
cp target/wasm32-wasip2/release/wasi-fs.wasm \
13+
../wasi-fs-p2.wasm
14+
```

spec/fixtures/wasi-fs/src/main.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
use std::io::{Write, Read};
2+
use std::fs::File;
3+
4+
fn main() {
5+
let args: Vec<String> = std::env::args().collect();
6+
7+
let counter_file = File::open(&args[1]).expect("failed to open counter file");
8+
9+
let mut counter_str = String::new();
10+
counter_file.take(100).read_to_string(&mut counter_str)
11+
.expect("failed to read counter file");
12+
let mut counter: u32 = counter_str.trim().parse().expect("failed to parse counter");
13+
14+
counter += 1;
15+
16+
let mut counter_file = File::create(&args[1]).expect("failed to create counter file");
17+
write!(counter_file, "{}", counter).expect("failed to write counter file");
18+
}

spec/unit/wasi_spec.rb

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ module Wasmtime
1212
# Compile module only once for speed
1313
@compiled_wasi_module = @engine.precompile_module(IO.binread("spec/fixtures/wasi-debug.wasm"))
1414
@compiled_wasi_deterministic_module = @engine.precompile_module(IO.binread("spec/fixtures/wasi-deterministic.wasm"))
15+
@compiled_wasi_fs_module = @engine.precompile_module(IO.binread("spec/fixtures/wasi-fs.wasm"))
1516

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

2022
describe "Linker.new" do
@@ -233,13 +235,103 @@ module Wasmtime
233235
end
234236
end
235237
end
238+
239+
it "writes to mapped directory" do
240+
Dir.mkdir(tempfile_path("tmp"))
241+
File.write(tempfile_path(File.join("tmp", "counter")), "0")
242+
243+
wasi_config = WasiConfig.new
244+
.set_argv(["wasi-fs", "/tmp/counter"])
245+
.set_mapped_directory(tempfile_path("tmp"), "/tmp", :all, :all)
246+
247+
expect { run_fs.call(wasi_config) }.not_to raise_error
248+
249+
expect(File.read(tempfile_path(File.join("tmp", "counter")))).to eq("1")
250+
end
251+
252+
it "fails to write to mapped directory if not permitted" do
253+
Dir.mkdir(tempfile_path("tmp"))
254+
File.write(tempfile_path(File.join("tmp", "counter")), "0")
255+
256+
stderr_str = ""
257+
wasi_config = WasiConfig.new
258+
.set_argv(["wasi-fs", "/tmp/counter"])
259+
.set_stderr_buffer(stderr_str, 40000)
260+
.set_mapped_directory(tempfile_path("tmp"), "/tmp", :read, :read)
261+
262+
expect { run_fs.call(wasi_config) }.to raise_error do |error|
263+
expect(error).to be_a(Wasmtime::Error)
264+
end
265+
266+
expect(stderr_str).to match(/failed to create counter file/)
267+
268+
expect(File.read(tempfile_path(File.join("tmp", "counter")))).to eq("0")
269+
end
270+
271+
it "fails to read from mapped directory if not permitted" do
272+
Dir.mkdir(tempfile_path("tmp"))
273+
File.write(tempfile_path(File.join("tmp", "counter")), "0")
274+
275+
stderr_str = ""
276+
wasi_config = WasiConfig.new
277+
.set_argv(["wasi-fs", "/tmp/counter"])
278+
.set_stderr_buffer(stderr_str, 40000)
279+
.set_mapped_directory(tempfile_path("tmp"), "/tmp", :mutate, :write)
280+
281+
expect { run_fs.call(wasi_config) }.to raise_error do |error|
282+
expect(error).to be_a(Wasmtime::Error)
283+
end
284+
285+
expect(stderr_str).to match(/failed to open counter file/)
286+
287+
expect(File.read(tempfile_path(File.join("tmp", "counter")))).to eq("0")
288+
end
289+
290+
it "fails to access non-mapped directories" do
291+
Dir.mkdir(tempfile_path("tmp"))
292+
File.write(tempfile_path(File.join("tmp", "counter")), "0")
293+
294+
stderr_str = ""
295+
wasi_config = WasiConfig.new
296+
.set_argv(["wasi-fs", File.join(tempfile_path("tmp"), "counter")])
297+
.set_stderr_buffer(stderr_str, 40000)
298+
299+
expect { run_fs.call(wasi_config) }.to raise_error do |error|
300+
expect(error).to be_a(Wasmtime::Error)
301+
end
302+
303+
expect(stderr_str).to match(/failed to find a pre-opened file descriptor/)
304+
305+
expect(File.read(tempfile_path(File.join("tmp", "counter")))).to eq("0")
306+
end
307+
308+
it "does not accept an invalid host path" do
309+
wasi_config = WasiConfig.new
310+
.set_mapped_directory(tempfile_path("tmp"), "/tmp", :all, :all)
311+
312+
expect { run_fs.call(wasi_config) }.to raise_error do |error|
313+
expect(error).to be_a(Wasmtime::Error)
314+
# error message is os-specific
315+
end
316+
end
317+
318+
it "does not accept invalid permissions" do
319+
wasi_config = WasiConfig.new
320+
.set_mapped_directory(tempfile_path("tmp"), "/tmp", :mutate, :invalid_permission)
321+
322+
expect { run_fs.call(wasi_config) }.to raise_error do |error|
323+
expect(error).to be_a(ArgumentError)
324+
expect(error.message).to match(/invalid :file_perms, expected one of \[:read, :write, :all\], got :invalid_permission/)
325+
end
326+
end
236327
end
237328

238329
describe "WasiConfig preview 1" do
239330
it_behaves_like WasiConfig do
240331
let(:run) { method(:run_wasi_module) }
241332
let(:wasi_env) { method(:wasi_module_env) }
242333
let(:run_deterministic) { method(:run_wasi_module_deterministic) }
334+
let(:run_fs) { method(:run_wasi_module_fs) }
243335
end
244336
end
245337

@@ -248,6 +340,7 @@ module Wasmtime
248340
let(:run) { method(:run_wasi_component) }
249341
let(:wasi_env) { method(:wasi_component_env) }
250342
let(:run_deterministic) { method(:run_wasi_component_deterministic) }
343+
let(:run_fs) { method(:run_wasi_component_fs) }
251344
end
252345
end
253346

@@ -272,6 +365,13 @@ def run_wasi_module_deterministic(wasi_config)
272365
.invoke("_start")
273366
end
274367

368+
def run_wasi_module_fs(wasi_config)
369+
linker = Linker.new(@engine)
370+
WASI::P1.add_to_linker_sync(linker)
371+
store = Store.new(@engine, wasi_p1_config: wasi_config)
372+
linker.instantiate(store, Module.deserialize(@engine, @compiled_wasi_fs_module)).invoke("_start")
373+
end
374+
275375
def wasi_module_env
276376
stdout_file = tempfile_path("stdout")
277377

@@ -318,6 +418,17 @@ def run_wasi_component_deterministic(wasi_config)
318418
).call_run(store)
319419
end
320420

421+
def run_wasi_component_fs(wasi_config)
422+
linker = Component::Linker.new(@engine)
423+
WASI::P2.add_to_linker_sync(linker)
424+
store = Store.new(@engine, wasi_config: wasi_config)
425+
Component::WasiCommand.new(
426+
store,
427+
Component::Component.deserialize(@engine, @compiled_wasi_fs_component),
428+
linker
429+
).call_run(store)
430+
end
431+
321432
def tempfile_path(name)
322433
File.join(tmpdir, name)
323434
end

0 commit comments

Comments
 (0)