feat(wasm): harden module integrity and symlink policy
This commit is contained in:
parent
0b172c4554
commit
163f2fb524
@ -96,7 +96,9 @@ Recommended path:
|
||||
|
||||
1. Start with `dev` for module integration (`capability_escalation_mode = "clamp"`).
|
||||
2. Move to `staging` and fix denied escalation paths.
|
||||
3. Promote to `prod` with minimal permissions.
|
||||
3. Pin module digests with `runtime.wasm.security.module_sha256`.
|
||||
4. Promote to `prod` with minimal permissions.
|
||||
5. Set `runtime.wasm.security.module_hash_policy = "enforce"` after all module pins are in place.
|
||||
|
||||
Example apply flow:
|
||||
|
||||
@ -104,6 +106,20 @@ Example apply flow:
|
||||
cp dev/config.wasm.staging.toml target/.zeroclaw/config.toml
|
||||
```
|
||||
|
||||
Example SHA-256 pin generation:
|
||||
|
||||
```bash
|
||||
sha256sum tools/wasm/*.wasm
|
||||
```
|
||||
|
||||
Then copy each digest into:
|
||||
|
||||
```toml
|
||||
[runtime.wasm.security.module_sha256]
|
||||
calc = "<64-char sha256>"
|
||||
formatter = "<64-char sha256>"
|
||||
```
|
||||
|
||||
## Local CI/CD (Docker-Only)
|
||||
|
||||
Use this when you want CI-style validation without relying on GitHub Actions and without running Rust toolchain commands on your host.
|
||||
|
||||
@ -21,5 +21,11 @@ allowed_hosts = ["localhost:3000", "127.0.0.1:8080", "api.dev.internal"]
|
||||
[runtime.wasm.security]
|
||||
require_workspace_relative_tools_dir = true
|
||||
reject_symlink_modules = true
|
||||
reject_symlink_tools_dir = true
|
||||
strict_host_validation = true
|
||||
capability_escalation_mode = "clamp"
|
||||
module_hash_policy = "warn"
|
||||
|
||||
[runtime.wasm.security.module_sha256]
|
||||
# Pin digests by module name (without ".wasm") before promoting to enforce mode.
|
||||
# calc = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
|
||||
@ -21,5 +21,11 @@ allowed_hosts = []
|
||||
[runtime.wasm.security]
|
||||
require_workspace_relative_tools_dir = true
|
||||
reject_symlink_modules = true
|
||||
reject_symlink_tools_dir = true
|
||||
strict_host_validation = true
|
||||
capability_escalation_mode = "deny"
|
||||
module_hash_policy = "warn"
|
||||
|
||||
[runtime.wasm.security.module_sha256]
|
||||
# Production recommendation: pin all deployed modules and then set module_hash_policy = "enforce".
|
||||
# calc = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
|
||||
@ -21,5 +21,11 @@ allowed_hosts = ["api.staging.internal", "cdn.staging.internal:443"]
|
||||
[runtime.wasm.security]
|
||||
require_workspace_relative_tools_dir = true
|
||||
reject_symlink_modules = true
|
||||
reject_symlink_tools_dir = true
|
||||
strict_host_validation = true
|
||||
capability_escalation_mode = "deny"
|
||||
module_hash_policy = "warn"
|
||||
|
||||
[runtime.wasm.security.module_sha256]
|
||||
# Populate pins and switch module_hash_policy to "enforce" after validation.
|
||||
# calc = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
|
||||
@ -303,8 +303,18 @@ Notes:
|
||||
|---|---|---|
|
||||
| `require_workspace_relative_tools_dir` | `true` | Require `runtime.wasm.tools_dir` to be workspace-relative and reject `..` traversal |
|
||||
| `reject_symlink_modules` | `true` | Block symlinked `.wasm` module files during execution |
|
||||
| `reject_symlink_tools_dir` | `true` | Block execution when `runtime.wasm.tools_dir` is itself a symlink |
|
||||
| `strict_host_validation` | `true` | Fail config/invocation on invalid host entries instead of dropping them |
|
||||
| `capability_escalation_mode` | `"deny"` | Escalation policy: `deny` or `clamp` |
|
||||
| `module_hash_policy` | `"warn"` | Module integrity policy: `disabled`, `warn`, or `enforce` |
|
||||
| `module_sha256` | `{}` | Optional map of module names to pinned SHA-256 digests |
|
||||
|
||||
Notes:
|
||||
|
||||
- `module_sha256` keys must match module names (without `.wasm`) and use `[A-Za-z0-9_-]` only.
|
||||
- `module_sha256` values must be 64-character hexadecimal SHA-256 strings.
|
||||
- `module_hash_policy = "warn"` allows execution but logs missing/mismatched digests.
|
||||
- `module_hash_policy = "enforce"` blocks execution on missing/mismatched digests and requires at least one pin.
|
||||
|
||||
WASM profile templates:
|
||||
|
||||
|
||||
@ -18,7 +18,8 @@ pub use schema::{
|
||||
SecretsConfig, SecurityConfig, SkillsConfig, SkillsPromptInjectionMode, SlackConfig,
|
||||
StorageConfig, StorageProviderConfig, StorageProviderSection, StreamMode, SyscallAnomalyConfig,
|
||||
TelegramConfig, TranscriptionConfig, TunnelConfig, WasmCapabilityEscalationMode,
|
||||
WasmRuntimeConfig, WasmSecurityConfig, WebFetchConfig, WebSearchConfig, WebhookConfig,
|
||||
WasmModuleHashPolicy, WasmRuntimeConfig, WasmSecurityConfig, WebFetchConfig, WebSearchConfig,
|
||||
WebhookConfig,
|
||||
};
|
||||
|
||||
pub fn name_and_presence<T: traits::ChannelConfig>(channel: Option<&T>) -> (&'static str, bool) {
|
||||
|
||||
@ -5,7 +5,7 @@ use anyhow::{Context, Result};
|
||||
use directories::UserDirs;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{OnceLock, RwLock};
|
||||
#[cfg(unix)]
|
||||
@ -2420,6 +2420,19 @@ pub enum WasmCapabilityEscalationMode {
|
||||
Clamp,
|
||||
}
|
||||
|
||||
/// Integrity policy for WASM modules pinned by SHA-256 digest.
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WasmModuleHashPolicy {
|
||||
/// Disable module hash validation.
|
||||
Disabled,
|
||||
/// Warn on missing or mismatched hashes, but allow execution.
|
||||
#[default]
|
||||
Warn,
|
||||
/// Require exact hash match before execution.
|
||||
Enforce,
|
||||
}
|
||||
|
||||
/// Security policy controls for WASM runtime hardening.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WasmSecurityConfig {
|
||||
@ -2431,6 +2444,10 @@ pub struct WasmSecurityConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub reject_symlink_modules: bool,
|
||||
|
||||
/// Reject `runtime.wasm.tools_dir` when it is itself a symlink.
|
||||
#[serde(default = "default_true")]
|
||||
pub reject_symlink_tools_dir: bool,
|
||||
|
||||
/// Strictly validate host allowlist entries (`host` or `host:port` only).
|
||||
#[serde(default = "default_true")]
|
||||
pub strict_host_validation: bool,
|
||||
@ -2438,6 +2455,14 @@ pub struct WasmSecurityConfig {
|
||||
/// Capability escalation handling policy.
|
||||
#[serde(default)]
|
||||
pub capability_escalation_mode: WasmCapabilityEscalationMode,
|
||||
|
||||
/// Module digest verification policy.
|
||||
#[serde(default)]
|
||||
pub module_hash_policy: WasmModuleHashPolicy,
|
||||
|
||||
/// Optional pinned SHA-256 digest map keyed by module name (without `.wasm`).
|
||||
#[serde(default)]
|
||||
pub module_sha256: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
fn default_runtime_kind() -> String {
|
||||
@ -2510,8 +2535,11 @@ impl Default for WasmSecurityConfig {
|
||||
Self {
|
||||
require_workspace_relative_tools_dir: true,
|
||||
reject_symlink_modules: true,
|
||||
reject_symlink_tools_dir: true,
|
||||
strict_host_validation: true,
|
||||
capability_escalation_mode: WasmCapabilityEscalationMode::Deny,
|
||||
module_hash_policy: WasmModuleHashPolicy::Warn,
|
||||
module_sha256: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6240,11 +6268,17 @@ allowed_roots = []
|
||||
assert!(r.wasm.allowed_hosts.is_empty());
|
||||
assert!(r.wasm.security.require_workspace_relative_tools_dir);
|
||||
assert!(r.wasm.security.reject_symlink_modules);
|
||||
assert!(r.wasm.security.reject_symlink_tools_dir);
|
||||
assert!(r.wasm.security.strict_host_validation);
|
||||
assert_eq!(
|
||||
r.wasm.security.capability_escalation_mode,
|
||||
WasmCapabilityEscalationMode::Deny
|
||||
);
|
||||
assert_eq!(
|
||||
r.wasm.security.module_hash_policy,
|
||||
WasmModuleHashPolicy::Warn
|
||||
);
|
||||
assert!(r.wasm.security.module_sha256.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -6554,8 +6588,13 @@ allowed_hosts = ["api.example.com", "cdn.example.com:443"]
|
||||
[runtime.wasm.security]
|
||||
require_workspace_relative_tools_dir = false
|
||||
reject_symlink_modules = false
|
||||
reject_symlink_tools_dir = false
|
||||
strict_host_validation = false
|
||||
capability_escalation_mode = "clamp"
|
||||
module_hash_policy = "enforce"
|
||||
|
||||
[runtime.wasm.security.module_sha256]
|
||||
calc = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
"#;
|
||||
|
||||
let parsed: Config = toml::from_str(raw).unwrap();
|
||||
@ -6578,11 +6617,20 @@ capability_escalation_mode = "clamp"
|
||||
.require_workspace_relative_tools_dir
|
||||
);
|
||||
assert!(!parsed.runtime.wasm.security.reject_symlink_modules);
|
||||
assert!(!parsed.runtime.wasm.security.reject_symlink_tools_dir);
|
||||
assert!(!parsed.runtime.wasm.security.strict_host_validation);
|
||||
assert_eq!(
|
||||
parsed.runtime.wasm.security.capability_escalation_mode,
|
||||
WasmCapabilityEscalationMode::Clamp
|
||||
);
|
||||
assert_eq!(
|
||||
parsed.runtime.wasm.security.module_hash_policy,
|
||||
WasmModuleHashPolicy::Enforce
|
||||
);
|
||||
assert_eq!(
|
||||
parsed.runtime.wasm.security.module_sha256.get("calc"),
|
||||
Some(&"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -12,9 +12,10 @@
|
||||
//! The default ZeroClaw binary excludes it to maintain the 4.6 MB size target.
|
||||
|
||||
use super::traits::RuntimeAdapter;
|
||||
use crate::config::{WasmCapabilityEscalationMode, WasmRuntimeConfig};
|
||||
use crate::config::{WasmCapabilityEscalationMode, WasmModuleHashPolicy, WasmRuntimeConfig};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::collections::BTreeSet;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
/// WASM sandbox runtime — executes tool modules in an isolated interpreter.
|
||||
@ -35,6 +36,8 @@ pub struct WasmExecutionResult {
|
||||
pub exit_code: i32,
|
||||
/// Fuel consumed during execution
|
||||
pub fuel_consumed: u64,
|
||||
/// SHA-256 digest (hex) of the executed module bytes.
|
||||
pub module_sha256: String,
|
||||
}
|
||||
|
||||
/// Capabilities granted to a WASM tool module.
|
||||
@ -120,6 +123,16 @@ impl WasmRuntime {
|
||||
self.config.allowed_hosts.iter().map(String::as_str),
|
||||
"runtime.wasm.allowed_hosts",
|
||||
)?;
|
||||
let normalized_pins = self.normalize_module_sha256_pins()?;
|
||||
if matches!(
|
||||
self.config.security.module_hash_policy,
|
||||
WasmModuleHashPolicy::Enforce
|
||||
) && normalized_pins.is_empty()
|
||||
{
|
||||
bail!(
|
||||
"runtime.wasm.security.module_hash_policy='enforce' requires at least one module pin in runtime.wasm.security.module_sha256"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -249,6 +262,68 @@ impl WasmRuntime {
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
fn normalize_sha256_pin(module_name: &str, raw: &str) -> Result<String> {
|
||||
let normalized = raw.trim().to_ascii_lowercase();
|
||||
if normalized.len() != 64 || !normalized.chars().all(|ch| ch.is_ascii_hexdigit()) {
|
||||
bail!(
|
||||
"runtime.wasm.security.module_sha256.{module_name} must be a 64-character hex SHA-256 digest"
|
||||
);
|
||||
}
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
fn normalize_module_sha256_pins(&self) -> Result<BTreeMap<String, String>> {
|
||||
let mut normalized = BTreeMap::new();
|
||||
for (module_name, digest) in &self.config.security.module_sha256 {
|
||||
Self::validate_module_name(module_name)?;
|
||||
normalized.insert(
|
||||
module_name.clone(),
|
||||
Self::normalize_sha256_pin(module_name, digest)?,
|
||||
);
|
||||
}
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
fn check_module_integrity(&self, module_name: &str, wasm_bytes: &[u8]) -> Result<String> {
|
||||
let digest = hex::encode(Sha256::digest(wasm_bytes));
|
||||
let normalized_pins = self.normalize_module_sha256_pins()?;
|
||||
match self.config.security.module_hash_policy {
|
||||
WasmModuleHashPolicy::Disabled => {}
|
||||
WasmModuleHashPolicy::Warn => match normalized_pins.get(module_name) {
|
||||
Some(expected) if expected == &digest => {}
|
||||
Some(expected) => {
|
||||
tracing::warn!(
|
||||
module = module_name,
|
||||
expected_sha256 = expected,
|
||||
actual_sha256 = digest,
|
||||
"WASM module SHA-256 mismatch (warn mode)"
|
||||
);
|
||||
}
|
||||
None => {
|
||||
tracing::warn!(
|
||||
module = module_name,
|
||||
actual_sha256 = digest,
|
||||
"WASM module has no SHA-256 pin configured (warn mode)"
|
||||
);
|
||||
}
|
||||
},
|
||||
WasmModuleHashPolicy::Enforce => match normalized_pins.get(module_name) {
|
||||
Some(expected) if expected == &digest => {}
|
||||
Some(expected) => {
|
||||
bail!(
|
||||
"WASM module integrity mismatch for '{module_name}': expected sha256={expected}, got sha256={digest}"
|
||||
);
|
||||
}
|
||||
None => {
|
||||
bail!(
|
||||
"WASM module '{module_name}' is missing required SHA-256 pin (runtime.wasm.security.module_hash_policy='enforce')"
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(digest)
|
||||
}
|
||||
|
||||
fn validate_capabilities(&self, caps: &WasmCapabilities) -> Result<WasmCapabilities> {
|
||||
let default_hosts = self.normalize_hosts_with_policy(
|
||||
self.config.allowed_hosts.iter().map(String::as_str),
|
||||
@ -384,6 +459,20 @@ impl WasmRuntime {
|
||||
tools_path.display()
|
||||
);
|
||||
}
|
||||
if self.config.security.reject_symlink_tools_dir {
|
||||
let tools_meta = std::fs::symlink_metadata(&tools_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to inspect WASM tools directory metadata: {}",
|
||||
tools_path.display()
|
||||
)
|
||||
})?;
|
||||
if tools_meta.file_type().is_symlink() {
|
||||
bail!(
|
||||
"WASM tools directory must not be a symlink: {}",
|
||||
tools_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
let canonical_tools_path = std::fs::canonicalize(&tools_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to canonicalize WASM tools directory: {}",
|
||||
@ -474,6 +563,7 @@ impl WasmRuntime {
|
||||
canonical_module_path.display()
|
||||
)
|
||||
})?;
|
||||
let module_sha256 = self.check_module_integrity(module_name, &wasm_bytes)?;
|
||||
|
||||
// Configure engine with fuel metering
|
||||
let mut engine_config = wasmi::Config::default();
|
||||
@ -526,6 +616,7 @@ impl WasmRuntime {
|
||||
),
|
||||
exit_code: -1,
|
||||
fuel_consumed: fuel,
|
||||
module_sha256: module_sha256.clone(),
|
||||
});
|
||||
}
|
||||
bail!("WASM execution error in '{module_name}': {e}");
|
||||
@ -539,6 +630,7 @@ impl WasmRuntime {
|
||||
stderr: String::new(),
|
||||
exit_code,
|
||||
fuel_consumed,
|
||||
module_sha256,
|
||||
})
|
||||
}
|
||||
|
||||
@ -820,6 +912,38 @@ mod tests {
|
||||
assert!(rt.validate_config().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_invalid_module_sha256_pin_format() {
|
||||
let mut cfg = default_config();
|
||||
cfg.security
|
||||
.module_sha256
|
||||
.insert("calc".into(), "not-a-sha256".into());
|
||||
let rt = WasmRuntime::new(cfg);
|
||||
let err = rt.validate_config().unwrap_err();
|
||||
assert!(err.to_string().contains("64-character hex"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_invalid_module_sha256_pin_name() {
|
||||
let mut cfg = default_config();
|
||||
cfg.security.module_sha256.insert(
|
||||
"bad$name".into(),
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into(),
|
||||
);
|
||||
let rt = WasmRuntime::new(cfg);
|
||||
let err = rt.validate_config().unwrap_err();
|
||||
assert!(err.to_string().contains("invalid characters"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_enforce_hash_policy_without_pins() {
|
||||
let mut cfg = default_config();
|
||||
cfg.security.module_hash_policy = WasmModuleHashPolicy::Enforce;
|
||||
let rt = WasmRuntime::new(cfg);
|
||||
let err = rt.validate_config().unwrap_err();
|
||||
assert!(err.to_string().contains("requires at least one module pin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_max_memory() {
|
||||
let mut cfg = default_config();
|
||||
@ -1065,6 +1189,97 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute_module_enforce_hash_policy_rejects_mismatch() {
|
||||
if !WasmRuntime::is_available() {
|
||||
return;
|
||||
}
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let tools_dir = dir.path().join("tools/wasm");
|
||||
std::fs::create_dir_all(&tools_dir).unwrap();
|
||||
std::fs::write(tools_dir.join("calc.wasm"), b"\0asm\x01\0\0\0").unwrap();
|
||||
|
||||
let mut cfg = default_config();
|
||||
cfg.security.module_hash_policy = WasmModuleHashPolicy::Enforce;
|
||||
cfg.security.module_sha256.insert(
|
||||
"calc".into(),
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".into(),
|
||||
);
|
||||
|
||||
let rt = WasmRuntime::new(cfg);
|
||||
let result = rt.execute_module("calc", dir.path(), &WasmCapabilities::default());
|
||||
let err = result.unwrap_err().to_string();
|
||||
assert!(err.contains("integrity mismatch"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute_module_warn_hash_policy_allows_execution_path() {
|
||||
if !WasmRuntime::is_available() {
|
||||
return;
|
||||
}
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let tools_dir = dir.path().join("tools/wasm");
|
||||
std::fs::create_dir_all(&tools_dir).unwrap();
|
||||
std::fs::write(tools_dir.join("calc.wasm"), b"\0asm\x01\0\0\0").unwrap();
|
||||
|
||||
let mut cfg = default_config();
|
||||
cfg.security.module_hash_policy = WasmModuleHashPolicy::Warn;
|
||||
cfg.security.module_sha256.insert(
|
||||
"calc".into(),
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".into(),
|
||||
);
|
||||
|
||||
let rt = WasmRuntime::new(cfg);
|
||||
let result = rt.execute_module("calc", dir.path(), &WasmCapabilities::default());
|
||||
let err = result.unwrap_err().to_string();
|
||||
assert!(err.contains("must export a 'run() -> i32'"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn execute_module_rejects_symlink_tools_dir_when_enabled() {
|
||||
if !WasmRuntime::is_available() {
|
||||
return;
|
||||
}
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let real_tools_dir = dir.path().join("real-tools");
|
||||
std::fs::create_dir_all(&real_tools_dir).unwrap();
|
||||
std::fs::write(real_tools_dir.join("calc.wasm"), b"\0asm\x01\0\0\0").unwrap();
|
||||
|
||||
let tools_parent = dir.path().join("tools");
|
||||
std::fs::create_dir_all(&tools_parent).unwrap();
|
||||
std::os::unix::fs::symlink(&real_tools_dir, tools_parent.join("wasm")).unwrap();
|
||||
|
||||
let rt = WasmRuntime::new(default_config());
|
||||
let result = rt.execute_module("calc", dir.path(), &WasmCapabilities::default());
|
||||
let err = result.unwrap_err().to_string();
|
||||
assert!(err.contains("tools directory must not be a symlink"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn execute_module_allows_symlink_tools_dir_when_disabled() {
|
||||
if !WasmRuntime::is_available() {
|
||||
return;
|
||||
}
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let real_tools_dir = dir.path().join("real-tools");
|
||||
std::fs::create_dir_all(&real_tools_dir).unwrap();
|
||||
std::fs::write(real_tools_dir.join("calc.wasm"), b"\0asm\x01\0\0\0").unwrap();
|
||||
|
||||
let tools_parent = dir.path().join("tools");
|
||||
std::fs::create_dir_all(&tools_parent).unwrap();
|
||||
std::os::unix::fs::symlink(&real_tools_dir, tools_parent.join("wasm")).unwrap();
|
||||
|
||||
let mut cfg = default_config();
|
||||
cfg.security.reject_symlink_tools_dir = false;
|
||||
cfg.security.module_hash_policy = WasmModuleHashPolicy::Disabled;
|
||||
let rt = WasmRuntime::new(cfg);
|
||||
let result = rt.execute_module("calc", dir.path(), &WasmCapabilities::default());
|
||||
let err = result.unwrap_err().to_string();
|
||||
assert!(err.contains("must export a 'run() -> i32'"));
|
||||
}
|
||||
|
||||
// ── Feature gate check ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
@ -176,6 +176,7 @@ impl Tool for WasmModuleTool {
|
||||
Ok(result) => {
|
||||
let output = serde_json::to_string_pretty(&json!({
|
||||
"module": module,
|
||||
"module_sha256": result.module_sha256,
|
||||
"exit_code": result.exit_code,
|
||||
"fuel_consumed": result.fuel_consumed,
|
||||
"stdout": result.stdout,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user