Skip to content

Commit 1533739

Browse files
Add socket_addr_check support
1 parent 44c8c2e commit 1533739

3 files changed

Lines changed: 185 additions & 13 deletions

File tree

ext/src/ruby_api/store.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,23 @@ impl Store {
167167
let wasi_config = kw.optional.0;
168168
let wasi_p1_config = kw.optional.1;
169169

170-
let wasi = wasi_config
170+
let (wasi, wasi_proc) = wasi_config
171171
.map(|wasi_config| wasi_config.build(&ruby))
172-
.transpose()?;
173-
let wasi_p1 = wasi_p1_config
172+
.transpose()?
173+
.unzip();
174+
let (wasi_p1, wasi_p1_proc) = wasi_p1_config
174175
.map(|wasi_config| wasi_config.build_p1(&ruby))
175-
.transpose()?;
176+
.transpose()?
177+
.unzip();
178+
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+
}
176187

177188
let limiter = match kw.optional.2 {
178189
None => StoreLimitsBuilder::new(),
@@ -186,7 +197,7 @@ impl Store {
186197
user_data,
187198
wasi_p1,
188199
wasi,
189-
refs: Default::default(),
200+
refs,
190201
last_error: Default::default(),
191202
store_limits: limiter,
192203
resource_table: Default::default(),

ext/src/ruby_api/wasi_config.rs

Lines changed: 101 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,27 @@ use crate::helpers::OutputLimitedBuffer;
44
use crate::ruby_api::convert::ToValType;
55
use crate::{define_rb_intern, helpers::SymbolEnum};
66
use lazy_static::lazy_static;
7+
use magnus::block::Proc;
8+
use magnus::value::ReprValue;
79
use magnus::{
810
class, function, gc::Marker, method, typed_data::Obj, value::Opaque, DataTypeFunctions, Error,
911
IntoValue, Module, Object, RArray, RHash, RString, Ruby, Symbol, TryConvert, TypedData, Value,
1012
};
1113
use rb_sys::ruby_rarray_flags::RARRAY_EMBED_FLAG;
14+
use rb_sys::VALUE;
1215
use std::cell::RefCell;
1316
use std::convert::TryFrom;
1417
use std::fs;
18+
use std::future::Future;
19+
use std::net::SocketAddr;
1520
use std::path::Path;
21+
use std::pin::Pin;
22+
use std::sync::Arc;
1623
use std::{fs::File, path::PathBuf};
1724
use wasmtime_wasi::cli::{InputFile, OutputFile};
1825
use wasmtime_wasi::p1::WasiP1Ctx;
1926
use wasmtime_wasi::p2::pipe::MemoryInputPipe;
27+
use wasmtime_wasi::sockets::SocketAddrUse;
2028
use wasmtime_wasi::{DirPerms, FilePerms, WasiCtx, WasiCtxBuilder};
2129

2230
define_rb_intern!(
@@ -96,6 +104,43 @@ impl MappedDirectory {
96104
}
97105
}
98106

107+
struct SocketAddrProc {
108+
proc: Proc,
109+
}
110+
111+
impl SocketAddrProc {
112+
fn call(&self, addr: SocketAddr, use_: SocketAddrUse) -> bool {
113+
let ruby = Ruby::get().unwrap();
114+
115+
// Convert arguments to Ruby values
116+
let addr_str = ruby.str_new(&addr.to_string());
117+
let use_sym = socket_addr_use_to_symbol(&ruby, use_);
118+
119+
match self.proc.call::<_, Value>((addr_str, use_sym)) {
120+
Ok(result) => bool::try_convert(result).unwrap_or(false),
121+
Err(_) => {
122+
// Exception in Ruby block, deny access
123+
false
124+
}
125+
}
126+
}
127+
}
128+
129+
// SAFETY: We only access the Ruby proc when we have the GVL (during WASI operations).
130+
// The Proc is kept alive by the Store's refs field, which is marked during GC.
131+
unsafe impl Send for SocketAddrProc {}
132+
unsafe impl Sync for SocketAddrProc {}
133+
134+
fn socket_addr_use_to_symbol(ruby: &Ruby, use_: SocketAddrUse) -> Symbol {
135+
match use_ {
136+
SocketAddrUse::TcpBind => ruby.to_symbol("tcp_bind"),
137+
SocketAddrUse::TcpConnect => ruby.to_symbol("tcp_connect"),
138+
SocketAddrUse::UdpBind => ruby.to_symbol("udp_bind"),
139+
SocketAddrUse::UdpConnect => ruby.to_symbol("udp_connect"),
140+
SocketAddrUse::UdpOutgoingDatagram => ruby.to_symbol("udp_outgoing_datagram"),
141+
}
142+
}
143+
99144
#[derive(Default)]
100145
struct WasiConfigInner {
101146
stdin: Option<ReadStream>,
@@ -109,6 +154,7 @@ struct WasiConfigInner {
109154
allow_tcp: Option<bool>,
110155
allow_udp: Option<bool>,
111156
allow_ip_name_lookup: Option<bool>,
157+
socket_addr_check: Option<Opaque<Proc>>,
112158
}
113159

114160
impl WasiConfigInner {
@@ -131,6 +177,9 @@ impl WasiConfigInner {
131177
for v in &self.mapped_directories {
132178
v.mark(marker);
133179
}
180+
if let Some(v) = self.socket_addr_check.as_ref() {
181+
marker.mark(*v);
182+
}
134183
}
135184
}
136185

@@ -384,21 +433,47 @@ impl WasiConfig {
384433
rb_self
385434
}
386435

387-
pub fn build_p1(&self, ruby: &Ruby) -> Result<WasiP1Ctx, Error> {
388-
let mut builder = self.build_impl(ruby)?;
436+
/// @yard
437+
/// Set a custom check function for socket address access control.
438+
/// The block will be called for each socket operation with the socket address (as a String)
439+
/// and the operation type (as a Symbol: :tcp_bind, :tcp_connect, :udp_bind, :udp_connect,
440+
/// :udp_outgoing_datagram).
441+
/// The block should return true to allow the operation or false to deny it.
442+
/// If the block raises an exception, the operation will be denied.
443+
///
444+
/// Note: any network access happens while the Global VM Lock (GVL) is held, so other
445+
/// threads will be blocked in the meantime.
446+
///
447+
/// @yieldparam addr [String] The socket address (e.g., "127.0.0.1:8080")
448+
/// @yieldparam use [Symbol] The type of socket operation
449+
/// @yieldreturn [Boolean] true to allow the operation, false to deny it
450+
/// @def socket_addr_check
451+
/// @return [WasiConfig] +self+
452+
pub fn socket_addr_check(ruby: &Ruby, rb_self: RbSelf) -> RbSelf {
453+
if ruby.block_given() {
454+
let proc = ruby.block_proc().unwrap();
455+
let mut inner = rb_self.inner.borrow_mut();
456+
inner.socket_addr_check = Some(proc.into());
457+
}
458+
rb_self
459+
}
460+
461+
pub fn build_p1(&self, ruby: &Ruby) -> Result<(WasiP1Ctx, Option<Value>), Error> {
462+
let (mut builder, proc_value) = self.build_impl(ruby)?;
389463
let ctx = builder.build_p1();
390-
Ok(ctx)
464+
Ok((ctx, proc_value))
391465
}
392466

393-
pub fn build(&self, ruby: &Ruby) -> Result<WasiCtx, Error> {
394-
let mut builder = self.build_impl(ruby)?;
467+
pub fn build(&self, ruby: &Ruby) -> Result<(WasiCtx, Option<Value>), Error> {
468+
let (mut builder, proc_value) = self.build_impl(ruby)?;
395469
let ctx = builder.build();
396-
Ok(ctx)
470+
Ok((ctx, proc_value))
397471
}
398472

399-
fn build_impl(&self, ruby: &Ruby) -> Result<WasiCtxBuilder, Error> {
473+
fn build_impl(&self, ruby: &Ruby) -> Result<(WasiCtxBuilder, Option<Value>), Error> {
400474
let mut builder = WasiCtxBuilder::new();
401475
let inner = self.inner.borrow();
476+
let mut proc_to_retain = None;
402477

403478
if let Some(stdin) = inner.stdin.as_ref() {
404479
match stdin {
@@ -487,6 +562,20 @@ impl WasiConfig {
487562
}
488563
}
489564

565+
if let Some(check_proc) = inner.socket_addr_check.as_ref() {
566+
let proc = ruby.get_inner(*check_proc);
567+
let socket_addr_proc = Arc::new(SocketAddrProc { proc });
568+
569+
builder.socket_addr_check(move |addr, use_| {
570+
let socket_addr_proc = socket_addr_proc.clone();
571+
Box::pin(async move { socket_addr_proc.call(addr, use_) })
572+
as Pin<Box<dyn Future<Output = bool> + Send + Sync>>
573+
});
574+
575+
// Store the Proc as a Value so the Store can retain it
576+
proc_to_retain = Some(proc.as_value());
577+
}
578+
490579
for mapped_dir in &inner.mapped_directories {
491580
let host_path = ruby.get_inner(mapped_dir.host_path).to_string()?;
492581
let guest_path = ruby.get_inner(mapped_dir.guest_path).to_string()?;
@@ -503,7 +592,7 @@ impl WasiConfig {
503592
.map_err(|e| error!("{}", e))?;
504593
}
505594

506-
Ok(builder)
595+
Ok((builder, proc_to_retain))
507596
}
508597
}
509598

@@ -558,6 +647,10 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
558647
"allow_ip_name_lookup",
559648
method!(WasiConfig::allow_ip_name_lookup, 1),
560649
)?;
650+
class.define_method(
651+
"socket_addr_check",
652+
method!(WasiConfig::socket_addr_check, 0),
653+
)?;
561654

562655
Ok(())
563656
}

spec/unit/wasi_spec.rb

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,74 @@ module Wasmtime
560560
expect(result["test_type"]).to eq("dns")
561561
expect(result["success"]).to eq(false)
562562
end
563+
564+
it "allows selective access with socket_addr_check" do
565+
port_file = tempfile_path("tcp_port_selective")
566+
server_pid = spawn_tcp_server(port_file)
567+
port = wait_for_port(port_file)
568+
569+
stdout_str = ""
570+
wasi_config = WasiConfig.new
571+
.set_argv(["wasi-network", "tcp", "127.0.0.1", port.to_s])
572+
.set_stdout_buffer(stdout_str, 40000)
573+
.socket_addr_check do |addr, use|
574+
# Only allow connections to localhost on the specific port
575+
addr.start_with?("127.0.0.1:") && use == :tcp_connect
576+
end
577+
578+
run_wasi_component_network(wasi_config)
579+
580+
result = JSON.parse(stdout_str)
581+
expect(result["test_type"]).to eq("tcp")
582+
expect(result["message"]).to match(/and exchanged data/)
583+
expect(result["success"]).to eq(true)
584+
ensure
585+
cleanup_server(server_pid)
586+
end
587+
588+
it "blocks access when socket_addr_check returns false" do
589+
port_file = tempfile_path("tcp_port_blocked")
590+
server_pid = spawn_tcp_server(port_file)
591+
port = wait_for_port(port_file)
592+
593+
stdout_str = ""
594+
wasi_config = WasiConfig.new
595+
.set_argv(["wasi-network", "tcp", "127.0.0.1", port.to_s])
596+
.set_stdout_buffer(stdout_str, 40000)
597+
.socket_addr_check do |_addr, _use|
598+
false # Block all access
599+
end
600+
601+
run_wasi_component_network(wasi_config)
602+
603+
result = JSON.parse(stdout_str)
604+
expect(result["test_type"]).to eq("tcp")
605+
expect(result["success"]).to eq(false)
606+
ensure
607+
cleanup_server(server_pid)
608+
end
609+
610+
it "blocks access when socket_addr_check raises an exception" do
611+
port_file = tempfile_path("tcp_port_exception")
612+
server_pid = spawn_tcp_server(port_file)
613+
port = wait_for_port(port_file)
614+
615+
stdout_str = ""
616+
wasi_config = WasiConfig.new
617+
.set_argv(["wasi-network", "tcp", "127.0.0.1", port.to_s])
618+
.set_stdout_buffer(stdout_str, 40000)
619+
.socket_addr_check do |_addr, _use|
620+
raise "Intentional error for testing"
621+
end
622+
623+
run_wasi_component_network(wasi_config)
624+
625+
result = JSON.parse(stdout_str)
626+
expect(result["test_type"]).to eq("tcp")
627+
expect(result["success"]).to eq(false)
628+
ensure
629+
cleanup_server(server_pid)
630+
end
563631
end
564632

565633
describe "WasiConfig preview 1" do

0 commit comments

Comments
 (0)