Repository: bytecodealliance/wasmtime
Pull request #7231 (2023-10-12 19:24:38)

PCC: check facts on loaded and stored values, infer facts where needed, and add a basic vmctx/memory example.
This PR incorporates a few more steps in PCC's development:
* We now check that the fact, if any, provided for a field in a memory type supports (subsumes) the claimed fact on the result of a load from that field. Likewise, we check that the fact on any value stored to a field supports the field's fact.
* We check that facts are preserved across control-flow edges by validating that branch arguments' facts subsume blockparams' facts.
* For integer-typed values, when no other fact is present and one is needed during validation, we infer a minimal fact based on the type's full range.

With all of these together, we can validate a simple "mock `vmctx`" example that looks a lot like what static memory accesses in Wasmtime do:

```
test compile
set enable_pcc=true
target aarch64

;; Equivalent to a Wasm `i64.load` from a static memory.
function %f0(i64, i32) -> i64 {
    ;; mock vmctx struct:
    mt0 = struct 8 { 0: i64 readonly ! mem(mt1, 0) }
    ;; mock static memory: 4GiB range, plus 2GiB guard
    mt1 = memory 0x1_8000_0000

block0(v0 ! mem(mt0, 0): i64, v1: i32):
    ;; Compute the address: base + offset. Guard region (2GiB) is
    ;; sufficient for an 8-byte I64 load.
    v2 ! mem(mt1, 0) = load.i64 checked v0+0    ;; base pointer
    v3 ! max(64, 0xffff_ffff) = uextend.i64 v1  ;; offset
    v4 ! mem(mt1, 0xffff_ffff) = iadd.i64 v2, v3
    v5 = load.i64 checked v4
    return v5
}
```

