Repository: bytecodealliance/wasmtime
Pull request #8742 (2024-06-04 19:31:17)

Add a fuzz target for exercising bounds checks with various memory configs

github-actions[bot]: #### Subscribe to Label Action

cc @fitzgen

<details>
This issue or pull request has been labeled: "fuzzing"

Thus the following users have been cc'd because of the following labels:

* fitzgen: fuzzing

To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.

[Learn more.](https://github.com/bytecodealliance/subscribe-to-label-action)
</details>
fitzgen: > Any particular motivation for this or just curious to see how it fares?

I started writing this for my implementation of the custom-page-sizes proposal, but figured I'd split it out and land it first to keep PRs small.

Patch:
diff --git a/cranelift/codegen/src/machinst/lower.rs b/cranelift/codegen/src/machinst/lower.rs
index 5ebbb24bd..ea2be76d4 100644
--- a/cranelift/codegen/src/machinst/lower.rs
+++ b/cranelift/codegen/src/machinst/lower.rs
@@ -767,7 +767,7 @@ impl<'func, I: VCodeInst> Lower<'func, I> {
             // Normal instruction: codegen if the instruction is side-effecting
             // or any of its outputs is used.
             if has_side_effect || value_needed {
-                trace!("lowering: inst {}: {:?}", inst, self.f.dfg.insts[inst]);
+                trace!("lowering: inst {}: {}", inst, self.f.dfg.display_inst(inst));
                 let temp_regs = backend.lower(self, inst).unwrap_or_else(|| {
                     let ty = if self.num_outputs(inst) > 0 {
                         Some(self.output_ty(inst, 0))
diff --git a/crates/cranelift/src/debug.rs b/crates/cranelift/src/debug.rs
index ed9c47310..542a07092 100644
--- a/crates/cranelift/src/debug.rs
+++ b/crates/cranelift/src/debug.rs
@@ -5,8 +5,8 @@ use cranelift_codegen::isa::TargetIsa;
 use object::write::SymbolId;
 use std::collections::HashMap;
 use wasmtime_environ::{
-    DefinedFuncIndex, EntityRef, MemoryIndex, ModuleTranslation, OwnedMemoryIndex, PrimaryMap,
-    PtrSize, StaticModuleIndex, Tunables, VMOffsets,
+    DefinedFuncIndex, DefinedMemoryIndex, EntityRef, MemoryIndex, ModuleTranslation,
+    OwnedMemoryIndex, PrimaryMap, PtrSize, StaticModuleIndex, Tunables, VMOffsets,
 };
 
 /// Memory definition offset in the VMContext structure.
@@ -88,25 +88,21 @@ impl<'a> Compilation<'a> {
             );
 
             let memory_offset = if ofs.num_imported_memories > 0 {
+                let index = MemoryIndex::new(0);
                 ModuleMemoryOffset::Imported {
-                    offset_to_vm_memory_definition: ofs.vmctx_vmmemory_import(MemoryIndex::new(0))
+                    offset_to_vm_memory_definition: ofs.vmctx_vmmemory_import(index)
                         + u32::from(ofs.vmmemory_import_from()),
                     offset_to_memory_base: ofs.ptr.vmmemory_definition_base().into(),
                 }
+            } else if ofs.num_owned_memories > 0 {
+                let index = OwnedMemoryIndex::new(0);
+                ModuleMemoryOffset::Defined(ofs.vmctx_vmmemory_definition_base(index))
             } else if ofs.num_defined_memories > 0 {
-                // The addition of shared memory makes the following assumption,
-                // "owned memory index = 0", possibly false. If the first memory
-                // is a shared memory, the base pointer will not be stored in
-                // the `owned_memories` array. The following code should
-                // eventually be fixed to not only handle shared memories but
-                // also multiple memories.
-                assert_eq!(
-                    ofs.num_defined_memories, ofs.num_owned_memories,
-                    "the memory base pointer may be incorrect due to sharing memory"
-                );
-                ModuleMemoryOffset::Defined(
-                    ofs.vmctx_vmmemory_definition_base(OwnedMemoryIndex::new(0)),
-                )
+                let index = DefinedMemoryIndex::new(0);
+                ModuleMemoryOffset::Imported {
+                    offset_to_vm_memory_definition: ofs.vmctx_vmmemory_pointer(index),
+                    offset_to_memory_base: ofs.ptr.vmmemory_definition_base().into(),
+                }
             } else {
                 ModuleMemoryOffset::None
             };
diff --git a/crates/fuzzing/src/generators.rs b/crates/fuzzing/src/generators.rs
index 155895a78..de264add2 100644
--- a/crates/fuzzing/src/generators.rs
+++ b/crates/fuzzing/src/generators.rs
@@ -28,7 +28,10 @@ pub use codegen_settings::CodegenSettings;
 pub use config::CompilerStrategy;
 pub use config::{Config, WasmtimeConfig};
 pub use instance_allocation_strategy::InstanceAllocationStrategy;
-pub use memory::{MemoryConfig, NormalMemoryConfig, UnalignedMemory, UnalignedMemoryCreator};
+pub use memory::{
+    HeapImage, MemoryAccesses, MemoryConfig, NormalMemoryConfig, UnalignedMemory,
+    UnalignedMemoryCreator,
+};
 pub use module::ModuleConfig;
 pub use pooling_config::PoolingAllocationConfig;
 pub use single_inst_module::SingleInstModule;
diff --git a/crates/fuzzing/src/generators/config.rs b/crates/fuzzing/src/generators/config.rs
index c42c5f8d2..69f321e86 100644
--- a/crates/fuzzing/src/generators/config.rs
+++ b/crates/fuzzing/src/generators/config.rs
@@ -172,7 +172,6 @@ impl Config {
             .cranelift_opt_level(self.wasmtime.opt_level.to_wasmtime())
             .consume_fuel(self.wasmtime.consume_fuel)
             .epoch_interruption(self.wasmtime.epoch_interruption)
-            .memory_init_cow(self.wasmtime.memory_init_cow)
             .memory_guaranteed_dense_image_size(std::cmp::min(
                 // Clamp this at 16MiB so we don't get huge in-memory
                 // images during fuzzing.
@@ -259,7 +258,9 @@ impl Config {
                     static_memory_maximum_size: Some(4 << 30), // 4 GiB
                     static_memory_guard_size: Some(2 << 30),   // 2 GiB
                     dynamic_memory_guard_size: Some(0),
+                    dynamic_memory_reserved_for_growth: Some(0),
                     guard_before_linear_memory: false,
+                    memory_init_cow: true,
                 })
             } else {
                 self.wasmtime.memory_config.clone()
@@ -267,19 +268,16 @@ impl Config {
 
             match &memory_config {
                 MemoryConfig::Normal(memory_config) => {
-                    cfg.static_memory_maximum_size(
-                        memory_config.static_memory_maximum_size.unwrap_or(0),
-                    )
-                    .static_memory_guard_size(memory_config.static_memory_guard_size.unwrap_or(0))
-                    .dynamic_memory_guard_size(memory_config.dynamic_memory_guard_size.unwrap_or(0))
-                    .guard_before_linear_memory(memory_config.guard_before_linear_memory);
+                    memory_config.apply_to(&mut cfg);
                 }
                 MemoryConfig::CustomUnaligned => {
                     cfg.with_host_memory(Arc::new(UnalignedMemoryCreator))
                         .static_memory_maximum_size(0)
                         .dynamic_memory_guard_size(0)
+                        .dynamic_memory_reserved_for_growth(0)
                         .static_memory_guard_size(0)
-                        .guard_before_linear_memory(false);
+                        .guard_before_linear_memory(false)
+                        .memory_init_cow(false);
                 }
             }
         }
diff --git a/crates/fuzzing/src/generators/memory.rs b/crates/fuzzing/src/generators/memory.rs
index bdf679b58..814b9dc7f 100644
--- a/crates/fuzzing/src/generators/memory.rs
+++ b/crates/fuzzing/src/generators/memory.rs
@@ -5,6 +5,95 @@ use arbitrary::{Arbitrary, Unstructured};
 use std::ops::Range;
 use wasmtime::{LinearMemory, MemoryCreator, MemoryType};
 
+/// A description of a memory config, image, etc... that can be used to test
+/// memory accesses.
+#[derive(Debug)]
+pub struct MemoryAccesses {
+    /// The configuration to use with this test case.
+    pub config: crate::generators::Config,
+    /// The heap image to use with this test case.
+    pub image: HeapImage,
+    /// The offset immediate to encode in the `load{8,16,32,64}` functions'
+    /// various load instructions.
+    pub offset: u32,
+    /// The amount (in pages) to grow the memory.
+    pub growth: u32,
+}
+
+impl<'a> Arbitrary<'a> for MemoryAccesses {
+    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+        Ok(MemoryAccesses {
+            config: u.arbitrary()?,
+            image: u.arbitrary()?,
+            offset: u.arbitrary()?,
+            // Don't grow too much, since oss-fuzz/asan get upset if we try,
+            // even if we allow it to fail.
+            growth: u.int_in_range(0..=10)?,
+        })
+    }
+}
+
+/// A memory heap image.
+pub struct HeapImage {
+    /// The minimum size (in pages) of this memory.
+    pub minimum: u32,
+    /// The maximum size (in pages) of this memory.
+    pub maximum: Option<u32>,
+    /// Whether this memory should be indexed with `i64` (rather than `i32`).
+    pub memory64: bool,
+    /// Data segments for this memory.
+    pub segments: Vec<(u32, Vec<u8>)>,
+}
+
+impl std::fmt::Debug for HeapImage {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        struct Segments<'a>(&'a [(u32, Vec<u8>)]);
+        impl std::fmt::Debug for Segments<'_> {
+            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+                write!(f, "[..; {}]", self.0.len())
+            }
+        }
+
+        f.debug_struct("HeapImage")
+            .field("minimum", &self.minimum)
+            .field("maximum", &self.maximum)
+            .field("memory64", &self.memory64)
+            .field("segments", &Segments(&self.segments))
+            .finish()
+    }
+}
+
+impl<'a> Arbitrary<'a> for HeapImage {
+    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+        let minimum = u.int_in_range(0..=4)?;
+        let maximum = if u.arbitrary()? {
+            Some(u.int_in_range(minimum..=10)?)
+        } else {
+            None
+        };
+        let memory64 = u.arbitrary()?;
+        let mut segments = vec![];
+        if minimum > 0 {
+            for _ in 0..u.int_in_range(0..=4)? {
+                const WASM_PAGE_SIZE: u32 = 65536;
+                let last_addressable = WASM_PAGE_SIZE * minimum - 1;
+                let offset = u.int_in_range(0..=last_addressable)?;
+                let max_len =
+                    std::cmp::min(u.len(), usize::try_from(last_addressable - offset).unwrap());
+                let len = u.int_in_range(0..=max_len)?;
+                let data = u.bytes(len)?.to_vec();
+                segments.push((offset, data));
+            }
+        }
+        Ok(HeapImage {
+            minimum,
+            maximum,
+            memory64,
+            segments,
+        })
+    }
+}
+
 /// Configuration for linear memories in Wasmtime.
 #[derive(Arbitrary, Clone, Debug, Eq, Hash, PartialEq)]
 pub enum MemoryConfig {
@@ -27,7 +116,9 @@ pub struct NormalMemoryConfig {
     pub static_memory_maximum_size: Option<u64>,
     pub static_memory_guard_size: Option<u64>,
     pub dynamic_memory_guard_size: Option<u64>,
+    pub dynamic_memory_reserved_for_growth: Option<u64>,
     pub guard_before_linear_memory: bool,
+    pub memory_init_cow: bool,
 }
 
 impl<'a> Arbitrary<'a> for NormalMemoryConfig {
@@ -38,17 +129,36 @@ impl<'a> Arbitrary<'a> for NormalMemoryConfig {
             static_memory_maximum_size: <Option<u32> as Arbitrary>::arbitrary(u)?.map(Into::into),
             static_memory_guard_size: <Option<u32> as Arbitrary>::arbitrary(u)?.map(Into::into),
             dynamic_memory_guard_size: <Option<u32> as Arbitrary>::arbitrary(u)?.map(Into::into),
+            dynamic_memory_reserved_for_growth: <Option<u32> as Arbitrary>::arbitrary(u)?
+                .map(Into::into),
             guard_before_linear_memory: u.arbitrary()?,
+            memory_init_cow: u.arbitrary()?,
         };
 
         if let Some(dynamic) = ret.dynamic_memory_guard_size {
             let statik = ret.static_memory_guard_size.unwrap_or(2 << 30);
             ret.static_memory_guard_size = Some(statik.max(dynamic));
         }
+
         Ok(ret)
     }
 }
 
+impl NormalMemoryConfig {
+    /// Apply this memory configuration to the given `wasmtime::Config`.
+    pub fn apply_to(&self, config: &mut wasmtime::Config) {
+        config
+            .static_memory_maximum_size(self.static_memory_maximum_size.unwrap_or(0))
+            .static_memory_guard_size(self.static_memory_guard_size.unwrap_or(0))
+            .dynamic_memory_guard_size(self.dynamic_memory_guard_size.unwrap_or(0))
+            .dynamic_memory_reserved_for_growth(
+                self.dynamic_memory_reserved_for_growth.unwrap_or(0),
+            )
+            .guard_before_linear_memory(self.guard_before_linear_memory)
+            .memory_init_cow(self.memory_init_cow);
+    }
+}
+
 /// A custom "linear memory allocator" for wasm which only works with the
 /// "dynamic" mode of configuration where wasm always does explicit bounds
 /// checks.
diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs
index 686e22fdb..38cef3d17 100644
--- a/crates/fuzzing/src/oracles.rs
+++ b/crates/fuzzing/src/oracles.rs
@@ -16,6 +16,7 @@ pub mod diff_wasmi;
 pub mod diff_wasmtime;
 pub mod dummy;
 pub mod engine;
+pub mod memory;
 mod stacks;
 
 use self::diff_wasmtime::WasmtimeInstance;
diff --git a/crates/fuzzing/src/oracles/memory.rs b/crates/fuzzing/src/oracles/memory.rs
new file mode 100644
index 000000000..60f740099
--- /dev/null
+++ b/crates/fuzzing/src/oracles/memory.rs
@@ -0,0 +1,396 @@
+//! Oracles related to memory.
+
+use crate::generators::{HeapImage, MemoryAccesses};
+use wasmtime::*;
+
+/// Oracle to perform the described memory accesses and check that they are all
+/// in- or out-of-bounds as expected
+pub fn check_memory_accesses(input: MemoryAccesses) {
+    crate::init_fuzzing();
+    log::info!("Testing memory accesses: {input:#x?}");
+
+    let offset = input.offset;
+    let growth = input.growth;
+    let wasm = build_wasm(&input.image, offset);
+    crate::oracles::log_wasm(&wasm);
+    let offset = u64::from(offset);
+
+    let mut config = input.config.to_wasmtime();
+
+    // Force-enable the memory64 proposal if the heap image wants it.
+    if input.image.memory64 {
+        config.wasm_memory64(true);
+    }
+
+    let engine = Engine::new(&config).unwrap();
+    let module = match Module::new(&engine, &wasm) {
+        Ok(m) => m,
+        Err(e) => {
+            let e = format!("{e:?}");
+            log::info!("Failed to create `Module`: {e}");
+            assert!(
+                e.contains("bytes which exceeds the configured maximum of")
+                    || e.contains("exceeds the limit of")
+            );
+            return;
+        }
+    };
+
+    let limits = super::StoreLimits::new();
+    let mut store = Store::new(&engine, limits);
+    input.config.configure_store(&mut store);
+
+    // If we are using fuel, make sure we add enough that we won't ever run out.
+    if input.config.wasmtime.consume_fuel {
+        store.set_fuel(u64::MAX).unwrap();
+    }
+
+    let instance = match Instance::new(&mut store, &module, &[]) {
+        Ok(x) => x,
+        Err(e) => {
+            log::info!("Failed to instantiate: {e:?}");
+            assert!(format!("{e:?}").contains("Cannot allocate memory"));
+            return;
+        }
+    };
+
+    let memory = instance.get_memory(&mut store, "memory").unwrap();
+    let load8 = instance
+        .get_typed_func::<u64, u32>(&mut store, "load8")
+        .unwrap();
+    let load16 = instance
+        .get_typed_func::<u64, u32>(&mut store, "load16")
+        .unwrap();
+    let load32 = instance
+        .get_typed_func::<u64, u32>(&mut store, "load32")
+        .unwrap();
+    let load64 = instance
+        .get_typed_func::<u64, u64>(&mut store, "load64")
+        .unwrap();
+
+    let do_accesses = |store: &mut Store<_>, msg: &str| {
+        let len = memory.data_size(&mut *store);
+        let len = u64::try_from(len).unwrap();
+
+        if let Some(n) = len.checked_sub(8).and_then(|n| n.checked_sub(offset)) {
+            // Test various in-bounds accesses near the bound.
+            for i in 0..=7 {
+                let addr = n + i;
+                assert!(addr + offset + 1 <= len);
+                let result = load8.call(&mut *store, addr);
+                assert!(
+                    result.is_ok(),
+                    "{msg}: len={len:#x}, offset={offset:#x}, load8({n:#x} + {i:#x} = {addr:#x}) \
+                     should be in bounds, got {result:?}"
+                );
+            }
+            for i in 0..=6 {
+                let addr = n + offset + i;
+                assert!(addr + 2 <= len);
+                let result = load16.call(&mut *store, n + i);
+                assert!(
+                    result.is_ok(),
+                    "{msg}: len={len:#x}, offset={offset:#x}, load16({n:#x} + {i:#x} = {addr:#x}) \
+                     should be in bounds, got {result:?}"
+                );
+            }
+            for i in 0..=4 {
+                let addr = n + offset + i;
+                assert!(addr + 4 <= len);
+                let result = load32.call(&mut *store, n + i);
+                assert!(
+                    result.is_ok(),
+                    "{msg}: len={len:#x}, offset={offset:#x}, load32({n:#x} + {i:#x} = {addr:#x}) \
+                     should be in bounds, got {result:?}"
+                );
+            }
+            assert!(n + offset + 8 <= len);
+            let result = load64.call(&mut *store, n);
+            assert!(
+                result.is_ok(),
+                "{msg}: len={len:#x}, offset={offset:#x}, load64({n:#x}) should be in bounds, \
+                 got {result:?}"
+            );
+
+            // Test various out-of-bounds accesses overlapping the memory bound.
+            for i in 1..2 {
+                let addr = len - i;
+                assert!(addr + offset + 2 > len);
+                let result = load16.call(&mut *store, addr);
+                assert!(
+                    result.is_err(),
+                    "{msg}: len={len:#x}, offset={offset:#x}, load16({len:#x} - {i:#x} = {addr:#x}) \
+                     should trap, got {result:?}"
+                );
+            }
+            for i in 1..4 {
+                let addr = len - i;
+                assert!(addr + offset + 4 > len);
+                let result = load32.call(&mut *store, addr);
+                assert!(
+                    result.is_err(),
+                    "{msg}: len={len:#x}, offset={offset:#x}, load32({len:#x} - {i:#x} = {addr:#x}) \
+                     should trap, got {result:?}"
+                );
+            }
+            for i in 1..8 {
+                let addr = len - i;
+                assert!(addr + offset + 8 > len);
+                let result = load64.call(&mut *store, addr);
+                assert!(
+                    result.is_err(),
+                    "{msg}: len={len:#x}, offset={offset:#x}, load64({len:#x} - {i:#x} = {addr:#x}) \
+                     should trap, got {result:?}"
+                );
+            }
+        }
+
+        // Test that out-of-bounds accesses just after the memory bound trap.
+        if let Some(n) = len.checked_sub(offset) {
+            for i in 0..=1 {
+                let addr = n + i;
+                assert!(addr + offset + 1 > len);
+                let result = load8.call(&mut *store, addr);
+                assert!(
+                    result.is_err(),
+                    "{msg}: len={len:#x}, offset={offset:#x}, load8({n:#x} + {i:#x} = {addr:#x}) \
+                     should trap, got {result:?}"
+                );
+                assert!(addr + offset + 2 > len);
+                let result = load16.call(&mut *store, addr);
+                assert!(
+                    result.is_err(),
+                    "{msg}: len={len:#x}, offset={offset:#x}, load16({n:#x} + {i:#x} = {addr:#x}) \
+                     should trap, got {result:?}"
+                );
+                assert!(addr + offset + 4 > len);
+                let result = load32.call(&mut *store, addr);
+                assert!(
+                    result.is_err(),
+                    "{msg}: len={len:#x}, offset={offset:#x}, load32({n:#x} + {i:#x} = {addr:#x}) \
+                     should trap, got {result:?}"
+                );
+                assert!(addr + offset + 8 > len);
+                let result = load64.call(&mut *store, addr);
+                assert!(
+                    result.is_err(),
+                    "{msg}: len={len:#x}, offset={offset:#x}, load64({n:#x} + {i:#x} = {addr:#x}) \
+                     should trap, got {result:?}"
+                );
+            }
+        }
+
+        // Test out-of-bounds accesses near the end of the index type's range to
+        // double check our overflow handling inside the bounds checks.
+        let len_is_4gib = len == u64::from(u32::MAX) + 1;
+        let end_delta = (input.image.memory64 && len_is_4gib) as u64;
+        let max = if input.image.memory64 {
+            u64::MAX
+        } else {
+            u64::from(u32::MAX)
+        };
+        for i in 0..(1 - end_delta) {
+            let addr = max - i;
+            let result = load8.call(&mut *store, addr);
+            assert!(
+                result.is_err(),
+                "{msg}: len={len:#x}, offset={offset:#x}, load8({max:#x} - {i:#x} = {addr:#x}) \
+                 should trap, got {result:?}"
+            );
+        }
+        for i in 0..(2 - end_delta) {
+            let addr = max - i;
+            let result = load16.call(&mut *store, addr);
+            assert!(
+                result.is_err(),
+                "{msg}: len={len:#x}, offset={offset:#x}, load16({max:#x} - {i:#x} = {addr:#x}) \
+                 should trap, got {result:?}"
+            );
+        }
+        for i in 0..(4 - end_delta) {
+            let addr = max - i;
+            let result = load32.call(&mut *store, addr);
+            assert!(
+                result.is_err(),
+                "{msg}: len={len:#x}, offset={offset:#x}, load32({max:#x} - {i:#x} = {addr:#x}) \
+                 should trap, got {result:?}"
+            );
+        }
+        for i in 0..(8 - end_delta) {
+            let addr = max - i;
+            let result = load64.call(&mut *store, addr);
+            assert!(
+                result.is_err(),
+                "{msg}: len={len:#x}, offset={offset:#x}, load64({max:#x} - {i:#x} = {addr:#x}) \
+                 should trap, got {result:?}"
+            );
+        }
+    };
+
+    do_accesses(&mut store, "initial size");
+    let _ = memory.grow(&mut store, u64::from(growth));
+    do_accesses(&mut store, "after growing");
+}
+
+/// Build a Wasm module with a single memory in the shape of the given heap
+/// image, exports that memory, and also exports four functions:
+/// `load{8,16,32,64}`. Each of these functions takes an `i64` address,
+/// truncates it to `i32` if the memory is not 64-bit, and loads its associated
+/// number of bits from memory at `address + offset`.
+///
+/// ```wat
+/// (module
+///   (memory (export "memory") ...)
+///   (func (export "load8") (param i64) (result i32)
+///     (i32.load8_u offset=${offset} (local.get 0))
+///   )
+///   ...
+/// )
+/// ```
+fn build_wasm(image: &HeapImage, offset: u32) -> Vec<u8> {
+    let mut module = wasm_encoder::Module::new();
+
+    {
+        let mut types = wasm_encoder::TypeSection::new();
+        types.function([wasm_encoder::ValType::I64], [wasm_encoder::ValType::I32]);
+        types.function([wasm_encoder::ValType::I64], [wasm_encoder::ValType::I64]);
+        module.section(&types);
+    }
+
+    {
+        let mut funcs = wasm_encoder::FunctionSection::new();
+        funcs.function(0);
+        funcs.function(0);
+        funcs.function(0);
+        funcs.function(1);
+        module.section(&funcs);
+    }
+
+    {
+        let mut memories = wasm_encoder::MemorySection::new();
+        memories.memory(wasm_encoder::MemoryType {
+            minimum: u64::from(image.minimum),
+            maximum: image.maximum.map(Into::into),
+            memory64: image.memory64,
+            shared: false,
+            page_size_log2: None,
+        });
+        module.section(&memories);
+    }
+
+    {
+        let mut exports = wasm_encoder::ExportSection::new();
+        exports.export("memory", wasm_encoder::ExportKind::Memory, 0);
+        exports.export("load8", wasm_encoder::ExportKind::Func, 0);
+        exports.export("load16", wasm_encoder::ExportKind::Func, 1);
+        exports.export("load32", wasm_encoder::ExportKind::Func, 2);
+        exports.export("load64", wasm_encoder::ExportKind::Func, 3);
+        module.section(&exports);
+    }
+
+    {
+        let mut code = wasm_encoder::CodeSection::new();
+        {
+            let mut func = wasm_encoder::Function::new([]);
+            func.instruction(&wasm_encoder::Instruction::LocalGet(0));
+            if !image.memory64 {
+                func.instruction(&wasm_encoder::Instruction::I32WrapI64);
+            }
+            func.instruction(&wasm_encoder::Instruction::I32Load8U(
+                wasm_encoder::MemArg {
+                    offset: u64::from(offset),
+                    align: 0,
+                    memory_index: 0,
+                },
+            ));
+            func.instruction(&wasm_encoder::Instruction::End);
+            code.function(&func);
+        }
+        {
+            let mut func = wasm_encoder::Function::new([]);
+            func.instruction(&wasm_encoder::Instruction::LocalGet(0));
+            if !image.memory64 {
+                func.instruction(&wasm_encoder::Instruction::I32WrapI64);
+            }
+            func.instruction(&wasm_encoder::Instruction::I32Load16U(
+                wasm_encoder::MemArg {
+                    offset: u64::from(offset),
+                    align: 0,
+                    memory_index: 0,
+                },
+            ));
+            func.instruction(&wasm_encoder::Instruction::End);
+            code.function(&func);
+        }
+        {
+            let mut func = wasm_encoder::Function::new([]);
+            func.instruction(&wasm_encoder::Instruction::LocalGet(0));
+            if !image.memory64 {
+                func.instruction(&wasm_encoder::Instruction::I32WrapI64);
+            }
+            func.instruction(&wasm_encoder::Instruction::I32Load(wasm_encoder::MemArg {
+                offset: u64::from(offset),
+                align: 0,
+                memory_index: 0,
+            }));
+            func.instruction(&wasm_encoder::Instruction::End);
+            code.function(&func);
+        }
+        {
+            let mut func = wasm_encoder::Function::new([]);
+            func.instruction(&wasm_encoder::Instruction::LocalGet(0));
+            if !image.memory64 {
+                func.instruction(&wasm_encoder::Instruction::I32WrapI64);
+            }
+            func.instruction(&wasm_encoder::Instruction::I64Load(wasm_encoder::MemArg {
+                offset: u64::from(offset),
+                align: 0,
+                memory_index: 0,
+            }));
+            func.instruction(&wasm_encoder::Instruction::End);
+            code.function(&func);
+        }
+        module.section(&code);
+    }
+
+    {
+        let mut datas = wasm_encoder::DataSection::new();
+        for (offset, data) in image.segments.iter() {
+            datas.segment(wasm_encoder::DataSegment {
+                mode: wasm_encoder::DataSegmentMode::Active {
+                    memory_index: 0,
+                    offset: &if image.memory64 {
+                        wasm_encoder::ConstExpr::i64_const(*offset as i64)
+                    } else {
+                        wasm_encoder::ConstExpr::i32_const(*offset as i32)
+                    },
+                },
+                data: data.iter().copied(),
+            });
+        }
+        module.section(&datas);
+    }
+
+    module.finish()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use arbitrary::{Arbitrary, Unstructured};
+    use rand::prelude::*;
+
+    #[test]
+    fn smoke_test_memory_access() {
+        let mut rng = SmallRng::seed_from_u64(0);
+        let mut buf = vec![0; 1024];
+
+        for _ in 0..1024 {
+            rng.fill_bytes(&mut buf);
+            let u = Unstructured::new(&buf);
+            if let Ok(input) = MemoryAccesses::arbitrary_take_rest(u) {
+                check_memory_accesses(input);
+            }
+        }
+    }
+}
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index 1f8e2cee1..f424c424c 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -114,3 +114,10 @@ name = "call_async"
 path = "fuzz_targets/call_async.rs"
 test = false
 doc = false
+
+[[bin]]
+name = "memory_accesses"
+path = "fuzz_targets/memory_accesses.rs"
+test = false
+doc = false
+bench = false
diff --git a/fuzz/fuzz_targets/memory_accesses.rs b/fuzz/fuzz_targets/memory_accesses.rs
new file mode 100644
index 000000000..1386a069b
--- /dev/null
+++ b/fuzz/fuzz_targets/memory_accesses.rs
@@ -0,0 +1,8 @@
+#![no_main]
+
+use libfuzzer_sys::fuzz_target;
+use wasmtime_fuzzing::{generators::MemoryAccesses, oracles::memory::check_memory_accesses};
+
+fuzz_target!(|input: MemoryAccesses| {
+    check_memory_accesses(input);
+});


Test patch:
diff --git a/crates/test-programs/artifacts/build.rs b/crates/test-programs/artifacts/build.rs
index 0fa76093e..20209eaf6 100644
--- a/crates/test-programs/artifacts/build.rs
+++ b/crates/test-programs/artifacts/build.rs
@@ -90,7 +90,7 @@ fn build_and_generate_tests() {
         }
 
         // Generate a component from each test.
-        if kind == "nn" || target == "dwarf_imported_memory" {
+        if kind == "nn" || target == "dwarf_imported_memory" || target == "dwarf_shared_memory" {
             continue;
         }
         let adapter = match target.as_str() {
diff --git a/crates/test-programs/build.rs b/crates/test-programs/build.rs
index 734cbddbb..8f055264c 100644
--- a/crates/test-programs/build.rs
+++ b/crates/test-programs/build.rs
@@ -1,4 +1,6 @@
 fn main() {
     println!("cargo:rustc-link-arg-bin=dwarf_imported_memory=--import-memory");
     println!("cargo:rustc-link-arg-bin=dwarf_imported_memory=--export-memory");
+    println!("cargo:rustc-link-arg-bin=dwarf_shared_memory=--no-check-features");
+    println!("cargo:rustc-link-arg-bin=dwarf_shared_memory=--shared-memory");
 }
diff --git a/crates/test-programs/src/bin/dwarf_shared_memory.rs b/crates/test-programs/src/bin/dwarf_shared_memory.rs
new file mode 100644
index 000000000..fe4ca3784
--- /dev/null
+++ b/crates/test-programs/src/bin/dwarf_shared_memory.rs
@@ -0,0 +1,1 @@
+include!("./dwarf_simple.rs");
diff --git a/tests/all/debug/lldb.rs b/tests/all/debug/lldb.rs
index dadf8a154..ee347a162 100644
--- a/tests/all/debug/lldb.rs
+++ b/tests/all/debug/lldb.rs
@@ -370,4 +370,10 @@ check: exited with status = 0
             &["--preload=env=./tests/all/debug/satisfy_memory_import.wat"],
         )
     }
+
+    #[test]
+    #[ignore]
+    fn dwarf_shared_memory() -> Result<()> {
+        test_dwarf_simple(DWARF_SHARED_MEMORY, &[])
+    }
 }