From 27846886f97b4b324a2415d6cf701f27c6a5ef7a Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Sun, 1 Mar 2026 12:08:18 +0100 Subject: [PATCH 1/4] Fix use-after-free issue in moonbit --- crates/moonbit/src/lib.rs | 29 +++++++++++---- tests/runtime/list-in-variant/runner.mbt | 36 ++++++++++++++++++ tests/runtime/list-in-variant/runner.rs | 39 ++++++++++++++++++++ tests/runtime/list-in-variant/test.rs | 47 ++++++++++++++++++++++++ tests/runtime/list-in-variant/test.wit | 31 ++++++++++++++++ 5 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 tests/runtime/list-in-variant/runner.mbt create mode 100644 tests/runtime/list-in-variant/runner.rs create mode 100644 tests/runtime/list-in-variant/test.rs create mode 100644 tests/runtime/list-in-variant/test.wit diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index 3f0d13b0a..141925253 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -2369,6 +2369,22 @@ impl Bindgen for FunctionBindgen<'_, '_> { } Instruction::Return { amt, .. } => { + // Bind return operands to locals BEFORE cleanup to avoid + // use-after-free when operands contain inline loads from + // return_area or other freed memory. + let return_locals: Vec = if *amt > 0 { + operands + .iter() + .map(|op| { + let local = self.locals.tmp("ret"); + uwriteln!(self.src, "let {local} = {op}"); + local + }) + .collect() + } else { + Vec::new() + }; + for clean in &self.cleanup { let address = &clean.address; self.r#gen.ffi_imports.insert(ffi::FREE); @@ -2377,19 +2393,17 @@ impl Bindgen for FunctionBindgen<'_, '_> { if self.needs_cleanup_list { self.r#gen.ffi_imports.insert(ffi::FREE); - uwrite!( + uwriteln!( self.src, - " - cleanup_list.each(mbt_ffi_free) - ", + "cleanup_list.each(mbt_ffi_free)", ); } match *amt { 0 => (), - 1 => uwriteln!(self.src, "return {}", operands[0]), + 1 => uwriteln!(self.src, "return {}", return_locals[0]), _ => { - let results = operands.join(", "); + let results = return_locals.join(", "); uwriteln!(self.src, "return ({results})"); } } @@ -2747,11 +2761,10 @@ impl Bindgen for FunctionBindgen<'_, '_> { if !self.cleanup.is_empty() { self.needs_cleanup_list = true; - self.r#gen.ffi_imports.insert(ffi::FREE); for cleanup in &self.cleanup { let address = &cleanup.address; - uwriteln!(self.src, "mbt_ffi_free({address})",); + uwriteln!(self.src, "cleanup_list.push({address})",); } } diff --git a/tests/runtime/list-in-variant/runner.mbt b/tests/runtime/list-in-variant/runner.mbt new file mode 100644 index 000000000..00a92dc48 --- /dev/null +++ b/tests/runtime/list-in-variant/runner.mbt @@ -0,0 +1,36 @@ +//@ [lang] +//@ path = 'gen/world/runner/stub.mbt' +//@ pkg_config = """{ "import": ["test/list-in-variant/interface/test_/list_in_variant/toTest"] }""" + +///| +pub fn run() -> Unit { + // list-in-option + let r1 = @toTest.list_in_option(Some(["hello", "world"])) + guard r1 == "hello,world" + let r2 = @toTest.list_in_option(None) + guard r2 == "none" + + // list-in-variant + let r3 = @toTest.list_in_variant(@toTest.PayloadOrEmpty::WithData(["foo", "bar", "baz"])) + guard r3 == "foo,bar,baz" + let r4 = @toTest.list_in_variant(@toTest.PayloadOrEmpty::Empty) + guard r4 == "empty" + + // list-in-result + let r5 = @toTest.list_in_result(Ok(["a", "b", "c"])) + guard r5 == "a,b,c" + let r6 = @toTest.list_in_result(Err("oops")) + guard r6 == "err:oops" + + // list-in-option-with-return (Bug 1 + Bug 2) + let s1 = @toTest.list_in_option_with_return(Some(["hello", "world"])) + guard s1.count == 2U + guard s1.label == "hello,world" + let s2 = @toTest.list_in_option_with_return(None) + guard s2.count == 0U + guard s2.label == "none" + + // top-level-list (contrast) + let r7 = @toTest.top_level_list(["x", "y", "z"]) + guard r7 == "x,y,z" +} diff --git a/tests/runtime/list-in-variant/runner.rs b/tests/runtime/list-in-variant/runner.rs new file mode 100644 index 000000000..1aac734b7 --- /dev/null +++ b/tests/runtime/list-in-variant/runner.rs @@ -0,0 +1,39 @@ +include!(env!("BINDINGS")); + +use crate::test::list_in_variant::to_test::*; + +struct Component; + +export!(Component); + +impl Guest for Component { + fn run() { + // list-in-option (Bug 1: list freed inside match arm before FFI call) + let hw: Vec = ["hello", "world"].into_iter().map(Into::into).collect(); + assert_eq!(list_in_option(Some(&hw)), "hello,world"); + assert_eq!(list_in_option(None), "none"); + + // list-in-variant (Bug 1: same pattern with variant) + let fbb = PayloadOrEmpty::WithData(vec!["foo".into(), "bar".into(), "baz".into()]); + assert_eq!(list_in_variant(&fbb), "foo,bar,baz"); + assert_eq!(list_in_variant(&PayloadOrEmpty::Empty), "empty"); + + // list-in-result (Bug 1: same pattern with result) + let abc: Vec = ["a", "b", "c"].into_iter().map(Into::into).collect(); + assert_eq!(list_in_result(Ok(&abc)), "a,b,c"); + assert_eq!(list_in_result(Err("oops")), "err:oops"); + + // list-in-option-with-return (Bug 1 + Bug 2: freed list + return_area read-after-free) + let hw2: Vec = ["hello", "world"].into_iter().map(Into::into).collect(); + let s = list_in_option_with_return(Some(&hw2)); + assert_eq!(s.count, 2); + assert_eq!(s.label, "hello,world"); + let s = list_in_option_with_return(None); + assert_eq!(s.count, 0); + assert_eq!(s.label, "none"); + + // top-level-list (NOT affected — contrast case) + let xyz: Vec = ["x", "y", "z"].into_iter().map(Into::into).collect(); + assert_eq!(top_level_list(&xyz), "x,y,z"); + } +} diff --git a/tests/runtime/list-in-variant/test.rs b/tests/runtime/list-in-variant/test.rs new file mode 100644 index 000000000..e94b51dd6 --- /dev/null +++ b/tests/runtime/list-in-variant/test.rs @@ -0,0 +1,47 @@ +include!(env!("BINDINGS")); + +use crate::exports::test::list_in_variant::to_test::*; + +struct Component; + +export!(Component); + +impl exports::test::list_in_variant::to_test::Guest for Component { + fn list_in_option(data: Option>) -> String { + match data { + Some(list) => list.join(","), + None => "none".to_string(), + } + } + + fn list_in_variant(data: PayloadOrEmpty) -> String { + match data { + PayloadOrEmpty::WithData(list) => list.join(","), + PayloadOrEmpty::Empty => "empty".to_string(), + } + } + + fn list_in_result(data: Result, String>) -> String { + match data { + Ok(list) => list.join(","), + Err(e) => format!("err:{}", e), + } + } + + fn list_in_option_with_return(data: Option>) -> Summary { + match data { + Some(list) => Summary { + count: list.len() as u32, + label: list.join(","), + }, + None => Summary { + count: 0, + label: "none".to_string(), + }, + } + } + + fn top_level_list(items: Vec) -> String { + items.join(",") + } +} diff --git a/tests/runtime/list-in-variant/test.wit b/tests/runtime/list-in-variant/test.wit new file mode 100644 index 000000000..da156e5d1 --- /dev/null +++ b/tests/runtime/list-in-variant/test.wit @@ -0,0 +1,31 @@ +package test:list-in-variant; + +interface to-test { + list-in-option: func(data: option>) -> string; + + variant payload-or-empty { + empty, + with-data(list), + } + list-in-variant: func(data: payload-or-empty) -> string; + + list-in-result: func(data: result, string>) -> string; + + record summary { + count: u32, + label: string, + } + list-in-option-with-return: func(data: option>) -> summary; + + top-level-list: func(items: list) -> string; +} + +world test { + export to-test; +} + +world runner { + import to-test; + + export run: func(); +} From ff5a274384a309dcc757fe06f70d30a5a4820c0d Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Sun, 1 Mar 2026 12:11:12 +0100 Subject: [PATCH 2/4] Make the moonbit test runner work with the latest compiler --- crates/test/src/moonbit.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/test/src/moonbit.rs b/crates/test/src/moonbit.rs index 42c6bbad2..a967f97d9 100644 --- a/crates/test/src/moonbit.rs +++ b/crates/test/src/moonbit.rs @@ -60,11 +60,12 @@ impl LanguageMethods for MoonBit { } // Compile the MoonBit bindings to a wasm file + let manifest = compile.bindings_dir.join("moon.mod.json"); let mut cmd = Command::new("moon"); cmd.arg("build") .arg("--no-strip") // for debugging - .arg("-C") - .arg(compile.bindings_dir); + .arg("--manifest-path") + .arg(&manifest); runner.run_command(&mut cmd)?; // Build the component let artifact = compile @@ -101,19 +102,18 @@ impl LanguageMethods for MoonBit { } fn verify(&self, runner: &Runner, verify: &crate::Verify) -> anyhow::Result<()> { + let manifest = verify.bindings_dir.join("moon.mod.json"); let mut cmd = Command::new("moon"); cmd.arg("check") .arg("--warn-list") .arg("-28") // .arg("--deny-warn") - .arg("--source-dir") - .arg(verify.bindings_dir); + .arg("--manifest-path") + .arg(&manifest); runner.run_command(&mut cmd)?; let mut cmd = Command::new("moon"); - cmd.arg("build") - .arg("--source-dir") - .arg(verify.bindings_dir); + cmd.arg("build").arg("--manifest-path").arg(&manifest); runner.run_command(&mut cmd)?; Ok(()) From 799506f417bbca25e0580d6c15961220509f16a3 Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Sun, 1 Mar 2026 15:07:13 +0100 Subject: [PATCH 3/4] Make existing tests pass including fixed-length list --- crates/moonbit/src/lib.rs | 95 ++++++++++++++++++++++++++++++++++++-- crates/moonbit/src/pkg.rs | 3 ++ crates/test/src/moonbit.rs | 6 ++- 3 files changed, 98 insertions(+), 6 deletions(-) diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index 141925253..1a98768e5 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -2722,10 +2722,97 @@ impl Bindgen for FunctionBindgen<'_, '_> { Instruction::ErrorContextLower { .. } | Instruction::ErrorContextLift { .. } | Instruction::DropHandle { .. } => todo!(), - Instruction::FixedLengthListLift { .. } => todo!(), - Instruction::FixedLengthListLower { .. } => todo!(), - Instruction::FixedLengthListLowerToMemory { .. } => todo!(), - Instruction::FixedLengthListLiftFromMemory { .. } => todo!(), + Instruction::FixedLengthListLift { + element: _, + size, + id: _, + } => { + let array = self.locals.tmp("array"); + let mut elements = String::new(); + for a in operands.drain(0..(*size as usize)) { + elements.push_str(&a); + elements.push_str(", "); + } + uwriteln!( + self.src, + "let {array} : FixedArray[_] = [{elements}]" + ); + results.push(array); + } + Instruction::FixedLengthListLower { + element: _, + size, + id: _, + } => { + for i in 0..(*size as usize) { + results.push(format!("({})[{i}]", operands[0])); + } + } + Instruction::FixedLengthListLowerToMemory { + element, + size: _, + id: _, + } => { + let Block { + body, + results: block_results, + } = self.blocks.pop().unwrap(); + assert!(block_results.is_empty()); + + let vec = operands[0].clone(); + let target = operands[1].clone(); + let size = self.r#gen.r#gen.sizes.size(element).size_wasm32(); + let index = self.locals.tmp("index"); + + uwrite!( + self.src, + " + for {index} = 0; {index} < ({vec}).length(); {index} = {index} + 1 {{ + let iter_elem = ({vec})[{index}] + let iter_base = ({target}) + ({index} * {size}) + {body} + }} + ", + ); + } + Instruction::FixedLengthListLiftFromMemory { + element, + size: fll_size, + id: _, + } => { + let Block { + body, + results: block_results, + } = self.blocks.pop().unwrap(); + let address = &operands[0]; + let array = self.locals.tmp("array"); + let ty = self + .r#gen + .r#gen + .pkg_resolver + .type_name(self.r#gen.name, element); + let elem_size = self.r#gen.r#gen.sizes.size(element).size_wasm32(); + let index = self.locals.tmp("index"); + + let result = match &block_results[..] { + [result] => result, + _ => todo!("result count == {}", block_results.len()), + }; + + uwrite!( + self.src, + " + let {array} : Array[{ty}] = [] + for {index} = 0; {index} < {fll_size}; {index} = {index} + 1 {{ + let iter_base = ({address}) + ({index} * {elem_size}) + {body} + {array}.push({result}) + }} + ", + ); + + results.push(format!("FixedArray::from_array({array}[:])")); + } } } diff --git a/crates/moonbit/src/pkg.rs b/crates/moonbit/src/pkg.rs index bdaa5b34d..e7c1a1057 100644 --- a/crates/moonbit/src/pkg.rs +++ b/crates/moonbit/src/pkg.rs @@ -203,6 +203,9 @@ impl PkgResolver { } _ => format!("Array[{}]", self.type_name(this, &ty)), }, + TypeDefKind::FixedLengthList(ty, _size) => { + format!("FixedArray[{}]", self.type_name(this, &ty)) + } TypeDefKind::Tuple(tuple) => { format!( "({})", diff --git a/crates/test/src/moonbit.rs b/crates/test/src/moonbit.rs index a967f97d9..e22d9489d 100644 --- a/crates/test/src/moonbit.rs +++ b/crates/test/src/moonbit.rs @@ -94,11 +94,13 @@ impl LanguageMethods for MoonBit { fn should_fail_verify( &self, - _name: &str, + name: &str, config: &crate::config::WitConfig, _args: &[String], ) -> bool { - config.async_ + // async-resource-func actually works, but most other async tests + // fail during codegen or verification + config.async_ && name != "async-resource-func.wit" } fn verify(&self, runner: &Runner, verify: &crate::Verify) -> anyhow::Result<()> { From 549463135aff552ab3fd4db27bea8d17086ffedf Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Sun, 1 Mar 2026 15:18:58 +0100 Subject: [PATCH 4/4] fmt --- crates/moonbit/src/lib.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index 1a98768e5..7f8edbc6c 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -2393,10 +2393,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { if self.needs_cleanup_list { self.r#gen.ffi_imports.insert(ffi::FREE); - uwriteln!( - self.src, - "cleanup_list.each(mbt_ffi_free)", - ); + uwriteln!(self.src, "cleanup_list.each(mbt_ffi_free)",); } match *amt { @@ -2733,10 +2730,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { elements.push_str(&a); elements.push_str(", "); } - uwriteln!( - self.src, - "let {array} : FixedArray[_] = [{elements}]" - ); + uwriteln!(self.src, "let {array} : FixedArray[_] = [{elements}]"); results.push(array); } Instruction::FixedLengthListLower {