From 21e5e1d8d237b889e0be1dffb724aea5e84709b1 Mon Sep 17 00:00:00 2001 From: Manuel Guilherme Date: Fri, 14 Aug 2026 18:52:28 -0300 Subject: [PATCH] Fix panic in `contract info interface` when WASM has env meta but no spec `contractenvmetav0` and `contractspecv0` are independent custom sections, so a WASM can carry env meta without a spec. The command guarded on `env_meta_base64` being present but then unwrapped `spec_base64`, panicking instead of reporting "no interface present". Extract the WASM branch into `spec_from_wasm` so it is unit-testable, replace the unwrap with a clean `NoInterfacePresent` error, and add regression tests that build minimal WASM modules with `wasm-encoder` (already used by soroban-spec-tools tests). Fixes #2429 --- Cargo.lock | 1 + cmd/soroban-cli/Cargo.toml | 1 + .../src/commands/contract/info/interface.rs | 66 ++++++++++++++++--- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a63546cca6..a236ce8349 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5464,6 +5464,7 @@ dependencies = [ "ulid", "url", "walkdir", + "wasm-encoder 0.235.0", "wasm-gen", "wasm-opt", "wasmparser 0.116.1", diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index 76d6fc0323..c379d71d5e 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -142,3 +142,4 @@ predicates = { workspace = true } walkdir = "2.5.0" mockito = "1.5.0" serial_test = "3.0.0" +wasm-encoder = "0.235.0" diff --git a/cmd/soroban-cli/src/commands/contract/info/interface.rs b/cmd/soroban-cli/src/commands/contract/info/interface.rs index a2dfa1de38..9658cd7e7a 100644 --- a/cmd/soroban-cli/src/commands/contract/info/interface.rs +++ b/cmd/soroban-cli/src/commands/contract/info/interface.rs @@ -51,15 +51,7 @@ impl Cmd { let Fetched { contract, .. } = fetch(&self.common, &print).await?; let (base64, spec) = match contract { - shared::Contract::Wasm { wasm_bytes } => { - let spec = Spec::new(&wasm_bytes)?; - - if spec.env_meta_base64.is_none() { - return Err(NoInterfacePresent()); - } - - (spec.spec_base64.unwrap(), spec.spec) - } + shared::Contract::Wasm { wasm_bytes } => spec_from_wasm(&wasm_bytes)?, shared::Contract::StellarAssetContract => { Spec::spec_to_base64(stellar_asset_spec::xdr())? } @@ -85,3 +77,59 @@ impl Cmd { Ok(()) } } + +fn spec_from_wasm(wasm_bytes: &[u8]) -> Result<(String, Vec), Error> { + let spec = Spec::new(wasm_bytes)?; + + if spec.env_meta_base64.is_none() { + return Err(NoInterfacePresent()); + } + + // `contractenvmetav0` and `contractspecv0` are independent custom + // sections, so the presence of env meta does not imply a spec is present. + let Some(base64) = spec.spec_base64 else { + return Err(NoInterfacePresent()); + }; + + Ok((base64, spec.spec)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::borrow::Cow; + + /// A minimal WASM module containing only the given custom sections. + fn wasm_with_custom_sections(sections: &[(&str, &[u8])]) -> Vec { + let mut module = wasm_encoder::Module::new(); + for (name, data) in sections { + module.section(&wasm_encoder::CustomSection { + name: Cow::Borrowed(name), + data: Cow::Borrowed(data), + }); + } + module.finish() + } + + #[test] + fn env_meta_without_spec_returns_no_interface_error() { + // Regression test for https://github.com/stellar/stellar-cli/issues/2429: + // `contractenvmetav0` and `contractspecv0` are independent custom + // sections, so a WASM can carry env meta without a spec. This used to + // panic on `spec.spec_base64.unwrap()`. + let wasm = wasm_with_custom_sections(&[("contractenvmetav0", &[])]); + + let result = spec_from_wasm(&wasm); + + assert!(matches!(result, Err(Error::NoInterfacePresent()))); + } + + #[test] + fn missing_env_meta_returns_no_interface_error() { + let wasm = wasm_with_custom_sections(&[]); + + let result = spec_from_wasm(&wasm); + + assert!(matches!(result, Err(Error::NoInterfacePresent()))); + } +}