Skip to content

Commit 45ebe6d

Browse files
Retain error raised by ruby socket check proc
1 parent adcbd41 commit 45ebe6d

6 files changed

Lines changed: 148 additions & 28 deletions

File tree

ext/src/ruby_api/component/func.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ impl Func {
105105
func.call(store.context_mut(), &params, &mut results)
106106
.map_err(|e| store_context_value.handle_wasm_error(ruby, e))?;
107107

108+
// Check for any errors stored during execution (e.g., from socket checks)
109+
if let Some(error) = store_context_value.take_last_error()? {
110+
return Err(error);
111+
}
112+
108113
match results_ty.len() {
109114
0 => Ok(ruby.qnil().as_value()),
110115
1 => component_val_to_rb(

ext/src/ruby_api/component/wasi_command.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::{
99
ruby_api::{
1010
component::{linker::Linker, Component},
1111
errors,
12+
store::StoreContextValue,
1213
},
1314
Store,
1415
};
@@ -39,13 +40,21 @@ impl WasiCommand {
3940
/// @def call_run(store)
4041
/// @param store [Store]
4142
/// @return [nil]
42-
pub fn call_run(_ruby: &Ruby, rb_self: Obj<Self>, store: &Store) -> Result<(), Error> {
43+
pub fn call_run(_ruby: &Ruby, rb_self: Obj<Self>, store: Obj<Store>) -> Result<(), Error> {
44+
let store_context_value = StoreContextValue::from(store);
4345
rb_self
4446
.command
4547
.wasi_cli_run()
4648
.call_run(store.context_mut())
4749
.map_err(|err| error!("{err}"))?
48-
.map_err(|_| error!("Error running `run`"))
50+
.map_err(|_| error!("Error running `run`"))?;
51+
52+
// Check for any errors stored during execution (e.g., from socket checks)
53+
if let Some(error) = store_context_value.take_last_error()? {
54+
return Err(error);
55+
}
56+
57+
Ok(())
4958
}
5059
}
5160

ext/src/ruby_api/func.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ impl<'a> Func<'a> {
221221
func.call(context, &params, &mut results)
222222
.map_err(|e| store.handle_wasm_error(ruby, e))?;
223223

224+
// Check for any errors stored during execution (e.g., from socket checks)
225+
if let Some(error) = store.take_last_error()? {
226+
return Err(error);
227+
}
228+
224229
match results.as_slice() {
225230
[] => Ok(().into_value_with(ruby)),
226231
[result] => result.to_ruby_value(ruby, store),

ext/src/ruby_api/store.rs

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
use super::errors::wasi_exit_error;
22
use super::{caller::Caller, engine::Engine, root, trap::Trap};
33
use crate::{define_rb_intern, error, WasiConfig};
4+
use magnus::value::ReprValue;
45
use magnus::value::StaticSymbol;
56
use magnus::{
67
class, function,
78
gc::{Compactor, Marker},
89
method, scan_args,
910
typed_data::Obj,
1011
value::Opaque,
11-
DataTypeFunctions, Error, IntoValue, Module, Object, Ruby, TypedData, Value,
12+
DataTypeFunctions, Error, ExceptionClass, IntoValue, Module, Object, Ruby, TryConvert,
13+
TypedData, Value,
1214
};
1315
use magnus::{Class, RHash};
1416
use rb_sys::tracking_allocator::{ManuallyTracked, TrackingAllocator};
@@ -70,6 +72,33 @@ impl StoreData {
7072
self.last_error.take()
7173
}
7274

75+
pub fn check_socket_errors(&mut self) {
76+
use crate::ruby_api::wasi_config::WasiRetainedData;
77+
78+
// Check all retained values for WasiRetainedData and extract error storages
79+
for value in &self.refs {
80+
let ruby = Ruby::get().unwrap();
81+
82+
// Try to convert to WasiRetainedData
83+
if let Ok(retained) = Obj::<WasiRetainedData>::try_convert(*value) {
84+
if let Some(storage) = retained.error_storage() {
85+
if let Ok(mut guard) = storage.lock() {
86+
if let Some((class_name, error_msg)) = guard.take() {
87+
// Look up the exception class by name and reconstruct the error
88+
let exception_class = ruby
89+
.class_object()
90+
.const_get::<_, ExceptionClass>(class_name)
91+
.unwrap_or_else(|_| ruby.exception_runtime_error());
92+
93+
self.last_error = Some(Error::new(exception_class, error_msg));
94+
return;
95+
}
96+
}
97+
}
98+
}
99+
}
100+
}
101+
73102
pub fn mark(&self, marker: &Marker) {
74103
marker.mark_movable(self.user_data);
75104

@@ -167,23 +196,21 @@ impl Store {
167196
let wasi_config = kw.optional.0;
168197
let wasi_p1_config = kw.optional.1;
169198

170-
let (wasi, wasi_proc) = wasi_config
199+
let (wasi, wasi_retained) = wasi_config
171200
.map(|wasi_config| wasi_config.build(&ruby))
172201
.transpose()?
173202
.unzip();
174-
let (wasi_p1, wasi_p1_proc) = wasi_p1_config
203+
let (wasi_p1, wasi_p1_retained) = wasi_p1_config
175204
.map(|wasi_config| wasi_config.build_p1(&ruby))
176205
.transpose()?
177206
.unzip();
178207

179-
// Collect any Procs that need to be retained
180-
let mut refs = Vec::new();
181-
if let Some(proc) = wasi_proc.flatten() {
182-
refs.push(proc);
183-
}
184-
if let Some(proc) = wasi_p1_proc.flatten() {
185-
refs.push(proc);
186-
}
208+
// Collect any Values that need to be retained for GC
209+
let refs: Vec<Value> = [wasi_retained, wasi_p1_retained]
210+
.into_iter()
211+
.flatten()
212+
.flatten()
213+
.collect();
187214

188215
let limiter = match kw.optional.2 {
189216
None => StoreLimitsBuilder::new(),
@@ -374,8 +401,14 @@ impl StoreContextValue<'_> {
374401
Ok(())
375402
}
376403

377-
fn take_last_error(&self) -> Result<Option<Error>, Error> {
404+
pub fn take_last_error(&self) -> Result<Option<Error>, Error> {
378405
let ruby = Ruby::get().unwrap();
406+
407+
// Check for socket errors first and merge into last_error
408+
if let Ok(mut context) = self.context_mut() {
409+
context.data_mut().check_socket_errors();
410+
}
411+
379412
match self {
380413
Self::Store(store) => Ok(ruby.get_inner(*store).take_last_error()),
381414
Self::Caller(caller) => Ok(ruby

ext/src/ruby_api/wasi_config.rs

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::{define_rb_intern, helpers::SymbolEnum};
66
use lazy_static::lazy_static;
77
use magnus::block::Proc;
88
use magnus::value::ReprValue;
9+
use magnus::Class;
910
use magnus::{
1011
class, function, gc::Marker, method, typed_data::Obj, value::Opaque, DataTypeFunctions, Error,
1112
IntoValue, Module, Object, RArray, RHash, RString, Ruby, Symbol, TryConvert, TypedData, Value,
@@ -19,14 +20,49 @@ use std::future::Future;
1920
use std::net::SocketAddr;
2021
use std::path::Path;
2122
use std::pin::Pin;
22-
use std::sync::Arc;
23+
use std::sync::{Arc, Mutex};
2324
use std::{fs::File, path::PathBuf};
2425
use wasmtime_wasi::cli::{InputFile, OutputFile};
2526
use wasmtime_wasi::p1::WasiP1Ctx;
2627
use wasmtime_wasi::p2::pipe::MemoryInputPipe;
2728
use wasmtime_wasi::sockets::SocketAddrUse;
2829
use wasmtime_wasi::{DirPerms, FilePerms, WasiCtx, WasiCtxBuilder};
2930

31+
/// Storage for errors that occur in socket_addr_check callbacks.
32+
/// We store (class_name, message) tuples since magnus::Error is not Send+Sync.
33+
/// Storing the class name allows us to reconstruct the original exception type.
34+
type SocketErrorStorage = Arc<Mutex<Option<(String, String)>>>;
35+
36+
/// Container for data that needs to be retained by the Store for WASI functionality.
37+
/// This includes Ruby procs (for GC marking) and error storages (for error propagation).
38+
#[derive(TypedData)]
39+
#[magnus(class = "Wasmtime::WasiRetainedData", mark, free_immediately)]
40+
pub struct WasiRetainedData {
41+
proc: Option<Opaque<Proc>>,
42+
error_storage: Option<SocketErrorStorage>,
43+
}
44+
45+
impl DataTypeFunctions for WasiRetainedData {
46+
fn mark(&self, marker: &Marker) {
47+
if let Some(proc) = self.proc {
48+
marker.mark(proc);
49+
}
50+
}
51+
}
52+
53+
impl WasiRetainedData {
54+
pub fn new(proc: Option<Proc>, error_storage: Option<SocketErrorStorage>) -> Self {
55+
Self {
56+
proc: proc.map(|p| p.into()),
57+
error_storage,
58+
}
59+
}
60+
61+
pub fn error_storage(&self) -> Option<&SocketErrorStorage> {
62+
self.error_storage.as_ref()
63+
}
64+
}
65+
3066
define_rb_intern!(
3167
READ => "read",
3268
WRITE => "write",
@@ -106,6 +142,7 @@ impl MappedDirectory {
106142

107143
struct SocketAddrProc {
108144
proc: Proc,
145+
error_storage: SocketErrorStorage,
109146
}
110147

111148
impl SocketAddrProc {
@@ -118,8 +155,18 @@ impl SocketAddrProc {
118155

119156
match self.proc.call::<_, Value>((addr_str, use_sym)) {
120157
Ok(result) => bool::try_convert(result).unwrap_or(false),
121-
Err(_) => {
122-
// Exception in Ruby block, deny access
158+
Err(error) => {
159+
// Store both class name and message for later retrieval
160+
if let Ok(mut storage) = self.error_storage.lock() {
161+
// Get the exception class name from the error's value
162+
let class_name = if let Some(exception_value) = error.value() {
163+
unsafe { exception_value.class().name().into_owned() }
164+
} else {
165+
"RuntimeError".to_string()
166+
};
167+
*storage = Some((class_name, error.to_string()));
168+
}
169+
// Deny access when an exception occurs
123170
false
124171
}
125172
}
@@ -459,21 +506,22 @@ impl WasiConfig {
459506
}
460507

461508
pub fn build_p1(&self, ruby: &Ruby) -> Result<(WasiP1Ctx, Option<Value>), Error> {
462-
let (mut builder, proc_value) = self.build_impl(ruby)?;
509+
let (mut builder, retained_data) = self.build_impl(ruby)?;
463510
let ctx = builder.build_p1();
464-
Ok((ctx, proc_value))
511+
Ok((ctx, retained_data))
465512
}
466513

467514
pub fn build(&self, ruby: &Ruby) -> Result<(WasiCtx, Option<Value>), Error> {
468-
let (mut builder, proc_value) = self.build_impl(ruby)?;
515+
let (mut builder, retained_data) = self.build_impl(ruby)?;
469516
let ctx = builder.build();
470-
Ok((ctx, proc_value))
517+
Ok((ctx, retained_data))
471518
}
472519

473520
fn build_impl(&self, ruby: &Ruby) -> Result<(WasiCtxBuilder, Option<Value>), Error> {
474521
let mut builder = WasiCtxBuilder::new();
475522
let inner = self.inner.borrow();
476523
let mut proc_to_retain = None;
524+
let mut error_storage_to_retain = None;
477525

478526
if let Some(stdin) = inner.stdin.as_ref() {
479527
match stdin {
@@ -550,16 +598,21 @@ impl WasiConfig {
550598

551599
if let Some(check_proc) = inner.socket_addr_check.as_ref() {
552600
let proc = ruby.get_inner(*check_proc);
553-
let socket_addr_proc = Arc::new(SocketAddrProc { proc });
601+
let error_storage = Arc::new(Mutex::new(None));
602+
let socket_addr_proc = Arc::new(SocketAddrProc {
603+
proc,
604+
error_storage: error_storage.clone(),
605+
});
554606

555607
builder.socket_addr_check(move |addr, use_| {
556608
let socket_addr_proc = socket_addr_proc.clone();
557609
Box::pin(async move { socket_addr_proc.call(addr, use_) })
558610
as Pin<Box<dyn Future<Output = bool> + Send + Sync>>
559611
});
560612

561-
// Store the Proc as a Value so the Store can retain it
562-
proc_to_retain = Some(proc.as_value());
613+
// Store the Proc and error storage together
614+
proc_to_retain = Some(proc);
615+
error_storage_to_retain = Some(error_storage);
563616
}
564617

565618
for mapped_dir in &inner.mapped_directories {
@@ -578,7 +631,15 @@ impl WasiConfig {
578631
.map_err(|e| error!("{}", e))?;
579632
}
580633

581-
Ok((builder, proc_to_retain))
634+
// Wrap retained data in a single Ruby object if we have anything to retain
635+
let retained_value = if proc_to_retain.is_some() || error_storage_to_retain.is_some() {
636+
let retained_data = WasiRetainedData::new(proc_to_retain, error_storage_to_retain);
637+
Some(ruby.obj_wrap(retained_data).as_value())
638+
} else {
639+
None
640+
};
641+
642+
Ok((builder, retained_value))
582643
}
583644

584645
fn check_determinism(&self) -> Result<(), Error> {
@@ -589,7 +650,9 @@ impl WasiConfig {
589650
|| inner.allow_ip_name_lookup == Some(true);
590651

591652
if inner.deterministic && has_network_enabled {
592-
Err(error!("Sources of indeterminism cannot be combined with determinism"))
653+
Err(error!(
654+
"Sources of indeterminism cannot be combined with determinism"
655+
))
593656
} else {
594657
Ok(())
595658
}
@@ -608,6 +671,9 @@ pub fn file_w(path: RString) -> Result<File, Error> {
608671
}
609672

610673
pub fn init(ruby: &Ruby) -> Result<(), Error> {
674+
// Register internal WasiRetainedData class (not exposed to Ruby API)
675+
root().define_class("WasiRetainedData", ruby.class_object())?;
676+
611677
let class = root().define_class("WasiConfig", ruby.class_object())?;
612678
class.define_singleton_method("new", function!(WasiConfig::new, 0))?;
613679

spec/unit/wasi_spec.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -617,10 +617,12 @@ module Wasmtime
617617
.set_argv(["wasi-network", "tcp", "127.0.0.1", port.to_s])
618618
.set_stdout_buffer(stdout_str, 40000)
619619
.socket_addr_check do |_addr, _use|
620-
raise "Intentional error for testing"
620+
raise ArgumentError, "Intentional error for testing"
621621
end
622622

623-
run_wasi_component_network(wasi_config)
623+
expect {
624+
run_wasi_component_network(wasi_config)
625+
}.to raise_error(ArgumentError, /Intentional error for testing/)
624626

625627
result = JSON.parse(stdout_str)
626628
expect(result["test_type"]).to eq("tcp")

0 commit comments

Comments
 (0)