dwara_core/wasm/host.rs
1//! proxy-wasm host implementation (DW-055).
2//!
3//! This module implements the host side of the proxy-wasm ABI: the
4//! import functions that a WebAssembly plugin calls to interact with
5//! the proxy. The plugin exports phase callbacks (e.g.
6//! `proxy_on_request_headers`) that the host calls at the appropriate
7//! point in the request pipeline; the plugin calls back into the host
8//! via the imports defined here to read/write request data, log, send
9//! responses, etc.
10//!
11//! ## Architecture
12//!
13//! - [`WasmEngine`] — process-wide wasmtime engine + linker, compiled
14//! once at startup. Holds the compiled modules keyed by config name.
15//! Cheap to clone (Arc internals).
16//! - [`PluginModule`] — a compiled wasmtime module for one plugin
17//! config entry. Created once at config publish time.
18//! - [`PluginInstance`] — a per-request plugin instance (store +
19//! instance + context). Created for each request that passes through
20//! a route with plugins.
21//! - [`PluginContext`] — the per-instance state the host imports read
22//! from and write to: request/response headers, body, the action
23//! returned by the plugin, etc. Stored in the wasmtime store's
24//! user data.
25//!
26//! ## Fuel and epoch preemption
27//!
28//! Each instance is created with a fuel budget
29//! ([`PluginLimits::fuel`]). wasmtime consumes fuel on every operation;
30//! when the budget is exhausted, the plugin traps with an out-of-fuel
31//! error, which the host catches and converts to a 500 (the plugin is
32//! misbehaving, not the request). Epoch-based interruption is used for
33//! time caps: a background thread increments the epoch, and the plugin
34//! is interrupted if it runs past [`PluginLimits::timeout_ms`].
35//!
36//! ## Memory caps
37//!
38//! [`PluginLimits::memory_mb`] caps the linear memory the plugin can
39//! allocate. wasmtime's `ResourceLimiter` trait enforces this at the
40//! allocation boundary.
41//!
42//! ## Import names and WASI
43//!
44//! Modules built with the Rust `proxy-wasm` SDK (what
45//! `dwara-cli plugin new` scaffolds) import the spec ABI names, so the
46//! host registers BOTH the spec names (`proxy_send_local_response`,
47//! `proxy_get_current_time_nanoseconds`) and dwara's original spellings
48//! (`proxy_send_http_response`, `proxy_get_current_time`) with
49//! identical behavior. The `wasm32-wasip1` target additionally emits a
50//! small `wasi_snapshot_preview1` import set (environment, fd_write,
51//! proc_exit, random_get, clock_time_get, sched_yield); the host
52//! provides minimal stubs for exactly that set — a module importing
53//! other WASI functions fails to instantiate (fail-closed).
54
55use std::collections::HashMap;
56use std::sync::{Arc, Mutex};
57
58use wasmtime::{Engine, Linker, Module, ResourceLimiter, Store};
59
60use super::abi;
61use super::callout::CalloutResponse;
62
63/// The VM-scoped shared-data store (DW-167): one map per compiled
64/// plugin module, shared by every per-request instance through the
65/// Mutex. Keys are strings; values are bytes with a CAS version for
66/// optimistic concurrency.
67pub type SharedData = HashMap<String, (Vec<u8>, u32)>;
68
69/// Per-module cap on the VM-scoped shared-data store: total bytes
70/// (key + value lengths summed over the entries). The store outlives
71/// every request (module lifetime), so an unbounded map is a slow OOM
72/// the wasm `ResourceLimiter` cannot see. A set that would exceed the
73/// cap FAILS (the plugin gets an error status it can branch on) —
74/// entries are never evicted: a silently evicted rate-limit counter
75/// or cache entry is a correctness bug, a refused set is a signal.
76pub const SHARED_DATA_CAP_BYTES: usize = 1024 * 1024;
77/// Per-module cap on the VM-scoped shared-data store: entry count
78/// (same posture as [`SHARED_DATA_CAP_BYTES`]; bounds key overhead a
79/// byte cap alone would miss).
80pub const SHARED_DATA_CAP_ENTRIES: usize = 1024;
81
82/// Whether inserting `key` -> `value` (replacing the current value if
83/// any) fits the shared-data caps ([`SHARED_DATA_CAP_BYTES`] /
84/// [`SHARED_DATA_CAP_ENTRIES`]). Runs inside the caller's critical
85/// section; the entry cap bounds the byte-sum walk to 1024 entries.
86fn shared_data_fits(shared: &SharedData, key: &str, value: &[u8]) -> bool {
87 let exists = shared.contains_key(key);
88 if !exists && shared.len() >= SHARED_DATA_CAP_ENTRIES {
89 return false;
90 }
91 let occupied: usize = shared.iter().map(|(k, (v, _))| k.len() + v.len()).sum();
92 let replaced = shared
93 .get(key)
94 .map(|(v, _)| key.len() + v.len())
95 .unwrap_or(0);
96 occupied - replaced + key.len() + value.len() <= SHARED_DATA_CAP_BYTES
97}
98
99/// Per-instance cap on buffered plugin log output (`proxy_log` plus
100/// the `fd_write` sink), in bytes. Fuel bounds plugin EXECUTION but
101/// not host-side copies: without this cap a plugin looping over
102/// `proxy_log`/`fd_write` would grow the host-side buffer without
103/// bound. When the cap is hit the head of the overflowing line is
104/// kept (if any budget remains), a marker line is appended, and every
105/// further line is dropped. 64 KiB is far above any legitimate
106/// diagnostic output.
107const LOG_BUFFER_CAP_BYTES: usize = 64 * 1024;
108
109/// Process-wide wasmtime engine + linker for proxy-wasm plugins.
110///
111/// Created once at startup and shared across all plugin instances. The
112/// engine is configured with fuel consumption and epoch interruption
113/// enabled. The linker connects the proxy-wasm ABI imports to the host
114/// functions.
115///
116/// Cheap to clone (Arc internals).
117#[derive(Clone)]
118pub struct WasmEngine {
119 engine: Engine,
120 linker: Arc<Linker<PluginContext>>,
121}
122
123/// A compiled plugin module (one per config entry).
124pub struct PluginModule {
125 module: Module,
126 limits: PluginLimits,
127 plugin_config: Vec<u8>,
128 vm_config: Vec<u8>,
129 /// The VM-scoped shared-data store (DW-167): every per-request
130 /// instance of THIS module sees the same map — the proxy-wasm
131 /// `proxy_get/set_shared_data` contract Envoy implements (shared
132 /// across the contexts of one VM). Instances on different tokio
133 /// tasks share it through the Mutex.
134 shared: Arc<Mutex<SharedData>>,
135}
136
137/// Per-plugin resource limits (DW-055 decision 4; §9.3).
138#[derive(Clone, Debug)]
139pub struct PluginLimits {
140 /// Maximum fuel (wasmtime operations). Each wasm operation consumes
141 /// a small amount of fuel; when the budget is exhausted, the plugin
142 /// traps. Default: 1,000,000 (enough for typical header inspection
143 /// and response short-circuiting).
144 pub fuel: u64,
145 /// Maximum linear memory in MB. Default: 32.
146 pub memory_mb: usize,
147 /// Maximum execution time in milliseconds. Enforced via epoch
148 /// interruption. Default: 100.
149 pub timeout_ms: u64,
150}
151
152impl Default for PluginLimits {
153 fn default() -> Self {
154 Self {
155 fuel: 1_000_000,
156 memory_mb: 32,
157 timeout_ms: 100,
158 }
159 }
160}
161
162/// The per-instance context the host imports read from and write to.
163///
164/// Stored as the wasmtime store's `T` type. The host import functions
165/// receive a `&mut PluginContext` via `Caller` and use it to exchange
166/// data with the plugin.
167pub struct PluginContext {
168 /// Request headers (set by the host before `proxy_on_request_headers`).
169 pub request_headers: Vec<(String, String)>,
170 /// Response headers (set by the host before `proxy_on_response_headers`).
171 pub response_headers: Vec<(String, String)>,
172 /// Request body (set by the host before `proxy_on_request_body`).
173 pub request_body: Vec<u8>,
174 /// Response body (set by the host before `proxy_on_response_body`).
175 pub response_body: Vec<u8>,
176 /// Plugin configuration (passed to `proxy_on_configure`).
177 pub plugin_config: Vec<u8>,
178 /// VM configuration (passed to `proxy_on_vm_start`).
179 pub vm_config: Vec<u8>,
180 /// The action the plugin returned from the last phase callback.
181 pub action: u32,
182 /// Whether the plugin has called `proxy_send_http_response` (a
183 /// short-circuit response). When set, the host should stop
184 /// processing and return the stored response.
185 pub local_response: Option<LocalResponse>,
186 /// Log lines emitted by the plugin via `proxy_log`.
187 pub logs: Vec<(u32, String)>,
188 /// The VM-scoped shared-data store handle (see [`SharedData`]).
189 pub shared_data: Arc<Mutex<SharedData>>,
190 /// Callouts registered by `proxy_http_call` and not yet answered
191 /// (DW-167). One instance can queue several; the dispatch driver
192 /// performs them in FIFO order, one round each.
193 pub pending_callouts: Vec<PendingCallout>,
194 /// Monotonic callout-token counter (tokens are handed out by the
195 /// host and delivered back with `proxy_on_http_call_response`).
196 pub callout_token_counter: u32,
197 /// The last delivered callout response's header map (`:status`
198 /// plus ordinary headers, hop-by-hop stripped) — what MapType 6
199 /// reads return inside `proxy_on_http_call_response`.
200 pub callout_response_headers: Vec<(String, String)>,
201 /// The last delivered callout response's body — what BufferType 4
202 /// reads return inside `proxy_on_http_call_response`.
203 pub callout_response_body: Vec<u8>,
204 /// Metrics registered by the plugin.
205 pub metrics: HashMap<String, PluginMetric>,
206 /// The current context ID (set by the host before calling exports).
207 pub current_context_id: u32,
208 /// The effective context ID (set by `proxy_set_effective_context`).
209 pub effective_context_id: u32,
210 /// Whether `proxy_done` was called.
211 pub done: bool,
212 /// Memory limiter state (tracks current allocation against the cap).
213 pub memory_used: usize,
214 /// Memory cap in bytes.
215 pub memory_cap: usize,
216 /// Log bytes buffered so far (the buffer is capped at
217 /// [`LOG_BUFFER_CAP_BYTES`]; see [`PluginContext::push_log`]).
218 log_bytes: usize,
219 /// Whether the log cap was hit — every further line is dropped.
220 log_dropped: bool,
221}
222
223/// A local response set by the plugin via `proxy_send_http_response`.
224#[derive(Clone, Debug)]
225pub struct LocalResponse {
226 pub status: u16,
227 pub headers: Vec<(String, String)>,
228 pub body: Vec<u8>,
229}
230
231/// A plugin-registered metric.
232#[derive(Clone, Debug)]
233pub struct PluginMetric {
234 pub metric_type: PluginMetricType,
235 pub value: f64,
236}
237
238/// One `proxy_http_call` dispatch, parsed and validated at the
239/// hostcall boundary (DW-167). The instance queues it; the dispatch
240/// driver (the async plugin_dispatch boundary) performs the exchange
241/// and delivers the response back through
242/// [`PluginInstance::deliver_callout_response`].
243#[derive(Clone, Debug)]
244pub struct PendingCallout {
245 /// The token the host handed the plugin (returned with the
246 /// response; the SDK dispatcher routes by it).
247 pub token: u32,
248 /// The full `http(s)://host[:port]/path?query` URI string.
249 pub uri: String,
250 /// The request method (`:method`; GET when the dispatch map
251 /// carried none).
252 pub method: String,
253 /// Ordinary headers (pseudo-headers stripped; `:authority` folds
254 /// into the URI's authority when the map carried one).
255 pub headers: Vec<(String, String)>,
256 /// The request body (may be empty).
257 pub body: Vec<u8>,
258 /// The plugin-requested timeout in milliseconds (the unit the
259 /// Rust SDK's `dispatch_http_call` sends); clamped at perform
260 /// time.
261 pub timeout_ms: u32,
262}
263
264/// Metric types (proxy-wasm §2.4).
265#[derive(Clone, Debug, PartialEq, Eq)]
266pub enum PluginMetricType {
267 Counter,
268 Gauge,
269 Histogram,
270}
271
272impl PluginContext {
273 fn new(
274 plugin_config: Vec<u8>,
275 vm_config: Vec<u8>,
276 memory_cap: usize,
277 shared: Arc<Mutex<SharedData>>,
278 ) -> Self {
279 Self {
280 request_headers: Vec::new(),
281 response_headers: Vec::new(),
282 request_body: Vec::new(),
283 response_body: Vec::new(),
284 plugin_config,
285 vm_config,
286 action: abi::ACTION_CONTINUE,
287 local_response: None,
288 logs: Vec::new(),
289 shared_data: shared,
290 pending_callouts: Vec::new(),
291 callout_token_counter: 0,
292 callout_response_headers: Vec::new(),
293 callout_response_body: Vec::new(),
294 metrics: HashMap::new(),
295 current_context_id: 0,
296 effective_context_id: 0,
297 done: false,
298 memory_used: 0,
299 memory_cap,
300 log_bytes: 0,
301 log_dropped: false,
302 }
303 }
304
305 /// Append a log line to the per-instance buffer, enforcing the
306 /// [`LOG_BUFFER_CAP_BYTES`] cap: once the budget is spent, the
307 /// head of the overflowing line is kept (if it fits), a truncation
308 /// marker is appended, and every further line is dropped. Use this
309 /// instead of pushing to `logs` directly.
310 fn push_log(&mut self, level: u32, msg: String) {
311 if self.log_dropped {
312 return;
313 }
314 if self.log_bytes + msg.len() > LOG_BUFFER_CAP_BYTES {
315 let remaining = LOG_BUFFER_CAP_BYTES - self.log_bytes;
316 if remaining > 0 {
317 // Keep the head of the overflowing line (a panic's
318 // first line is the diagnostic that matters).
319 let mut cut = remaining.min(msg.len());
320 while cut > 0 && !msg.is_char_boundary(cut) {
321 cut -= 1;
322 }
323 if cut > 0 {
324 self.log_bytes += cut;
325 let head = msg[..cut].to_string();
326 self.logs.push((level, head));
327 }
328 }
329 self.log_dropped = true;
330 self.logs.push((
331 level,
332 format!(
333 "... log buffer cap ({} KiB) reached; further plugin log output dropped",
334 LOG_BUFFER_CAP_BYTES / 1024
335 ),
336 ));
337 return;
338 }
339 self.log_bytes += msg.len();
340 self.logs.push((level, msg));
341 }
342}
343
344/// The memory cap for a plugin instance, enforced via wasmtime's
345/// `ResourceLimiter` trait on the store's user data (`PluginContext`).
346impl ResourceLimiter for PluginContext {
347 fn memory_growing(
348 &mut self,
349 _current: usize,
350 desired: usize,
351 _maximum: Option<usize>,
352 ) -> wasmtime::Result<bool> {
353 Ok(desired <= self.memory_cap)
354 }
355
356 fn table_growing(
357 &mut self,
358 _current: usize,
359 desired: usize,
360 _maximum: Option<usize>,
361 ) -> wasmtime::Result<bool> {
362 Ok(desired <= 10_000)
363 }
364}
365
366impl WasmEngine {
367 /// Create a new engine with the proxy-wasm ABI linker.
368 pub fn new() -> Result<Self, String> {
369 let mut config = wasmtime::Config::new();
370 config.consume_fuel(true);
371 config.epoch_interruption(true);
372 // cranelift is the compiler (enabled via the `cranelift` feature).
373 config.strategy(wasmtime::Strategy::Cranelift);
374
375 let engine = Engine::new(&config).map_err(|e| format!("wasmtime engine: {e}"))?;
376
377 let mut linker: Linker<PluginContext> = Linker::new(&engine);
378
379 // --- proxy-wasm ABI imports ---
380 //
381 // Each import is a function the plugin can call. The names
382 // follow the proxy-wasm spec (§3). All return i32 (WasmResult:
383 // 0 = OK, non-zero = error).
384
385 // proxy_log(level: i32, msg_ptr: i32, msg_size: i32) -> i32
386 linker
387 .func_wrap(
388 "env",
389 "proxy_log",
390 |mut caller: wasmtime::Caller<PluginContext>,
391 level: i32,
392 msg_ptr: i32,
393 msg_size: i32|
394 -> i32 {
395 if msg_ptr < 0 || msg_size < 0 {
396 return 1;
397 }
398 let memory = match caller.get_export("memory") {
399 Some(wasmtime::Extern::Memory(m)) => m,
400 _ => return 1,
401 };
402 let msg = match memory
403 .data(&caller)
404 .get(msg_ptr as usize..(msg_ptr as usize + msg_size as usize))
405 {
406 Some(slice) => slice.to_vec(),
407 None => return 1,
408 };
409 let msg = String::from_utf8_lossy(&msg).into_owned();
410 caller.data_mut().push_log(level as u32, msg);
411 0
412 },
413 )
414 .map_err(|e| format!("linker proxy_log: {e}"))?;
415
416 // proxy_get_buffer_bytes(bt, offset, max_size, ptr_ptr, size_ptr) -> i32
417 linker
418 .func_wrap(
419 "env",
420 "proxy_get_buffer_bytes",
421 |mut caller: wasmtime::Caller<PluginContext>,
422 bt: i32,
423 offset: i32,
424 max_size: i32,
425 ptr_ptr: i32,
426 size_ptr: i32|
427 -> i32 {
428 // Extract the requested chunk into an owned Vec so the
429 // mutable borrow of `caller` ends before we touch memory.
430 let chunk: Vec<u8> = {
431 let ctx = caller.data_mut();
432 let data = match bt as u32 {
433 abi::BUFFER_REQUEST_BODY => &ctx.request_body,
434 abi::BUFFER_RESPONSE_BODY => &ctx.response_body,
435 abi::BUFFER_CALLOUT_RESPONSE_BODY => &ctx.callout_response_body,
436 abi::BUFFER_PLUGIN_CONFIGURATION => &ctx.plugin_config,
437 abi::BUFFER_VM_CONFIGURATION => &ctx.vm_config,
438 _ => return 1,
439 };
440 let offset = offset as usize;
441 let max_size = max_size as usize;
442 if offset > data.len() {
443 return 1;
444 }
445 let end = (offset + max_size).min(data.len());
446 data[offset..end].to_vec()
447 };
448 let memory = match caller.get_export("memory") {
449 Some(wasmtime::Extern::Memory(m)) => m,
450 _ => return 1,
451 };
452 if chunk.is_empty() {
453 // Write zero to ptr_ptr and 0 to size_ptr.
454 if write_i32_to_memory(&memory, &mut caller, ptr_ptr, 0).is_err() {
455 return 1;
456 }
457 if write_i32_to_memory(&memory, &mut caller, size_ptr, 0).is_err() {
458 return 1;
459 }
460 return 0;
461 }
462 // Allocate space in plugin memory by calling the
463 // plugin's `proxy_on_memory_allocate` export (the
464 // standard proxy-wasm allocation pattern). If the
465 // export doesn't exist, use a simple bump approach
466 // via memory.grow.
467 let alloc_ptr = match allocate_in_plugin(&mut caller, &memory, chunk.len()) {
468 Ok(p) => p,
469 Err(_) => return 1,
470 };
471 if memory
472 .data_mut(&mut caller)
473 .get_mut(alloc_ptr..alloc_ptr + chunk.len())
474 .map(|dst| {
475 dst.copy_from_slice(&chunk);
476 })
477 .is_none()
478 {
479 return 1;
480 }
481 if write_i32_to_memory(&memory, &mut caller, ptr_ptr, alloc_ptr as i32).is_err()
482 {
483 return 1;
484 }
485 if write_i32_to_memory(&memory, &mut caller, size_ptr, chunk.len() as i32)
486 .is_err()
487 {
488 return 1;
489 }
490 0
491 },
492 )
493 .map_err(|e| format!("linker proxy_get_buffer_bytes: {e}"))?;
494
495 // proxy_get_buffer_status(bt, ptr_ptr, size_ptr) -> i32
496 linker
497 .func_wrap(
498 "env",
499 "proxy_get_buffer_status",
500 |mut caller: wasmtime::Caller<PluginContext>,
501 bt: i32,
502 _ptr_ptr: i32,
503 size_ptr: i32|
504 -> i32 {
505 let ctx = caller.data();
506 let len = match bt as u32 {
507 abi::BUFFER_REQUEST_BODY => ctx.request_body.len(),
508 abi::BUFFER_RESPONSE_BODY => ctx.response_body.len(),
509 abi::BUFFER_CALLOUT_RESPONSE_BODY => ctx.callout_response_body.len(),
510 abi::BUFFER_PLUGIN_CONFIGURATION => ctx.plugin_config.len(),
511 abi::BUFFER_VM_CONFIGURATION => ctx.vm_config.len(),
512 _ => return 1,
513 };
514 let memory = match caller.get_export("memory") {
515 Some(wasmtime::Extern::Memory(m)) => m,
516 _ => return 1,
517 };
518 if write_i32_to_memory(&memory, &mut caller, size_ptr, len as i32).is_err() {
519 return 1;
520 }
521 0
522 },
523 )
524 .map_err(|e| format!("linker proxy_get_buffer_status: {e}"))?;
525
526 // proxy_set_buffer_bytes(bt, offset, size, ptr, size) -> i32
527 linker
528 .func_wrap(
529 "env",
530 "proxy_set_buffer_bytes",
531 |mut caller: wasmtime::Caller<PluginContext>,
532 bt: i32,
533 offset: i32,
534 _size: i32,
535 ptr: i32,
536 data_size: i32|
537 -> i32 {
538 if ptr < 0 || data_size < 0 {
539 return 1;
540 }
541 let memory = match caller.get_export("memory") {
542 Some(wasmtime::Extern::Memory(m)) => m,
543 _ => return 1,
544 };
545 let data = match memory
546 .data(&caller)
547 .get(ptr as usize..(ptr as usize + data_size as usize))
548 {
549 Some(slice) => slice.to_vec(),
550 None => return 1,
551 };
552 let ctx = caller.data_mut();
553 let offset = offset as usize;
554 let buf = match bt as u32 {
555 abi::BUFFER_REQUEST_BODY => &mut ctx.request_body,
556 abi::BUFFER_RESPONSE_BODY => &mut ctx.response_body,
557 _ => return 1,
558 };
559 if offset > buf.len() {
560 buf.resize(offset, 0);
561 }
562 buf.splice(offset.., data);
563 0
564 },
565 )
566 .map_err(|e| format!("linker proxy_set_buffer_bytes: {e}"))?;
567
568 // proxy_get_header_map_pairs(bt, ptr_ptr, size_ptr) -> i32
569 // Serialized in the spec/SDK wire layout (LE count + length
570 // table + NUL-terminated strings — what the Rust proxy-wasm
571 // SDK's get_map parses; see abi::serialize_header_map_spec).
572 // An empty map reports ptr=0/size=0 (the SDK reads that as an
573 // empty map without allocating).
574 linker
575 .func_wrap(
576 "env",
577 "proxy_get_header_map_pairs",
578 |mut caller: wasmtime::Caller<PluginContext>,
579 bt: i32,
580 ptr_ptr: i32,
581 size_ptr: i32|
582 -> i32 {
583 let headers = {
584 let ctx = caller.data();
585 match bt as u32 {
586 abi::BUFFER_REQUEST_HEADERS => ctx.request_headers.clone(),
587 abi::BUFFER_RESPONSE_HEADERS => ctx.response_headers.clone(),
588 // The delivered callout response's headers
589 // (DW-167): `:status` first, then the
590 // ordinary headers (hop-by-hop stripped).
591 abi::BUFFER_CALLOUT_RESPONSE_HEADERS => {
592 ctx.callout_response_headers.clone()
593 }
594 // Trailers are not plumbed through dwara's
595 // pipeline: they read as an empty map.
596 abi::BUFFER_REQUEST_TRAILERS
597 | abi::BUFFER_RESPONSE_TRAILERS
598 | abi::BUFFER_CALLOUT_RESPONSE_TRAILERS => Vec::new(),
599 _ => return 1,
600 }
601 };
602 let encoded = abi::serialize_header_map_spec(&headers);
603 let memory = match caller.get_export("memory") {
604 Some(wasmtime::Extern::Memory(m)) => m,
605 _ => return 1,
606 };
607 if encoded.len() <= 4 {
608 if write_i32_to_memory(&memory, &mut caller, ptr_ptr, 0).is_err() {
609 return 1;
610 }
611 if write_i32_to_memory(&memory, &mut caller, size_ptr, 0).is_err() {
612 return 1;
613 }
614 return 0;
615 }
616 let alloc_ptr = match allocate_in_plugin(&mut caller, &memory, encoded.len()) {
617 Ok(p) => p,
618 Err(_) => return 1,
619 };
620 if memory
621 .data_mut(&mut caller)
622 .get_mut(alloc_ptr..alloc_ptr + encoded.len())
623 .map(|dst| dst.copy_from_slice(&encoded))
624 .is_none()
625 {
626 return 1;
627 }
628 if write_i32_to_memory(&memory, &mut caller, ptr_ptr, alloc_ptr as i32).is_err()
629 {
630 return 1;
631 }
632 if write_i32_to_memory(&memory, &mut caller, size_ptr, encoded.len() as i32)
633 .is_err()
634 {
635 return 1;
636 }
637 0
638 },
639 )
640 .map_err(|e| format!("linker proxy_get_header_map_pairs: {e}"))?;
641
642 // proxy_set_header_map_pairs(bt, ptr, size) -> i32
643 // Parses the spec/SDK wire layout (what the Rust proxy-wasm
644 // SDK's set_map serializes; see
645 // abi::deserialize_header_map_spec). A zero-size buffer is an
646 // empty map.
647 linker
648 .func_wrap(
649 "env",
650 "proxy_set_header_map_pairs",
651 |mut caller: wasmtime::Caller<PluginContext>,
652 bt: i32,
653 ptr: i32,
654 size: i32|
655 -> i32 {
656 if ptr < 0 || size < 0 {
657 return 1;
658 }
659 let memory = match caller.get_export("memory") {
660 Some(wasmtime::Extern::Memory(m)) => m,
661 _ => return 1,
662 };
663 let data = match memory
664 .data(&caller)
665 .get(ptr as usize..(ptr as usize + size as usize))
666 {
667 Some(slice) => slice.to_vec(),
668 None => return 1,
669 };
670 let headers = match abi::deserialize_header_map_spec(&data) {
671 Some(h) => h,
672 None => return 1,
673 };
674 let ctx = caller.data_mut();
675 match bt as u32 {
676 abi::BUFFER_REQUEST_HEADERS => ctx.request_headers = headers,
677 abi::BUFFER_RESPONSE_HEADERS => ctx.response_headers = headers,
678 // Trailer and callout-response-map stores are
679 // accepted and discarded (neither is plumbed
680 // through the pipeline); reporting success
681 // keeps SDK plugins from panicking on a
682 // benign store.
683 abi::BUFFER_REQUEST_TRAILERS
684 | abi::BUFFER_RESPONSE_TRAILERS
685 | abi::BUFFER_CALLOUT_RESPONSE_HEADERS
686 | abi::BUFFER_CALLOUT_RESPONSE_TRAILERS => {}
687 _ => return 1,
688 }
689 0
690 },
691 )
692 .map_err(|e| format!("linker proxy_set_header_map_pairs: {e}"))?;
693
694 // proxy_get_header_map_value(bt, key_ptr, key_size, value_ptr_ptr, value_size_ptr) -> i32
695 linker
696 .func_wrap(
697 "env",
698 "proxy_get_header_map_value",
699 |mut caller: wasmtime::Caller<PluginContext>,
700 bt: i32,
701 key_ptr: i32,
702 key_size: i32,
703 value_ptr_ptr: i32,
704 value_size_ptr: i32|
705 -> i32 {
706 if key_ptr < 0 || key_size < 0 {
707 return 1;
708 }
709 let memory = match caller.get_export("memory") {
710 Some(wasmtime::Extern::Memory(m)) => m,
711 _ => return 1,
712 };
713 let key = match memory
714 .data(&caller)
715 .get(key_ptr as usize..(key_ptr as usize + key_size as usize))
716 {
717 Some(slice) => String::from_utf8_lossy(slice).into_owned(),
718 None => return 1,
719 };
720 let value = {
721 let ctx = caller.data();
722 let headers: &[(String, String)] = match bt as u32 {
723 abi::BUFFER_REQUEST_HEADERS => &ctx.request_headers,
724 abi::BUFFER_RESPONSE_HEADERS => &ctx.response_headers,
725 // The delivered callout response's headers
726 // (DW-167): a lookup lands on `:status` or
727 // an ordinary header the target sent.
728 abi::BUFFER_CALLOUT_RESPONSE_HEADERS => &ctx.callout_response_headers,
729 // Trailers are not plumbed through dwara's
730 // pipeline: they read as empty (the SDK
731 // maps an empty result to None).
732 abi::BUFFER_REQUEST_TRAILERS
733 | abi::BUFFER_RESPONSE_TRAILERS
734 | abi::BUFFER_CALLOUT_RESPONSE_TRAILERS => &[],
735 _ => return 1,
736 };
737 headers
738 .iter()
739 .find(|(k, _)| k.eq_ignore_ascii_case(&key))
740 .map(|(_, v)| v.clone())
741 .unwrap_or_default()
742 };
743 let memory = match caller.get_export("memory") {
744 Some(wasmtime::Extern::Memory(m)) => m,
745 _ => return 1,
746 };
747 if value.is_empty() {
748 if write_i32_to_memory(&memory, &mut caller, value_ptr_ptr, 0).is_err() {
749 return 1;
750 }
751 if write_i32_to_memory(&memory, &mut caller, value_size_ptr, 0).is_err() {
752 return 1;
753 }
754 return 0;
755 }
756 let alloc_ptr = match allocate_in_plugin(&mut caller, &memory, value.len()) {
757 Ok(p) => p,
758 Err(_) => return 1,
759 };
760 if memory
761 .data_mut(&mut caller)
762 .get_mut(alloc_ptr..alloc_ptr + value.len())
763 .map(|dst| dst.copy_from_slice(value.as_bytes()))
764 .is_none()
765 {
766 return 1;
767 }
768 if write_i32_to_memory(&memory, &mut caller, value_ptr_ptr, alloc_ptr as i32)
769 .is_err()
770 {
771 return 1;
772 }
773 if write_i32_to_memory(&memory, &mut caller, value_size_ptr, value.len() as i32)
774 .is_err()
775 {
776 return 1;
777 }
778 0
779 },
780 )
781 .map_err(|e| format!("linker proxy_get_header_map_value: {e}"))?;
782
783 // proxy_add_header_map_value(bt, key_ptr, key_size, value_ptr, value_size) -> i32
784 linker
785 .func_wrap(
786 "env",
787 "proxy_add_header_map_value",
788 |mut caller: wasmtime::Caller<PluginContext>,
789 bt: i32,
790 key_ptr: i32,
791 key_size: i32,
792 value_ptr: i32,
793 value_size: i32|
794 -> i32 {
795 if key_ptr < 0 || key_size < 0 || value_ptr < 0 || value_size < 0 {
796 return 1;
797 }
798 let memory = match caller.get_export("memory") {
799 Some(wasmtime::Extern::Memory(m)) => m,
800 _ => return 1,
801 };
802 let key = match memory
803 .data(&caller)
804 .get(key_ptr as usize..(key_ptr as usize + key_size as usize))
805 {
806 Some(slice) => String::from_utf8_lossy(slice).into_owned(),
807 None => return 1,
808 };
809 let value = match memory
810 .data(&caller)
811 .get(value_ptr as usize..(value_ptr as usize + value_size as usize))
812 {
813 Some(slice) => String::from_utf8_lossy(slice).into_owned(),
814 None => return 1,
815 };
816 let ctx = caller.data_mut();
817 // Trailer and callout-response-map mutations are
818 // accepted and discarded (neither is plumbed
819 // through the pipeline); reporting success keeps
820 // SDK plugins from panicking on a benign store.
821 if bt as u32 == abi::BUFFER_REQUEST_TRAILERS
822 || bt as u32 == abi::BUFFER_RESPONSE_TRAILERS
823 || bt as u32 == abi::BUFFER_CALLOUT_RESPONSE_HEADERS
824 || bt as u32 == abi::BUFFER_CALLOUT_RESPONSE_TRAILERS
825 {
826 return 0;
827 }
828 let headers = match bt as u32 {
829 abi::BUFFER_REQUEST_HEADERS => &mut ctx.request_headers,
830 abi::BUFFER_RESPONSE_HEADERS => &mut ctx.response_headers,
831 _ => return 1,
832 };
833 headers.push((key, value));
834 0
835 },
836 )
837 .map_err(|e| format!("linker proxy_add_header_map_value: {e}"))?;
838
839 // proxy_replace_header_map_value(bt, key_ptr, key_size, value_ptr, value_size) -> i32
840 linker
841 .func_wrap(
842 "env",
843 "proxy_replace_header_map_value",
844 |mut caller: wasmtime::Caller<PluginContext>,
845 bt: i32,
846 key_ptr: i32,
847 key_size: i32,
848 value_ptr: i32,
849 value_size: i32|
850 -> i32 {
851 if key_ptr < 0 || key_size < 0 || value_ptr < 0 || value_size < 0 {
852 return 1;
853 }
854 let memory = match caller.get_export("memory") {
855 Some(wasmtime::Extern::Memory(m)) => m,
856 _ => return 1,
857 };
858 let key = match memory
859 .data(&caller)
860 .get(key_ptr as usize..(key_ptr as usize + key_size as usize))
861 {
862 Some(slice) => String::from_utf8_lossy(slice).into_owned(),
863 None => return 1,
864 };
865 let value = match memory
866 .data(&caller)
867 .get(value_ptr as usize..(value_ptr as usize + value_size as usize))
868 {
869 Some(slice) => String::from_utf8_lossy(slice).into_owned(),
870 None => return 1,
871 };
872 let ctx = caller.data_mut();
873 // Trailer and callout-response-map mutations are
874 // accepted and discarded (neither is plumbed
875 // through the pipeline); reporting success keeps
876 // SDK plugins from panicking on a benign store.
877 if bt as u32 == abi::BUFFER_REQUEST_TRAILERS
878 || bt as u32 == abi::BUFFER_RESPONSE_TRAILERS
879 || bt as u32 == abi::BUFFER_CALLOUT_RESPONSE_HEADERS
880 || bt as u32 == abi::BUFFER_CALLOUT_RESPONSE_TRAILERS
881 {
882 return 0;
883 }
884 let headers = match bt as u32 {
885 abi::BUFFER_REQUEST_HEADERS => &mut ctx.request_headers,
886 abi::BUFFER_RESPONSE_HEADERS => &mut ctx.response_headers,
887 _ => return 1,
888 };
889 if let Some(entry) = headers
890 .iter_mut()
891 .find(|(k, _)| k.eq_ignore_ascii_case(&key))
892 {
893 entry.1 = value;
894 } else {
895 headers.push((key, value));
896 }
897 0
898 },
899 )
900 .map_err(|e| format!("linker proxy_replace_header_map_value: {e}"))?;
901
902 // proxy_remove_header_map_value(bt, key_ptr, key_size) -> i32
903 linker
904 .func_wrap(
905 "env",
906 "proxy_remove_header_map_value",
907 |mut caller: wasmtime::Caller<PluginContext>,
908 bt: i32,
909 key_ptr: i32,
910 key_size: i32|
911 -> i32 {
912 if key_ptr < 0 || key_size < 0 {
913 return 1;
914 }
915 let memory = match caller.get_export("memory") {
916 Some(wasmtime::Extern::Memory(m)) => m,
917 _ => return 1,
918 };
919 let key = match memory
920 .data(&caller)
921 .get(key_ptr as usize..(key_ptr as usize + key_size as usize))
922 {
923 Some(slice) => String::from_utf8_lossy(slice).into_owned(),
924 None => return 1,
925 };
926 let ctx = caller.data_mut();
927 // Trailer and callout-response-map mutations are
928 // accepted and discarded (neither is plumbed
929 // through the pipeline); reporting success keeps
930 // SDK plugins from panicking on a benign store.
931 if bt as u32 == abi::BUFFER_REQUEST_TRAILERS
932 || bt as u32 == abi::BUFFER_RESPONSE_TRAILERS
933 || bt as u32 == abi::BUFFER_CALLOUT_RESPONSE_HEADERS
934 || bt as u32 == abi::BUFFER_CALLOUT_RESPONSE_TRAILERS
935 {
936 return 0;
937 }
938 let headers = match bt as u32 {
939 abi::BUFFER_REQUEST_HEADERS => &mut ctx.request_headers,
940 abi::BUFFER_RESPONSE_HEADERS => &mut ctx.response_headers,
941 _ => return 1,
942 };
943 headers.retain(|(k, _)| !k.eq_ignore_ascii_case(&key));
944 0
945 },
946 )
947 .map_err(|e| format!("linker proxy_remove_header_map_value: {e}"))?;
948
949 // proxy_send_http_response(status, headers_ptr, headers_size, body_ptr, body_size, trailers_ptr, trailers_size) -> i32
950 // dwara's original spelling (the WAT fixtures and host tests
951 // use it).
952 linker
953 .func_wrap("env", "proxy_send_http_response", send_http_response_impl)
954 .map_err(|e| format!("linker proxy_send_http_response: {e}"))?;
955
956 // proxy_send_local_response(status_code, details_ptr, details_size,
957 // body_ptr, body_size, headers_ptr,
958 // headers_size, grpc_status) -> i32
959 // The proxy-wasm SPEC hostcall (a different signature and
960 // argument order than dwara's original spelling above): the
961 // Rust proxy-wasm SDK's `send_http_response` imports this name
962 // with this shape, and its header argument is always the
963 // spec/SDK map serialization (serialize_map — for empty
964 // headers that is the 4-byte LE count 0, so a 4-byte buffer
965 // parses as an empty map, never a parse failure). Status-code
966 // details and the gRPC status are carried on the
967 // LocalResponse's status/body path only — dwara does not
968 // forward them anywhere today.
969 linker
970 .func_wrap(
971 "env",
972 "proxy_send_local_response",
973 |mut caller: wasmtime::Caller<PluginContext>,
974 status_code: i32,
975 _details_ptr: i32,
976 _details_size: i32,
977 body_ptr: i32,
978 body_size: i32,
979 headers_ptr: i32,
980 headers_size: i32,
981 _grpc_status: i32|
982 -> i32 {
983 let memory = match caller.get_export("memory") {
984 Some(wasmtime::Extern::Memory(m)) => m,
985 _ => return 1,
986 };
987 let headers = if headers_ptr > 0 && headers_size > 0 {
988 let data = match memory.data(&caller).get(
989 headers_ptr as usize..(headers_ptr as usize + headers_size as usize),
990 ) {
991 Some(slice) => slice.to_vec(),
992 None => return 1,
993 };
994 match abi::deserialize_header_map_spec(&data) {
995 Some(h) => h,
996 None => return 1,
997 }
998 } else {
999 Vec::new()
1000 };
1001 let body = if body_ptr > 0 && body_size > 0 {
1002 match memory
1003 .data(&caller)
1004 .get(body_ptr as usize..(body_ptr as usize + body_size as usize))
1005 {
1006 Some(slice) => slice.to_vec(),
1007 None => return 1,
1008 }
1009 } else {
1010 Vec::new()
1011 };
1012 let ctx = caller.data_mut();
1013 ctx.local_response = Some(LocalResponse {
1014 status: status_code as u16,
1015 headers,
1016 body,
1017 });
1018 ctx.action = abi::ACTION_END_STREAM;
1019 0
1020 },
1021 )
1022 .map_err(|e| format!("linker proxy_send_local_response: {e}"))?;
1023
1024 // proxy_continue_stream(bt) -> i32
1025 linker
1026 .func_wrap(
1027 "env",
1028 "proxy_continue_stream",
1029 |_caller: wasmtime::Caller<PluginContext>, _bt: i32| -> i32 { 0 },
1030 )
1031 .map_err(|e| format!("linker proxy_continue_stream: {e}"))?;
1032
1033 // proxy_close_stream(bt) -> i32
1034 linker
1035 .func_wrap(
1036 "env",
1037 "proxy_close_stream",
1038 |mut caller: wasmtime::Caller<PluginContext>, _bt: i32| -> i32 {
1039 caller.data_mut().action = abi::ACTION_END_STREAM;
1040 0
1041 },
1042 )
1043 .map_err(|e| format!("linker proxy_close_stream: {e}"))?;
1044
1045 // proxy_get_shared_data(key_ptr, key_size, value_ptr_ptr, value_size_ptr, cas_ptr) -> i32
1046 linker
1047 .func_wrap(
1048 "env",
1049 "proxy_get_shared_data",
1050 |mut caller: wasmtime::Caller<PluginContext>,
1051 key_ptr: i32,
1052 key_size: i32,
1053 value_ptr_ptr: i32,
1054 value_size_ptr: i32,
1055 cas_ptr: i32|
1056 -> i32 {
1057 if key_ptr < 0 || key_size < 0 {
1058 return 1;
1059 }
1060 let memory = match caller.get_export("memory") {
1061 Some(wasmtime::Extern::Memory(m)) => m,
1062 _ => return 1,
1063 };
1064 let key = match memory
1065 .data(&caller)
1066 .get(key_ptr as usize..(key_ptr as usize + key_size as usize))
1067 {
1068 Some(slice) => String::from_utf8_lossy(slice).into_owned(),
1069 None => return 1,
1070 };
1071 // VM-scoped store (DW-167): the lock is held only
1072 // for the clone, never across a hostcall that
1073 // re-enters wasm or writes plugin memory. Poison
1074 // recovery: the critical section is a read+clone
1075 // (nothing can half-mutate the map), so a panic
1076 // elsewhere must not take every later request on
1077 // this module down with the lock.
1078 let lookup = {
1079 let shared = caller
1080 .data()
1081 .shared_data
1082 .lock()
1083 .unwrap_or_else(std::sync::PoisonError::into_inner);
1084 shared.get(&key).map(|(v, c)| (v.clone(), *c))
1085 };
1086 let Some((value, cas)) = lookup else {
1087 // Not found: return OK with zero values.
1088 if write_i32_to_memory(&memory, &mut caller, value_ptr_ptr, 0).is_err() {
1089 return 1;
1090 }
1091 if write_i32_to_memory(&memory, &mut caller, value_size_ptr, 0).is_err() {
1092 return 1;
1093 }
1094 if write_i32_to_memory(&memory, &mut caller, cas_ptr, 0).is_err() {
1095 return 1;
1096 }
1097 return 0;
1098 };
1099 let alloc_ptr = match allocate_in_plugin(&mut caller, &memory, value.len()) {
1100 Ok(p) => p,
1101 Err(_) => return 1,
1102 };
1103 if memory
1104 .data_mut(&mut caller)
1105 .get_mut(alloc_ptr..alloc_ptr + value.len())
1106 .map(|dst| dst.copy_from_slice(&value))
1107 .is_none()
1108 {
1109 return 1;
1110 }
1111 if write_i32_to_memory(&memory, &mut caller, value_ptr_ptr, alloc_ptr as i32)
1112 .is_err()
1113 {
1114 return 1;
1115 }
1116 if write_i32_to_memory(&memory, &mut caller, value_size_ptr, value.len() as i32)
1117 .is_err()
1118 {
1119 return 1;
1120 }
1121 if write_i32_to_memory(&memory, &mut caller, cas_ptr, cas as i32).is_err() {
1122 return 1;
1123 }
1124 0
1125 },
1126 )
1127 .map_err(|e| format!("linker proxy_get_shared_data: {e}"))?;
1128
1129 // proxy_set_shared_data(key_ptr, key_size, value_ptr, value_size, cas) -> i32
1130 linker
1131 .func_wrap(
1132 "env",
1133 "proxy_set_shared_data",
1134 |mut caller: wasmtime::Caller<PluginContext>,
1135 key_ptr: i32,
1136 key_size: i32,
1137 value_ptr: i32,
1138 value_size: i32,
1139 cas: i32|
1140 -> i32 {
1141 if key_ptr < 0 || key_size < 0 {
1142 return 1;
1143 }
1144 let memory = match caller.get_export("memory") {
1145 Some(wasmtime::Extern::Memory(m)) => m,
1146 _ => return 1,
1147 };
1148 let key = match memory
1149 .data(&caller)
1150 .get(key_ptr as usize..(key_ptr as usize + key_size as usize))
1151 {
1152 Some(slice) => String::from_utf8_lossy(slice).into_owned(),
1153 None => return 1,
1154 };
1155 let value = if value_ptr > 0 && value_size > 0 {
1156 match memory
1157 .data(&caller)
1158 .get(value_ptr as usize..(value_ptr as usize + value_size as usize))
1159 {
1160 Some(slice) => slice.to_vec(),
1161 None => return 1,
1162 }
1163 } else {
1164 Vec::new()
1165 };
1166 let ctx = caller.data_mut();
1167 // VM-scoped store (DW-167): the CAS read, the cap
1168 // check, and the write are one critical section, so
1169 // two instances racing a compare-and-set cannot
1170 // interleave. Poison recovery: the section is a
1171 // read plus one `insert` (the map cannot be left
1172 // half-mutated), so a panic elsewhere must not
1173 // poison every later request on this module.
1174 let mut shared = ctx
1175 .shared_data
1176 .lock()
1177 .unwrap_or_else(std::sync::PoisonError::into_inner);
1178 let current_cas = shared.get(&key).map(|(_, c)| *c).unwrap_or(0);
1179 if cas > 0 && cas != current_cas as i32 {
1180 // Status::CasMismatch — the ONE error status
1181 // the Rust SDK's set_shared_data maps to a
1182 // clean Err (any other status panics the
1183 // module).
1184 return 8;
1185 }
1186 if !shared_data_fits(&shared, &key, &value) {
1187 // Over-cap set: refused, never evicted (see
1188 // SHARED_DATA_CAP_BYTES). The same error-status
1189 // surface as the CAS mismatch — the SDK's only
1190 // branchable error for this hostcall.
1191 return 8;
1192 }
1193 let new_cas = current_cas + 1;
1194 shared.insert(key, (value, new_cas));
1195 0
1196 },
1197 )
1198 .map_err(|e| format!("linker proxy_set_shared_data: {e}"))?;
1199
1200 // proxy_set_effective_context(context_id) -> i32
1201 linker
1202 .func_wrap(
1203 "env",
1204 "proxy_set_effective_context",
1205 |mut caller: wasmtime::Caller<PluginContext>, context_id: i32| -> i32 {
1206 caller.data_mut().effective_context_id = context_id as u32;
1207 0
1208 },
1209 )
1210 .map_err(|e| format!("linker proxy_set_effective_context: {e}"))?;
1211
1212 // proxy_done() -> i32
1213 linker
1214 .func_wrap(
1215 "env",
1216 "proxy_done",
1217 |mut caller: wasmtime::Caller<PluginContext>| -> i32 {
1218 caller.data_mut().done = true;
1219 0
1220 },
1221 )
1222 .map_err(|e| format!("linker proxy_done: {e}"))?;
1223
1224 // proxy_get_property(path_ptr, path_size, value_ptr_ptr, value_size_ptr) -> i32
1225 // Minimal implementation: supports a few well-known properties.
1226 linker
1227 .func_wrap(
1228 "env",
1229 "proxy_get_property",
1230 |mut caller: wasmtime::Caller<PluginContext>,
1231 path_ptr: i32,
1232 path_size: i32,
1233 value_ptr_ptr: i32,
1234 value_size_ptr: i32|
1235 -> i32 {
1236 if path_ptr < 0 || path_size < 0 {
1237 return 1;
1238 }
1239 let memory = match caller.get_export("memory") {
1240 Some(wasmtime::Extern::Memory(m)) => m,
1241 _ => return 1,
1242 };
1243 let path = match memory
1244 .data(&caller)
1245 .get(path_ptr as usize..(path_ptr as usize + path_size as usize))
1246 {
1247 Some(slice) => String::from_utf8_lossy(slice).into_owned(),
1248 None => return 1,
1249 };
1250 // Minimal property support: return empty for unknown
1251 // properties (the plugin should handle this).
1252 let _ = path;
1253 if write_i32_to_memory(&memory, &mut caller, value_ptr_ptr, 0).is_err() {
1254 return 1;
1255 }
1256 if write_i32_to_memory(&memory, &mut caller, value_size_ptr, 0).is_err() {
1257 return 1;
1258 }
1259 0
1260 },
1261 )
1262 .map_err(|e| format!("linker proxy_get_property: {e}"))?;
1263
1264 // proxy_set_property(path_ptr, path_size, value_ptr, value_size) -> i32
1265 // Minimal implementation: no-op (returns OK).
1266 linker
1267 .func_wrap(
1268 "env",
1269 "proxy_set_property",
1270 |_caller: wasmtime::Caller<PluginContext>,
1271 _path_ptr: i32,
1272 _path_size: i32,
1273 _value_ptr: i32,
1274 _value_size: i32|
1275 -> i32 { 0 },
1276 )
1277 .map_err(|e| format!("linker proxy_set_property: {e}"))?;
1278
1279 // proxy_define_metric(metric_type, name_ptr, name_size, metric_id_ptr, metric_id_size_ptr) -> i32
1280 linker
1281 .func_wrap(
1282 "env",
1283 "proxy_define_metric",
1284 |mut caller: wasmtime::Caller<PluginContext>,
1285 metric_type: i32,
1286 name_ptr: i32,
1287 name_size: i32,
1288 _metric_id_ptr: i32,
1289 _metric_id_size_ptr: i32|
1290 -> i32 {
1291 if name_ptr < 0 || name_size < 0 {
1292 return 1;
1293 }
1294 let memory = match caller.get_export("memory") {
1295 Some(wasmtime::Extern::Memory(m)) => m,
1296 _ => return 1,
1297 };
1298 let name = match memory
1299 .data(&caller)
1300 .get(name_ptr as usize..(name_ptr as usize + name_size as usize))
1301 {
1302 Some(slice) => String::from_utf8_lossy(slice).into_owned(),
1303 None => return 1,
1304 };
1305 let mt = match metric_type {
1306 0 => PluginMetricType::Counter,
1307 1 => PluginMetricType::Gauge,
1308 2 => PluginMetricType::Histogram,
1309 _ => return 1,
1310 };
1311 let ctx = caller.data_mut();
1312 ctx.metrics.insert(
1313 name,
1314 PluginMetric {
1315 metric_type: mt,
1316 value: 0.0,
1317 },
1318 );
1319 0
1320 },
1321 )
1322 .map_err(|e| format!("linker proxy_define_metric: {e}"))?;
1323
1324 // proxy_record_metric(metric_id_ptr, metric_id_size, value) -> i32
1325 linker
1326 .func_wrap(
1327 "env",
1328 "proxy_record_metric",
1329 |mut caller: wasmtime::Caller<PluginContext>,
1330 name_ptr: i32,
1331 name_size: i32,
1332 value: f64|
1333 -> i32 {
1334 if name_ptr < 0 || name_size < 0 {
1335 return 1;
1336 }
1337 let memory = match caller.get_export("memory") {
1338 Some(wasmtime::Extern::Memory(m)) => m,
1339 _ => return 1,
1340 };
1341 let name = match memory
1342 .data(&caller)
1343 .get(name_ptr as usize..(name_ptr as usize + name_size as usize))
1344 {
1345 Some(slice) => String::from_utf8_lossy(slice).into_owned(),
1346 None => return 1,
1347 };
1348 let ctx = caller.data_mut();
1349 if let Some(m) = ctx.metrics.get_mut(&name) {
1350 m.value = value;
1351 }
1352 0
1353 },
1354 )
1355 .map_err(|e| format!("linker proxy_record_metric: {e}"))?;
1356
1357 // proxy_increment_metric(metric_id_ptr, metric_id_size, increment) -> i32
1358 linker
1359 .func_wrap(
1360 "env",
1361 "proxy_increment_metric",
1362 |mut caller: wasmtime::Caller<PluginContext>,
1363 name_ptr: i32,
1364 name_size: i32,
1365 increment: f64|
1366 -> i32 {
1367 if name_ptr < 0 || name_size < 0 {
1368 return 1;
1369 }
1370 let memory = match caller.get_export("memory") {
1371 Some(wasmtime::Extern::Memory(m)) => m,
1372 _ => return 1,
1373 };
1374 let name = match memory
1375 .data(&caller)
1376 .get(name_ptr as usize..(name_ptr as usize + name_size as usize))
1377 {
1378 Some(slice) => String::from_utf8_lossy(slice).into_owned(),
1379 None => return 1,
1380 };
1381 let ctx = caller.data_mut();
1382 if let Some(m) = ctx.metrics.get_mut(&name) {
1383 m.value += increment;
1384 }
1385 0
1386 },
1387 )
1388 .map_err(|e| format!("linker proxy_increment_metric: {e}"))?;
1389
1390 // proxy_get_metric(metric_id_ptr, metric_id_size, return_value_ptr) -> i32
1391 linker
1392 .func_wrap(
1393 "env",
1394 "proxy_get_metric",
1395 |mut caller: wasmtime::Caller<PluginContext>,
1396 name_ptr: i32,
1397 name_size: i32,
1398 return_value_ptr: i32|
1399 -> i32 {
1400 if name_ptr < 0 || name_size < 0 {
1401 return 1;
1402 }
1403 let memory = match caller.get_export("memory") {
1404 Some(wasmtime::Extern::Memory(m)) => m,
1405 _ => return 1,
1406 };
1407 let name = match memory
1408 .data(&caller)
1409 .get(name_ptr as usize..(name_ptr as usize + name_size as usize))
1410 {
1411 Some(slice) => String::from_utf8_lossy(slice).into_owned(),
1412 None => return 1,
1413 };
1414 let value = caller
1415 .data()
1416 .metrics
1417 .get(&name)
1418 .map(|m| m.value)
1419 .unwrap_or(0.0);
1420 // Write the f64 value to the return pointer.
1421 let bytes = value.to_le_bytes();
1422 if memory
1423 .data_mut(&mut caller)
1424 .get_mut(return_value_ptr as usize..(return_value_ptr as usize + 8))
1425 .map(|dst| dst.copy_from_slice(&bytes))
1426 .is_none()
1427 {
1428 return 1;
1429 }
1430 0
1431 },
1432 )
1433 .map_err(|e| format!("linker proxy_get_metric: {e}"))?;
1434
1435 // proxy_get_current_time(return_value_ptr) -> i32
1436 // proxy_get_current_time_nanoseconds(return_time) -> i32 (the
1437 // proxy-wasm spec name; the Rust SDK's `get_current_time`
1438 // imports this spelling). Both names share one implementation.
1439 for name in [
1440 "proxy_get_current_time",
1441 "proxy_get_current_time_nanoseconds",
1442 ] {
1443 linker
1444 .func_wrap("env", name, get_current_time_impl)
1445 .map_err(|e| format!("linker {name}: {e}"))?;
1446 }
1447
1448 // proxy_get_log_level(return_level_ptr) -> i32
1449 // The host buffers every proxy_log line (no level filtering),
1450 // so the query always reports Info. Registered because the Rust
1451 // SDK declares the import even when unused; plugins that gate
1452 // work on the reported level see Info.
1453 linker
1454 .func_wrap(
1455 "env",
1456 "proxy_get_log_level",
1457 |mut caller: wasmtime::Caller<PluginContext>, return_level_ptr: i32| -> i32 {
1458 let memory = match caller.get_export("memory") {
1459 Some(wasmtime::Extern::Memory(m)) => m,
1460 _ => return 1,
1461 };
1462 if write_i32_to_memory(
1463 &memory,
1464 &mut caller,
1465 return_level_ptr,
1466 abi::LOG_INFO as i32,
1467 )
1468 .is_err()
1469 {
1470 return 1;
1471 }
1472 0
1473 },
1474 )
1475 .map_err(|e| format!("linker proxy_get_log_level: {e}"))?;
1476
1477 // proxy_get_status(return_code_ptr, return_message_ptr_ptr, return_message_size_ptr) -> i32
1478 // The gRPC callout status fetch (the Rust SDK's
1479 // `get_grpc_status`). dwara's grpc_* hostcalls never put a
1480 // callout in flight, so there is no status to report — but the
1481 // SDK 0.2.5 wrapper PANICS on any non-Ok status (hostcalls.rs
1482 // `get_grpc_status`, ~line 1037), so answering NotFound would
1483 // trap every SDK caller. Return Ok with code 0 and a null
1484 // message (the SDK reads that as `(0, None)`: no gRPC status).
1485 linker
1486 .func_wrap(
1487 "env",
1488 "proxy_get_status",
1489 |mut caller: wasmtime::Caller<PluginContext>,
1490 return_code_ptr: i32,
1491 return_message_ptr_ptr: i32,
1492 return_message_size_ptr: i32|
1493 -> i32 {
1494 let memory = match caller.get_export("memory") {
1495 Some(wasmtime::Extern::Memory(m)) => m,
1496 _ => return 1,
1497 };
1498 for (ptr, value) in [
1499 (return_code_ptr, 0),
1500 (return_message_ptr_ptr, 0),
1501 (return_message_size_ptr, 0),
1502 ] {
1503 if write_i32_to_memory(&memory, &mut caller, ptr, value).is_err() {
1504 return 1;
1505 }
1506 }
1507 0 // Ok: no callout, no gRPC status (code 0, null message)
1508 },
1509 )
1510 .map_err(|e| format!("linker proxy_get_status: {e}"))?;
1511
1512 // Stub imports for functions we don't implement but the plugin
1513 // may call (returns error to signal unsupported). Each is
1514 // registered with its proxy-wasm spec ARITY: wasmtime's typed
1515 // linking rejects a module whose import signature does not
1516 // match, and the Rust SDK declares every import with its spec
1517 // signature even when the plugin never calls it.
1518 for name in [
1519 "proxy_set_tick_period_milliseconds",
1520 "proxy_grpc_cancel",
1521 "proxy_grpc_close",
1522 ] {
1523 linker
1524 .func_wrap("env", name, stub_unsupported_1)
1525 .map_err(|e| format!("linker {name}: {e}"))?;
1526 }
1527 for name in [
1528 "proxy_register_shared_queue",
1529 "proxy_dequeue_shared_queue",
1530 "proxy_enqueue_shared_queue",
1531 ] {
1532 linker
1533 .func_wrap("env", name, stub_unsupported_3)
1534 .map_err(|e| format!("linker {name}: {e}"))?;
1535 }
1536 linker
1537 .func_wrap("env", "proxy_resolve_shared_queue", stub_unsupported_5)
1538 .map_err(|e| format!("linker proxy_resolve_shared_queue: {e}"))?;
1539 linker
1540 .func_wrap("env", "proxy_grpc_stream", stub_unsupported_9)
1541 .map_err(|e| format!("linker proxy_grpc_stream: {e}"))?;
1542 // proxy_http_call(upstream_ptr, upstream_size, headers_ptr,
1543 // headers_size, body_ptr, body_size,
1544 // trailers_ptr, trailers_size, timeout_ms,
1545 // return_token_ptr) -> i32
1546 // The proxy-wasm HTTP callout dispatcher (DW-167). The Rust
1547 // SDK's `dispatch_http_call` (hostcalls.rs ~812) sends the URI
1548 // string, the headers/trailers in the spec map serialization
1549 // (what `proxy_set_header_map_pairs` parses), an optional
1550 // body, and the timeout in MILLISECONDS; it reads the token
1551 // from the return pointer and maps Ok/BadArgument/
1552 // InternalFailure to `Ok(token)`/`Err(BadArgument)`/
1553 // `Err(InternalFailure)` — every other status panics the
1554 // module, so this import answers only 0/2/10.
1555 linker
1556 .func_wrap(
1557 "env",
1558 "proxy_http_call",
1559 |mut caller: wasmtime::Caller<PluginContext>,
1560 upstream_ptr: i32,
1561 upstream_size: i32,
1562 headers_ptr: i32,
1563 headers_size: i32,
1564 body_ptr: i32,
1565 body_size: i32,
1566 _trailers_ptr: i32,
1567 _trailers_size: i32,
1568 timeout_ms: i32,
1569 return_token_ptr: i32|
1570 -> i32 {
1571 if upstream_ptr < 0 || upstream_size < 0 || headers_ptr < 0 || headers_size < 0
1572 {
1573 return 2; // Status::BadArgument
1574 }
1575 let memory = match caller.get_export("memory") {
1576 Some(wasmtime::Extern::Memory(m)) => m,
1577 _ => return 10,
1578 };
1579 let read = |ptr: i32, size: i32| -> Option<Vec<u8>> {
1580 memory
1581 .data(&caller)
1582 .get(ptr as usize..(ptr as usize + size as usize))
1583 .map(|s| s.to_vec())
1584 };
1585 let uri_bytes = match read(upstream_ptr, upstream_size) {
1586 Some(b) => b,
1587 None => return 2,
1588 };
1589 let uri = match String::from_utf8(uri_bytes) {
1590 Ok(u) => u,
1591 Err(_) => return 2,
1592 };
1593 let headers_bytes = match read(headers_ptr, headers_size) {
1594 Some(b) => b,
1595 None => return 2,
1596 };
1597 let body = if body_ptr > 0 && body_size > 0 {
1598 match read(body_ptr, body_size) {
1599 Some(b) => b,
1600 None => return 2,
1601 }
1602 } else {
1603 Vec::new()
1604 };
1605 // Trailers are accepted and ignored (dwara does
1606 // not plumb trailers on callout requests).
1607 match register_callout(
1608 caller.data_mut(),
1609 &uri,
1610 &headers_bytes,
1611 body,
1612 timeout_ms,
1613 ) {
1614 Some(token) => {
1615 let memory = match caller.get_export("memory") {
1616 Some(wasmtime::Extern::Memory(m)) => m,
1617 _ => return 10,
1618 };
1619 if write_i32_to_memory(
1620 &memory,
1621 &mut caller,
1622 return_token_ptr,
1623 token as i32,
1624 )
1625 .is_err()
1626 {
1627 return 10;
1628 }
1629 0 // Status::Ok
1630 }
1631 None => 2, // Status::BadArgument (invalid URI/map/head)
1632 }
1633 },
1634 )
1635 .map_err(|e| format!("linker proxy_http_call: {e}"))?;
1636 linker
1637 .func_wrap("env", "proxy_grpc_call", stub_unsupported_12)
1638 .map_err(|e| format!("linker proxy_grpc_call: {e}"))?;
1639 linker
1640 .func_wrap("env", "proxy_grpc_send", stub_unsupported_4)
1641 .map_err(|e| format!("linker proxy_grpc_send: {e}"))?;
1642 linker
1643 .func_wrap("env", "proxy_call_foreign_function", stub_unsupported_6)
1644 .map_err(|e| format!("linker proxy_call_foreign_function: {e}"))?;
1645
1646 // --- WASI p1 stubs (wasm32-wasip1) ----------------------------------
1647 //
1648 // Modules built for wasm32-wasip1 emit a small
1649 // `wasi_snapshot_preview1` import set even when they never
1650 // touch WASI consciously (Rust std's panic printing, HashMap
1651 // seeding, environment access). The host provides minimal
1652 // stubs for exactly that observed set; anything beyond it
1653 // (files, sockets) fails to instantiate, fail-closed.
1654
1655 // environ_sizes_get(environ_count_ptr, environ_buf_size_ptr) -> errno
1656 linker
1657 .func_wrap(
1658 "wasi_snapshot_preview1",
1659 "environ_sizes_get",
1660 |mut caller: wasmtime::Caller<PluginContext>,
1661 count_ptr: i32,
1662 buf_size_ptr: i32|
1663 -> i32 {
1664 let memory = match caller.get_export("memory") {
1665 Some(wasmtime::Extern::Memory(m)) => m,
1666 _ => return 1,
1667 };
1668 let _ = caller.data_mut();
1669 // Empty environment: count 0, buffer size 0.
1670 for (ptr, value) in [(count_ptr, 0), (buf_size_ptr, 0)] {
1671 if write_i32_to_memory(&memory, &mut caller, ptr, value).is_err() {
1672 return 1; // EPERM-ish; any non-zero is an errno
1673 }
1674 }
1675 0 // ESUCCESS
1676 },
1677 )
1678 .map_err(|e| format!("linker environ_sizes_get: {e}"))?;
1679
1680 // environ_get(environ_ptrs_ptr, environ_buf_ptr) -> errno
1681 linker
1682 .func_wrap(
1683 "wasi_snapshot_preview1",
1684 "environ_get",
1685 |_caller: wasmtime::Caller<PluginContext>,
1686 _environ_ptrs_ptr: i32,
1687 _environ_buf_ptr: i32|
1688 -> i32 {
1689 0 // ESUCCESS: the (empty) environment needs no writes
1690 },
1691 )
1692 .map_err(|e| format!("linker environ_get: {e}"))?;
1693
1694 // fd_write(fd, iovs_ptr, iovs_len, nwritten_ptr) -> errno
1695 // Route the plugin's stderr (panic output, debug prints) into
1696 // the per-instance log buffer so operators can see why a plugin
1697 // died without the bytes escaping the sandbox. The buffer is
1698 // capped (PluginContext::push_log): a plugin that spews through
1699 // stderr cannot grow host memory without bound — unlike fuel,
1700 // which bounds plugin execution, not host-side copies.
1701 linker
1702 .func_wrap(
1703 "wasi_snapshot_preview1",
1704 "fd_write",
1705 |mut caller: wasmtime::Caller<PluginContext>,
1706 _fd: i32,
1707 iovs_ptr: i32,
1708 iovs_len: i32,
1709 nwritten_ptr: i32|
1710 -> i32 {
1711 let memory = match caller.get_export("memory") {
1712 Some(wasmtime::Extern::Memory(m)) => m,
1713 _ => return 1,
1714 };
1715 if iovs_ptr < 0 || iovs_len < 0 || nwritten_ptr < 0 {
1716 return 1;
1717 }
1718 let mut out = Vec::new();
1719 for i in 0..iovs_len as usize {
1720 let iov_base = iovs_ptr as usize + i * 8;
1721 let (buf_ptr, buf_len) = {
1722 let data = memory.data(&caller);
1723 let Some(base) = data.get(iov_base..iov_base + 8) else {
1724 return 1;
1725 };
1726 let buf_ptr =
1727 u32::from_le_bytes(base[0..4].try_into().expect("4 bytes"))
1728 as usize;
1729 let buf_len =
1730 u32::from_le_bytes(base[4..8].try_into().expect("4 bytes"))
1731 as usize;
1732 (buf_ptr, buf_len)
1733 };
1734 match memory.data(&caller).get(buf_ptr..buf_ptr + buf_len) {
1735 Some(slice) => out.extend_from_slice(slice),
1736 None => return 1,
1737 }
1738 }
1739 let written = out.len() as i32;
1740 caller
1741 .data_mut()
1742 .push_log(abi::LOG_INFO, String::from_utf8_lossy(&out).into_owned());
1743 if write_i32_to_memory(&memory, &mut caller, nwritten_ptr, written).is_err() {
1744 return 1;
1745 }
1746 0 // ESUCCESS
1747 },
1748 )
1749 .map_err(|e| format!("linker fd_write: {e}"))?;
1750
1751 // proc_exit(code) -> ! : the plugin terminated itself. Trap the
1752 // instance (the runner reports the trap like any other plugin
1753 // failure; the code rides the trap message).
1754 linker
1755 .func_wrap(
1756 "wasi_snapshot_preview1",
1757 "proc_exit",
1758 |_caller: wasmtime::Caller<PluginContext>,
1759 code: i32|
1760 -> Result<(), wasmtime::Error> {
1761 Err(wasmtime::format_err!(
1762 "wasi proc_exit({code}): the plugin terminated itself"
1763 ))
1764 },
1765 )
1766 .map_err(|e| format!("linker proc_exit: {e}"))?;
1767
1768 // random_get(buf_ptr, buf_len) -> errno
1769 // Non-cryptographic: splitmix64 seeded from the wall clock and
1770 // a process-global counter. Enough to seed std collections;
1771 // plugins must not use it for key material.
1772 linker
1773 .func_wrap(
1774 "wasi_snapshot_preview1",
1775 "random_get",
1776 |mut caller: wasmtime::Caller<PluginContext>, buf_ptr: i32, buf_len: i32| -> i32 {
1777 if buf_ptr < 0 || buf_len < 0 {
1778 return 1;
1779 }
1780 let memory = match caller.get_export("memory") {
1781 Some(wasmtime::Extern::Memory(m)) => m,
1782 _ => return 1,
1783 };
1784 let mut state = wasi_random_seed();
1785 let mut filled = 0usize;
1786 while filled < buf_len as usize {
1787 state = state.wrapping_add(0x9E3779B97F4A7C15);
1788 let mut z = state;
1789 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
1790 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
1791 z ^= z >> 31;
1792 for byte in z.to_le_bytes() {
1793 if filled >= buf_len as usize {
1794 break;
1795 }
1796 let dst = buf_ptr as usize + filled;
1797 match memory.data_mut(&mut caller).get_mut(dst..dst + 1) {
1798 Some(slot) => slot[0] = byte,
1799 None => return 1,
1800 }
1801 filled += 1;
1802 }
1803 }
1804 0 // ESUCCESS
1805 },
1806 )
1807 .map_err(|e| format!("linker random_get: {e}"))?;
1808
1809 // clock_time_get(clock_id, precision, time_ptr) -> errno
1810 linker
1811 .func_wrap(
1812 "wasi_snapshot_preview1",
1813 "clock_time_get",
1814 |mut caller: wasmtime::Caller<PluginContext>,
1815 _clock_id: i32,
1816 _precision: i64,
1817 time_ptr: i32|
1818 -> i32 {
1819 let memory = match caller.get_export("memory") {
1820 Some(wasmtime::Extern::Memory(m)) => m,
1821 _ => return 1,
1822 };
1823 let now = std::time::SystemTime::now()
1824 .duration_since(std::time::UNIX_EPOCH)
1825 .map(|d| d.as_nanos() as u64)
1826 .unwrap_or(0);
1827 let bytes = now.to_le_bytes();
1828 if memory
1829 .data_mut(&mut caller)
1830 .get_mut(time_ptr as usize..(time_ptr as usize + 8))
1831 .map(|dst| dst.copy_from_slice(&bytes))
1832 .is_none()
1833 {
1834 return 1;
1835 }
1836 0 // ESUCCESS
1837 },
1838 )
1839 .map_err(|e| format!("linker clock_time_get: {e}"))?;
1840
1841 // sched_yield() -> errno
1842 linker
1843 .func_wrap(
1844 "wasi_snapshot_preview1",
1845 "sched_yield",
1846 |_caller: wasmtime::Caller<PluginContext>| -> i32 { 0 },
1847 )
1848 .map_err(|e| format!("linker sched_yield: {e}"))?;
1849
1850 Ok(Self {
1851 engine,
1852 linker: Arc::new(linker),
1853 })
1854 }
1855
1856 /// The underlying wasmtime engine.
1857 pub fn engine(&self) -> &Engine {
1858 &self.engine
1859 }
1860
1861 /// Compile a .wasm module from bytes.
1862 pub fn compile(
1863 &self,
1864 wasm_bytes: &[u8],
1865 limits: PluginLimits,
1866 plugin_config: Vec<u8>,
1867 vm_config: Vec<u8>,
1868 ) -> Result<PluginModule, String> {
1869 let module =
1870 Module::new(&self.engine, wasm_bytes).map_err(|e| format!("wasm compile: {e}"))?;
1871 Ok(PluginModule {
1872 module,
1873 limits,
1874 plugin_config,
1875 vm_config,
1876 shared: Arc::new(Mutex::new(SharedData::new())),
1877 })
1878 }
1879
1880 /// The linker (used by instance creation).
1881 pub fn linker(&self) -> &Linker<PluginContext> {
1882 &self.linker
1883 }
1884}
1885
1886impl PluginModule {
1887 /// Create a new per-request instance and run the VM start + configure
1888 /// lifecycle. Returns the store and instance ready for phase calls.
1889 pub fn instantiate(&self, engine: &WasmEngine) -> Result<PluginInstance, String> {
1890 let memory_cap = self.limits.memory_mb * 1024 * 1024;
1891 let ctx = PluginContext::new(
1892 self.plugin_config.clone(),
1893 self.vm_config.clone(),
1894 memory_cap,
1895 Arc::clone(&self.shared),
1896 );
1897 let mut store = Store::new(&engine.engine, ctx);
1898 store.limiter(move |ctx| ctx as &mut dyn ResourceLimiter);
1899
1900 // Set the fuel budget.
1901 store
1902 .set_fuel(self.limits.fuel)
1903 .map_err(|e| format!("set_fuel: {e}"))?;
1904
1905 // Set the epoch deadline so epoch interruption does not fire
1906 // immediately. A background thread (when configured) increments
1907 // the engine epoch; the store traps when the current epoch
1908 // reaches the deadline. Without this call the default deadline
1909 // is 0 and any wasm execution traps right away.
1910 store.set_epoch_deadline(self.limits.timeout_ms.max(1));
1911
1912 // Instantiate the module.
1913 let instance = engine
1914 .linker
1915 .instantiate(&mut store, &self.module)
1916 .map_err(|e| format!("wasm instantiate: {e}"))?;
1917
1918 let mut inst = PluginInstance {
1919 store,
1920 instance,
1921 http_context_created: false,
1922 };
1923
1924 // Standard proxy-wasm VM initialization (SDK modules, e.g.
1925 // anything built from `dwara-cli plugin new`): the module
1926 // registers its root-context factory in `_start` and builds its
1927 // contexts on `proxy_on_context_create`; calling
1928 // `proxy_on_vm_start` without both panics inside the module.
1929 // Both exports are optional (dwara's WAT fixtures implement
1930 // neither) — call them when present, skip when absent. A trap
1931 // inside `_start` fails the instantiation with the trap itself.
1932 inst.call_optional_start()?;
1933 inst.call_optional_context_create(1, 0);
1934
1935 // Call proxy_on_vm_start(root_context_id=1, vm_config_size).
1936 inst.set_context_id(1);
1937 if let Some(export) = inst
1938 .instance
1939 .get_export(&mut inst.store, "proxy_on_vm_start")
1940 {
1941 let func = export
1942 .into_func()
1943 .ok_or("proxy_on_vm_start is not a func")?;
1944 let vm_config_size = self.vm_config.len() as i32;
1945 let typed: wasmtime::TypedFunc<(i32, i32), i32> = func
1946 .typed(&inst.store)
1947 .map_err(|e| format!("proxy_on_vm_start typed: {e}"))?;
1948 let result = typed
1949 .call(&mut inst.store, (1, vm_config_size))
1950 .map_err(|e| format!("proxy_on_vm_start call: {e}"))?;
1951 if result == 0 {
1952 return Err("proxy_on_vm_start returned false".to_string());
1953 }
1954 }
1955
1956 // Call proxy_on_configure(root_context_id=1, plugin_config_size).
1957 if let Some(export) = inst
1958 .instance
1959 .get_export(&mut inst.store, "proxy_on_configure")
1960 {
1961 let func = export
1962 .into_func()
1963 .ok_or("proxy_on_configure is not a func")?;
1964 let plugin_config_size = self.plugin_config.len() as i32;
1965 let typed: wasmtime::TypedFunc<(i32, i32), i32> = func
1966 .typed(&inst.store)
1967 .map_err(|e| format!("proxy_on_configure typed: {e}"))?;
1968 let result = typed
1969 .call(&mut inst.store, (1, plugin_config_size))
1970 .map_err(|e| format!("proxy_on_configure call: {e}"))?;
1971 if result == 0 {
1972 return Err("proxy_on_configure returned false".to_string());
1973 }
1974 }
1975
1976 Ok(inst)
1977 }
1978
1979 /// The resource limits for this plugin.
1980 pub fn limits(&self) -> &PluginLimits {
1981 &self.limits
1982 }
1983}
1984
1985/// A per-request plugin instance: a wasmtime store + instance.
1986pub struct PluginInstance {
1987 store: Store<PluginContext>,
1988 instance: wasmtime::Instance,
1989 /// Whether `proxy_on_context_create(2, 1)` has been called for the
1990 /// per-request HTTP context (SDK modules panic on a phase call for
1991 /// a context they were never told to create; fixtures without the
1992 /// export skip it).
1993 http_context_created: bool,
1994}
1995
1996/// The result of a phase callback.
1997#[derive(Clone, Debug)]
1998pub enum PhaseResult {
1999 /// Continue processing the request.
2000 Continue,
2001 /// The plugin short-circuited with a local response.
2002 LocalResponse(LocalResponse),
2003 /// The plugin trapped (out of fuel, memory error, or panic).
2004 Trap(String),
2005 /// The plugin registered one or more HTTP callouts and paused
2006 /// (DW-167). The dispatch driver performs the exchanges and calls
2007 /// [`PluginInstance::deliver_callout_response`]; the phase's
2008 /// outcome is then re-collected from the instance.
2009 Pause,
2010}
2011
2012impl PluginInstance {
2013 /// Set the current context ID (used by exports that receive a
2014 /// context_id parameter).
2015 fn set_context_id(&mut self, id: u32) {
2016 self.store.data_mut().current_context_id = id;
2017 self.store.data_mut().effective_context_id = id;
2018 }
2019
2020 /// Call `_start()` when the module exports it (SDK modules register
2021 /// their context factories there). Absent in dwara's WAT fixtures —
2022 /// a missing export is skipped, not an error. A trapping `_start`
2023 /// is PROPAGATED as the instance's error: swallowing it here would
2024 /// surface later as an unrelated panic inside
2025 /// `proxy_on_context_create` (the SDK dispatcher finds no
2026 /// registered root context), hiding the real failure.
2027 fn call_optional_start(&mut self) -> Result<(), String> {
2028 let Some(export) = self.instance.get_export(&mut self.store, "_start") else {
2029 return Ok(());
2030 };
2031 let Some(func) = export.into_func() else {
2032 return Ok(());
2033 };
2034 let Ok(typed) = func.typed::<(), ()>(&self.store) else {
2035 return Ok(());
2036 };
2037 typed
2038 .call(&mut self.store, ())
2039 .map_err(|e| format!("_start: {e}"))
2040 }
2041
2042 /// Call `proxy_on_context_create(context_id, root_context_id)` when
2043 /// the module exports it (SDK modules build their contexts there).
2044 /// Absent in dwara's WAT fixtures — a missing export is skipped.
2045 fn call_optional_context_create(&mut self, context_id: i32, root_context_id: i32) {
2046 let Some(export) = self
2047 .instance
2048 .get_export(&mut self.store, "proxy_on_context_create")
2049 else {
2050 return;
2051 };
2052 let Some(func) = export.into_func() else {
2053 return;
2054 };
2055 let Ok(typed) = func.typed::<(i32, i32), ()>(&self.store) else {
2056 return;
2057 };
2058 let _: () = typed
2059 .call(&mut self.store, (context_id, root_context_id))
2060 .unwrap_or_default();
2061 }
2062
2063 /// Ensure the per-request HTTP context exists before a phase call:
2064 /// SDK modules panic inside `proxy_on_*` for a context they were
2065 /// never told to create, so the first phase (or `on_done`) for
2066 /// context 2 first calls `proxy_on_context_create(2, 1)`. Modules
2067 /// without the export (dwara's WAT fixtures) skip it.
2068 fn ensure_http_context(&mut self) {
2069 if self.http_context_created {
2070 return;
2071 }
2072 self.http_context_created = true;
2073 self.call_optional_context_create(2, 1);
2074 }
2075
2076 /// Call `proxy_on_request_headers(context_id, num_headers, end_of_stream)`.
2077 /// Sets the request headers in the context before calling.
2078 pub fn on_request_headers(&mut self, headers: Vec<(String, String)>) -> PhaseResult {
2079 self.store.data_mut().request_headers = headers;
2080 let num_headers = self.store.data().request_headers.len() as i32;
2081 self.set_context_id(2);
2082 self.ensure_http_context();
2083 self.call_phase_export("proxy_on_request_headers", (2, num_headers, 1))
2084 }
2085
2086 /// Call `proxy_on_request_body(context_id, body_size, end_of_stream)`.
2087 /// Sets the request body in the context before calling.
2088 pub fn on_request_body(&mut self, body: Vec<u8>) -> PhaseResult {
2089 self.store.data_mut().request_body = body;
2090 let body_size = self.store.data().request_body.len() as i32;
2091 self.set_context_id(2);
2092 self.ensure_http_context();
2093 self.call_phase_export("proxy_on_request_body", (2, body_size, 1))
2094 }
2095
2096 /// Call `proxy_on_response_headers(context_id, num_headers, end_of_stream)`.
2097 /// Sets the response headers in the context before calling.
2098 pub fn on_response_headers(&mut self, headers: Vec<(String, String)>) -> PhaseResult {
2099 self.store.data_mut().response_headers = headers;
2100 let num_headers = self.store.data().response_headers.len() as i32;
2101 self.set_context_id(2);
2102 self.ensure_http_context();
2103 self.call_phase_export("proxy_on_response_headers", (2, num_headers, 1))
2104 }
2105
2106 /// Call `proxy_on_response_body(context_id, body_size, end_of_stream)`.
2107 /// Sets the response body in the context before calling.
2108 pub fn on_response_body(&mut self, body: Vec<u8>) -> PhaseResult {
2109 self.store.data_mut().response_body = body;
2110 let body_size = self.store.data().response_body.len() as i32;
2111 self.set_context_id(2);
2112 self.ensure_http_context();
2113 self.call_phase_export("proxy_on_response_body", (2, body_size, 1))
2114 }
2115
2116 /// Get the (possibly modified) request headers from the context.
2117 pub fn request_headers(&self) -> &[(String, String)] {
2118 &self.store.data().request_headers
2119 }
2120
2121 /// Get the (possibly modified) response headers from the context.
2122 pub fn response_headers(&self) -> &[(String, String)] {
2123 &self.store.data().response_headers
2124 }
2125
2126 /// Get the (possibly modified) request body from the context.
2127 pub fn request_body(&self) -> &[u8] {
2128 &self.store.data().request_body
2129 }
2130
2131 /// Get the (possibly modified) response body from the context.
2132 pub fn response_body(&self) -> &[u8] {
2133 &self.store.data().response_body
2134 }
2135
2136 /// Get the logs emitted by the plugin.
2137 pub fn logs(&self) -> &[(u32, String)] {
2138 &self.store.data().logs
2139 }
2140
2141 /// Read `len` bytes of the plugin's linear memory at `offset`
2142 /// (test support: asserting a callback's in-memory side effects).
2143 pub fn memory_bytes(&mut self, offset: usize, len: usize) -> Option<Vec<u8>> {
2144 let export = self.instance.get_export(&mut self.store, "memory")?;
2145 let memory = export.into_memory()?;
2146 memory
2147 .data(&self.store)
2148 .get(offset..offset.checked_add(len)?)
2149 .map(|s| s.to_vec())
2150 }
2151
2152 /// A snapshot of the VM-scoped shared-data map (test support: the
2153 /// cross-instance store DW-167 introduced).
2154 pub fn shared_data_snapshot(&self) -> SharedData {
2155 self.store
2156 .data()
2157 .shared_data
2158 .lock()
2159 .unwrap_or_else(std::sync::PoisonError::into_inner)
2160 .clone()
2161 }
2162
2163 /// Drain the callouts registered since the last phase/callback
2164 /// invocation (DW-167). The dispatch driver performs them in FIFO
2165 /// order (one round each).
2166 pub fn take_pending_callouts(&mut self) -> Vec<PendingCallout> {
2167 std::mem::take(&mut self.store.data_mut().pending_callouts)
2168 }
2169
2170 /// Re-enqueue callouts the dispatch driver drained but did NOT
2171 /// perform (DW-167): when the first of several queued deliveries
2172 /// re-dispatches from its callback, the un-performed remainder
2173 /// goes back AHEAD of the newly registered queue — preserving the
2174 /// global dispatch order and guaranteeing every dispatched callout
2175 /// is eventually performed (or the loop guard trips).
2176 pub fn requeue_callouts(&mut self, pending: Vec<PendingCallout>) {
2177 if pending.is_empty() {
2178 return;
2179 }
2180 let ctx = self.store.data_mut();
2181 let mut merged = pending;
2182 merged.append(&mut ctx.pending_callouts);
2183 ctx.pending_callouts = merged;
2184 }
2185
2186 /// Deliver a completed callout response and collect the plugin's
2187 /// resumed outcome (DW-167): the response's headers (`:status`
2188 /// first, hop-by-hop stripped) and body land in the context's
2189 /// callout-response stores, then the host calls
2190 /// `proxy_on_http_call_response(context_id, token, num_headers,
2191 /// body_size, num_trailers)` — the 5-parameter shape the Rust SDK
2192 /// 0.2.5 exports (its first parameter is the ABI's context slot,
2193 /// which the SDK ignores; it routes by token inside the module).
2194 /// The export returns nothing in the SDK: the plugin's decision is
2195 /// expressed through what it does inside the callback
2196 /// (`send_http_response`, header mutations, another dispatch, or
2197 /// nothing = resume Continue). A module without the export resumes
2198 /// Continue (nothing to tell).
2199 pub fn deliver_callout_response(&mut self, token: u32, resp: &CalloutResponse) -> PhaseResult {
2200 {
2201 let ctx = self.store.data_mut();
2202 ctx.callout_response_headers =
2203 std::iter::once((":status".to_string(), resp.status.to_string()))
2204 .chain(resp.headers.iter().cloned())
2205 .collect();
2206 ctx.callout_response_body = resp.body.clone();
2207 }
2208 let num_headers = self.store.data().callout_response_headers.len() as i32;
2209 let body_size = self.store.data().callout_response_body.len() as i32;
2210 self.set_context_id(2);
2211 self.ensure_http_context();
2212 let Some(export) = self
2213 .instance
2214 .get_export(&mut self.store, "proxy_on_http_call_response")
2215 else {
2216 return PhaseResult::Continue;
2217 };
2218 let Some(func) = export.into_func() else {
2219 return PhaseResult::Continue;
2220 };
2221 let args = (2, token as i32, num_headers, body_size, 0);
2222 // The SDK's export returns (); a hand-written fixture may
2223 // return an action — tolerate both shapes (the value is
2224 // recorded but the outcome comes from the instance state
2225 // below, matching the SDK's hostcall-driven resume model).
2226 let result: Result<u32, wasmtime::Error> =
2227 if let Ok(typed) = func.typed::<(i32, i32, i32, i32, i32), ()>(&self.store) {
2228 typed
2229 .call(&mut self.store, args)
2230 .map(|()| abi::ACTION_CONTINUE)
2231 } else if let Ok(typed) = func.typed::<(i32, i32, i32, i32, i32), i32>(&self.store) {
2232 typed.call(&mut self.store, args).map(|a| a as u32)
2233 } else {
2234 return PhaseResult::Trap(
2235 "proxy_on_http_call_response has an unusable signature".to_string(),
2236 );
2237 };
2238 match result {
2239 Ok(action) => {
2240 let ctx = self.store.data_mut();
2241 ctx.action = action;
2242 if let Some(resp) = ctx.local_response.take() {
2243 PhaseResult::LocalResponse(resp)
2244 } else if !ctx.pending_callouts.is_empty() {
2245 // The callback dispatched another callout: pause
2246 // again (the driver's next round picks it up).
2247 PhaseResult::Pause
2248 } else {
2249 PhaseResult::Continue
2250 }
2251 }
2252 Err(e) => {
2253 let fuel_left = self.store.get_fuel().unwrap_or(0);
2254 if fuel_left == 0 {
2255 PhaseResult::Trap(format!("proxy_on_http_call_response: fuel exhausted: {e}"))
2256 } else {
2257 PhaseResult::Trap(format!("proxy_on_http_call_response: {e}"))
2258 }
2259 }
2260 }
2261 }
2262
2263 /// Call `proxy_on_done` and `proxy_on_log` (the cleanup path).
2264 /// The SDK's `proxy_on_done` returns a bool while the WAT fixtures
2265 /// return nothing — the result is ignored either way, so both
2266 /// shapes are accepted.
2267 pub fn on_done(&mut self) {
2268 self.set_context_id(2);
2269 self.ensure_http_context();
2270 let _ = self.call_optional_i32_export("proxy_on_done", 2);
2271 let _ = self.call_optional_i32_export("proxy_on_log", 2);
2272 let _ = self.call_optional_i32_export("proxy_on_delete", 2);
2273 }
2274
2275 /// Call an optional `(i32) -> ()`-or-`(i32) -> i32` export with
2276 /// `arg`, tolerating either return shape (fixtures return nothing;
2277 /// the SDK returns a bool). Missing exports are skipped.
2278 fn call_optional_i32_export(&mut self, name: &str, arg: i32) -> Result<(), String> {
2279 let Some(export) = self.instance.get_export(&mut self.store, name) else {
2280 return Ok(());
2281 };
2282 let Some(func) = export.into_func() else {
2283 return Ok(());
2284 };
2285 if let Ok(typed) = func.typed::<(i32,), ()>(&self.store) {
2286 return typed
2287 .call(&mut self.store, (arg,))
2288 .map_err(|e| format!("{name}: {e}"));
2289 }
2290 if let Ok(typed) = func.typed::<(i32,), i32>(&self.store) {
2291 return typed
2292 .call(&mut self.store, (arg,))
2293 .map(|_| ())
2294 .map_err(|e| format!("{name}: {e}"));
2295 }
2296 Ok(())
2297 }
2298
2299 /// Call a phase export that takes (context_id, a, b) and returns an action.
2300 fn call_phase_export(&mut self, name: &str, args: (i32, i32, i32)) -> PhaseResult {
2301 let export = match self.instance.get_export(&mut self.store, name) {
2302 Some(e) => e,
2303 None => return PhaseResult::Continue,
2304 };
2305 let func = match export.into_func() {
2306 Some(f) => f,
2307 None => return PhaseResult::Continue,
2308 };
2309 let typed: wasmtime::TypedFunc<(i32, i32, i32), i32> = match func.typed(&self.store) {
2310 Ok(t) => t,
2311 Err(e) => return PhaseResult::Trap(format!("{name} typed: {e}")),
2312 };
2313 match typed.call(&mut self.store, args) {
2314 Ok(action) => {
2315 let ctx = self.store.data_mut();
2316 ctx.action = action as u32;
2317 if let Some(resp) = ctx.local_response.take() {
2318 PhaseResult::LocalResponse(resp)
2319 } else if action == abi::ACTION_END_STREAM as i32 {
2320 PhaseResult::LocalResponse(LocalResponse {
2321 status: 200,
2322 headers: Vec::new(),
2323 body: Vec::new(),
2324 })
2325 } else if !ctx.pending_callouts.is_empty() {
2326 // The phase registered callouts (proxy_http_call)
2327 // and did not short-circuit: pause for the host to
2328 // perform them (DW-167). The returned action value
2329 // is advisory — a plugin that dispatches a callout
2330 // and returns Continue still pauses, because the
2331 // phase's outcome cannot be known until the
2332 // callout's callback ran.
2333 PhaseResult::Pause
2334 } else {
2335 PhaseResult::Continue
2336 }
2337 }
2338 Err(e) => {
2339 // Detect fuel exhaustion: wasmtime's trap Display does
2340 // not include the word "fuel", so check the remaining
2341 // fuel budget and annotate the message when it is zero.
2342 let fuel_left = self.store.get_fuel().unwrap_or(0);
2343 if fuel_left == 0 {
2344 PhaseResult::Trap(format!("{name}: fuel exhausted: {e}"))
2345 } else {
2346 PhaseResult::Trap(format!("{name}: {e}"))
2347 }
2348 }
2349 }
2350 }
2351}
2352
2353// --- Helper functions ----------------------------------------------------
2354
2355/// Validate and register one `proxy_http_call` dispatch (DW-167) into
2356/// the instance's pending list, handing out the next token. `None`
2357/// maps to `Status::BadArgument` at the import (the Rust SDK surfaces
2358/// that as `Err(Status::BadArgument)` from `dispatch_http_call`).
2359///
2360/// Validation: the URI must parse with an `http://`/`https://` scheme
2361/// and a host — the URI is the whole callout target (scheme,
2362/// authority, and optionally its own path+query), and the callout's
2363/// `Host` header derives from it. The dispatch header map's
2364/// pseudo-headers steer what they can: `:method` (default `GET`) and
2365/// `:path` (applied only when the URI carries no path of its own —
2366/// the URI-embedded target wins, and must be origin-form). `:authority`
2367/// and unknown pseudo-headers are advisory here and dropped (the
2368/// plugin shapes neither the dial target nor the Host; the same
2369/// authority-shapes-Host-only posture the request-path rewrite
2370/// enforces would need a cluster abstraction dwara does not have).
2371///
2372/// Request-head integrity (fail-closed BEFORE any network work): the
2373/// callout request head interpolates the method, the header names and
2374/// values, and the map `:path` verbatim, so each must be
2375/// splitting-safe — the method and every header name an RFC 7230
2376/// token, and no CR, LF, or NUL anywhere in a name, value, or the map
2377/// `:path`. A plugin reflecting client data into a callout cannot
2378/// smuggle a second request into the gateway-initiated connection;
2379/// violations register nothing and answer BadArgument.
2380fn register_callout(
2381 ctx: &mut PluginContext,
2382 uri: &str,
2383 headers_bytes: &[u8],
2384 body: Vec<u8>,
2385 timeout_ms: i32,
2386) -> Option<u32> {
2387 let parsed: http::Uri = uri.parse().ok()?;
2388 let scheme = match parsed.scheme_str() {
2389 Some("http") | Some("https") => parsed.scheme_str().unwrap(),
2390 _ => return None,
2391 };
2392 let host = parsed.host()?;
2393 let map = abi::deserialize_header_map_spec(headers_bytes)?;
2394 let mut method = "GET".to_string();
2395 let mut map_path: Option<String> = None;
2396 let mut headers = Vec::with_capacity(map.len());
2397 for (name, value) in map {
2398 match name.as_str() {
2399 ":method" if !value.is_empty() => method = value,
2400 ":path" if !value.is_empty() => map_path = Some(value),
2401 n if n.starts_with(':') => {} // :authority and unknown pseudo-headers dropped
2402 _ => headers.push((name, value)),
2403 }
2404 }
2405 if !super::callout::is_rfc7230_token(&method) {
2406 return None;
2407 }
2408 if let Some(p) = &map_path {
2409 if !super::callout::is_head_safe(p) {
2410 return None;
2411 }
2412 }
2413 for (name, value) in &headers {
2414 if !super::callout::is_rfc7230_token(name) || !super::callout::is_head_safe(name) {
2415 return None;
2416 }
2417 if !super::callout::is_head_safe(value) {
2418 return None;
2419 }
2420 }
2421 let mut effective = format!("{scheme}://{host}");
2422 if let Some(port) = parsed.port_u16() {
2423 effective.push_str(&format!(":{port}"));
2424 }
2425 let target = match parsed.path_and_query() {
2426 Some(pq) if pq.as_str() != "/" => pq.as_str().to_string(),
2427 _ => match map_path {
2428 Some(p) if p.starts_with('/') => p,
2429 _ => "/".to_string(),
2430 },
2431 };
2432 effective.push_str(&target);
2433 ctx.callout_token_counter = ctx.callout_token_counter.saturating_add(1);
2434 let token = ctx.callout_token_counter;
2435 ctx.pending_callouts.push(PendingCallout {
2436 token,
2437 uri: effective,
2438 method,
2439 headers,
2440 body,
2441 timeout_ms: timeout_ms.max(0) as u32,
2442 });
2443 Some(token)
2444}
2445
2446// Unsupported-hostcall stubs, one per proxy-wasm spec ARITY (wasmtime's
2447// typed linking requires the import signature to match exactly; the
2448// Rust SDK declares every import with its spec signature even when the
2449// plugin never calls it). All return Status::InternalFailure (10).
2450// SDK 0.2.5 mapping of that status, exactly (hostcalls.rs):
2451// - clean Err(Status::InternalFailure): the two remaining callout
2452// dispatchers — dispatch_grpc_call and open_grpc_stream
2453// (dispatch_http_call is implemented, DW-167).
2454// - panic! on any non-Ok status: every other wrapper, including
2455// send_grpc_stream_message and cancel_grpc_call/cancel_grpc_stream
2456// (they map only BadArgument/NotFound to Err), set_tick_period,
2457// the shared-queue calls, and call_foreign_function.
2458// A panicking wrapper traps the module, which the host reports as a
2459// plugin failure (trap -> 500 on the referencing route) — host-safe
2460// either way; the point of returning InternalFailure rather than some
2461// other status is that it is the one status the largest set of SDK
2462// wrappers handles without trapping. It is NOT a general "clean Err"
2463// for all callers.
2464fn stub_unsupported_1(_: wasmtime::Caller<PluginContext>, _a: i32) -> i32 {
2465 10 // Status::InternalFailure
2466}
2467
2468fn stub_unsupported_3(_: wasmtime::Caller<PluginContext>, _a: i32, _b: i32, _c: i32) -> i32 {
2469 10 // Status::InternalFailure
2470}
2471
2472fn stub_unsupported_4(
2473 _: wasmtime::Caller<PluginContext>,
2474 _a: i32,
2475 _b: i32,
2476 _c: i32,
2477 _d: i32,
2478) -> i32 {
2479 10 // Status::InternalFailure
2480}
2481
2482fn stub_unsupported_5(
2483 _: wasmtime::Caller<PluginContext>,
2484 _a: i32,
2485 _b: i32,
2486 _c: i32,
2487 _d: i32,
2488 _e: i32,
2489) -> i32 {
2490 10 // Status::InternalFailure
2491}
2492
2493fn stub_unsupported_6(
2494 _: wasmtime::Caller<PluginContext>,
2495 _a: i32,
2496 _b: i32,
2497 _c: i32,
2498 _d: i32,
2499 _e: i32,
2500 _f: i32,
2501) -> i32 {
2502 10 // Status::InternalFailure
2503}
2504
2505#[allow(clippy::too_many_arguments)]
2506fn stub_unsupported_9(
2507 _: wasmtime::Caller<PluginContext>,
2508 _a: i32,
2509 _b: i32,
2510 _c: i32,
2511 _d: i32,
2512 _e: i32,
2513 _f: i32,
2514 _g: i32,
2515 _h: i32,
2516 _i: i32,
2517) -> i32 {
2518 10 // Status::InternalFailure
2519}
2520
2521#[allow(clippy::too_many_arguments)]
2522fn stub_unsupported_12(
2523 _: wasmtime::Caller<PluginContext>,
2524 _a: i32,
2525 _b: i32,
2526 _c: i32,
2527 _d: i32,
2528 _e: i32,
2529 _f: i32,
2530 _g: i32,
2531 _h: i32,
2532 _i: i32,
2533 _j: i32,
2534 _k: i32,
2535 _l: i32,
2536) -> i32 {
2537 10 // Status::InternalFailure
2538}
2539
2540/// The body of dwara's original `proxy_send_http_response` hostcall
2541/// (7 params; the WAT fixtures and host tests use this spelling). The
2542/// spec name `proxy_send_local_response` has a different signature and
2543/// is registered separately in [`WasmEngine::new`]. The header
2544/// argument here deliberately keeps dwara's LEGACY big-endian map
2545/// layout (abi::deserialize_header_map): modules written against this
2546/// spelling (dwara's example plugins) serialize that way, while the
2547/// spec-named hostcalls speak the SDK layout. The additive rule: the
2548/// legacy spelling keeps the legacy wire format.
2549#[allow(clippy::too_many_arguments)]
2550fn send_http_response_impl(
2551 mut caller: wasmtime::Caller<PluginContext>,
2552 status: i32,
2553 headers_ptr: i32,
2554 headers_size: i32,
2555 body_ptr: i32,
2556 body_size: i32,
2557 _trailers_ptr: i32,
2558 _trailers_size: i32,
2559) -> i32 {
2560 let memory = match caller.get_export("memory") {
2561 Some(wasmtime::Extern::Memory(m)) => m,
2562 _ => return 1,
2563 };
2564 let headers = if headers_ptr > 0 && headers_size > 0 {
2565 let data = match memory
2566 .data(&caller)
2567 .get(headers_ptr as usize..(headers_ptr as usize + headers_size as usize))
2568 {
2569 Some(slice) => slice.to_vec(),
2570 None => return 1,
2571 };
2572 match abi::deserialize_header_map(&data) {
2573 Some(h) => h,
2574 None => return 1,
2575 }
2576 } else {
2577 Vec::new()
2578 };
2579 let body = if body_ptr > 0 && body_size > 0 {
2580 match memory
2581 .data(&caller)
2582 .get(body_ptr as usize..(body_ptr as usize + body_size as usize))
2583 {
2584 Some(slice) => slice.to_vec(),
2585 None => return 1,
2586 }
2587 } else {
2588 Vec::new()
2589 };
2590 let ctx = caller.data_mut();
2591 ctx.local_response = Some(LocalResponse {
2592 status: status as u16,
2593 headers,
2594 body,
2595 });
2596 ctx.action = abi::ACTION_END_STREAM;
2597 0
2598}
2599
2600/// The shared body of `proxy_get_current_time` /
2601/// `proxy_get_current_time_nanoseconds` (dwara's spelling and the
2602/// proxy-wasm spec spelling): nanoseconds since the Unix epoch.
2603fn get_current_time_impl(
2604 mut caller: wasmtime::Caller<PluginContext>,
2605 return_value_ptr: i32,
2606) -> i32 {
2607 let memory = match caller.get_export("memory") {
2608 Some(wasmtime::Extern::Memory(m)) => m,
2609 _ => return 1,
2610 };
2611 let now = std::time::SystemTime::now()
2612 .duration_since(std::time::UNIX_EPOCH)
2613 .map(|d| d.as_nanos() as u64)
2614 .unwrap_or(0);
2615 let bytes = now.to_le_bytes();
2616 if memory
2617 .data_mut(&mut caller)
2618 .get_mut(return_value_ptr as usize..(return_value_ptr as usize + 8))
2619 .map(|dst| dst.copy_from_slice(&bytes))
2620 .is_none()
2621 {
2622 return 1;
2623 }
2624 0
2625}
2626
2627/// Seed for the WASI `random_get` stub: wall-clock nanoseconds mixed
2628/// with a process-global counter (two plugins seeded in the same
2629/// nanosecond still diverge). Not cryptographic.
2630fn wasi_random_seed() -> u64 {
2631 use std::sync::atomic::{AtomicU64, Ordering};
2632
2633 static COUNTER: AtomicU64 = AtomicU64::new(0);
2634 let now = std::time::SystemTime::now()
2635 .duration_since(std::time::UNIX_EPOCH)
2636 .map(|d| d.as_nanos() as u64)
2637 .unwrap_or(0);
2638 now ^ COUNTER
2639 .fetch_add(1, Ordering::Relaxed)
2640 .wrapping_mul(0x9E3779B97F4A7C15)
2641}
2642
2643/// Write an i32 value to a pointer in plugin memory.
2644fn write_i32_to_memory(
2645 memory: &wasmtime::Memory,
2646 mut store: impl wasmtime::AsContextMut,
2647 ptr: i32,
2648 value: i32,
2649) -> Result<(), String> {
2650 if ptr < 0 {
2651 return Err("negative pointer".to_string());
2652 }
2653 let bytes = value.to_le_bytes();
2654 let mut ctx = store.as_context_mut();
2655 memory
2656 .data_mut(&mut ctx)
2657 .get_mut(ptr as usize..(ptr as usize + 4))
2658 .ok_or("pointer out of bounds")?
2659 .copy_from_slice(&bytes);
2660 Ok(())
2661}
2662
2663/// Allocate `size` bytes in the plugin's linear memory by calling the
2664/// plugin's `proxy_on_memory_allocate` export (the standard proxy-wasm
2665/// allocation pattern). Falls back to `memory.grow` if the export is
2666/// not present.
2667fn allocate_in_plugin(
2668 caller: &mut wasmtime::Caller<PluginContext>,
2669 memory: &wasmtime::Memory,
2670 size: usize,
2671) -> Result<usize, String> {
2672 // Try the standard proxy_on_memory_allocate export first.
2673 if let Some(wasmtime::Extern::Func(alloc_func)) = caller.get_export("proxy_on_memory_allocate")
2674 {
2675 if let Ok(typed) = alloc_func.typed::<(i32,), i32>(&mut *caller) {
2676 let ptr = typed
2677 .call(&mut *caller, (size as i32,))
2678 .map_err(|e| format!("proxy_on_memory_allocate: {e}"))?;
2679 if ptr > 0 {
2680 return Ok(ptr as usize);
2681 }
2682 }
2683 }
2684 // Fallback: grow the memory by one page and use the new space.
2685 let current = memory.data_size(&mut *caller);
2686 let needed = current + size;
2687 let pages_needed = (needed - current).div_ceil(65536);
2688 if pages_needed > 0 {
2689 memory
2690 .grow(&mut *caller, pages_needed as u64)
2691 .map_err(|e| format!("memory.grow: {e}"))?;
2692 }
2693 Ok(current)
2694}