In theory, once we propagate facts during aegraph rewriting (we don't currently; opts are off in all tests), this should be enough for end-to-end checking of static memories by emitting the right facts in `cranelift-wasm`. Those two steps (opts, then Wasm frontend) are next!

Co-authored-by: Nick Fitzgerald <fitzgen@gmail.com>


Patch:
diff --git a/cranelift/codegen/src/ir/memtype.rs b/cranelift/codegen/src/ir/memtype.rs
index 04def7cf7..711e761ab 100644
--- a/cranelift/codegen/src/ir/memtype.rs
+++ b/cranelift/codegen/src/ir/memtype.rs
@@ -159,6 +159,16 @@ pub struct MemoryTypeField {
     pub readonly: bool,
 }
 
+impl MemoryTypeField {
+    /// Get the fact, if any, on a field. Fills in a default inferred
+    /// fact based on the type if no explicit fact is present.
+    pub fn fact(&self) -> Option<&Fact> {
+        self.fact
+            .as_ref()
+            .or_else(|| Fact::infer_from_type(self.ty))
+    }
+}
+
 impl MemoryTypeData {
     /// Provide the static size of this type, if known.
     ///
diff --git a/cranelift/codegen/src/ir/pcc.rs b/cranelift/codegen/src/ir/pcc.rs
index fc98bab18..d1fcc2ad1 100644
--- a/cranelift/codegen/src/ir/pcc.rs
+++ b/cranelift/codegen/src/ir/pcc.rs
@@ -46,20 +46,40 @@
 //! `FactContext::add()` and friends to forward-propagate facts.
 //!
 //! TODO:
-//! - Check blockparams' preds against blockparams' facts.
+//!
+//! Completeness:
 //! - Propagate facts through optimization (egraph layer).
 //! - Generate facts in cranelift-wasm frontend when lowering memory ops.
-//! - Implement richer "points-to" facts that describe the pointed-to
-//!   memory, so the loaded values can also have facts.
 //! - Support bounds-checking-type operations for dynamic memories and
 //!   tables.
+//!
+//! Generality:
+//! - facts on outputs (in func signature)?
 //! - Implement checking at the CLIF level as well.
 //! - Check instructions that can trap as well?
+//!
+//! Nicer errors:
+//! - attach instruction index or some other identifier to errors
+//!
+//! Refactoring:
+//! - avoid the "default fact" infra everywhere we fetch facts,
+//!   instead doing it in the subsume check (and take the type with
+//!   subsume)?
+//!
+//! Text format cleanup:
+//! - make the bitwidth on `max` facts optional in the CLIF text
+//!   format?
+//! - make offset in `mem` fact optional in the text format?
+//!
+//! Bikeshed colors (syntax):
+//! - Put fact bang-annotations after types?
+//!   `v0: i64 ! fact(..)` vs. `v0 ! fact(..): i64`
 
 use crate::ir;
+use crate::ir::types::*;
 use crate::isa::TargetIsa;
-use crate::machinst::{InsnIndex, LowerBackend, MachInst, VCode};
-use cranelift_entity::PrimaryMap;
+use crate::machinst::{BlockIndex, LowerBackend, MachInst, VCode};
+use regalloc2::Function as _;
 use std::fmt;
 
 #[cfg(feature = "enable-serde")]
@@ -81,6 +101,9 @@ pub enum PccError {
     /// A derivation of an output fact is unsupported (incorrect or
     /// not derivable).
     UnsupportedFact,
+    /// A block parameter claims a fact that one of its predecessors
+    /// does not support.
+    UnsupportedBlockparam,
     /// A memory access is out of bounds.
     OutOfBounds,
     /// Proof-carrying-code checking is not implemented for a
@@ -90,6 +113,15 @@ pub enum PccError {
     /// particular instruction that instruction-selection chose. This
     /// is an internal compiler error.
     UnimplementedInst,
+    /// Access to an invalid or undefined field offset in a struct.
+    InvalidFieldOffset,
+    /// Access to a field via the wrong type.
+    BadFieldType,
+    /// Store to a read-only field.
+    WriteToReadOnlyField,
+    /// Store of data to a field with a fact that does not subsume the
+    /// field's fact.
+    InvalidStoredFact,
 }
 
 /// A fact on a value.
@@ -130,6 +162,37 @@ impl fmt::Display for Fact {
     }
 }
 
+impl Fact {
+    /// Try to infer a minimal fact for a value of the given IR type.
+    pub fn infer_from_type(ty: ir::Type) -> Option<&'static Self> {
+        static FACTS: [Fact; 4] = [
+            Fact::ValueMax {
+                bit_width: 8,
+                max: u8::MAX as u64,
+            },
+            Fact::ValueMax {
+                bit_width: 16,
+                max: u16::MAX as u64,
+            },
+            Fact::ValueMax {
+                bit_width: 32,
+                max: u32::MAX as u64,
+            },
+            Fact::ValueMax {
+                bit_width: 64,
+                max: u64::MAX,
+            },
+        ];
+        match ty {
+            I8 => Some(&FACTS[0]),
+            I16 => Some(&FACTS[1]),
+            I32 => Some(&FACTS[2]),
+            I64 => Some(&FACTS[3]),
+            _ => None,
+        }
+    }
+}
+
 macro_rules! ensure {
     ( $condition:expr, $err:tt $(,)? ) => {
         if !$condition {
@@ -148,18 +211,15 @@ macro_rules! bail {
 /// context carries environment/global properties, such as the machine
 /// pointer width.
 pub struct FactContext<'a> {
-    memory_types: &'a PrimaryMap<ir::MemoryType, ir::MemoryTypeData>,
+    function: &'a ir::Function,
     pointer_width: u16,
 }
 
 impl<'a> FactContext<'a> {
     /// Create a new "fact context" in which to evaluate facts.
-    pub fn new(
-        memory_types: &'a PrimaryMap<ir::MemoryType, ir::MemoryTypeData>,
-        pointer_width: u16,
-    ) -> Self {
+    pub fn new(function: &'a ir::Function, pointer_width: u16) -> Self {
         FactContext {
-            memory_types,
+            function,
             pointer_width,
         }
     }
@@ -170,6 +230,14 @@ impl<'a> FactContext<'a> {
             // Reflexivity.
             (l, r) if l == r => true,
 
+            // Any value on the LHS subsumes a minimal (always-true)
+            // fact about the max value of a given bitwidth on the
+            // RHS: e.g., no matter the value, the bottom 8 bits will
+            // always be <= 255.
+            (_, Fact::ValueMax { bit_width, max }) if *max == max_value_for_width(*bit_width) => {
+                true
+            }
+
             (
                 Fact::ValueMax {
                     bit_width: bw_lhs,
@@ -195,6 +263,18 @@ impl<'a> FactContext<'a> {
         }
     }
 
+    /// Computes whether the optional fact `lhs` subsumes (implies)
+    /// the optional fact `lhs`. A `None` never subsumes any fact, and
+    /// is always subsumed by any fact at all (or no fact).
+    pub fn subsumes_fact_optionals(&self, lhs: Option<&Fact>, rhs: Option<&Fact>) -> bool {
+        match (lhs, rhs) {
+            (None, None) => true,
+            (Some(_), None) => true,
+            (None, Some(_)) => false,
+            (Some(lhs), Some(rhs)) => self.subsumes(lhs, rhs),
+        }
+    }
+
     /// Computes whatever fact can be known about the sum of two
     /// values with attached facts. The add is performed to the given
     /// bit-width. Note that this is distinct from the machine or
@@ -213,6 +293,7 @@ impl<'a> FactContext<'a> {
                 },
             ) if bw_lhs == bw_rhs && add_width >= *bw_lhs => {
                 let computed_max = lhs.checked_add(*rhs)?;
+                let computed_max = std::cmp::min(max_value_for_width(add_width), computed_max);
                 Some(Fact::ValueMax {
                     bit_width: *bw_lhs,
                     max: computed_max,
@@ -352,7 +433,10 @@ impl<'a> FactContext<'a> {
 
     /// Check that accessing memory via a pointer with this fact, with
     /// a memory access of the given size, is valid.
-    pub fn check_address(&self, fact: &Fact, size: u32) -> PccResult<()> {
+    ///
+    /// If valid, returns the memory type and offset into that type
+    /// that this address accesses.
+    fn check_address(&self, fact: &Fact, size: u32) -> PccResult<(ir::MemoryType, i64)> {
         match fact {
             Fact::Mem { ty, offset } => {
                 let end_offset: i64 = offset
@@ -360,17 +444,70 @@ impl<'a> FactContext<'a> {
                     .ok_or(PccError::Overflow)?;
                 let end_offset: u64 =
                     u64::try_from(end_offset).map_err(|_| PccError::OutOfBounds)?;
-                match &self.memory_types[*ty] {
+                match &self.function.memory_types[*ty] {
                     ir::MemoryTypeData::Struct { size, .. }
                     | ir::MemoryTypeData::Memory { size } => {
                         ensure!(end_offset <= *size, OutOfBounds)
                     }
                     ir::MemoryTypeData::Empty => bail!(OutOfBounds),
                 }
+                Ok((*ty, *offset))
             }
             _ => bail!(OutOfBounds),
         }
+    }
+
+    /// Get the access struct field, if any, by a pointer with the
+    /// given fact and an access of the given type.
+    pub fn struct_field<'b>(
+        &'b self,
+        fact: &Fact,
+        access_ty: ir::Type,
+    ) -> PccResult<Option<&'b ir::MemoryTypeField>> {
+        let (ty, offset) = self.check_address(fact, access_ty.bytes())?;
+        let offset =
+            u64::try_from(offset).expect("valid access address cannot have a negative offset");
 
+        if let ir::MemoryTypeData::Struct { fields, .. } = &self.function.memory_types[ty] {
+            let field = fields
+                .iter()
+                .find(|field| field.offset == offset)
+                .ok_or(PccError::InvalidFieldOffset)?;
+            if field.ty != access_ty {
+                bail!(BadFieldType);
+            }
+            Ok(Some(field))
+        } else {
+            // Access to valid memory, but not a struct: no facts can be attached to the result.
+            Ok(None)
+        }
+    }
+
+    /// Check a load, and determine what fact, if any, the result of the load might have.
+    pub fn load<'b>(&'b self, fact: &Fact, access_ty: ir::Type) -> PccResult<Option<&'b Fact>> {
+        Ok(self
+            .struct_field(fact, access_ty)?
+            .and_then(|field| field.fact())
+            .or_else(|| Fact::infer_from_type(access_ty)))
+    }
+
+    /// Check a store.
+    pub fn store(
+        &self,
+        fact: &Fact,
+        access_ty: ir::Type,
+        data_fact: Option<&Fact>,
+    ) -> PccResult<()> {
+        if let Some(field) = self.struct_field(fact, access_ty)? {
+            // If it's a read-only field, disallow.
+            if field.readonly {
+                bail!(WriteToReadOnlyField);
+            }
+            // Check that the fact on the stored data subsumes the field's fact.
+            if !self.subsumes_fact_optionals(data_fact, field.fact()) {
+                bail!(InvalidStoredFact);
+            }
+        }
         Ok(())
     }
 }
@@ -391,15 +528,39 @@ pub fn check_vcode_facts<B: LowerBackend + TargetIsa>(
     vcode: &VCode<B::MInst>,
     backend: &B,
 ) -> PccResult<()> {
-    for inst in 0..vcode.num_insts() {
-        let inst = InsnIndex::new(inst);
-        if vcode.inst_defines_facts(inst) || vcode[inst].is_mem_access() {
-            // This instruction defines a register with a new fact, or
-            // has some side-effect we want to be careful to
-            // verify. We'll call into the backend to validate this
-            // fact with respect to the instruction and the input
-            // facts.
-            backend.check_fact(&vcode[inst], f, vcode)?;
+    let ctx = FactContext::new(f, backend.triple().pointer_width().unwrap().bits().into());
+
+    // Check that individual instructions are valid according to input
+    // facts, and support the stated output facts.
+    for block in 0..vcode.num_blocks() {
+        let block = BlockIndex::new(block);
+        for inst in vcode.block_insns(block).iter() {
+            if vcode.inst_defines_facts(inst) || vcode[inst].is_mem_access() {
+                // This instruction defines a register with a new fact, or
+                // has some side-effect we want to be careful to
+                // verify. We'll call into the backend to validate this
+                // fact with respect to the instruction and the input
+                // facts.
+                backend.check_fact(&ctx, vcode, &vcode[inst])?;
+            }
+
+            // If this is a branch, check that all block arguments subsume
+            // the assumed facts on the blockparams of successors.
+            if vcode.is_branch(inst) {
+                for (succ_idx, succ) in vcode.block_succs(block).iter().enumerate() {
+                    for (arg, param) in vcode
+                        .branch_blockparams(block, inst, succ_idx)
+                        .iter()
+                        .zip(vcode.block_params(*succ).iter())
+                    {
+                        let arg_fact = vcode.vreg_fact(*arg);
+                        let param_fact = vcode.vreg_fact(*param);
+                        if !ctx.subsumes_fact_optionals(arg_fact, param_fact) {
+                            return Err(PccError::UnsupportedBlockparam);
+                        }
+                    }
+                }
+            }
         }
     }
     Ok(())
diff --git a/cranelift/codegen/src/isa/aarch64/inst/args.rs b/cranelift/codegen/src/isa/aarch64/inst/args.rs
index 9abb50150..c6394e659 100644
--- a/cranelift/codegen/src/isa/aarch64/inst/args.rs
+++ b/cranelift/codegen/src/isa/aarch64/inst/args.rs
@@ -647,6 +647,17 @@ impl ScalarSize {
             ScalarSize::Size128 => ScalarSize::Size64,
         }
     }
+
+    /// Return a type with the same size as this scalar.
+    pub fn ty(&self) -> Type {
+        match self {
+            ScalarSize::Size8 => I8,
+            ScalarSize::Size16 => I16,
+            ScalarSize::Size32 => I32,
+            ScalarSize::Size64 => I64,
+            ScalarSize::Size128 => I128,
+        }
+    }
 }
 
 /// Type used to communicate the size of a vector operand.
diff --git a/cranelift/codegen/src/isa/aarch64/lower.rs b/cranelift/codegen/src/isa/aarch64/lower.rs
index f3bbfd1e1..9a85e8fa1 100644
--- a/cranelift/codegen/src/isa/aarch64/lower.rs
+++ b/cranelift/codegen/src/isa/aarch64/lower.rs
@@ -8,9 +8,9 @@
 //! - Floating-point immediates (FIMM instruction).
 
 use crate::ir::condcodes::{FloatCC, IntCC};
-use crate::ir::pcc::PccResult;
+use crate::ir::pcc::{FactContext, PccResult};
 use crate::ir::Inst as IRInst;
-use crate::ir::{Function, Opcode, Value};
+use crate::ir::{Opcode, Value};
 use crate::isa::aarch64::inst::*;
 use crate::isa::aarch64::pcc;
 use crate::isa::aarch64::AArch64Backend;
@@ -133,10 +133,10 @@ impl LowerBackend for AArch64Backend {
 
     fn check_fact(
         &self,
-        inst: &Self::MInst,
-        function: &Function,
+        ctx: &FactContext<'_>,
         vcode: &VCode<Self::MInst>,
+        inst: &Self::MInst,
     ) -> PccResult<()> {
-        pcc::check(inst, function, vcode)
+        pcc::check(ctx, vcode, inst)
     }
 }
diff --git a/cranelift/codegen/src/isa/aarch64/pcc.rs b/cranelift/codegen/src/isa/aarch64/pcc.rs
index 81d330884..dea2515b3 100644
--- a/cranelift/codegen/src/isa/aarch64/pcc.rs
+++ b/cranelift/codegen/src/isa/aarch64/pcc.rs
@@ -1,7 +1,8 @@
 //! Proof-carrying code checking for AArch64 VCode.
 
 use crate::ir::pcc::*;
-use crate::ir::{Function, MemFlags};
+use crate::ir::types::*;
+use crate::ir::MemFlags;
 use crate::isa::aarch64::inst::args::{PairAMode, ShiftOp};
 use crate::isa::aarch64::inst::ALUOp;
 use crate::isa::aarch64::inst::Inst;
@@ -68,10 +69,7 @@ fn check_output<F: Fn() -> PccResult<Fact>>(
     }
 }
 
-pub(crate) fn check(inst: &Inst, function: &Function, vcode: &VCode<Inst>) -> PccResult<()> {
-    // Create a new fact context with the machine's pointer width.
-    let ctx = FactContext::new(&function.memory_types, 64);
-
+pub(crate) fn check(ctx: &FactContext, vcode: &VCode<Inst>, inst: &Inst) -> PccResult<()> {
     trace!("Checking facts on inst: {:?}", inst);
 
     match inst {
@@ -81,60 +79,50 @@ pub(crate) fn check(inst: &Inst, function: &Function, vcode: &VCode<Inst>) -> Pc
             // facts given to us in the CLIF should still be true.
             Ok(())
         }
-        Inst::ULoad8 { mem, flags, .. } | Inst::SLoad8 { mem, flags, .. } => {
-            check_addr(&ctx, *flags, mem, vcode, 1)
+        Inst::ULoad8 { rd, mem, flags } | Inst::SLoad8 { rd, mem, flags } => {
+            check_load(&ctx, Some(rd.to_reg()), *flags, mem, vcode, I8)
+        }
+        Inst::ULoad16 { rd, mem, flags } | Inst::SLoad16 { rd, mem, flags } => {
+            check_load(&ctx, Some(rd.to_reg()), *flags, mem, vcode, I16)
         }
-        Inst::ULoad16 { mem, flags, .. } | Inst::SLoad16 { mem, flags, .. } => {
-            check_addr(&ctx, *flags, mem, vcode, 2)
+        Inst::ULoad32 { rd, mem, flags } | Inst::SLoad32 { rd, mem, flags } => {
+            check_load(&ctx, Some(rd.to_reg()), *flags, mem, vcode, I32)
         }
-        Inst::ULoad32 { mem, flags, .. } | Inst::SLoad32 { mem, flags, .. } => {
-            check_addr(&ctx, *flags, mem, vcode, 4)
+        Inst::ULoad64 { rd, mem, flags } => {
+            check_load(&ctx, Some(rd.to_reg()), *flags, mem, vcode, I64)
         }
-        Inst::ULoad64 { mem, flags, .. } => check_addr(&ctx, *flags, mem, vcode, 8),
-        Inst::FpuLoad32 { mem, flags, .. } => check_addr(&ctx, *flags, mem, vcode, 4),
-        Inst::FpuLoad64 { mem, flags, .. } => check_addr(&ctx, *flags, mem, vcode, 8),
-        Inst::FpuLoad128 { mem, flags, .. } => check_addr(&ctx, *flags, mem, vcode, 16),
-        Inst::LoadP64 { mem, flags, .. } => check_addr_pair(&ctx, *flags, mem, vcode, 16),
-        Inst::FpuLoadP64 { mem, flags, .. } => check_addr_pair(&ctx, *flags, mem, vcode, 16),
-        Inst::FpuLoadP128 { mem, flags, .. } => check_addr_pair(&ctx, *flags, mem, vcode, 32),
-        Inst::VecLoadReplicate { rn, flags, .. } => check_scalar_addr(&ctx, *flags, *rn, vcode, 16),
+        Inst::FpuLoad32 { mem, flags, .. } => check_load(&ctx, None, *flags, mem, vcode, F32),
+        Inst::FpuLoad64 { mem, flags, .. } => check_load(&ctx, None, *flags, mem, vcode, F64),
+        Inst::FpuLoad128 { mem, flags, .. } => check_load(&ctx, None, *flags, mem, vcode, I8X16),
+        Inst::LoadP64 { mem, flags, .. } => check_load_pair(&ctx, *flags, mem, vcode, 16),
+        Inst::FpuLoadP64 { mem, flags, .. } => check_load_pair(&ctx, *flags, mem, vcode, 16),
+        Inst::FpuLoadP128 { mem, flags, .. } => check_load_pair(&ctx, *flags, mem, vcode, 32),
+        Inst::VecLoadReplicate {
+            rn, flags, size, ..
+        } => check_load_addr(&ctx, *flags, *rn, vcode, size.lane_size().ty()),
         Inst::LoadAcquire {
             access_ty,
             rn,
             flags,
             ..
-        } => check_scalar_addr(
-            &ctx,
-            *flags,
-            *rn,
-            vcode,
-            u8::try_from(access_ty.bytes()).unwrap(),
-        ),
+        } => check_load_addr(&ctx, *flags, *rn, vcode, *access_ty),
 
-        // TODO: stores: once we have memcaps, check that we aren't
-        // overwriting a field that has a pointee type.
-        Inst::Store8 { mem, flags, .. } => check_addr(&ctx, *flags, mem, vcode, 1),
-        Inst::Store16 { mem, flags, .. } => check_addr(&ctx, *flags, mem, vcode, 2),
-        Inst::Store32 { mem, flags, .. } => check_addr(&ctx, *flags, mem, vcode, 4),
-        Inst::Store64 { mem, flags, .. } => check_addr(&ctx, *flags, mem, vcode, 8),
-        Inst::FpuStore32 { mem, flags, .. } => check_addr(&ctx, *flags, mem, vcode, 4),
-        Inst::FpuStore64 { mem, flags, .. } => check_addr(&ctx, *flags, mem, vcode, 8),
-        Inst::FpuStore128 { mem, flags, .. } => check_addr(&ctx, *flags, mem, vcode, 16),
-        Inst::StoreP64 { mem, flags, .. } => check_addr_pair(&ctx, *flags, mem, vcode, 16),
-        Inst::FpuStoreP64 { mem, flags, .. } => check_addr_pair(&ctx, *flags, mem, vcode, 16),
-        Inst::FpuStoreP128 { mem, flags, .. } => check_addr_pair(&ctx, *flags, mem, vcode, 32),
+        Inst::Store8 { rd, mem, flags } => check_store(&ctx, Some(*rd), *flags, mem, vcode, I8),
+        Inst::Store16 { rd, mem, flags } => check_store(&ctx, Some(*rd), *flags, mem, vcode, I16),
+        Inst::Store32 { rd, mem, flags } => check_store(&ctx, Some(*rd), *flags, mem, vcode, I32),
+        Inst::Store64 { rd, mem, flags } => check_store(&ctx, Some(*rd), *flags, mem, vcode, I64),
+        Inst::FpuStore32 { mem, flags, .. } => check_store(&ctx, None, *flags, mem, vcode, F32),
+        Inst::FpuStore64 { mem, flags, .. } => check_store(&ctx, None, *flags, mem, vcode, F64),
+        Inst::FpuStore128 { mem, flags, .. } => check_store(&ctx, None, *flags, mem, vcode, I8X16),
+        Inst::StoreP64 { mem, flags, .. } => check_store_pair(&ctx, *flags, mem, vcode, 16),
+        Inst::FpuStoreP64 { mem, flags, .. } => check_store_pair(&ctx, *flags, mem, vcode, 16),
+        Inst::FpuStoreP128 { mem, flags, .. } => check_store_pair(&ctx, *flags, mem, vcode, 32),
         Inst::StoreRelease {
             access_ty,
             rn,
             flags,
             ..
-        } => check_scalar_addr(
-            &ctx,
-            *flags,
-            *rn,
-            vcode,
-            u8::try_from(access_ty.bytes()).unwrap(),
-        ),
+        } => check_store_addr(&ctx, *flags, *rn, vcode, *access_ty),
 
         Inst::AluRRR {
             alu_op: ALUOp::Add,
@@ -241,12 +229,63 @@ pub(crate) fn check(inst: &Inst, function: &Function, vcode: &VCode<Inst>) -> Pc
     }
 }
 
-fn check_addr(
+/// The operation we're checking against an amode: either
+///
+/// - a *load*, and we need to validate that the field's fact subsumes
+///   the load result's fact, OR
+///
+/// - a *store*, and we need to validate that the stored data's fact
+///   subsumes the field's fact.
+enum LoadOrStore<'a> {
+    Load { result_fact: Option<&'a Fact> },
+    Store { stored_fact: Option<&'a Fact> },
+}
+
+fn check_load(
+    ctx: &FactContext,
+    rd: Option<Reg>,
+    flags: MemFlags,
+    addr: &AMode,
+    vcode: &VCode<Inst>,
+    ty: Type,
+) -> PccResult<()> {
+    let result_fact = rd.map(|rd| get_fact(vcode, rd)).transpose()?;
+    check_addr(
+        ctx,
+        flags,
+        addr,
+        vcode,
+        ty,
+        LoadOrStore::Load { result_fact },
+    )
+}
+
+fn check_store(
     ctx: &FactContext,
+    rd: Option<Reg>,
     flags: MemFlags,
     addr: &AMode,
     vcode: &VCode<Inst>,
-    size: u8,
+    ty: Type,
+) -> PccResult<()> {
+    let stored_fact = rd.map(|rd| get_fact(vcode, rd)).transpose()?;
+    check_addr(
+        ctx,
+        flags,
+        addr,
+        vcode,
+        ty,
+        LoadOrStore::Store { stored_fact },
+    )
+}
+
+fn check_addr<'a>(
+    ctx: &FactContext,
+    flags: MemFlags,
+    addr: &AMode,
+    vcode: &VCode<Inst>,
+    ty: Type,
+    op: LoadOrStore<'a>,
 ) -> PccResult<()> {
     if !flags.checked() {
         return Ok(());
@@ -254,19 +293,36 @@ fn check_addr(
 
     trace!("check_addr: {:?}", addr);
 
+    let check = |addr: &Fact, ty: Type| -> PccResult<()> {
+        match op {
+            LoadOrStore::Load { result_fact } => {
+                let loaded_fact = ctx.load(addr, ty)?;
+                trace!(
+                    "checking a load: loaded_fact = {loaded_fact:?} result_fact = {result_fact:?}"
+                );
+                if ctx.subsumes_fact_optionals(loaded_fact, result_fact) {
+                    Ok(())
+                } else {
+                    Err(PccError::UnsupportedFact)
+                }
+            }
+            LoadOrStore::Store { stored_fact } => ctx.store(addr, ty, stored_fact),
+        }
+    };
+
     match addr {
         &AMode::RegReg { rn, rm } => {
             let rn = get_fact(vcode, rn)?;
             let rm = get_fact(vcode, rm)?;
             let sum = fail_if_missing(ctx.add(&rn, &rm, 64))?;
-            ctx.check_address(&sum, size.into())
+            check(&sum, ty)
         }
         &AMode::RegScaled { rn, rm, ty } => {
             let rn = get_fact(vcode, rn)?;
             let rm = get_fact(vcode, rm)?;
             let rm_scaled = fail_if_missing(ctx.scale(&rm, 64, ty.bytes()))?;
             let sum = fail_if_missing(ctx.add(&rn, &rm_scaled, 64))?;
-            ctx.check_address(&sum, size.into())
+            check(&sum, ty)
         }
         &AMode::RegScaledExtended {
             rn,
@@ -279,19 +335,22 @@ fn check_addr(
             let rm_extended = fail_if_missing(extend_fact(ctx, rm, extendop))?;
             let rm_scaled = fail_if_missing(ctx.scale(&rm_extended, 64, ty.bytes()))?;
             let sum = fail_if_missing(ctx.add(&rn, &rm_scaled, 64))?;
-            ctx.check_address(&sum, size.into())
+            check(&sum, ty)
         }
         &AMode::RegExtended { rn, rm, extendop } => {
             let rn = get_fact(vcode, rn)?;
             let rm = get_fact(vcode, rm)?;
             let rm_extended = fail_if_missing(extend_fact(ctx, rm, extendop))?;
             let sum = fail_if_missing(ctx.add(&rn, &rm_extended, 64))?;
-            ctx.check_address(&sum, size.into())
+            trace!("rn = {rn:?} rm = {rm:?} rm_extended = {rm_extended:?} sum = {sum:?}");
+            check(&sum, ty)?;
+            trace!(" -> checks out!");
+            Ok(())
         }
         &AMode::Unscaled { rn, simm9 } => {
             let rn = get_fact(vcode, rn)?;
             let sum = fail_if_missing(ctx.offset(&rn, 64, simm9.value.into()))?;
-            ctx.check_address(&sum, size.into())
+            check(&sum, ty)
         }
         &AMode::UnsignedOffset { rn, uimm12 } => {
             let rn = get_fact(vcode, rn)?;
@@ -299,12 +358,12 @@ fn check_addr(
             // most 32 or 64 for large vector ops, and the `uimm12`'s
             // value is at most 4095.
             let uimm12: u64 = uimm12.value.into();
-            let offset: u64 = uimm12.checked_mul(size.into()).unwrap();
+            let offset: u64 = uimm12.checked_mul(ty.bytes().into()).unwrap();
             // This `unwrap()` will always succeed because the value
             // will always be positive and much smaller than
             // `i64::MAX` (see above).
             let sum = fail_if_missing(ctx.offset(&rn, 64, i64::try_from(offset).unwrap()))?;
-            ctx.check_address(&sum, size.into())
+            check(&sum, ty)
         }
         &AMode::Label { .. } | &AMode::Const { .. } => {
             // Always accept: labels and constants must be within the
@@ -314,7 +373,7 @@ fn check_addr(
         &AMode::RegOffset { rn, off, .. } => {
             let rn = get_fact(vcode, rn)?;
             let sum = fail_if_missing(ctx.offset(&rn, 64, off))?;
-            ctx.check_address(&sum, size.into())
+            check(&sum, ty)
         }
         &AMode::SPOffset { .. }
         | &AMode::FPOffset { .. }
@@ -328,7 +387,7 @@ fn check_addr(
     }
 }
 
-fn check_addr_pair(
+fn check_load_pair(
     _ctx: &FactContext,
     _flags: MemFlags,
     _addr: &PairAMode,
@@ -338,16 +397,42 @@ fn check_addr_pair(
     Err(PccError::UnimplementedInst)
 }
 
-fn check_scalar_addr(
+fn check_store_pair(
+    _ctx: &FactContext,
+    _flags: MemFlags,
+    _addr: &PairAMode,
+    _vcode: &VCode<Inst>,
+    _size: u8,
+) -> PccResult<()> {
+    Err(PccError::UnimplementedInst)
+}
+
+fn check_load_addr(
+    ctx: &FactContext,
+    flags: MemFlags,
+    reg: Reg,
+    vcode: &VCode<Inst>,
+    ty: Type,
+) -> PccResult<()> {
+    if !flags.checked() {
+        return Ok(());
+    }
+    let fact = get_fact(vcode, reg)?;
+    let _output_fact = ctx.load(fact, ty)?;
+    Ok(())
+}
+
+fn check_store_addr(
     ctx: &FactContext,
     flags: MemFlags,
     reg: Reg,
     vcode: &VCode<Inst>,
-    size: u8,
+    ty: Type,
 ) -> PccResult<()> {
     if !flags.checked() {
         return Ok(());
     }
     let fact = get_fact(vcode, reg)?;
-    ctx.check_address(fact, size.into())
+    let _output_fact = ctx.store(fact, ty, None)?;
+    Ok(())
 }
diff --git a/cranelift/codegen/src/machinst/lower.rs b/cranelift/codegen/src/machinst/lower.rs
index 4b360fd96..9268a5c5a 100644
--- a/cranelift/codegen/src/machinst/lower.rs
+++ b/cranelift/codegen/src/machinst/lower.rs
@@ -8,7 +8,7 @@
 use crate::entity::SecondaryMap;
 use crate::fx::{FxHashMap, FxHashSet};
 use crate::inst_predicates::{has_lowering_side_effect, is_constant_64bit};
-use crate::ir::pcc::{PccError, PccResult};
+use crate::ir::pcc::{FactContext, PccError, PccResult};
 use crate::ir::{
     ArgumentPurpose, Block, Constant, ConstantData, DataFlowGraph, ExternalName, Function,
     GlobalValue, GlobalValueData, Immediate, Inst, InstructionData, MemFlags, RelSourceLoc, Type,
@@ -151,9 +151,9 @@ pub trait LowerBackend {
     /// on VRegs.
     fn check_fact(
         &self,
-        _inst: &Self::MInst,
-        _function: &Function,
+        _ctx: &FactContext<'_>,
         _vcode: &VCode<Self::MInst>,
+        _inst: &Self::MInst,
     ) -> PccResult<()> {
         Err(PccError::UnimplementedBackend)
     }
diff --git a/cranelift/codegen/src/machinst/vcode.rs b/cranelift/codegen/src/machinst/vcode.rs
index aa88e19c6..010452519 100644
--- a/cranelift/codegen/src/machinst/vcode.rs
+++ b/cranelift/codegen/src/machinst/vcode.rs
@@ -1290,7 +1290,13 @@ impl<I: VCodeInst> VCode<I> {
     /// Get the fact, if any, for a given VReg.
     pub fn vreg_fact(&self, vreg: VReg) -> Option<&Fact> {
         let vreg = self.resolve_vreg_alias(vreg);
-        self.facts[vreg.vreg()].as_ref()
+        match &self.facts[vreg.vreg()] {
+            // The vreg has a stated fact -- return that.
+            Some(fact) => Some(fact),
+            // The vreg has no fact, but maybe we can infer a minimal
+            // one (e.g., an integer range) from the type.
+            None => Fact::infer_from_type(self.vreg_types[vreg.vreg()]),
+        }
     }
 
     /// Does a given instruction define any facts?
@@ -1299,7 +1305,7 @@ impl<I: VCodeInst> VCode<I> {
             .iter()
             .filter(|o| o.kind() == OperandKind::Def)
             .map(|o| o.vreg())
-            .any(|vreg| self.vreg_fact(vreg).is_some())
+            .any(|vreg| self.facts[vreg.vreg()].is_some())
     }
 }
 
diff --git a/crates/c-api/include/wasmtime/async.h b/crates/c-api/include/wasmtime/async.h
index 3919dc3f1..955944c72 100644
--- a/crates/c-api/include/wasmtime/async.h
+++ b/crates/c-api/include/wasmtime/async.h
@@ -353,7 +353,7 @@ typedef struct {
  */
 WASM_API_EXTERN void wasmtime_config_host_stack_creator_set(
     wasm_config_t*,
-    wasmtime_memory_creator_t*);
+    wasmtime_stack_creator_t*);
 
 
 #ifdef __cplusplus
diff --git a/crates/c-api/include/wasmtime/config.h b/crates/c-api/include/wasmtime/config.h
index 38f1edc1b..38429f84b 100644
--- a/crates/c-api/include/wasmtime/config.h
+++ b/crates/c-api/include/wasmtime/config.h
@@ -460,6 +460,22 @@ WASM_API_EXTERN void wasmtime_config_host_memory_creator_set(
     wasm_config_t*,
     wasmtime_memory_creator_t*);
 
+/**
+ * \brief Configures whether copy-on-write memory-mapped data is used to initialize a linear memory.
+ *
+ * Initializing linear memory via a copy-on-write mapping can drastically improve instantiation costs 
+ * of a WebAssembly module because copying memory is deferred. Additionally if a page of memory is 
+ * only ever read from WebAssembly and never written too then the same underlying page of data will
+ * be reused between all instantiations of a module meaning that if a module is instantiated many
+ * times this can lower the overall memory required needed to run that module.
+ *
+ * This option defaults to true.
+ *
+ * For more information see the Rust documentation at
+ * https://docs.wasmtime.dev/api/wasmtime/struct.Config.html#method.memory_init_cow
+ */
+WASMTIME_CONFIG_PROP(void, memory_init_cow, bool)
+
 #ifdef __cplusplus
 }  // extern "C"
 #endif
diff --git a/crates/c-api/src/config.rs b/crates/c-api/src/config.rs
index bcf8acde6..201e05196 100644
--- a/crates/c-api/src/config.rs
+++ b/crates/c-api/src/config.rs
@@ -424,3 +424,8 @@ pub unsafe extern "C" fn wasmtime_config_host_memory_creator_set(
         new_memory: creator.new_memory,
     }));
 }
+
+#[no_mangle]
+pub extern "C" fn wasmtime_config_memory_init_cow_set(c: &mut wasm_config_t, enable: bool) {
+    c.config.memory_init_cow(enable);
+}
diff --git a/crates/wasi-http/wit/command-extended.wit b/crates/wasi-http/wit/command-extended.wit
index b4d08aed3..37e4fcb92 100644
--- a/crates/wasi-http/wit/command-extended.wit
+++ b/crates/wasi-http/wit/command-extended.wit
@@ -1,37 +1,6 @@
 // All of the same imports and exports available in the wasi:cli/command world
 // with addition of HTTP proxy related imports:
 world command-extended {
-  import wasi:clocks/wall-clock;
-  import wasi:clocks/monotonic-clock;
-  import wasi:clocks/timezone;
-  import wasi:filesystem/types;
-  import wasi:filesystem/preopens;
-  import wasi:sockets/instance-network;
-  import wasi:sockets/ip-name-lookup;
-  import wasi:sockets/network;
-  import wasi:sockets/tcp-create-socket;
-  import wasi:sockets/tcp;
-  import wasi:sockets/udp-create-socket;
-  import wasi:sockets/udp;
-  import wasi:random/random;
-  import wasi:random/insecure;
-  import wasi:random/insecure-seed;
-  import wasi:io/poll;
-  import wasi:io/streams;
-  import wasi:cli/environment;
-  import wasi:cli/exit;
-  import wasi:cli/stdin;
-  import wasi:cli/stdout;
-  import wasi:cli/stderr;
-  import wasi:cli/terminal-input;
-  import wasi:cli/terminal-output;
-  import wasi:cli/terminal-stdin;
-  import wasi:cli/terminal-stdout;
-  import wasi:cli/terminal-stderr;
-
-  // We should replace all others with `include self.command`
-  // as soon as the unioning of worlds is available:
-  // https://github.com/WebAssembly/component-model/issues/169
-  import wasi:logging/logging;
+  include wasi:cli/command;
   import wasi:http/outgoing-handler;
 }
diff --git a/crates/wasi-http/wit/deps/logging/logging.wit b/crates/wasi-http/wit/deps/logging/logging.wit
deleted file mode 100644
index da537e479..000000000
--- a/crates/wasi-http/wit/deps/logging/logging.wit
+++ /dev/null
@@ -1,37 +0,0 @@
-package wasi:logging;
-
-/// WASI Logging is a logging API intended to let users emit log messages with
-/// simple priority levels and context values.
-interface logging {
-    /// A log level, describing a kind of message.
-    enum level {
-       /// Describes messages about the values of variables and the flow of
-       /// control within a program.
-       trace,
-
-       /// Describes messages likely to be of interest to someone debugging a
-       /// program.
-       debug,
-
-       /// Describes messages likely to be of interest to someone monitoring a
-       /// program.
-       info,
-
-       /// Describes messages indicating hazardous situations.
-       warn,
-
-       /// Describes messages indicating serious errors.
-       error,
-
-       /// Describes messages indicating fatal errors.
-       critical,
-    }
-
-    /// Emit a log message.
-    ///
-    /// A log message has a `level` describing what kind of message is being
-    /// sent, a context, which is an uninterpreted string meant to help
-    /// consumers group similar messages, and a string containing the message
-    /// text.
-    log: func(level: level, context: string, message: string);
-}
diff --git a/crates/wasi-http/wit/deps/logging/world.wit b/crates/wasi-http/wit/deps/logging/world.wit
deleted file mode 100644
index ede628643..000000000
--- a/crates/wasi-http/wit/deps/logging/world.wit
+++ /dev/null
@@ -1,5 +0,0 @@
-package wasi:logging;
-
-world imports {
-    import logging;
-}
diff --git a/crates/wasi/wit/command-extended.wit b/crates/wasi/wit/command-extended.wit
index b4d08aed3..37e4fcb92 100644
--- a/crates/wasi/wit/command-extended.wit
+++ b/crates/wasi/wit/command-extended.wit
@@ -1,37 +1,6 @@
 // All of the same imports and exports available in the wasi:cli/command world
 // with addition of HTTP proxy related imports:
 world command-extended {
-  import wasi:clocks/wall-clock;
-  import wasi:clocks/monotonic-clock;
-  import wasi:clocks/timezone;
-  import wasi:filesystem/types;
-  import wasi:filesystem/preopens;
-  import wasi:sockets/instance-network;
-  import wasi:sockets/ip-name-lookup;
-  import wasi:sockets/network;
-  import wasi:sockets/tcp-create-socket;
-  import wasi:sockets/tcp;
-  import wasi:sockets/udp-create-socket;
-  import wasi:sockets/udp;
-  import wasi:random/random;
-  import wasi:random/insecure;
-  import wasi:random/insecure-seed;
-  import wasi:io/poll;
-  import wasi:io/streams;
-  import wasi:cli/environment;
-  import wasi:cli/exit;
-  import wasi:cli/stdin;
-  import wasi:cli/stdout;
-  import wasi:cli/stderr;
-  import wasi:cli/terminal-input;
-  import wasi:cli/terminal-output;
-  import wasi:cli/terminal-stdin;
-  import wasi:cli/terminal-stdout;
-  import wasi:cli/terminal-stderr;
-
-  // We should replace all others with `include self.command`
-  // as soon as the unioning of worlds is available:
-  // https://github.com/WebAssembly/component-model/issues/169
-  import wasi:logging/logging;
+  include wasi:cli/command;
   import wasi:http/outgoing-handler;
 }
diff --git a/crates/wasi/wit/deps/logging/logging.wit b/crates/wasi/wit/deps/logging/logging.wit
deleted file mode 100644
index da537e479..000000000
--- a/crates/wasi/wit/deps/logging/logging.wit
+++ /dev/null
@@ -1,37 +0,0 @@
-package wasi:logging;
-
-/// WASI Logging is a logging API intended to let users emit log messages with
-/// simple priority levels and context values.
-interface logging {
-    /// A log level, describing a kind of message.
-    enum level {
-       /// Describes messages about the values of variables and the flow of
-       /// control within a program.
-       trace,
-
-       /// Describes messages likely to be of interest to someone debugging a
-       /// program.
-       debug,
-
-       /// Describes messages likely to be of interest to someone monitoring a
-       /// program.
-       info,
-
-       /// Describes messages indicating hazardous situations.
-       warn,
-
-       /// Describes messages indicating serious errors.
-       error,
-
-       /// Describes messages indicating fatal errors.
-       critical,
-    }
-
-    /// Emit a log message.
-    ///
-    /// A log message has a `level` describing what kind of message is being
-    /// sent, a context, which is an uninterpreted string meant to help
-    /// consumers group similar messages, and a string containing the message
-    /// text.
-    log: func(level: level, context: string, message: string);
-}
diff --git a/crates/wasi/wit/deps/logging/world.wit b/crates/wasi/wit/deps/logging/world.wit
deleted file mode 100644
index ede628643..000000000
--- a/crates/wasi/wit/deps/logging/world.wit
+++ /dev/null
@@ -1,5 +0,0 @@
-package wasi:logging;
-
-world imports {
-    import logging;
-}


Test patch:
diff --git a/cranelift/filetests/filetests/pcc/fail/blockparams.clif b/cranelift/filetests/filetests/pcc/fail/blockparams.clif
new file mode 100644
index 000000000..29863e06b
--- /dev/null
+++ b/cranelift/filetests/filetests/pcc/fail/blockparams.clif
@@ -0,0 +1,19 @@
+test compile expect-fail
+set enable_pcc=true
+target aarch64
+
+function %f0(i64, i32) -> i64 {
+block0(v0 ! max(64, 0x100): i64, v1: i32):
+    v2 ! max(64, 0x100) = iconst.i64 0x100
+    v3 ! max(64, 0x200) = iadd v0, v2
+    brif v1, block1(v0), block2(v3)
+
+block1(v4 ! max(64, 0xff): i64):  ;; shrink the range -- should be caught
+    jump block3(v4)
+
+block2(v5 ! max(64, 0x1ff): i64):
+    jump block3(v5)
+
+block3(v6 ! max(64, 1): i64):
+    return v6
+}
diff --git a/cranelift/filetests/filetests/pcc/fail/struct.clif b/cranelift/filetests/filetests/pcc/fail/struct.clif
new file mode 100644
index 000000000..22e455bea
--- /dev/null
+++ b/cranelift/filetests/filetests/pcc/fail/struct.clif
@@ -0,0 +1,19 @@
+test compile expect-fail
+set enable_pcc=true
+target aarch64
+
+function %f0(i64) -> i64 {
+    mt0 = struct 8 { 0: i64 ! mem(mt1, 0) }
+    mt1 = memory 0x1_0000_0000
+block0(v0 ! mem(mt0, 0): i64):
+    v1 ! mem(mt1, 8) = load.i64 checked v0
+    return v1
+}
+
+function %f1(i64, i64) {
+    mt0 = struct 8 { 0: i64 ! mem(mt1, 0) }
+    mt1 = memory 0x1_0000_0000
+block0(v0 ! mem(mt0, 0): i64, v1 ! mem(mt1, 8): i64):
+    store.i64 checked v1, v0
+    return
+}
diff --git a/cranelift/filetests/filetests/pcc/fail/vmctx.clif b/cranelift/filetests/filetests/pcc/fail/vmctx.clif
new file mode 100644
index 000000000..f29e17344
--- /dev/null
+++ b/cranelift/filetests/filetests/pcc/fail/vmctx.clif
@@ -0,0 +1,37 @@
+test compile expect-fail
+set enable_pcc=true
+target aarch64
+
+;; Equivalent to a Wasm `i64.load` from a static memory.
+function %f0(i64, i32) -> i64 {
+    ;; mock vmctx struct:
+    mt0 = struct 8 { 0: i64 readonly ! mem(mt1, 0) }
+    ;; mock static memory: 4GiB range, *but insufficient guard*
+    mt1 = memory 0x1_0000_0000
+
+block0(v0 ! mem(mt0, 0): i64, v1: i32):
+    ;; Compute the address: base + offset. Guard region (2GiB) is
+    ;; sufficient for an 8-byte I64 load.
+    v2 ! mem(mt1, 0) = load.i64 checked v0+0    ;; base pointer
+    v3 ! max(64, 0xffff_ffff) = uextend.i64 v1  ;; offset
+    v4 ! mem(mt1, 0xffff_ffff) = iadd.i64 v2, v3
+    v5 = load.i64 checked v4
+    return v5
+}
+
+;; Equivalent to a Wasm `i64.load` from a static memory.
+function %f1(i64, i32) -> i64 {
+    ;; mock vmctx struct:
+    mt0 = struct 16 { 0: i64 readonly ! mem(mt1, 0), 8: i64 readonly }
+    ;; mock static memory: 4GiB range, *but insufficient guard*
+    mt1 = memory 0x1_8000_0000
+
+block0(v0 ! mem(mt0, 0): i64, v1: i32):
+    ;; Compute the address: base + offset. Guard region (2GiB) is
+    ;; sufficient for an 8-byte I64 load.
+    v2 ! mem(mt1, 0) = load.i64 checked v0+8    ;; base pointer, but the wrong one
+    v3 ! max(64, 0xffff_ffff) = uextend.i64 v1  ;; offset
+    v4 ! mem(mt1, 0xffff_ffff) = iadd.i64 v2, v3
+    v5 = load.i64 checked v4
+    return v5
+}
diff --git a/cranelift/filetests/filetests/pcc/succeed/blockparams.clif b/cranelift/filetests/filetests/pcc/succeed/blockparams.clif
new file mode 100644
index 000000000..092c1838c
--- /dev/null
+++ b/cranelift/filetests/filetests/pcc/succeed/blockparams.clif
@@ -0,0 +1,19 @@
+test compile
+set enable_pcc=true
+target aarch64
+
+function %f0(i64, i32) -> i64 {
+block0(v0 ! max(64, 0x100): i64, v1: i32):
+    v2 ! max(64, 0x100) = iconst.i64 0x100
+    v3 ! max(64, 0x200) = iadd v0, v2
+    brif v1, block1(v0), block2(v3)
+
+block1(v4 ! max(64, 0x1000): i64):  ;; broaden the range -- always allowed
+    jump block3(v4)
+
+block2(v5 ! max(64, 0x2000): i64):
+    jump block3(v5)
+
+block3(v6 ! max(64, 0x2001): i64):
+    return v6
+}
diff --git a/cranelift/filetests/filetests/pcc/succeed/struct.clif b/cranelift/filetests/filetests/pcc/succeed/struct.clif
new file mode 100644
index 000000000..6555b22ad
--- /dev/null
+++ b/cranelift/filetests/filetests/pcc/succeed/struct.clif
@@ -0,0 +1,19 @@
+test compile
+set enable_pcc=true
+target aarch64
+
+function %f0(i64) -> i64 {
+    mt0 = struct 8 { 0: i64 ! mem(mt1, 0) }
+    mt1 = memory 0x1_0000_0000
+block0(v0 ! mem(mt0, 0): i64):
+    v1 ! mem(mt1, 0) = load.i64 checked v0
+    return v1
+}
+
+function %f1(i64, i64) {
+    mt0 = struct 8 { 0: i64 ! mem(mt1, 0) }
+    mt1 = memory 0x1_0000_0000
+block0(v0 ! mem(mt0, 0): i64, v1 ! mem(mt1, 0): i64):
+    store.i64 checked v1, v0
+    return
+}
diff --git a/cranelift/filetests/filetests/pcc/succeed/vmctx.clif b/cranelift/filetests/filetests/pcc/succeed/vmctx.clif
new file mode 100644
index 000000000..974718293
--- /dev/null
+++ b/cranelift/filetests/filetests/pcc/succeed/vmctx.clif
@@ -0,0 +1,20 @@
+test compile
+set enable_pcc=true
+target aarch64
+
+;; Equivalent to a Wasm `i64.load` from a static memory.
+function %f0(i64, i32) -> i64 {
+    ;; mock vmctx struct:
+    mt0 = struct 8 { 0: i64 readonly ! mem(mt1, 0) }
+    ;; mock static memory: 4GiB range, plus 2GiB guard
+    mt1 = memory 0x1_8000_0000
+
+block0(v0 ! mem(mt0, 0): i64, v1: i32):
+    ;; Compute the address: base + offset. Guard region (2GiB) is
+    ;; sufficient for an 8-byte I64 load.
+    v2 ! mem(mt1, 0) = load.i64 checked v0+0    ;; base pointer
+    v3 ! max(64, 0xffff_ffff) = uextend.i64 v1  ;; offset
+    v4 ! mem(mt1, 0xffff_ffff) = iadd.i64 v2, v3
+    v5 = load.i64 checked v4
+    return v5
+}
diff --git a/crates/test-programs/src/bin/cli_panic.rs b/crates/test-programs/src/bin/cli_panic.rs
deleted file mode 100644
index bf9d3b49e..000000000
--- a/crates/test-programs/src/bin/cli_panic.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-fn main() {
-    panic!("idk!!!");
-}
diff --git a/crates/test-programs/src/bin/cli_stream_pollable_lifetimes.rs b/crates/test-programs/src/bin/cli_stream_pollable_lifetimes.rs
deleted file mode 100644
index 11fa0abc4..000000000
--- a/crates/test-programs/src/bin/cli_stream_pollable_lifetimes.rs
+++ /dev/null
@@ -1,29 +0,0 @@
-use test_programs::wasi::cli::environment;
-use test_programs::wasi::cli::stdin;
-use test_programs::wasi::io::poll;
-use test_programs::wasi::io::streams;
-
-fn main() {
-    let args = environment::get_arguments();
-    let args = &args[1..];
-
-    if args == &["correct"] {
-        let stdin: streams::InputStream = stdin::get_stdin();
-        let stdin_pollable = stdin.subscribe();
-        let ready = poll::poll_list(&[&stdin_pollable]);
-        assert_eq!(ready, &[0]);
-        drop(stdin_pollable);
-        drop(stdin);
-    } else if args == &["trap"] {
-        let stdin: streams::InputStream = stdin::get_stdin();
-        let stdin_pollable = stdin.subscribe();
-        let ready = poll::poll_list(&[&stdin_pollable]);
-        assert_eq!(ready, &[0]);
-        drop(stdin);
-        unreachable!(
-            "execution should have trapped in line above when stream dropped before pollable"
-        );
-    } else {
-        panic!("bad value for args: expected `[\"correct\"]` or `[\"trap\"]`, got {args:?}")
-    }
-}
diff --git a/crates/test-programs/src/bin/preview2_stream_pollable_correct.rs b/crates/test-programs/src/bin/preview2_stream_pollable_correct.rs
new file mode 100644
index 000000000..dfb155734
--- /dev/null
+++ b/crates/test-programs/src/bin/preview2_stream_pollable_correct.rs
@@ -0,0 +1,12 @@
+use test_programs::wasi::cli::stdin;
+use test_programs::wasi::io::poll;
+use test_programs::wasi::io::streams;
+
+fn main() {
+    let stdin: streams::InputStream = stdin::get_stdin();
+    let stdin_pollable = stdin.subscribe();
+    let ready = poll::poll_list(&[&stdin_pollable]);
+    assert_eq!(ready, &[0]);
+    drop(stdin_pollable);
+    drop(stdin);
+}
diff --git a/crates/test-programs/src/bin/preview2_stream_pollable_traps.rs b/crates/test-programs/src/bin/preview2_stream_pollable_traps.rs
new file mode 100644
index 000000000..13331fb02
--- /dev/null
+++ b/crates/test-programs/src/bin/preview2_stream_pollable_traps.rs
@@ -0,0 +1,12 @@
+use test_programs::wasi::cli::stdin;
+use test_programs::wasi::io::poll;
+use test_programs::wasi::io::streams;
+
+fn main() {
+    let stdin: streams::InputStream = stdin::get_stdin();
+    let stdin_pollable = stdin.subscribe();
+    let ready = poll::poll_list(&[&stdin_pollable]);
+    assert_eq!(ready, &[0]);
+    drop(stdin);
+    unreachable!("execution should have trapped in line above when stream dropped before pollable");
+}
diff --git a/crates/wasi/tests/all/async_.rs b/crates/wasi/tests/all/async_.rs
index c6e60d1e6..c1813476b 100644
--- a/crates/wasi/tests/all/async_.rs
+++ b/crates/wasi/tests/all/async_.rs
@@ -328,3 +328,19 @@ async fn preview2_udp_sample_application() {
 async fn preview2_tcp_connect() {
     run(PREVIEW2_TCP_CONNECT_COMPONENT, false).await.unwrap()
 }
+#[test_log::test(tokio::test(flavor = "multi_thread"))]
+async fn preview2_stream_pollable_correct() {
+    run(PREVIEW2_STREAM_POLLABLE_CORRECT_COMPONENT, false)
+        .await
+        .unwrap()
+}
+#[test_log::test(tokio::test(flavor = "multi_thread"))]
+async fn preview2_stream_pollable_traps() {
+    let e = run(PREVIEW2_STREAM_POLLABLE_TRAPS_COMPONENT, false)
+        .await
+        .unwrap_err();
+    assert_eq!(
+        format!("{}", e.source().expect("trap source")),
+        "entry still has children"
+    )
+}
diff --git a/crates/wasi/tests/all/sync.rs b/crates/wasi/tests/all/sync.rs
index c7e9da1d7..4347e01d9 100644
--- a/crates/wasi/tests/all/sync.rs
+++ b/crates/wasi/tests/all/sync.rs
@@ -269,3 +269,15 @@ fn preview2_udp_sample_application() {
 fn preview2_tcp_connect() {
     run(PREVIEW2_TCP_CONNECT_COMPONENT, false).unwrap()
 }
+#[test_log::test]
+fn preview2_stream_pollable_correct() {
+    run(PREVIEW2_STREAM_POLLABLE_CORRECT_COMPONENT, false).unwrap()
+}
+#[test_log::test]
+fn preview2_stream_pollable_traps() {
+    let e = run(PREVIEW2_STREAM_POLLABLE_TRAPS_COMPONENT, false).unwrap_err();
+    assert_eq!(
+        format!("{}", e.source().expect("trap source")),
+        "entry still has children"
+    )
+}
diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs
index 93e19b64c..997738cdd 100644
--- a/tests/all/cli_tests.rs
+++ b/tests/all/cli_tests.rs
@@ -1026,29 +1026,6 @@ mod test_programs {
         Ok(())
     }
 
-    #[test]
-    fn cli_panic() -> Result<()> {
-        let output = get_wasmtime_command()?
-            .args(&[
-                "run",
-                "-Wcomponent-model",
-                CLI_PANIC_COMPONENT,
-                "diesel",
-                "the",
-                "cat",
-                "scratched",
-                "me",
-                "real",
-                "good",
-                "yesterday",
-            ])
-            .output()?;
-        assert!(!output.status.success());
-        let stderr = String::from_utf8_lossy(&output.stderr);
-        assert!(stderr.contains("idk!!!"));
-        Ok(())
-    }
-
     #[test]
     fn cli_args() -> Result<()> {
         run_wasmtime(&[
@@ -1182,6 +1159,8 @@ mod test_programs {
             .args(&["run", "-Wcomponent-model", CLI_EXIT_PANIC_COMPONENT])
             .output()?;
         assert!(!output.status.success());
+        let stderr = String::from_utf8_lossy(&output.stderr);
+        assert!(stderr.contains("Curiouser and curiouser!"));
         Ok(())
     }
 
@@ -1221,32 +1200,6 @@ mod test_programs {
         Ok(())
     }
 
-    #[test]
-    fn cli_stream_pollable_lifetimes() -> Result<()> {
-        // Test program has two modes, dispatching based on argument.
-        run_wasmtime(&[
-            "run",
-            "-Wcomponent-model",
-            CLI_STREAM_POLLABLE_LIFETIMES_COMPONENT,
-            "correct",
-        ])?;
-        let output = get_wasmtime_command()?
-            .args(&[
-                "run",
-                "-Wcomponent-model",
-                CLI_STREAM_POLLABLE_LIFETIMES_COMPONENT,
-                "trap",
-            ])
-            .output()?;
-        assert!(!output.status.success());
-        let stderr = String::from_utf8_lossy(&output.stderr);
-        assert!(
-            stderr.contains("entry still has children"),
-            "bad stderr: {stderr}"
-        );
-        Ok(())
-    }
-
     #[test]
     fn run_wasi_http_component() -> Result<()> {
         let output = super::run_wasmtime_for_output(