Repository: stellar/rs-soroban-env
Pull request #1171 (2023-11-01 20:45:10)

`Arbitrary`-compatible expr generation and host fuzzing
### What

#1136 

### Why

[TODO: Why this change is being made. Include any context required to understand the why.]

### Known limitations

[TODO or N/A]



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index f4106c8e..973edc9a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1435,8 +1435,10 @@ dependencies = [
 name = "soroban-synth-wasm"
 version = "20.0.0-rc2"
 dependencies = [
+ "arbitrary",
  "expect-test",
  "soroban-env-common",
+ "soroban-env-macros",
  "wasm-encoder",
  "wasmparser",
  "wasmprinter",
diff --git a/cackle.toml b/cackle.toml
index 7a5c313c..c639a4d6 100644
--- a/cackle.toml
+++ b/cackle.toml
@@ -99,9 +99,6 @@ allow_apis = [
 ]
 
 [pkg.rand]
-allow_apis = [
-    "thread",
-]
 allow_unsafe = true
 
 [pkg.backtrace]
@@ -384,3 +381,9 @@ allow_unsafe = true
 
 [pkg.soroban-bench-utils]
 allow_unsafe = true
+
+[pkg.derive_arbitrary]
+allow_proc_macro = true
+
+[pkg.arbitrary]
+allow_unsafe = true
diff --git a/soroban-env-common/src/arbitrary.rs b/soroban-env-common/src/arbitrary.rs
index f20a24b7..407543eb 100644
--- a/soroban-env-common/src/arbitrary.rs
+++ b/soroban-env-common/src/arbitrary.rs
@@ -2,14 +2,57 @@
 
 extern crate alloc;
 
-use crate::xdr::ScError;
-use crate::Error;
+use crate::symbol::MAX_SMALL_CHARS;
+use crate::xdr::{ScError, ScErrorCode};
+use crate::{Error, StorageType, Symbol, SymbolSmall, Val, Void};
 use arbitrary::{Arbitrary, Unstructured};
 
 impl<'a> Arbitrary<'a> for Error {
     fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
         let scerror = ScError::arbitrary(u)?;
         let error = Error::from(scerror);
-        Ok(error)
+        // FIXME: fuzzer discovered that it can just return "InternalError" from
+        // a contract to make the host think it had an InternalError. See
+        // https://github.com/stellar/rs-soroban-env/issues/1175
+        if error.is_code(ScErrorCode::InternalError) {
+            Err(arbitrary::Error::IncorrectFormat)
+        } else {
+            Ok(error)
+        }
+    }
+}
+
+impl<'a> Arbitrary<'a> for Void {
+    fn arbitrary(_u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+        Ok(Val::VOID)
+    }
+}
+
+impl<'a> Arbitrary<'a> for Symbol {
+    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+        let len: usize = u.int_in_range(0..=MAX_SMALL_CHARS)?;
+        let mut buf = [0u8; MAX_SMALL_CHARS];
+        for i in 0..len {
+            buf[i] = (*u.choose(&[
+                'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
+                'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F',
+                'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
+                'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_',
+            ])?) as u8;
+        }
+        let small =
+            SymbolSmall::try_from(&buf[0..len]).map_err(|_| arbitrary::Error::IncorrectFormat)?;
+        Ok(small.into())
+    }
+}
+
+impl<'a> Arbitrary<'a> for StorageType {
+    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+        u.choose(&[
+            StorageType::Instance,
+            StorageType::Persistent,
+            StorageType::Temporary,
+        ])
+        .map(|x| *x)
     }
 }
diff --git a/soroban-env-common/src/storage_type.rs b/soroban-env-common/src/storage_type.rs
index 59e2db99..2804a683 100644
--- a/soroban-env-common/src/storage_type.rs
+++ b/soroban-env-common/src/storage_type.rs
@@ -8,7 +8,7 @@ use num_derive::FromPrimitive;
 /// an argument to storage functions. It doesn't correspond to any [`Val`] types,
 /// and is passed by direct marshalling as a u64.
 #[repr(u64)]
