Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions kiln-decoder/src/resource_limits_section.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,79 @@ use kiln_foundation::{
/// Standard custom section name for resource limits
pub const RESOURCE_LIMITS_SECTION_NAME: &str = "kiln.resource_limits";

/// Extract the `kiln.resource_limits` custom section from a raw WebAssembly
/// binary (SR-45).
///
/// This is the on-target read side of the AD-WCMC-001 trust chain: the
/// section is authored and signed off-target (sigil), and the runtime applies
/// it at load time. The three outcomes are strictly distinguished:
///
/// - `Ok(None)` — the binary carries NO `kiln.resource_limits` section. This
/// is the only "no manifest" case; host CLI limits (if any) still apply.
/// - `Ok(Some(section))` — the section is present, decodes, and validates.
/// - `Err(_)` — the binary's section framing is malformed, or the section is
/// PRESENT but undecodable/invalid. A broken manifest must fail loud and
/// must never be treated as absent (that would silently unbound a module
/// that declared itself bounded).
pub fn extract_resource_limits_from_binary(
binary: &[u8],
) -> Result<Option<ResourceLimitsSection>, Error> {
use kiln_format::binary::read_leb128_u32;

// WebAssembly header: 4-byte magic + 4-byte version.
if binary.len() < 8 || &binary[0..4] != b"\0asm" {
return Err(Error::parse_error(
"Not a WebAssembly binary (bad magic) while scanning for resource limits",
));
}

let mut offset = 8;
while offset < binary.len() {
// Section framing: id byte + LEB128 payload size + payload.
let section_id = binary[offset];
offset += 1;
let (payload_len, len_size) = read_leb128_u32(binary, offset)?;
offset += len_size;
let payload_end = offset
.checked_add(payload_len as usize)
.ok_or_else(|| Error::parse_error("Section size overflows while scanning"))?;
if payload_end > binary.len() {
return Err(Error::parse_error(
"Section extends past end of binary while scanning for resource limits",
));
}
let payload = &binary[offset..payload_end];
offset = payload_end;

if section_id != 0 {
continue; // non-custom section
}

// Custom section payload: name (LEB128 length + bytes) + contents.
let (name_len, name_len_size) = read_leb128_u32(payload, 0)?;
let name_end = name_len_size
.checked_add(name_len as usize)
.ok_or_else(|| Error::parse_error("Custom section name overflows"))?;
if name_end > payload.len() {
return Err(Error::parse_error(
"Custom section name extends past section payload",
));
}
let name = core::str::from_utf8(&payload[name_len_size..name_end])
.map_err(|_| Error::parse_error("Custom section name is not valid UTF-8"))?;
if name != RESOURCE_LIMITS_SECTION_NAME {
continue;
}

// Section is PRESENT: decode + validate errors propagate (fail loud).
let section = ResourceLimitsSection::decode(&payload[name_end..])?;
section.validate()?;
return Ok(Some(section));
}

Ok(None)
}

/// Version of the resource limits format (for future compatibility)
pub const RESOURCE_LIMITS_VERSION: u32 = 1;

Expand Down Expand Up @@ -1282,6 +1355,75 @@ mod tests {
Ok(())
}

/// Minimal valid WebAssembly binary: magic + version, no sections.
const EMPTY_MODULE: [u8; 8] = [0, b'a', b's', b'm', 1, 0, 0, 0];

fn module_with_section(name: &str, payload: &[u8]) -> alloc::vec::Vec<u8> {
let mut wasm = EMPTY_MODULE.to_vec();
let mut content = alloc::vec![name.len() as u8]; // single-byte LEB128
content.extend_from_slice(name.as_bytes());
content.extend_from_slice(payload);
wasm.push(0); // custom section id
wasm.push(content.len() as u8); // single-byte LEB128
wasm.extend(content);
wasm
}

#[test]
fn test_extract_absent_section_is_none() -> kiln_error::Result<()> {
// No custom section at all.
assert_eq!(extract_resource_limits_from_binary(&EMPTY_MODULE)?, None);
// An unrelated custom section does not count as a manifest.
let wasm = module_with_section("some.other.section", &[1, 2, 3]);
assert_eq!(extract_resource_limits_from_binary(&wasm)?, None);
Ok(())
}

#[test]
fn test_extract_present_section_roundtrips() -> kiln_error::Result<()> {
let provider = kiln_foundation::safe_managed_alloc!(
4096,
kiln_foundation::budget_aware_provider::CrateId::Decoder
)?;
let section = ResourceLimitsSection::with_execution_limits(
provider,
Some(1000),
Some(64 * 1024),
Some(32),
None,
None,
)?;
let payload = section.encode()?;
let wasm = module_with_section(RESOURCE_LIMITS_SECTION_NAME, &payload);

let extracted =
extract_resource_limits_from_binary(&wasm)?.expect("present section must be extracted");
assert_eq!(extracted.max_fuel_per_step, Some(1000));
assert_eq!(extracted.max_memory_usage, Some(64 * 1024));
assert_eq!(extracted.max_call_depth, Some(32));
Ok(())
}

#[test]
fn test_extract_malformed_section_is_error() {
// Present but truncated mid-field: version + "present" flag with no value.
let wasm = module_with_section(RESOURCE_LIMITS_SECTION_NAME, &[1, 0, 0, 0, 1]);
assert!(
extract_resource_limits_from_binary(&wasm).is_err(),
"a present-but-malformed section must be an error, not treated as absent"
);

// Present but empty payload: not decodable either.
let wasm = module_with_section(RESOURCE_LIMITS_SECTION_NAME, &[]);
assert!(extract_resource_limits_from_binary(&wasm).is_err());
}

#[test]
fn test_extract_non_wasm_binary_is_error() {
assert!(extract_resource_limits_from_binary(b"not wasm at all").is_err());
assert!(extract_resource_limits_from_binary(&[]).is_err());
}

#[test]
fn test_lower_asil_levels() -> kiln_error::Result<()> {
let provider = kiln_foundation::safe_managed_alloc!(
Expand Down
24 changes: 0 additions & 24 deletions kiln-foundation/src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,27 +125,3 @@ impl ExecutionStats {
self.execution_time_us = time_us;
}
}

/// Extract resource limits configuration from WebAssembly binary
///
/// This function attempts to extract ASIL-compliant resource limits
/// from a WebAssembly binary's custom sections.
///
/// # Arguments
///
/// * `binary` - The WebAssembly binary data
/// * `asil_mode` - The target ASIL execution mode
///
/// # Returns
///
/// Returns `Ok(Some(config))` if resource limits are found and valid,
/// `Ok(None)` if no resource limits are found, or `Err` if the binary is
/// invalid.
pub fn extract_resource_limits_from_binary(
_binary: &[u8],
asil_mode: ASILExecutionMode,
) -> kiln_error::Result<Option<ASILExecutionConfig>> {
// TODO: Implement actual resource limits extraction from custom sections
// For now, return a default configuration based on ASIL mode
Ok(Some(ASILExecutionConfig::new(asil_mode)))
}
91 changes: 50 additions & 41 deletions kiln-runtime/src/engine/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@
//! This module provides a fluent builder interface for creating WebAssembly
//! engines with proper ASIL-level configuration and resource limits.

use kiln_error::Result;
use kiln_decoder::resource_limits_section::extract_resource_limits_from_binary;
use kiln_error::{Error, Result};
use kiln_foundation::{
capabilities::MemoryCapabilityContext,
execution::{
extract_resource_limits_from_binary,
ASILExecutionConfig,
ASILExecutionMode,
},
execution::ASILExecutionMode,
};

use crate::engine::{
Expand All @@ -23,23 +20,20 @@ use crate::engine::{
#[derive(Debug)]
pub struct EngineBuilder {
/// Target ASIL level for the engine
asil_level: Option<ASILExecutionMode>,
asil_level: Option<ASILExecutionMode>,
/// Engine preset (overrides ASIL level if set)
preset: Option<EnginePreset>,
preset: Option<EnginePreset>,
/// Custom capability context (overrides both ASIL level and preset)
custom_context: Option<MemoryCapabilityContext>,
/// Resource limits configuration from binary
resource_config: Option<ASILExecutionConfig>,
custom_context: Option<MemoryCapabilityContext>,
}

impl EngineBuilder {
/// Create a new engine builder
pub fn new() -> Self {
Self {
asil_level: None,
preset: None,
custom_context: None,
resource_config: None,
asil_level: None,
preset: None,
custom_context: None,
}
}

Expand All @@ -61,12 +55,6 @@ impl EngineBuilder {
self
}

/// Set resource limits configuration from a WebAssembly binary
pub fn with_resource_config(mut self, config: ASILExecutionConfig) -> Self {
self.resource_config = Some(config);
self
}

/// Create an engine for QM (Quality Management) level
pub fn qm() -> Self {
Self::new().with_preset(EnginePreset::QM)
Expand All @@ -92,26 +80,24 @@ impl EngineBuilder {
Self::new().with_preset(EnginePreset::AsilD)
}

/// Create an engine from a WebAssembly binary with embedded resource limits
/// Create an engine builder from a WebAssembly binary, selecting the ASIL
/// level from the binary's `kiln.resource_limits` manifest (SR-45).
///
/// - Manifest present with a qualified ASIL level: that level is selected.
/// - Manifest present without a qualified level: QM (the manifest's
/// numeric limits are enforced separately by
/// `CapabilityAwareEngine::load_module`, regardless of level).
/// - Manifest absent: QM.
/// - Manifest present but malformed, or an unknown qualified level:
/// `Err` — never silently downgraded (fail loud).
pub fn from_binary(binary: &[u8]) -> Result<Self> {
// Function is now imported at the top

// Try to extract resource limits from the binary
// Start with ASIL-D for maximum compatibility, then work down
for asil_mode in &[
ASILExecutionMode::AsilD,
ASILExecutionMode::AsilC,
ASILExecutionMode::AsilB,
ASILExecutionMode::AsilA,
ASILExecutionMode::QM,
] {
if let Ok(Some(config)) = extract_resource_limits_from_binary(binary, *asil_mode) {
return Ok(Self::new().with_asil_level(config.mode).with_resource_config(config));
}
}

// No resource limits found, default to QM
Ok(Self::qm())
let Some(section) = extract_resource_limits_from_binary(binary)? else {
return Ok(Self::qm());
};
let Some(level) = section.qualified_asil_level() else {
return Ok(Self::qm());
};
Ok(Self::new().with_asil_level(parse_qualified_asil_level(level)?))
}

/// Build the engine with the configured settings
Expand Down Expand Up @@ -143,9 +129,32 @@ impl EngineBuilder {
}
}

/// Map a manifest's qualified ASIL level string to an execution mode.
///
/// Whitespace is trimmed and matching is ASCII-case-insensitive (section
/// authors have historically emitted levels like `"ASIL-D "`). An unknown
/// level is an error — a signed manifest claiming a qualification the runtime
/// does not recognize must not be silently reinterpreted (SR-45, fail loud).
fn parse_qualified_asil_level(level: &str) -> Result<ASILExecutionMode> {
let normalized = level.trim();
for (name, mode) in [
("QM", ASILExecutionMode::QM),
("ASIL-A", ASILExecutionMode::AsilA),
("ASIL-B", ASILExecutionMode::AsilB),
("ASIL-C", ASILExecutionMode::AsilC),
("ASIL-D", ASILExecutionMode::AsilD),
] {
if normalized.eq_ignore_ascii_case(name) {
return Ok(mode);
}
}
Err(Error::parse_error(
"Unknown qualified ASIL level in kiln.resource_limits manifest",
))
}

impl Default for EngineBuilder {
fn default() -> Self {
Self::new()
}
}

Loading
Loading