waf_wasm/report.rs
1// SPDX-FileCopyrightText: 2026 0x00spor3
2// SPDX-License-Identifier: Apache-2.0
3
4//! Boot-time classification of a guest's declared imports (policy D3=A: explicit coverage,
5//! never a silent partial). A missing import would make instantiation fail, so the loader
6//! STUBS every import it does not implement; this report says which were stubbed and — the
7//! load-bearing distinction (paletto #4) — whether any stub *alters detection semantics*.
8//!
9//! - **implemented**: the host function does the real thing.
10//! - **cosmetic stub**: absence does not change what the plugin detects (metrics, ticks…).
11//! - **semantic stub**: the plugin's detection logic *may* depend on it (`http_call`,
12//! `shared_data`, header mutation…); stubbing it makes a plugin that CALLS it behave
13//! differently from how it was written.
14//!
15//! Crucial nuance learned from a real SDK filter (paletto #4): a Proxy-Wasm SDK *declares*
16//! every host import regardless of use, so a *declared* semantic import is NOT evidence the
17//! plugin depends on it. This boot report is therefore **informational** (what the plugin
18//! links + which links are semantic-capable); the accurate "degraded" signal is emitted at
19//! RUNTIME, the first time the plugin actually invokes a stubbed semantic host call
20//! (`runtime::register_stubs` → `HostState::semantic_stub_hit`). We never reject at load.
21
22use wasmi::Module;
23
24/// Host functions the runtime implements for real.
25pub const IMPLEMENTED: &[&str] = &[
26 "proxy_log",
27 "proxy_get_buffer_bytes",
28 "proxy_get_header_map_pairs",
29 "proxy_get_header_map_value",
30 "proxy_get_property",
31 "proxy_send_local_response",
32 "proxy_get_current_time_nanoseconds",
33 "proxy_set_tick_period_milliseconds",
34];
35
36/// Stubbing these changes detection semantics → a plugin that uses one is `degraded`.
37pub const SEMANTIC: &[&str] = &[
38 "proxy_http_call",
39 "proxy_dispatch_http_call",
40 "proxy_get_shared_data",
41 "proxy_set_shared_data",
42 "proxy_register_shared_queue",
43 "proxy_resolve_shared_queue",
44 "proxy_enqueue_shared_queue",
45 "proxy_dequeue_shared_queue",
46 "proxy_set_header_map_pairs",
47 "proxy_replace_header_map_value",
48 "proxy_add_header_map_value",
49 "proxy_remove_header_map_value",
50 "proxy_set_property",
51 "proxy_set_buffer_bytes",
52 "proxy_call_foreign_function",
53 "proxy_grpc_call",
54 "proxy_grpc_stream",
55 "proxy_grpc_send",
56 "proxy_grpc_cancel",
57 "proxy_grpc_close",
58];
59
60/// How a single stubbed import was classified.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum StubClass {
63 Cosmetic,
64 Semantic,
65}
66
67/// Outcome of classifying a module's imports.
68#[derive(Debug, Clone, Default)]
69pub struct ImportReport {
70 /// `(name, class)` for every import the loader had to stub.
71 pub stubbed: Vec<(String, StubClass)>,
72 /// Implemented host functions the guest actually imports.
73 pub implemented_used: Vec<String>,
74}
75
76impl ImportReport {
77 /// Count of semantic-capable stubbed imports the plugin LINKS (not necessarily calls).
78 pub fn semantic_linked(&self) -> usize {
79 self.stubbed.iter().filter(|(_, c)| *c == StubClass::Semantic).count()
80 }
81
82 /// Names of the semantic-capable stubs the plugin links.
83 pub fn semantic_stubs(&self) -> Vec<&str> {
84 self.stubbed
85 .iter()
86 .filter(|(_, c)| *c == StubClass::Semantic)
87 .map(|(n, _)| n.as_str())
88 .collect()
89 }
90
91 /// One-line boot summary — informational. Whether the plugin is actually degraded is a
92 /// runtime fact (see the module docs), not derivable from declared imports.
93 pub fn summary(&self) -> String {
94 let semantic = self.semantic_linked();
95 let cosmetic = self.stubbed.len() - semantic;
96 format!(
97 "WASM import: {} implemented, {} stubbed ({} semantic-capable, {} cosmetic); \
98 semantic host calls return Unimplemented IF the plugin invokes them at runtime",
99 self.implemented_used.len(),
100 self.stubbed.len(),
101 semantic,
102 cosmetic,
103 )
104 }
105}
106
107/// Classify the func imports a module declares (namespace `env`).
108pub fn classify(module: &Module) -> ImportReport {
109 let mut report = ImportReport::default();
110 for import in module.imports() {
111 // Only function imports matter here; memory/global/table imports are handled by
112 // instantiation directly.
113 if import.ty().func().is_none() {
114 continue;
115 }
116 let name = import.name();
117 if IMPLEMENTED.contains(&name) {
118 report.implemented_used.push(name.to_string());
119 } else if SEMANTIC.contains(&name) {
120 report.stubbed.push((name.to_string(), StubClass::Semantic));
121 } else {
122 report.stubbed.push((name.to_string(), StubClass::Cosmetic));
123 }
124 }
125 report
126}