-#[derive(Debug, FromPrimitive, PartialEq, Eq, Clone)]
+#[derive(Debug, FromPrimitive, PartialEq, Eq, Clone, Copy)]
 pub enum StorageType {
     Temporary = 0,
     Persistent = 1,
diff --git a/soroban-env-host/Cargo.toml b/soroban-env-host/Cargo.toml
index ff59cdab..a3b747f1 100644
--- a/soroban-env-host/Cargo.toml
+++ b/soroban-env-host/Cargo.toml
@@ -56,7 +56,7 @@ linregress = "0.5.1"
 pretty_assertions = "1.4.0"
 
 [features]
-testutils = ["soroban-env-common/testutils", "recording_auth"]
+testutils = ["soroban-env-common/testutils", "soroban-synth-wasm/testutils", "recording_auth"]
 next = ["soroban-env-common/next", "soroban-test-wasms/next", "soroban-synth-wasm/next", "soroban-bench-utils/next"]
 tracy = ["dep:tracy-client", "soroban-env-common/tracy"]
 recording_auth = []
diff --git a/soroban-env-host/benches/common/cost_types/wasm_insn_exec.rs b/soroban-env-host/benches/common/cost_types/wasm_insn_exec.rs
index 60c0c42f..516326e6 100644
--- a/soroban-env-host/benches/common/cost_types/wasm_insn_exec.rs
+++ b/soroban-env-host/benches/common/cost_types/wasm_insn_exec.rs
@@ -153,7 +153,7 @@ fn br_table_nested(n: u64, _rng: &mut StdRng) -> WasmModule {
 
 fn local_get(n: u64, _rng: &mut StdRng) -> WasmModule {
     let mut fe = ModEmitter::new().func(Arity(0), 1);
-    let s = fe.locals[0];
+    let s = fe.locals[0].0;
     for _i in 0..n {
         fe.local_get(s);
     }
@@ -164,7 +164,7 @@ fn local_get(n: u64, _rng: &mut StdRng) -> WasmModule {
 
 fn local_set(n: u64, _rng: &mut StdRng) -> WasmModule {
     let mut fe = ModEmitter::new().func(Arity(0), 1);
-    let s = fe.locals[0];
+    let s = fe.locals[0].0;
     for i in 0..n {
         fe.i64_const(i as i64);
         fe.local_set(s);
@@ -177,7 +177,7 @@ fn local_set(n: u64, _rng: &mut StdRng) -> WasmModule {
 
 fn local_tee(n: u64, _rng: &mut StdRng) -> WasmModule {
     let mut fe = ModEmitter::new().func(Arity(0), 1);
-    let s = fe.locals[0];
+    let s = fe.locals[0].0;
     for i in 0..n {
         fe.i64_const(i as i64);
         fe.local_tee(s);
diff --git a/soroban-env-host/fuzz/.gitignore b/soroban-env-host/fuzz/.gitignore
new file mode 100644
index 00000000..1a45eee7
--- /dev/null
+++ b/soroban-env-host/fuzz/.gitignore
@@ -0,0 +1,4 @@
+target
+corpus
+artifacts
+coverage
diff --git a/soroban-env-host/fuzz/Cargo.lock b/soroban-env-host/fuzz/Cargo.lock
new file mode 100644
index 00000000..9337beb0
--- /dev/null
+++ b/soroban-env-host/fuzz/Cargo.lock
@@ -0,0 +1,1095 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "arbitrary"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
+dependencies = [
+ "derive_arbitrary",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "backtrace"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
+[[package]]
+name = "base32"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
+
+[[package]]
+name = "cc"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+dependencies = [
+ "jobserver",
+ "libc",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "const-oid"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crate-git-revision"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c521bf1f43d31ed2f73441775ed31935d77901cb3451e44b38a1c1612fcbaf98"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "serde_json",
+]
+
+[[package]]
+name = "crypto-bigint"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124"
+dependencies = [
+ "generic-array",
+ "rand_core",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "digest",
+ "fiat-crypto",
+ "platforms",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "der"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
+[[package]]
+name = "derive_arbitrary"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "downcast-rs"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
+
+[[package]]
+name = "ecdsa"
+version = "0.16.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4"
+dependencies = [
+ "der",
+ "digest",
+ "elliptic-curve",
+ "rfc6979",
+ "signature",
+ "spki",
+]
+
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "rand_core",
+ "serde",
+ "sha2",
+ "zeroize",
+]
+
+[[package]]
+name = "either"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
+
+[[package]]
+name = "elliptic-curve"
+version = "0.13.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "digest",
+ "ff",
+ "generic-array",
+ "group",
+ "pkcs8",
+ "rand_core",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "ethnum"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c"
+
+[[package]]
+name = "ff"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
+dependencies = [
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "fiat-crypto"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a481586acf778f1b1455424c343f71124b048ffa5f4fc3f8f6ae9dc432dcb3c7"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+ "zeroize",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gimli"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
+
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "idna"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "indexmap-nostd"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590"
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
+
+[[package]]
+name = "jobserver"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "k256"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc"
+dependencies = [
+ "cfg-if",
+ "ecdsa",
+ "elliptic-curve",
+ "once_cell",
+ "sha2",
+ "signature",
+]
+
+[[package]]
+name = "keccak"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940"
+dependencies = [
+ "cpufeatures",
+]
+
+[[package]]
+name = "leb128"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
+
+[[package]]
+name = "libc"
+version = "0.2.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
+dependencies = [
+ "arbitrary",
+ "cc",
+ "once_cell",
+]
+
+[[package]]
+name = "libm"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+
+[[package]]
+name = "log"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+
+[[package]]
+name = "memchr"
+version = "2.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "num-derive"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "object"
+version = "0.32.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+
+[[package]]
+name = "paste"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "platforms"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rfc6979"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
+dependencies = [
+ "hmac",
+ "subtle",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
+
+[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "pkcs8",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
+
+[[package]]
+name = "serde"
+version = "1.0.190"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.190"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.108"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha3"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
+dependencies = [
+ "digest",
+ "keccak",
+]
+
+[[package]]
+name = "signature"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
+
+[[package]]
+name = "soroban-builtin-sdk-macros"
+version = "20.0.0-rc2"
+dependencies = [
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "soroban-env-common"
+version = "20.0.0-rc2"
+dependencies = [
+ "arbitrary",
+ "crate-git-revision",
+ "ethnum",
+ "num-derive",
+ "num-traits",
+ "soroban-env-macros",
+ "soroban-wasmi",
+ "static_assertions",
+ "stellar-xdr",
+]
+
+[[package]]
+name = "soroban-env-host"
+version = "20.0.0-rc2"
+dependencies = [
+ "backtrace",
+ "curve25519-dalek",
+ "ed25519-dalek",
+ "getrandom",
+ "k256",
+ "num-derive",
+ "num-integer",
+ "num-traits",
+ "rand",
+ "rand_chacha",
+ "sha2",
+ "sha3",
+ "soroban-builtin-sdk-macros",
+ "soroban-env-common",
+ "soroban-wasmi",
+ "static_assertions",
+ "stellar-strkey",
+]
+
+[[package]]
+name = "soroban-env-host-fuzz"
+version = "0.0.0"
+dependencies = [
+ "arbitrary",
+ "libfuzzer-sys",
+ "sha2",
+ "soroban-env-host",
+ "soroban-synth-wasm",
+]
+
+[[package]]
+name = "soroban-env-macros"
+version = "20.0.0-rc2"
+dependencies = [
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "stellar-xdr",
+ "syn",
+]
+
+[[package]]
+name = "soroban-synth-wasm"
+version = "20.0.0-rc2"
+dependencies = [
+ "arbitrary",
+ "soroban-env-common",
+ "soroban-env-macros",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "soroban-wasmi"
+version = "0.31.0-soroban1"
+source = "git+https://github.com/stellar/wasmi?rev=7e63b4c9e08c4163f417d118d81f7ea34789d0be#7e63b4c9e08c4163f417d118d81f7ea34789d0be"
+dependencies = [
+ "smallvec",
+ "spin",
+ "wasmi_arena",
+ "wasmi_core",
+ "wasmparser-nostd",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+[[package]]
+name = "spki"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "stellar-strkey"
+version = "0.0.7"
+source = "git+https://github.com/stellar/rs-stellar-strkey?rev=e6ba45c60c16de28c7522586b80ed0150157df73#e6ba45c60c16de28c7522586b80ed0150157df73"
+dependencies = [
+ "base32",
+ "thiserror",
+]
+
+[[package]]
+name = "stellar-xdr"
+version = "20.0.0-rc1"
+source = "git+https://github.com/stellar/rs-stellar-xdr?rev=9c97e4fa909a0b6455547a4f4a95800696b2a69a#9c97e4fa909a0b6455547a4f4a95800696b2a69a"
+dependencies = [
+ "arbitrary",
+ "base64",
+ "crate-git-revision",
+ "hex",
+]
+
+[[package]]
+name = "subtle"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
+
+[[package]]
+name = "syn"
+version = "2.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
+
+[[package]]
+name = "wasm-encoder"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c64ac98d5d61192cc45c701b7e4bd0b9aff91e2edfc7a088406cfe2288581e2c"
+dependencies = [
+ "leb128",
+]
+
+[[package]]
+name = "wasmi_arena"
+version = "0.4.0"
+source = "git+https://github.com/stellar/wasmi?rev=7e63b4c9e08c4163f417d118d81f7ea34789d0be#7e63b4c9e08c4163f417d118d81f7ea34789d0be"
+
+[[package]]
+name = "wasmi_core"
+version = "0.13.0"
+source = "git+https://github.com/stellar/wasmi?rev=7e63b4c9e08c4163f417d118d81f7ea34789d0be#7e63b4c9e08c4163f417d118d81f7ea34789d0be"
+dependencies = [
+ "downcast-rs",
+ "libm",
+ "num-traits",
+ "paste",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.106.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d014e33793cab91655fa6349b0bc974984de106b2e0f6b0dfe6f6594b260624d"
+dependencies = [
+ "indexmap",
+ "url",
+]
+
+[[package]]
+name = "wasmparser-nostd"
+version = "0.100.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9157cab83003221bfd385833ab587a039f5d6fa7304854042ba358a3b09e0724"
+dependencies = [
+ "indexmap-nostd",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
diff --git a/soroban-env-host/fuzz/Cargo.toml b/soroban-env-host/fuzz/Cargo.toml
new file mode 100644
index 00000000..15cdc1c8
--- /dev/null
+++ b/soroban-env-host/fuzz/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "soroban-env-host-fuzz"
+version = "0.0.0"
+publish = false
+edition = "2021"
+
+[package.metadata]
+cargo-fuzz = true
+
+[dependencies]
+libfuzzer-sys = "0.4"
+arbitrary = { version = "1.3.0", features = ["derive"] }
+soroban-env-host = { path = "..", features = ["testutils"]}
+soroban-synth-wasm = { path = "../../soroban-synth-wasm", features = ["testutils"]}
+sha2 = "0.10.0"
+
+# Prevent this from interfering with workspaces
+[workspace]
+members = ["."]
+
+[profile.release]
+debug = 1
+
+[[bin]]
+name = "log_from_linear_memory"
+path = "fuzz_targets/log_from_linear_memory.rs"
+test = false
+doc = false
+
+[[bin]]
+name = "expr"
+path = "fuzz_targets/expr.rs"
+test = false
+doc = false
diff --git a/soroban-env-host/fuzz/fuzz_targets/expr.rs b/soroban-env-host/fuzz/fuzz_targets/expr.rs
new file mode 100644
index 00000000..4ae5c4ff
--- /dev/null
+++ b/soroban-env-host/fuzz/fuzz_targets/expr.rs
@@ -0,0 +1,43 @@
+#![no_main]
+
+use soroban_env_host::{
+    budget::Budget,
+    xdr::{Hash, HostFunction, InvokeContractArgs, ScAddress, ScErrorCode, ScErrorType, ScSymbol},
+};
+use soroban_env_host_fuzz::{
+    storage_with_test_contract, test_host_with_storage_and_budget, test_scvec, TEST_CONTRACT_ID,
+    TEST_FN_NAME,
+};
+use soroban_synth_wasm::{Emit, Expr};
+
+use libfuzzer_sys::fuzz_target;
+
+fuzz_target!(|expr: Expr| {
+    // fuzzed code goes here
+    let wasm = expr.0.as_single_function_wasm_module("test");
+
+    let budget = Budget::default();
+    let storage = storage_with_test_contract(wasm, &budget);
+    let host = test_host_with_storage_and_budget(storage, budget);
+    host.enable_debug().unwrap();
+
+    let args = test_scvec::<u32>(&[]);
+
+    let hf = HostFunction::InvokeContract(InvokeContractArgs {
+        contract_address: ScAddress::Contract(Hash(TEST_CONTRACT_ID.clone())),
+        function_name: ScSymbol(TEST_FN_NAME.try_into().unwrap()),
+        args: args.try_into().unwrap(),
+    });
+
+    let res = host.invoke_function(hf);
+
+    // Non-internal error-code returns are ok, we are interested in _panics_ and
+    // internal errors.
+    if let Err(hosterror) = res {
+        if hosterror.error.is_code(ScErrorCode::InternalError)
+            && !hosterror.error.is_type(ScErrorType::Contract)
+        {
+            panic!("got internal error: {:?}", hosterror)
+        }
+    }
+});
diff --git a/soroban-env-host/fuzz/fuzz_targets/log_from_linear_memory.rs b/soroban-env-host/fuzz/fuzz_targets/log_from_linear_memory.rs
new file mode 100644
index 00000000..8a7eefd6
--- /dev/null
+++ b/soroban-env-host/fuzz/fuzz_targets/log_from_linear_memory.rs
@@ -0,0 +1,54 @@
+#![no_main]
+
+use arbitrary::Arbitrary;
+use libfuzzer_sys::fuzz_target;
+use soroban_env_host::{
+    budget::{AsBudget, Budget},
+    xdr::{Hash, HostFunction, InvokeContractArgs, ScAddress, ScSymbol},
+};
+use soroban_env_host_fuzz::{
+    storage_with_test_contract, test_host_with_storage_and_budget, test_scvec,
+    wasm_module_with_linear_memory_logging, TEST_CONTRACT_ID, TEST_FN_NAME,
+};
+
+#[derive(Arbitrary, Debug)]
+struct Input {
+    fn_input: FnInput,
+}
+
+#[derive(Arbitrary, Debug)]
+struct FnInput {
+    msg_pos: u32,
+    msg_len: u32,
+    vals_pos: u32,
+    vals_len: u32,
+}
+
+fuzz_target!(|input: Input| {
+    // fuzzed code goes here
+
+    let budget = Budget::default();
+    let contract_code = wasm_module_with_linear_memory_logging();
+    let storage = storage_with_test_contract(contract_code, &budget);
+    let host = test_host_with_storage_and_budget(storage, budget);
+    host.enable_debug().unwrap();
+
+    let args = test_scvec::<u32>(&[
+        input.fn_input.msg_pos,
+        input.fn_input.msg_len,
+        input.fn_input.vals_pos,
+        input.fn_input.vals_len,
+    ]);
+
+    let hf = HostFunction::InvokeContract(InvokeContractArgs {
+        contract_address: ScAddress::Contract(Hash(TEST_CONTRACT_ID.clone())),
+        function_name: ScSymbol(TEST_FN_NAME.try_into().unwrap()),
+        args: args.try_into().unwrap(),
+    });
+
+    let _res = host.invoke_function(hf).unwrap();
+
+    let cpu = host.as_budget().get_cpu_insns_consumed().unwrap();
+    let mem = host.as_budget().get_mem_bytes_consumed().unwrap();
+    assert_eq!((cpu, mem), (1077529, 204101));
+});
diff --git a/soroban-env-host/fuzz/src/debug_log.rs b/soroban-env-host/fuzz/src/debug_log.rs
new file mode 100644
index 00000000..d88238f7
--- /dev/null
+++ b/soroban-env-host/fuzz/src/debug_log.rs
@@ -0,0 +1,19 @@
+// use soroban_env_host::Symbol;
+use soroban_env_host::Symbol;
+use soroban_synth_wasm::{Arity, LocalRef, ModEmitter, Operand};
+
+pub fn wasm_module_with_linear_memory_logging() -> Vec<u8> {
+    let mut me = ModEmitter::new();
+    // log_from_linear_memory
+    let f0 = me.import_func("x", "_", Arity(4));
+    // the caller
+    let mut fe = me.func(Arity(4), 0);
+    fe.push(Operand::Local(LocalRef(0)));
+    fe.push(Operand::Local(LocalRef(1)));
+    fe.push(Operand::Local(LocalRef(2)));
+    fe.push(Operand::Local(LocalRef(3)));
+    fe.call_func(f0);
+    fe.drop();
+    fe.push(Symbol::try_from_small_str("pass").unwrap());
+    fe.finish_and_export("test").finish()
+}
diff --git a/soroban-env-host/fuzz/src/lib.rs b/soroban-env-host/fuzz/src/lib.rs
new file mode 100644
index 00000000..2ba9030a
--- /dev/null
+++ b/soroban-env-host/fuzz/src/lib.rs
@@ -0,0 +1,147 @@
+mod debug_log;
+pub use debug_log::*;
+
+use sha2::Sha256;
+use soroban_env_host::{
+    budget::Budget,
+    meta,
+    storage::{AccessType, Footprint, FootprintMap, FootprintMode, Storage, StorageMap},
+    xdr::{
+        AccountId, ContractCodeEntry, ContractDataDurability, ContractDataEntry,
+        ContractExecutable, ExtensionPoint, Hash, LedgerEntry, LedgerEntryData, LedgerEntryExt,
+        LedgerKey, LedgerKeyContractCode, LedgerKeyContractData, ScAddress, ScContractInstance,
+        ScVal, ScVec, Uint256,
+    },
+    Host, LedgerInfo,
+};
+use std::rc::Rc;
+
+pub trait AsScVal {
+    fn as_scval(&self) -> ScVal;
+}
+
+impl AsScVal for u32 {
+    fn as_scval(&self) -> ScVal {
+        ScVal::U32(*self)
+    }
+}
+
+impl AsScVal for i32 {
+    fn as_scval(&self) -> ScVal {
+        ScVal::I32(*self)
+    }
+}
+
+impl AsScVal for ScVec {
+    fn as_scval(&self) -> ScVal {
+        ScVal::Vec(Some(self.clone()))
+    }
+}
+
+pub fn test_scvec<T: AsScVal>(vals: &[T]) -> Vec<ScVal> {
+    vals.iter().map(|x| x.as_scval()).collect()
+}
+
+pub const TEST_PRNG_SEED: &'static [u8; 32] = b"12345678901234567890123456789012";
+
+pub const TEST_CONTRACT_ID: &'static [u8; 32] = &[0; 32];
+
+pub const TEST_SOURCE_ACCOUNT: &'static [u8; 32] = &[0; 32];
+
+pub const TEST_FN_NAME: &'static str = "test";
+
+pub fn storage_with_test_contract(contract_code: Vec<u8>, budget: &Budget) -> Storage {
+    let contract_code_hash: [u8; 32] = <Sha256 as sha2::Digest>::digest(contract_code.as_slice())
+        .try_into()
+        .unwrap();
+    let contract_code_lk = LedgerKey::ContractCode(LedgerKeyContractCode {
+        hash: Hash(contract_code_hash),
+    });
+
+    let contract_code_le = LedgerEntry {
+        last_modified_ledger_seq: 0,
+        data: LedgerEntryData::ContractCode(ContractCodeEntry {
+            hash: Hash(contract_code_hash),
+            ext: ExtensionPoint::V0,
+            code: contract_code.try_into().unwrap(),
+        }),
+        ext: LedgerEntryExt::V0,
+    };
+
+    let contract_id_hash = Hash(TEST_CONTRACT_ID.clone());
+    let contract_instance_lk = LedgerKey::ContractData(LedgerKeyContractData {
+        key: ScVal::LedgerKeyContractInstance,
+        durability: ContractDataDurability::Persistent,
+        contract: ScAddress::Contract(contract_id_hash.clone()),
+    });
+
+    let contract_instance_le = LedgerEntry {
+        // This is modified to the appropriate value on the core side during
+        // commiting the ledger transaction.
+        last_modified_ledger_seq: 0,
+        data: LedgerEntryData::ContractData(ContractDataEntry {
+            contract: ScAddress::Contract(contract_id_hash.clone()),
+            key: ScVal::LedgerKeyContractInstance,
+            val: ScVal::ContractInstance(ScContractInstance {
+                executable: ContractExecutable::Wasm(Hash(contract_code_hash)),
+                storage: None,
+            }),
+            durability: ContractDataDurability::Persistent,
+            ext: ExtensionPoint::V0,
+        }),
+        ext: LedgerEntryExt::V0,
+    };
+
+    let fpm = FootprintMap::from_map(
+        vec![
+            (Rc::new(contract_instance_lk.clone()), AccessType::ReadOnly),
+            (Rc::new(contract_code_lk.clone()), AccessType::ReadOnly),
+        ],
+        &budget,
+    )
+    .unwrap();
+    let footprint = Footprint(fpm);
+    let map = StorageMap::from_map(
+        vec![
+            (
+                Rc::new(contract_instance_lk),
+                Some((Rc::new(contract_instance_le), Some(u32::MAX))),
+            ),
+            (
+                Rc::new(contract_code_lk),
+                Some((Rc::new(contract_code_le), Some(u32::MAX))),
+            ),
+        ],
+        &budget,
+    )
+    .unwrap();
+
+    Storage {
+        footprint,
+        mode: FootprintMode::Enforcing,
+        map,
+    }
+}
+
+pub fn test_host_with_storage_and_budget(storage: Storage, budget: Budget) -> Host {
+    let host = Host::with_storage_and_budget(storage, budget);
+    host.set_source_account(AccountId(
+        soroban_env_host::xdr::PublicKey::PublicKeyTypeEd25519(Uint256(
+            TEST_SOURCE_ACCOUNT.clone(),
+        )),
+    ))
+    .unwrap();
+    host.set_base_prng_seed(*TEST_PRNG_SEED).unwrap();
+    host.set_ledger_info(LedgerInfo {
+        protocol_version: meta::get_ledger_protocol_version(meta::INTERFACE_VERSION),
+        sequence_number: 0,
+        timestamp: 0,
+        network_id: [0; 32],
+        base_reserve: 0,
+        min_persistent_entry_ttl: 4096,
+        min_temp_entry_ttl: 16,
+        max_entry_ttl: 6_312_000,
+    })
+    .unwrap();
+    host
+}
diff --git a/soroban-env-host/src/builtin_contracts/storage_utils.rs b/soroban-env-host/src/builtin_contracts/storage_utils.rs
index 989c2a7a..02f40a6b 100644
--- a/soroban-env-host/src/builtin_contracts/storage_utils.rs
+++ b/soroban-env-host/src/builtin_contracts/storage_utils.rs
@@ -6,7 +6,7 @@ pub(crate) struct StorageUtils {}
 
 impl StorageUtils {
     pub(crate) fn try_get(e: &Host, k: Val, t: StorageType) -> Result<Option<Val>, HostError> {
-        if e.has_contract_data(k, t.clone())?.into() {
+        if e.has_contract_data(k, t)?.into() {
             Ok(Some(e.get_contract_data(k, t)?))
         } else {
             Ok(None)
diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs
index 205efd1c..52fe94aa 100644
--- a/soroban-env-host/src/host.rs
+++ b/soroban-env-host/src/host.rs
@@ -328,7 +328,7 @@ impl Host {
 
     #[cfg(test)]
     pub(crate) fn source_account_id(&self) -> Result<Option<AccountId>, HostError> {
-        Ok(self.try_borrow_source_account()?.metered_clone(self)?)
+        self.try_borrow_source_account()?.metered_clone(self)
     }
 
     #[cfg(test)]
@@ -778,7 +778,7 @@ impl VmCallerEnv for Host {
                 let msg = String::from_utf8_lossy(&msg);
 
                 let VmSlice { vm, pos, len } = self.decode_vmslice(vals_pos, vals_len)?;
-                Vec::<Val>::charge_bulk_init_cpy((len + 1) as u64, self)?;
+                Vec::<Val>::charge_bulk_init_cpy((len.saturating_add(1)) as u64, self)?;
                 let mut vals: Vec<Val> = vec![Val::VOID.to_val(); len as usize];
                 // charge for conversion from bytes to `Val`s
                 self.charge_budget(ContractCostType::MemCpy, Some(len.saturating_mul(8) as u64))?;
diff --git a/soroban-env-macros/src/call_macro_with_all_host_functions.rs b/soroban-env-macros/src/call_macro_with_all_host_functions.rs
index 9698e940..a1926297 100644
--- a/soroban-env-macros/src/call_macro_with_all_host_functions.rs
+++ b/soroban-env-macros/src/call_macro_with_all_host_functions.rs
@@ -3,32 +3,13 @@ use proc_macro2::TokenStream;
 use quote::{format_ident, quote};
 use std::{
     collections::{btree_map::Entry, BTreeMap},
-    fs::File,
     iter,
 };
 use syn::{Error, LitStr};
 
-use serde::{Deserialize, Serialize};
-
-use crate::path;
-
 pub fn generate(file_lit: LitStr) -> Result<TokenStream, Error> {
     let file_str = file_lit.value();
-    let file_path = path::abs_from_rel_to_manifest(&file_str);
-
-    let file = File::open(&file_path).map_err(|e| {
-        Error::new(
-            file_lit.span(),
-            format!("error reading file '{file_str}': {e}"),
-        )
-    })?;
-
-    let root: Root = serde_json::from_reader(file).map_err(|e| {
-        Error::new(
-            file_lit.span(),
-            format!("error parsing file '{file_str}': {e}"),
-        )
-    })?;
+    let root: crate::Root = crate::load_env_file(file_lit.clone())?;
 
     let mut export_names = BTreeMap::<String, String>::new();
     for m in root.modules.iter() {
@@ -153,31 +134,3 @@ pub fn generate(file_lit: LitStr) -> Result<TokenStream, Error> {
         pub use _call_macro_with_all_host_functions as call_macro_with_all_host_functions;
     })
 }
-
-#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
-pub struct Root {
-    pub modules: Vec<Module>,
-}
-
-#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
-pub struct Module {
-    pub name: String,
-    pub export: String,
-    pub functions: Vec<Function>,
-}
-
-#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
-pub struct Function {
-    pub export: String,
-    pub name: String,
-    pub args: Vec<Arg>,
-    pub r#return: String,
-    pub docs: Option<String>,
-}
-
-#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Arg {
-    pub name: String,
-    pub r#type: String,
-}
diff --git a/soroban-env-macros/src/lib.rs b/soroban-env-macros/src/lib.rs
index 42d13bc4..2e2b7fb9 100644
--- a/soroban-env-macros/src/lib.rs
+++ b/soroban-env-macros/src/lib.rs
@@ -1,5 +1,7 @@
 mod call_macro_with_all_host_functions;
 mod path;
+mod synth_wasm_expr_type;
+use serde::{Deserialize, Serialize};
 
 extern crate proc_macro;
 
@@ -75,6 +77,53 @@ impl ToTokens for MetaConstsOutput {
     }
 }
 
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub(crate) struct Root {
+    pub(crate) modules: Vec<Module>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub(crate) struct Module {
+    pub(crate) name: String,
+    pub(crate) export: String,
+    pub(crate) functions: Vec<Function>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub(crate) struct Function {
+    pub(crate) export: String,
+    pub(crate) name: String,
+    pub(crate) args: Vec<Arg>,
+    pub(crate) r#return: String,
+    pub(crate) docs: Option<String>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct Arg {
+    pub(crate) name: String,
+    pub(crate) r#type: String,
+}
+
+fn load_env_file(file_lit: LitStr) -> Result<Root, syn::Error> {
+    let file_str = file_lit.value();
+    let file_path = path::abs_from_rel_to_manifest(&file_str);
+
+    let file = std::fs::File::open(file_path).map_err(|e| {
+        syn::Error::new(
+            file_lit.span(),
+            format!("error reading file '{file_str}': {e}"),
+        )
+    })?;
+
+    serde_json::from_reader(file).map_err(|e| {
+        syn::Error::new(
+            file_lit.span(),
+            format!("error parsing file '{file_str}': {e}"),
+        )
+    })
+}
+
 #[proc_macro]
 pub fn generate_env_meta_consts(input: TokenStream) -> TokenStream {
     let meta_input = parse_macro_input!(input as MetaInput);
@@ -90,3 +139,12 @@ pub fn generate_call_macro_with_all_host_functions(input: TokenStream) -> TokenS
         Err(e) => e.to_compile_error().into(),
     }
 }
+
+#[proc_macro]
+pub fn generate_synth_wasm_expr_type(input: TokenStream) -> TokenStream {
+    let file = parse_macro_input!(input as LitStr);
+    match synth_wasm_expr_type::generate(file) {
+        Ok(t) => t.into(),
+        Err(e) => e.to_compile_error().into(),
+    }
+}
diff --git a/soroban-env-macros/src/synth_wasm_expr_type.rs b/soroban-env-macros/src/synth_wasm_expr_type.rs
new file mode 100644
index 00000000..3414eef6
--- /dev/null
+++ b/soroban-env-macros/src/synth_wasm_expr_type.rs
@@ -0,0 +1,319 @@
+use proc_macro2::{Ident, TokenStream};
+use quote::{format_ident, quote};
+use std::collections::{BTreeMap, BTreeSet};
+use syn::{Error, LitStr};
+
+use crate::{Arg, Function};
+
+// This module is a somewhat gnarly metaprogram that generates a set of
+// "expression enums" and associated impls of the `Emit` trait for each possible
+// type and function call in the env.json environment interface definition. It's
+// very tightly connected to its sole user, the synth-wasm crate (where `Emit`
+// is defined to drive `FuncEmitter`). Unfortunately there are a lot of special
+// cases to make it work well.
+
+const CONST_SUFFIX: &str = "_const";
+const NEW_SUFFIX: &str = "_new";
+const OR_EXISTING_SUFFIX: &str = "_or_existing";
+const NEW_OR_EXISTING_SUFFIX: &str = "new_or_existing";
+
+const SUBTYPES: &[(&str, &str)] = &[
+    ("Val", "Bool"),
+    ("Val", "Void"),
+    ("Val", "I32Val"),
+    ("Val", "U32Val"),
+    ("Val", "Error"),
+    ("Val", "Symbol"),
+    ("Symbol", "SymbolObject"),
+    ("Val", "U64Val"),
+    ("U64Val", "U64Object"),
+    ("Val", "I64Val"),
+    ("I64Val", "I64Object"),
+    ("Val", "U128Val"),
+    ("U128Val", "U128Object"),
+    ("Val", "I128Val"),
+    ("I128Val", "I128Object"),
+    ("Val", "U256Val"),
+    ("U256Val", "U256Object"),
+    ("Val", "I256Val"),
+    ("I256Val", "I256Object"),
+    ("Val", "TimepointVal"),
+    ("TimepointVal", "TimepointObject"),
+    ("Val", "DurationVal"),
+    ("DurationVal", "DurationObject"),
+    ("Val", "MapObject"),
+    ("Val", "VecObject"),
+    ("Val", "BytesObject"),
+    ("Val", "StringObject"),
+    ("Val", "AddressObject"),
+];
+
+fn expr_enum_name(ty: &String) -> Ident {
+    format_ident!("Expr{}", ty)
+}
+
+fn type_const_expr_type(ty: &str) -> Option<String> {
+    match ty {
+        "Bool" => Some("bool".to_string()),
+        "I32Val" => Some("i32".to_string()),
+        "U32Val" => Some("u32".to_string()),
+        "i64" => Some("i64".to_string()),
+        "u64" => Some("u64".to_string()),
+        "Void" => Some("Void".to_string()),
+        "Error" => Some("Error".to_string()),
+        "StorageType" => Some("StorageType".to_string()),
+        "Symbol" => Some("Symbol".to_string()),
+        _ => None,
+    }
+}
+
+impl crate::Function {
+    fn specialize_unary_new_function(&mut self) -> bool {
+        if self.name.ends_with(NEW_SUFFIX) && self.args.is_empty() {
+            self.name.push_str(OR_EXISTING_SUFFIX);
+            self.args.push(Arg {
+                name: "index".to_string(),
+                r#type: "u8".to_string(),
+            });
+            true
+        } else {
+            false
+        }
+    }
+
+    fn synthesize_const_expr_function(ty: &str) -> Option<Self> {
+        if let Some(const_ty) = type_const_expr_type(ty) {
+            let mut f = Function {
+                name: format!("{}{}", ty, CONST_SUFFIX),
+                ..Default::default()
+            };
+            f.args.push(crate::Arg {
+                name: "x".to_string(),
+                r#type: const_ty,
+            });
+            Some(f)
+        } else {
+            None
+        }
+    }
+
+    // These are crude, but work for our purposes
+    fn is_const_expr_fn(&self) -> bool {
+        self.name.ends_with(CONST_SUFFIX) && self.args.len() == 1
+    }
+
+    fn is_new_or_existing_expr_fn(&self) -> bool {
+        self.name.ends_with(NEW_OR_EXISTING_SUFFIX) && self.args.len() == 1
+    }
+}
+
+pub fn generate(file_lit: LitStr) -> Result<TokenStream, Error> {
+    let root: crate::Root = crate::load_env_file(file_lit)?;
+
+    // First we collect all the types used anywhere in the env.json file as well
+    // as all the functions, grouped by return type.
+    let mut all_tys: BTreeSet<String> = BTreeSet::new();
+    let mut all_subty_fns: BTreeSet<String> = BTreeSet::new();
+    let mut fns_by_type = BTreeMap::<String, Vec<crate::Function>>::new();
+
+    for m in root.modules.iter() {
+        for f in m.functions.iter() {
+            all_tys.insert(f.r#return.clone());
+            for arg in f.args.iter() {
+                all_tys.insert(arg.r#type.clone());
+            }
+            fns_by_type
+                .entry(f.r#return.clone())
+                .or_default()
+                .push(f.clone())
+        }
+    }
+
+    // Several types have expression-subtype relationships, for example
+    // ExprSymbolObject is a subtype of ExprSymbol or ExprMapObject is a subtype
+    // of ExprVal. We encode these as extra pseudo-function cases in each
+    // supertype.
+    for (sup, sub) in SUBTYPES {
+        let subty_fn_name = format!("sub_{}", sub);
+        all_subty_fns.insert(subty_fn_name.clone());
+        let mut f = Function {
+            name: subty_fn_name,
+            ..Default::default()
+        };
+        f.args.push(Arg {
+            name: "sub".to_string(),
+            r#type: sub.to_string(),
+        });
+        fns_by_type.entry(sup.to_string()).or_default().push(f);
+        all_tys.insert(sup.to_string());
+        all_tys.insert(sub.to_string());
+    }
+
+    // Next for each type foo used anywhere we either synthesize a
+    // foo_const(some_const_ty) function or specialize any existing unary
+    // foo_new() function to foo_new_or_existing(index:u8)
+    for ty in all_tys.iter() {
+        let ty_fns = fns_by_type.entry(ty.clone()).or_default();
+        if let Some(f) = Function::synthesize_const_expr_function(ty.as_str()) {
+            ty_fns.push(f)
+        } else if !ty_fns.iter_mut().any(|f| f.specialize_unary_new_function()) && ty_fns.is_empty()
+        {
+            panic!("no way to synthesize {}", ty)
+        }
+    }
+
+    // Now we declare an enum ExprFoo for each type Foo in the input, with one case for
+    // every known function that can produce a Foo.
+    let enums = fns_by_type.iter().map(|(ty, fns)| {
+        let enum_name = expr_enum_name(ty);
+        let cases = fns.iter().map(|f| {
+            let case_name = format_ident!("{}", f.name);
+            let args: Vec<_> = f
+                .args
+                .iter()
+                .map(|a| {
+                    if f.is_const_expr_fn() || f.is_new_or_existing_expr_fn() {
+                        // The const and new-or-existing leaf nodes just contain
+                        // their constant or index args directly.
+                        let arg = format_ident!("{}", a.r#type);
+                        quote! { #arg }
+                    } else {
+                        // Interior nodes contain their Foo args as Box<ExprFoo>
+                        let arg_expr = expr_enum_name(&a.r#type);
+                        quote! { Box<#arg_expr> }
+                    }
+                })
+                .collect();
+            quote! {
+                #case_name(#(#args),*)
+            }
+        });
+        quote! {
+            #[derive(Arbitrary, Clone, Debug)]
+            pub enum #enum_name {
+                #(#cases),*
+            }
+        }
+    });
+
+    let impls = fns_by_type.iter().map(|(ty, fns)| {
+        let enum_name = expr_enum_name(ty);
+        let emit_cases = fns.iter().map(|f| {
+            let case_name = format_ident!("{}", f.name);
+            let enum_name_2 = enum_name.clone();
+            let arg_names: Vec<_> = f.args.iter().map(|a| format_ident!("{}", a.name)).collect();
+
+            if f.is_const_expr_fn() {
+                // Constant-expression nodes just form a constant type (eg. U32Val) and then
+                // call .into() to turn it into a constant Operand (eg. Operand::Const64).
+                let ty2 = format_ident!("{}", ty.clone());
+                let arg0 = arg_names[0].clone();
+                quote! {
+                    #enum_name_2::#case_name(#(#arg_names),*) => {
+                        #ty2::from(#arg0.clone()).into()
+                    }
+                }
+            } else if f.is_new_or_existing_expr_fn() {
+                // "New-or-existing-expression" nodes take a u8 index and pass
+                // it to a lookup function on the FuncEmitter, to find an
+                // existing Operand with the associated tag, falling back to
+                // calling _new when there is nothing found.
+                let arg0 = arg_names[0].clone();
+                let ty2 = ty.clone();
+                let new_stem = format_ident!("{}", f.name.trim_end_matches(OR_EXISTING_SUFFIX));
+                quote! {
+                    #enum_name_2::#case_name(#(#arg_names),*) => {
+                        _func_emitter.maybe_choose_local(#ty, *#arg0)
+                        .unwrap_or_else(|| {
+                            _func_emitter.#new_stem();
+                            _func_emitter.alloc_and_store_local(#ty2)
+                        })
+                        .into()
+                    }
+
+                }
+            } else if all_subty_fns.contains(&f.name) {
+                // Subtype-wrapping "functions" (cases in the enum) just pass
+                // through to the inner type.
+                quote! {
+                    #enum_name_2::#case_name(x) => x.emit(_func_emitter),
+                }
+            } else {
+                // Non-leaf functions involve making a subcall to each argument sub-expr, and
+                // then calling the named function with those subcall results.
+                let arg_names_2 = arg_names.clone();
+                let subcalls: Vec<_> = f
+                    .args
+                    .iter()
+                    .map(|a| {
+                        let arg_expr = format_ident!("{}", a.name);
+                        quote! { let #arg_expr = #arg_expr.emit(_func_emitter); }
+                    })
+                    .collect();
+                quote! {
+                    #enum_name_2::#case_name(#(#arg_names),*) => {
+                        #(#subcalls)*
+                        _func_emitter.#case_name(#(#arg_names_2),*);
+                        _func_emitter.alloc_and_store_local(#ty).into()
+                    }
+                }
+            }
+        });
+        let num_locals_cases = fns.iter().map(|f| {
+            let case_name = format_ident!("{}", f.name);
+            let arg_names: Vec<_> = if f.is_const_expr_fn() || f.is_new_or_existing_expr_fn() {
+                vec![format_ident!("_")]
+            } else {
+                f.args.iter().map(|a| format_ident!("{}", a.name)).collect()
+            };
+            let subcalls: Vec<_> = if f.is_const_expr_fn() {
+                vec![quote! { 0 }]
+            } else if f.is_new_or_existing_expr_fn() {
+                vec![quote! { 1 }]
+            } else if all_subty_fns.contains(&f.name) {
+                // Subtype-wrapping "functions" (cases in the enum) just pass
+                // through to the inner type.
+                let arg0 = arg_names[0].clone();
+                vec![quote! {
+                    #arg0.num_locals()
+                }]
+            } else {
+                f.args
+                    .iter()
+                    .map(|a| {
+                        let arg_expr = format_ident!("{}", a.name);
+                        quote! { #arg_expr.num_locals() + }
+                    })
+                    .chain(std::iter::once(quote! { 1 }))
+                    .collect()
+            };
+            let enum_name_2 = enum_name.clone();
+            quote! {
+                #enum_name_2::#case_name(#(#arg_names),*) => {
+                    #(#subcalls)*
+                }
+            }
+        });
+        quote! {
+            impl crate::Emit for #enum_name {
+                fn emit(&self, _func_emitter: &mut crate::FuncEmitter) -> crate::Operand {
+                    match self {
+                        #(#emit_cases)*
+                    }
+                }
+                fn num_locals(&self) -> u32 {
+                    match self {
+                        #(#num_locals_cases)*
+                    }
+                }
+            }
+        }
+    });
+
+    Ok(quote! {
+        use arbitrary::Arbitrary;
+        use soroban_env_common::{StorageType, xdr::ScError, Void, Bool, U32Val, I32Val, Error, Symbol};
+        #(#enums)*
+        #(#impls)*
+    })
+}
diff --git a/soroban-synth-wasm/Cargo.toml b/soroban-synth-wasm/Cargo.toml
index c025b2d0..40a112b6 100644
--- a/soroban-synth-wasm/Cargo.toml
+++ b/soroban-synth-wasm/Cargo.toml
@@ -14,9 +14,12 @@ publish = false
 wasm-encoder = "0.18.0"
 wasmparser = "0.106.0"
 soroban-env-common = { workspace = true }
+arbitrary = { version = "1.3.0", features = ["derive"] }
+soroban-env-macros = { workspace = true }
 
 [features]
 next = ["soroban-env-common/next"]
+testutils = ["soroban-env-common/testutils"]
 
 [dev-dependencies]
 expect-test = "1.4.0"
diff --git a/soroban-synth-wasm/src/expr.rs b/soroban-synth-wasm/src/expr.rs
new file mode 100644
index 00000000..10e0f950
--- /dev/null
+++ b/soroban-synth-wasm/src/expr.rs
@@ -0,0 +1,68 @@
+//! This module contains "expressions": a recursive datatype that describes
+//! well-formed (and well-typed) program fragments we can translate to WASM
+//! using the [`FuncEmitter`] interface.
+//!
+//! Each expresison type in this module implements the [`Emit`] trait that
+//! generates some WASM code (depending on the expression type and variant) and
+//! returns a weakly-typed [`Operand`], which can in turn be used as an input to
+//! code emitting any enclosing expression. The higher-level [`Val`] type that
+//! each such [`Operand`] actually encodes depends on the expression type. For
+//! example the [`Operand`]s returned from emitting any [`MapExpr`] will encode
+//! an object handle with tag [`Tag::MapObject`].
+//!
+//! Despite the fact that WASM is loosely-speaking "a stack machine", for a
+//! couple different reasons when generating code here we have each expression
+//! leave _nothing_ on the WASM stack, instead storing its result to a local and
+//! returning a [`LocalRef`] to it. This both fits other parts of the code
+//! generation interface better and allows us to recycle locals by type between
+//! expressions without having to model or shuffle the stack.
+//!
+//! The expression types exist partly to facilitate higher-level code generation
+//! but mostly so that the fuzzer can derive and generate [`Arbitrary`]
+//! instances of them, as concrete datatypes fed to a fuzz target.
+
+#![allow(non_camel_case_types)]
+#![allow(dead_code)]
+#![allow(unused_imports)]
+use crate::{Arity, FuncEmitter, ModEmitter, Operand};
+use soroban_env_macros::generate_synth_wasm_expr_type;
+
+pub trait Emit {
+    /// Drive a [`FuncEmitter`] to generate WASM code that evaluates `self`,
+    /// then return an [`Operand`] that refers to the result of that evaluation,
+    /// usually a [`LocalRef`]
+    fn emit(&self, func: &mut FuncEmitter) -> Operand;
+
+    /// Return the number of local variables required to emit this expression. We
+    /// need this because our WASM emitter needs a local-variable count up front
+    /// before it emits. Typically this will be the sum of all of the
+    /// num_locals() results from all of self's subexpressions plus one.
+    fn num_locals(&self) -> u32;
+
+    fn as_single_function_wasm_module(&self, name: &str) -> Vec<u8> {
+        let n_locals = self.num_locals();
+        let mut fe = ModEmitter::new().func(Arity(0), n_locals);
+        let result = self.emit(&mut fe);
+        fe.push(result);
+        fe.finish_and_export(name).finish()
+    }
+}
+
+// This is a little awkward, but ExprVal is such a branchy type that the default
+// "20 levels deep" recursion that Arbitrary allows in its depth limiter never
+// even manages to finish calculating `size_hint` recursively while starting up.
+// We avoid this by wrapping ExprVal in our own Arbitrary impl that is .. a lot
+// more aggressive.
+#[derive(Clone, Debug)]
+pub struct Expr(pub ExprVal);
+impl arbitrary::Arbitrary<'_> for Expr {
+    fn size_hint(_depth: usize) -> (usize, Option<usize>) {
+        (0, None)
+    }
+
+    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
+        Ok(Expr(ExprVal::arbitrary(u)?))
+    }
+}
+
+generate_synth_wasm_expr_type!("../soroban-env-common/env.json");
diff --git a/soroban-synth-wasm/src/func_emitter.rs b/soroban-synth-wasm/src/func_emitter.rs
index d29bd7cb..7f6b0208 100644
--- a/soroban-synth-wasm/src/func_emitter.rs
+++ b/soroban-synth-wasm/src/func_emitter.rs
@@ -1,5 +1,7 @@
 use crate::{Arity, FuncRef, GlobalRef, ModEmitter, TypeRef};
-use soroban_env_common::{xdr::ScError, Error, Symbol, Tag, Val};
+use soroban_env_common::{
+    xdr::ScError, Bool, Error, I32Val, StorageType, Symbol, Tag, U32Val, Val, Void,
+};
 use wasm_encoder::{BlockType, Function, Instruction, MemArg, ValType};
 
 /// An index into the _locals_ for the current function, which may refer
@@ -29,30 +31,69 @@ impl From<LocalRef> for Operand {
     }
 }
 
+impl From<i64> for Operand {
+    fn from(r: i64) -> Self {
+        Operand::Const64(r)
+    }
+}
+
+impl From<u64> for Operand {
+    fn from(r: u64) -> Self {
+        Operand::Const64(r as i64)
+    }
+}
+
+impl From<StorageType> for Operand {
+    fn from(r: StorageType) -> Self {
+        Operand::Const64(r as i64)
+    }
+}
+
 impl From<Val> for Operand {
     fn from(r: Val) -> Self {
         Operand::Const64(r.get_payload() as i64)
     }
 }
 
+impl From<Bool> for Operand {
+    fn from(s: Bool) -> Self {
+        Operand::from(Val::from(s))
+    }
+}
+
+impl From<Void> for Operand {
+    fn from(s: Void) -> Self {
+        Operand::from(Val::from(s))
+    }
+}
+
+impl From<U32Val> for Operand {
+    fn from(s: U32Val) -> Self {
+        Operand::from(Val::from(s))
+    }
+}
+
+impl From<I32Val> for Operand {
+    fn from(s: I32Val) -> Self {
+        Operand::from(Val::from(s))
+    }
+}
+
 impl From<Symbol> for Operand {
     fn from(s: Symbol) -> Self {
-        let r: Val = s.into();
-        r.into()
+        Operand::from(Val::from(s))
     }
 }
 
 impl From<Error> for Operand {
     fn from(s: Error) -> Self {
-        let r: Val = s.into();
-        r.into()
+        Operand::from(Val::from(s))
     }
 }
 
 impl From<ScError> for Operand {
     fn from(s: ScError) -> Self {
-        let r: Val = s.into();
-        r.into()
+        Operand::from(Val::from(s))
     }
 }
 
@@ -77,13 +118,18 @@ pub struct FuncEmitter {
     pub args: Vec<LocalRef>,
 
     /// A vector of [`LocalRef`]s that cover the locals declared inside the
-    /// function, with local indexes starting _after_ the argument indexes.
-    /// If the [`FuncEmitter`] is constructed with `n_locals=k` then the
-    /// `locals` vector will be `k` elements long, and the `n`th element of
-    /// it will be the `n`th local variable, regardless of the argument count
-    /// (it will happen to have index `n+a` where `a` is the arity of the
-    /// function, but the point here is to let client code ignore that fact).
-    pub locals: Vec<LocalRef>,
+    /// function, with local indexes starting _after_ the argument indexes. If
+    /// the [`FuncEmitter`] is constructed with `n_locals=k` then the `locals`
+    /// vector will be `k` elements long, and the `n`th element of it will be
+    /// the `n`th local variable, regardless of the argument count (it will
+    /// happen to have index `n+a` where `a` is the arity of the function, but
+    /// the point here is to let client code ignore that fact).
+    ///
+    /// We also store an optional `&'static str` associated with each local,
+    /// indicating that it's _allocated_ to a specific type of value (with
+    /// meaning of the strings decided by the caller). This is a convention
+    /// maintained by [`FuncEmitter::alloc_local`].
+    pub locals: Vec<(LocalRef, Option<&'static str>)>,
 }
 
 impl FuncEmitter {
@@ -95,7 +141,9 @@ impl FuncEmitter {
     pub fn new(mod_emit: ModEmitter, arity: Arity, n_locals: u32) -> Self {
         let func = Function::new([(n_locals, ValType::I64)]);
         let args = (0..arity.0).map(LocalRef).collect();
-        let locals = (arity.0..arity.0 + n_locals).map(LocalRef).collect();
+        let locals = (arity.0..arity.0 + n_locals)
+            .map(|n| (LocalRef(n), None))
+            .collect();
         Self {
             mod_emit,
             arity,
@@ -105,6 +153,44 @@ impl FuncEmitter {
         }
     }
 
+    pub fn alloc_local(&mut self, ty: &'static str) -> LocalRef {
+        for (lref, ltag) in self.locals.iter_mut() {
+            if ltag.is_none() {
+                *ltag = Some(ty);
+                return *lref;
+            }
+        }
+        panic!(
+            "alloc_local exhausted {} locals without finding room for {:?}",
+            self.locals.len(),
+            ty
+        )
+    }
+
+    pub fn alloc_and_store_local(&mut self, ty: &'static str) -> LocalRef {
+        let lr = self.alloc_local(ty);
+        self.local_set(lr);
+        lr
+    }
+
+    // A utility function for selecting a typed local based on an index into
+    // the subsequence of locals with a given tag. If no appropriate local can
+    // be found, we return None.
+    pub fn maybe_choose_local(&mut self, ty: &'static str, index: u8) -> Option<LocalRef> {
+        let index = (index as usize) % self.locals.len();
+        if let Some((local, _)) = self
+            .locals
+            .iter()
+            .filter(|(_, tyopt)| *tyopt == Some(ty))
+            .cycle()
+            .nth(index)
+        {
+            Some(*local)
+        } else {
+            None
+        }
+    }
+
     /// Emit an instruction into the function.
     pub fn insn(&mut self, i: &Instruction) -> &mut Self {
         self.func.instruction(i);
diff --git a/soroban-synth-wasm/src/lib.rs b/soroban-synth-wasm/src/lib.rs
index 10279f74..1ef6133b 100644
--- a/soroban-synth-wasm/src/lib.rs
+++ b/soroban-synth-wasm/src/lib.rs
@@ -17,3 +17,12 @@ mod test;
 
 pub use func_emitter::{FuncEmitter, LocalRef, Operand};
 pub use mod_emitter::{Arity, FuncRef, GlobalRef, ModEmitter, TypeRef};
+
+// mod expr is gated on testutils because it uses Arbitrary and
+// enables soroban-env-common/testutils to get its implementations
+// of arbitrary, and we only want to enable that selectively (when
+// the host we're linked to has also enabled common/testutils)
+#[cfg(feature = "testutils")]
+mod expr;
+#[cfg(feature = "testutils")]
+pub use expr::{Emit, Expr};


Test patch:
diff --git a/soroban-synth-wasm/src/test.rs b/soroban-synth-wasm/src/test.rs
index c41be588..0355cf9b 100644
--- a/soroban-synth-wasm/src/test.rs
+++ b/soroban-synth-wasm/src/test.rs
@@ -6,8 +6,8 @@ use wasmprinter::print_bytes;
 #[test]
 fn test_synth_wasm() {
     let mut fe = ModEmitter::new().func(Arity(0), 2);
-    let s = fe.locals[0];
-    let tmp = fe.locals[1];
+    let s = fe.locals[0].0;
+    let tmp = fe.locals[1].0;
 
     fe.push(Symbol::try_from_small_str("somekey").unwrap());
     fe.local_set(s);