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
16 changes: 16 additions & 0 deletions kiln-component/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,22 @@ impl ComponentInstance {
Ok(())
}
}

/// Pre-allocate memory for WASI filesystem preopens using cabi_realloc.
///
/// Must be called after setting the host handler (so the preopens are registered)
/// but before calling entry points that use `get-directories`. Mirrors
/// [`pre_allocate_wasi_args`]; the canonical ABI requires guest-owned memory for
/// returning the `list<tuple<descriptor,string>>` from get-directories.
#[cfg(all(feature = "std", feature = "kiln-execution", feature = "wasi"))]
pub fn pre_allocate_wasi_preopens(&mut self) -> kiln_error::Result<()> {
if let (Some(engine), Some(handle)) = (&mut self.runtime_engine, self.main_instance_handle)
{
engine.pre_allocate_wasi_preopens(handle)
} else {
Ok(())
}
}
}

/// State of a component instance
Expand Down
22 changes: 22 additions & 0 deletions kiln-foundation/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1648,4 +1648,26 @@ pub trait HostImportHandler: Send + Sync {
fn set_args_allocation(&mut self, _list_ptr: u32, _string_ptrs: Vec<(u32, u32)>) {
// Default no-op for handlers that don't need this
}

/// Report the registered filesystem preopens as (descriptor-handle, path) pairs.
///
/// The engine reads this before entry to size the cabi_realloc'd return buffer
/// for `wasi:filesystem/preopens::get-directories`. Default empty for handlers
/// with no filesystem preopens.
fn get_preopens(&self) -> Vec<(u32, alloc::string::String)> {
Vec::new()
}

/// Record the cabi_realloc'd memory backing the `get-directories` list return.
///
/// Mirrors [`set_args_allocation`]: the canonical-ABI return area for
/// `func() -> list<tuple<descriptor,string>>` is only 8 bytes (ptr,len), so the
/// list entries and path strings must live in guest-owned memory allocated via
/// `cabi_realloc`. `list_ptr` points to the N*12-byte entry array; `string_ptrs`
/// holds the (ptr, len) of each separately-allocated path string, in preopen order.
///
/// Default no-op for handlers that don't serve filesystem preopens.
fn set_preopens_allocation(&mut self, _list_ptr: u32, _string_ptrs: Vec<(u32, u32)>) {
// Default no-op for handlers that don't need this
}
}
11 changes: 11 additions & 0 deletions kiln-runtime/src/engine/capability_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,17 @@ impl CapabilityAwareEngine {
self.inner.pre_allocate_wasi_args(*instance_idx)
}

/// Pre-allocate cabi_realloc'd memory backing `get-directories` (filesystem preopens).
///
/// Mirrors [`pre_allocate_wasi_args`] for `wasi:filesystem/preopens::get-directories`.
#[cfg(feature = "wasi")]
pub fn pre_allocate_wasi_preopens(&mut self, instance_handle: InstanceHandle) -> Result<()> {
let instance_idx = self.handle_to_idx.get(&instance_handle)
.ok_or_else(|| Error::resource_not_found("Instance not found"))?;

self.inner.pre_allocate_wasi_preopens(*instance_idx)
}

/// Register a lowered function from a canon.lower operation
///
/// When a module calls a function at this (instance_id, func_idx), the engine
Expand Down
93 changes: 93 additions & 0 deletions kiln-runtime/src/stackless/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11981,6 +11981,99 @@ impl StacklessEngine {
}
}

/// Pre-allocate cabi_realloc'd memory backing `wasi:filesystem/preopens::get-directories`.
///
/// The canonical-ABI return area for `func() -> list<tuple<descriptor,string>>` is
/// only 8 bytes (ptr+len), so the entry array (N*12 bytes) and each path string must
/// live in guest-owned memory. Mirrors [`pre_allocate_wasi_args`]. The preopens are
/// read back from the host handler (they were registered via `add_preopen`).
#[cfg(feature = "wasi")]
pub fn pre_allocate_wasi_preopens(&mut self, instance_id: usize) -> Result<()> {
// Read the registered preopens from the host handler.
let preopens: Vec<(u32, String)> = match self.host_handler {
Some(ref handler) => handler.get_preopens(),
None => return Ok(()),
};
if preopens.is_empty() {
#[cfg(feature = "tracing")]
trace!("[WASI-PREALLOC] No preopens to pre-allocate");
return Ok(());
}

let paths: Vec<String> = preopens.into_iter().map(|(_fd, path)| path).collect();
match self.allocate_wasi_preopens_memory(instance_id, &paths)? {
Some((list_ptr, string_ptrs)) => {
if let Some(ref mut handler) = self.host_handler {
handler.set_preopens_allocation(list_ptr, string_ptrs);
}
Ok(())
}
None => {
#[cfg(feature = "tracing")]
trace!("[WASI-PREALLOC] preopens: cabi_realloc not available");
Ok(())
}
}
}

