Fix panic when loading malformed Wasm files (cherry-pick #10370) (#10371)

gcp-cherry-pick-bot[bot] , Marshall Bowers , and Max created

Cherry-picked Fix panic when loading malformed Wasm files (#10370)

This PR fixes a potential panic that could occur when loading malformed
Wasm files.

We now use the `parse_wasm_extension_version` function that was
previously used just to extract the Zed extension API version from the
Wasm bytes as a pre-validation step. By parsing the entirety of the Wasm
file here instead of returning as soon as we find the version, the
invalid Wasm bytes are now surfaced as an `Err` instead of a panic.

We were able to replicate the panic using the following test:

```rs
#[gpui::test]
async fn test_bad_wasm(cx: &mut TestAppContext) {
    init_test(cx);

    let wasm_host = cx.update(|cx| {
        WasmHost::new(
            FakeFs::new(cx.background_executor().clone()),
            FakeHttpClient::with_200_response(),
            FakeNodeRuntime::new(),
            Arc::new(LanguageRegistry::test(cx.background_executor().clone())),
            PathBuf::from("/the/work/dir".to_string()),
            cx,
        )
    });

    let mut wasm_bytes = std::fs::read("/Users/maxdeviant/Library/Application Support/Zed/extensions/installed/dart/extension.wasm").unwrap();

    // This is the error message we were seeing in the stack trace:
    // range end index 267037 out of range for slice of length 253952

    dbg!(&wasm_bytes.len());

    // Truncate the bytes to the same point:
    wasm_bytes.truncate(253952);

    std::fs::write("/tmp/bad-extension.wasm", wasm_bytes.clone()).unwrap();

    let manifest = Arc::new(ExtensionManifest {
        id: "the-extension".into(),
        name: "The Extension".into(),
        version: "0.0.1".into(),
        schema_version: SchemaVersion(1),
        description: Default::default(),
        repository: Default::default(),
        authors: Default::default(),
        lib: LibManifestEntry {
            kind: None,
            version: None,
        },
        themes: Default::default(),
        languages: Default::default(),
        grammars: Default::default(),
        language_servers: Default::default(),
    });

    // 💥
    let result = wasm_host
        .load_extension(wasm_bytes, manifest, cx.executor())
        .await;
    dbg!(result.map(|_| ()));
```



Release Notes:

- Fixed a crash that could occur when loading malformed Wasm extensions
([#10352](https://github.com/zed-industries/zed/issues/10352)).

---------

Co-authored-by: Max <max@zed.dev>

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
Co-authored-by: Max <max@zed.dev>

Change summary

crates/extension/src/wasm_host.rs | 20 ++++++++++++++------
1 file changed, 14 insertions(+), 6 deletions(-)

Detailed changes

crates/extension/src/wasm_host.rs 🔗

@@ -198,13 +198,15 @@ pub fn parse_wasm_extension_version(
     extension_id: &str,
     wasm_bytes: &[u8],
 ) -> Result<SemanticVersion> {
+    let mut version = None;
+
     for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) {
-        if let wasmparser::Payload::CustomSection(s) = part? {
+        if let wasmparser::Payload::CustomSection(s) =
+            part.context("error parsing wasm extension")?
+        {
             if s.name() == "zed:api-version" {
-                let version = parse_wasm_extension_version_custom_section(s.data());
-                if let Some(version) = version {
-                    return Ok(version);
-                } else {
+                version = parse_wasm_extension_version_custom_section(s.data());
+                if version.is_none() {
                     bail!(
                         "extension {} has invalid zed:api-version section: {:?}",
                         extension_id,
@@ -214,7 +216,13 @@ pub fn parse_wasm_extension_version(
             }
         }
     }
-    bail!("extension {} has no zed:api-version section", extension_id)
+
+    // The reason we wait until we're done parsing all of the Wasm bytes to return the version
+    // is to work around a panic that can happen inside of Wasmtime when the bytes are invalid.
+    //
+    // By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem
+    // earlier as an `Err` rather than as a panic.
+    version.ok_or_else(|| anyhow!("extension {} has no zed:api-version section", extension_id))
 }
 
 fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option<SemanticVersion> {