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
103 changes: 103 additions & 0 deletions kiln-runtime/src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ pub struct Table {
pub debug_name: Option<RuntimeString>,
/// Verification level for table operations
pub verification_level: VerificationLevel,
/// Runtime element cap (0 = unlimited). Enforced in `grow`/`grow_shared` in
/// ADDITION to the module-declared `ty.limits.max`, so a host can bound a
/// table below its declared max — the table analog of `Memory::
/// runtime_max_pages` (SR-44; closes the uncapped-table.grow gap AD-WCMC-001
/// found). 0 = unlimited; set before execution.
pub runtime_max_elements: core::sync::atomic::AtomicU32,
}

impl Debug for Table {
Expand All @@ -128,6 +134,9 @@ impl Clone for Table {
elements: Mutex::new(source_elements.clone()),
debug_name: self.debug_name.clone(),
verification_level: self.verification_level,
runtime_max_elements: core::sync::atomic::AtomicU32::new(
self.runtime_max_elements.load(core::sync::atomic::Ordering::Relaxed),
),
}
}
}
Expand Down Expand Up @@ -269,9 +278,18 @@ impl Table {
elements: Mutex::new(elements),
verification_level: VerificationLevel::default(),
debug_name: None,
runtime_max_elements: core::sync::atomic::AtomicU32::new(0),
})
}

/// Set a runtime element cap (0 = unlimited). Enforced in `grow`/`grow_shared`
/// in addition to the module-declared `ty.limits.max` (SR-44). Set before
/// execution.
pub fn set_runtime_max_elements(&self, n: u32) {
self.runtime_max_elements
.store(n, core::sync::atomic::Ordering::Relaxed);
}

/// Creates a new table with the specified capacity and element type
///
/// # Arguments
Expand Down Expand Up @@ -448,6 +466,18 @@ impl Table {
}
}

// Runtime host cap (SR-44), in addition to the module-declared max; 0 = unlimited.
let rt_cap = self
.runtime_max_elements
.load(core::sync::atomic::Ordering::Relaxed);
if rt_cap != 0 && new_size > rt_cap {
return Err(Error::new(
ErrorCategory::Runtime,
kiln_error::codes::CAPACITY_EXCEEDED,
"Table size exceeds runtime cap",
));
}

// Lock elements and push new values
let mut elements = self.elements.lock()
.map_err(|_| Error::runtime_error("Failed to lock table elements"))?;
Expand Down Expand Up @@ -595,6 +625,19 @@ impl Table {
}
}

// Runtime host cap (SR-44), in addition to the module-declared max; 0 = unlimited.
// Kept in sync with grow_shared so neither path can bypass the cap.
let rt_cap = self
.runtime_max_elements
.load(core::sync::atomic::Ordering::Relaxed);
if rt_cap != 0 && new_size > rt_cap {
return Err(Error::new(
ErrorCategory::Runtime,
kiln_error::codes::CAPACITY_EXCEEDED,
"Table size exceeds runtime cap",
));
}