/// Allocate guest memory for the get-directories list return via cabi_realloc.
///
/// Returns (list_ptr, string_ptrs) where list_ptr is the N*12-byte entry array
/// (descriptor, path_ptr, path_len per entry) and string_ptrs holds each path
/// string's (ptr, len), allocated separately for proper allocator metadata.
#[cfg(feature = "wasi")]
fn allocate_wasi_preopens_memory(
&mut self,
instance_id: usize,
paths: &[String],
) -> Result<Option<(u32, Vec<(u32, u32)>)>> {
let instance = self.instances.get(&instance_id)
.ok_or_else(|| kiln_error::Error::runtime_error("Instance not found"))?
.clone();
let module = instance.module();

let cabi_realloc_idx = match self.find_export_index(&module, "cabi_realloc") {
Ok(idx) => idx,
Err(_) => {
#[cfg(feature = "tracing")]
trace!("[WASI-ALLOC] cabi_realloc not found for preopens");
return Ok(None);
}
};

if paths.is_empty() {
return Ok(None);
}

// Entry array: 12 bytes per preopen (descriptor u32, path_ptr u32, path_len u32).
let list_size = (paths.len() * 12) as u32;
let list_ptr = self.call_cabi_realloc(
instance_id,
cabi_realloc_idx,
0, // old_ptr
0, // old_size
4, // align (4-byte for the u32 fields)
list_size,
)?;

// Allocate each path string separately (align 1 for byte data).
let mut string_ptrs: Vec<(u32, u32)> = Vec::with_capacity(paths.len());
for path in paths {
let path_len = path.len() as u32;
let string_ptr = self.call_cabi_realloc(
instance_id,
cabi_realloc_idx,
0,
0,
1,
path_len.max(1), // never request 0 bytes
)?;
string_ptrs.push((string_ptr, path_len));
}

Ok(Some((list_ptr, string_ptrs)))
}

/// Write data to WASM instance memory
fn write_to_instance(&self, instance_id: usize, addr: u32, data: &[u8]) -> Result<()> {
let instance = self.instances.get(&instance_id)
Expand Down
172 changes: 149 additions & 23 deletions kiln-wasi/src/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ pub struct WasiDispatcher {
/// Pre-opened directories (list of (handle, path) pairs)
#[cfg(feature = "std")]
preopens: Vec<(u32, PathBuf)>,
/// Pre-allocated memory for the get-directories list return (`list_ptr`, `string_ptrs`).
/// Set by the engine after calling `cabi_realloc`; mirrors `args_alloc`. The
/// canonical-ABI return area for `list<tuple<descriptor,string>>` is only 8 bytes,
/// so the entry array and path strings must live in this guest-owned memory.
#[cfg(feature = "std")]
preopens_alloc: Option<(u32, Vec<(u32, u32)>)>,
}

impl WasiDispatcher {
Expand Down Expand Up @@ -261,6 +267,8 @@ impl WasiDispatcher {
fd_table,
#[cfg(feature = "std")]
preopens: Vec::new(),
#[cfg(feature = "std")]
preopens_alloc: None,
})
}

Expand Down Expand Up @@ -2324,7 +2332,15 @@ impl WasiDispatcher {
// wasi:filesystem/preopens - core dispatch
#[cfg(feature = "wasi-filesystem")]
("wasi:filesystem/preopens", "get-directories") => {
// Return list of (descriptor, path) tuples at retptr
// Return list<tuple<descriptor, string>> at retptr.
//
// The canonical-ABI return area at retptr is only 8 bytes (the list
// header: ptr + len). The list entries and path strings MUST live in
// guest-owned memory allocated via cabi_realloc — writing them into
// memory adjacent to retptr (as an earlier version did) hands the guest
// a list backed by memory it never allocated, so preview2 libc registers
// no usable preopen and every file op returns noent. This mirrors the
// get-arguments path exactly (see preopens_alloc / pre_allocate_wasi_preopens).
if let Some(mem) = memory {
let retptr = match args.first() {
Some(CoreValue::I32(v)) => *v as u32,
Expand All @@ -2334,29 +2350,44 @@ impl WasiDispatcher {
// No preopens: empty list (ptr=0, len=0)
mem.write_bytes(retptr, &0u32.to_le_bytes())?;
mem.write_bytes(retptr + 4, &0u32.to_le_bytes())?;
} else {
// Each entry is (descriptor: u32, path_ptr: u32, path_len: u32) = 12 bytes
let entry_size = 12u32;
let list_size = self.preopens.len() as u32 * entry_size;
// Write entries starting after retptr+8 (the list header)
let data_start = retptr + 8;
for (i, (fd, path)) in self.preopens.iter().enumerate() {
let entry_offset = data_start + (i as u32 * entry_size);
let path_bytes = path.to_string_lossy();
let path_bytes = path_bytes.as_bytes();
// Write path string after all entries
let path_offset = data_start + list_size + (i as u32 * 256);
let path_len = core::cmp::min(path_bytes.len(), 255) as u32;
mem.write_bytes(path_offset, &path_bytes[..path_len as usize])?;
// Write entry: (fd, path_ptr, path_len)
mem.write_bytes(entry_offset, &(*fd).to_le_bytes())?;
mem.write_bytes(entry_offset + 4, &path_offset.to_le_bytes())?;
mem.write_bytes(entry_offset + 8, &path_len.to_le_bytes())?;
}
// Write list header: (ptr, len)
mem.write_bytes(retptr, &data_start.to_le_bytes())?;
mem.write_bytes(retptr + 4, &(self.preopens.len() as u32).to_le_bytes())?;
return Ok(vec![]);
}

// Use pre-allocated cabi_realloc memory. NO FALLBACK — per FAIL-LOUD,
// if the engine did not pre-allocate (no cabi_realloc export) we error
// rather than scribble into unowned guest memory.
let (list_ptr, string_ptrs) = match &self.preopens_alloc {
Some((lp, sp)) => (*lp, sp.clone()),
None => {
return Err(Error::runtime_error(
"get-directories requires cabi_realloc pre-allocation (preopens_alloc not set)",
));
},
};
if string_ptrs.len() != self.preopens.len() {
return Err(Error::runtime_error(
"get-directories: preopen string allocation count mismatch",
));
}

// Each entry is (descriptor: u32, path_ptr: u32, path_len: u32) = 12 bytes.
let entry_size = 12u32;
for (i, (fd, path)) in self.preopens.iter().enumerate() {
let (string_ptr, alloc_len) = string_ptrs[i];
let path_str = path.to_string_lossy();
let path_bytes = path_str.as_bytes();
let path_len = core::cmp::min(path_bytes.len() as u32, alloc_len);
// Write the path string into its own cabi_realloc'd buffer.
mem.write_bytes(string_ptr, &path_bytes[..path_len as usize])?;
// Write the entry (descriptor, path_ptr, path_len) into the list buffer.
let entry_offset = list_ptr + (i as u32 * entry_size);
mem.write_bytes(entry_offset, &(*fd).to_le_bytes())?;
mem.write_bytes(entry_offset + 4, &string_ptr.to_le_bytes())?;
mem.write_bytes(entry_offset + 8, &path_len.to_le_bytes())?;
}
// Write list header (ptr, len) into the return area.
mem.write_bytes(retptr, &list_ptr.to_le_bytes())?;
mem.write_bytes(retptr + 4, &(self.preopens.len() as u32).to_le_bytes())?;
}
Ok(vec![])
}
Expand Down Expand Up @@ -2646,6 +2677,17 @@ impl kiln_foundation::HostImportHandler for WasiDispatcher {
fn set_args_allocation(&mut self, list_ptr: u32, string_ptrs: Vec<(u32, u32)>) {
self.set_args_alloc(list_ptr, string_ptrs);
}

fn get_preopens(&self) -> Vec<(u32, String)> {
self.preopens
.iter()
.map(|(fd, path)| (*fd, path.to_string_lossy().into_owned()))
.collect()
}

fn set_preopens_allocation(&mut self, list_ptr: u32, string_ptrs: Vec<(u32, u32)>) {
self.preopens_alloc = Some((list_ptr, string_ptrs));
}
}

/// True iff `full` (a guest path already joined onto the preopen `base`) stays
Expand Down Expand Up @@ -2685,6 +2727,90 @@ mod tests {
assert!(!is_within_sandbox(base, &base.join("/etc/passwd")));
}

/// SR-37 / #405: get-directories must back its `list<tuple<descriptor,string>>`
/// return with cabi_realloc'd guest memory, NOT memory adjacent to the 8-byte
/// return area. The list header at retptr must point at the pre-allocated
/// `list_ptr` (not retptr+8), and each entry (descriptor, path_ptr, path_len)
/// must reference the pre-allocated string buffer — so a preview2 guest gets a
/// usable preopen instead of a list backed by memory it never allocated.
// rivet: verifies SR-37
#[cfg(all(feature = "wasi-filesystem", feature = "std"))]
#[test]
fn get_directories_uses_cabi_realloc_allocation_not_retptr_scribble() -> Result<()> {
use kiln_foundation::traits::SliceMemory;
use kiln_foundation::HostImportHandler;
MemoryInitializer::ensure_initialized()?;
let mut dispatcher = WasiDispatcher::with_defaults()?;

// Register a preopen "." (as `--wasi-fs .` would).
let fd = dispatcher.add_preopen(".")?;

// Simulate the engine's cabi_realloc pre-allocation: a 12-byte entry buffer
// at 0x2000 and a 1-byte path-string buffer at 0x3000 — both well away from
// the 8-byte return area at retptr.
let retptr = 0x1000u32;
let list_ptr = 0x2000u32;
let str_ptr = 0x3000u32;
dispatcher.set_preopens_allocation(list_ptr, vec![(str_ptr, 1)]);

let mem = SliceMemory::with_size(0x10000);
let res = dispatcher.dispatch_core(
"wasi:filesystem/preopens",
"get-directories",
&[kiln_foundation::values::Value::I32(retptr as i32)],
Some(&mem),
)?;
assert!(res.is_empty(), "get-directories returns void (writes via retptr)");

// Return area: (list_ptr, len) — MUST point at the allocation, not retptr+8.
let mut hdr = [0u8; 8];
mem.read_bytes(retptr, &mut hdr)?;
let got_ptr = u32::from_le_bytes([hdr[0], hdr[1], hdr[2], hdr[3]]);
let got_len = u32::from_le_bytes([hdr[4], hdr[5], hdr[6], hdr[7]]);
assert_eq!(got_ptr, list_ptr, "list header must point at cabi_realloc'd list_ptr");
assert_ne!(got_ptr, retptr + 8, "must NOT scribble into memory adjacent to the return area");
assert_eq!(got_len, 1, "exactly one preopen");

// Entry at list_ptr: (descriptor, path_ptr, path_len).
let mut entry = [0u8; 12];
mem.read_bytes(list_ptr, &mut entry)?;
let e_fd = u32::from_le_bytes([entry[0], entry[1], entry[2], entry[3]]);
let e_pptr = u32::from_le_bytes([entry[4], entry[5], entry[6], entry[7]]);
let e_plen = u32::from_le_bytes([entry[8], entry[9], entry[10], entry[11]]);
assert_eq!(e_fd, fd, "entry descriptor is the preopen handle");
assert_eq!(e_pptr, str_ptr, "entry path_ptr references the allocated string buffer");
assert_eq!(e_plen, 1, "path \".\" is 1 byte");

// The path string "." lives in the allocated buffer.
let mut pbuf = [0u8; 1];
mem.read_bytes(str_ptr, &mut pbuf)?;
assert_eq!(&pbuf, b".", "path bytes written into the cabi_realloc'd buffer");
Ok(())
}

/// SR-37 / #405: FAIL-LOUD — with preopens present but no cabi_realloc
/// allocation set, get-directories must error rather than scribble into
/// unowned guest memory (the pre-fix behavior that produced ENOENT).
// rivet: verifies SR-37
#[cfg(all(feature = "wasi-filesystem", feature = "std"))]
#[test]
fn get_directories_fails_loud_without_allocation() -> Result<()> {
use kiln_foundation::traits::SliceMemory;
MemoryInitializer::ensure_initialized()?;
let mut dispatcher = WasiDispatcher::with_defaults()?;
dispatcher.add_preopen(".")?;
// NOTE: no set_preopens_allocation call.
let mem = SliceMemory::with_size(0x10000);
let res = dispatcher.dispatch_core(
"wasi:filesystem/preopens",
"get-directories",
&[kiln_foundation::values::Value::I32(0x1000)],
Some(&mem),
);
assert!(res.is_err(), "must fail loud when cabi_realloc pre-allocation is missing");
Ok(())
}

#[test]
fn test_dispatcher_creation() -> Result<()> {
MemoryInitializer::ensure_initialized()?;
Expand Down
5 changes: 5 additions & 0 deletions kilnd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,11 @@ impl KilndEngine {
if let Err(e) = instance.pre_allocate_wasi_args() {
eprintln!("[WASI-PREALLOC] Failed: {}", e);
}
// Pre-allocate cabi_realloc'd memory for get-directories so a component
// can actually use --wasi-fs preopens (SR-37 / #405).
if let Err(e) = instance.pre_allocate_wasi_preopens() {
eprintln!("[WASI-PREALLOC] preopens failed: {}", e);
}
}

// Direct Component-Model hosting (#344, AD-COMPONENT-HOST-001):
Expand Down
Loading
Loading