// Lock elements and push new values
let mut elements = self.elements.lock()
.map_err(|_| Error::runtime_error("Failed to lock table elements"))?;
Expand Down Expand Up @@ -911,3 +954,63 @@ impl Clone for TableManager {
// type conversions This will be re-enabled once the Value types are properly
// unified across crates

#[cfg(test)]
mod sr44_cap_tests {
use super::*;

fn table(min: u32, max: Option<u32>) -> Table {
Table::new(KilnTableType {
element_type: KilnRefType::Funcref,
limits: KilnLimits { min, max },
table64: false,
})
.unwrap()
}

/// SR-44 / AD-WCMC-001: a runtime element cap bounds table.grow BELOW the
/// module-declared max — the table analog of Memory::runtime_max_pages.
/// 0 = unlimited.
// rivet: verifies SR-44
#[test]
fn runtime_cap_bounds_table_grow_below_declared_max() {
let t = table(0, Some(10)); // declared max 10
t.set_runtime_max_elements(3); // host caps at 3
let nullref = KilnValue::FuncRef(None);
assert!(t.grow_shared(3, nullref.clone()).is_ok(), "grow to the cap succeeds");
assert_eq!(t.size(), 3);
assert!(
t.grow_shared(1, nullref).is_err(),
"grow past the runtime cap is rejected even though declared max (10) allows it"
);
assert_eq!(t.size(), 3, "a rejected grow does not change the size");
}

/// SR-44: cap 0 = unlimited (only the declared max applies).
// rivet: verifies SR-44
#[test]
fn runtime_cap_zero_is_unlimited_for_tables() {
let t = table(0, Some(5));
let nullref = KilnValue::FuncRef(None);
assert!(t.grow_shared(5, nullref.clone()).is_ok(), "grow to declared max with no cap");
assert_eq!(t.size(), 5);
assert!(t.grow_shared(1, nullref).is_err(), "declared max still enforced");
}

/// SR-44: the &mut self `grow` path must ALSO honour the runtime cap (a
/// clean-room review found it initially did not — neither path may bypass it).
// rivet: verifies SR-44
#[test]
fn runtime_cap_bounds_mut_grow_path_too() {
let mut t = table(0, Some(10));
t.set_runtime_max_elements(3);
let nullref = KilnValue::FuncRef(None);
assert!(t.grow(3, nullref.clone()).is_ok(), "&mut grow to the cap succeeds");
assert_eq!(t.size(), 3);
assert!(
t.grow(1, nullref).is_err(),
"&mut grow past the runtime cap must be rejected (not just grow_shared)"
);
assert_eq!(t.size(), 3);
}
}

14 changes: 14 additions & 0 deletions kilnd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,20 @@ impl KilndEngine {
if cap_pages > 0 {
if let Ok(inst) = engine.get_instance(instance) {
if let Ok(mem) = inst.memory(0) {
// SR-43: reject-at-load when the module's DECLARED memory min
// exceeds the cap — refuse to run rather than trap mid-execution
// (moves a fixed-RAM overrun from mid-mission to load time).
// NOTE: this runs post-instantiate, so the declared `min` was
// already allocated by Memory::new; rejecting before eager `min`
// allocation needs a pre-instantiate engine API (follow-up).
let declared_min = mem.0.ty.limits.min;
if declared_min > cap_pages {
return Err(Error::new(
ErrorCategory::Resource,
codes::CAPACITY_EXCEEDED,
"Module declared memory min exceeds --memory cap (rejected at load)",
));
}
mem.0.set_runtime_max_pages(cap_pages);
}
}
Expand Down
42 changes: 42 additions & 0 deletions safety/requirements/functional-requirements.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1085,3 +1085,45 @@ artifacts:
model: claude-opus-4-8
timestamp: 2026-07-11T00:00:00Z
release: v0.4.2

- id: SR-43
type: requirement
title: kilnd rejects at load a module whose declared memory min exceeds the --memory cap
status: verified
description: "Closes the SR-41 ordering gap found by the WCMC analysis (AD-WCMC-001): the SR-41 runtime cap is set AFTER instantiation and only enforced in memory.grow, but Memory::new eagerly allocates the declared min pages BEFORE the cap exists. Fix: kilnd rejects-at-load when a module's declared memory min (pages*64KiB) exceeds config.max_memory (the --memory cap). Verified: (memory 2000)=128MB rejected at 64MB default, runs under --memory 256MB. KNOWN LIMITATION (in-code + clean-room confirmed): the check is post-instantiate, so the declared min was already allocated by Memory::new before rejection (Peak memory 131072000 observed in the allow case); a pre-allocation reject needs a pre-instantiate engine API (follow-up). Relates to SR-41. AD-WCMC-001. Issue #411."
tags: [kilnd, memory, resource-limit, reject-at-load, bug]
fields:
upstream-ref: https://github.com/pulseengine/kiln/issues/411
provenance:
created-by: ai
model: claude-opus-4-8
timestamp: 2026-07-11T10:00:00Z
release: v0.4.3

- id: SR-44
type: requirement
title: Table growth honours a runtime element cap (mirror SR-41 for tables)
status: verified
description: "The WCMC analysis (AD-WCMC-001) found table.grow was UNCAPPED — Table::grow/grow_shared checked only the module-declared ty.limits.max; a table without a declared max could grow to multi-GiB, the class of bug SR-41 fixed for linear memory. Fix: added runtime_max_elements to Table (AtomicU32, 0=unlimited) enforced in BOTH grow paths (a clean-room review caught the first pass only covered grow_shared; the &mut self grow was fixed + regression-tested). 3 unit oracles. Relates to SR-41. AD-WCMC-001. Issue #411."
tags: [kiln-runtime, table, resource-limit, bug]
fields:
upstream-ref: https://github.com/pulseengine/kiln/issues/411
provenance:
created-by: ai
model: claude-opus-4-8
timestamp: 2026-07-11T10:00:00Z
release: v0.4.3

- id: SR-45
type: requirement
title: The kiln.resource_limits manifest section is applied (wire the dead extraction)
status: proposed
description: "The WCMC analysis (AD-WCMC-001) found the kiln.resource_limits section is decoded but NEVER APPLIED (capability_engine.rs:561 TODO). DEEPER FINDING this pass: the extraction fn (kiln-foundation/src/execution.rs:144) is ALSO a stub — returns a default and does NOT read the section (the parser from_bytes_with_provider in kiln-decoder/src/resource_limits_section.rs is never called). So SR-45 is a bigger cross-crate wire: (1) make extract scan+parse the section; (2) thread limits to instantiate; (3) apply max_memory_usage -> set_runtime_max_pages (SR-41), max_call_depth -> engine cap. This is the on-target enforcement point the embedded trust chain (AD-WCMC-001) depends on. Own feature loop. AD-WCMC-001. Issue #415."
tags: [kiln-runtime, resource-limits, manifest, embedded, dead-code, bug]
fields:
upstream-ref: https://github.com/pulseengine/kiln/issues/415
provenance:
created-by: ai
model: claude-opus-4-8
timestamp: 2026-07-11T10:00:00Z
release: v0.4.3
Loading