From 772c48202c3e71d00f55a6d2f1195dc0065e02ed Mon Sep 17 00:00:00 2001 From: TomerStarkware Date: Sun, 19 Jul 2026 15:49:45 +0300 Subject: [PATCH] fix(values): un-box memory-allocated enum payload in Value::to_ptr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `Enum` arm of `Value::to_ptr` copied the payload straight from the pointer returned by the payload's own `to_ptr`. When the payload is a memory-allocated type (e.g. a nested >=2-variant enum), that pointer is a wrapper (a pointer to the data pointer), so the copy read the wrapper pointer bytes plus adjacent arena garbage into the variant slot instead of the actual payload — silently corrupting nested enums with no crash. Un-box the payload when it is memory-allocated, mirroring the existing `Struct` arm. Only reachable via the `invoke_dynamic(&[Value])` API; the Starknet contract path marshals felts directly and is unaffected. Adds a VM-vs-native regression test (`nested_enum_argument_matches_vm`) covering all four variant combinations of a nested 2-variant enum argument. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/values.rs | 10 +++++ test_data/programs/nested_enum_arg.cairo | 29 ++++++++++++++ tests/tests/enums.rs | 49 ++++++++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 test_data/programs/nested_enum_arg.cairo diff --git a/src/values.rs b/src/values.rs index abc0a19f4..c266cb482 100644 --- a/src/values.rs +++ b/src/values.rs @@ -327,8 +327,18 @@ impl Value { native_assert!(*tag < info.variants.len(), "Variant index out of range."); let payload_type_id = &info.variants[*tag]; + let payload_ty = registry.get_type(payload_type_id)?; let payload = value.to_ptr(arena, registry, payload_type_id)?; + // Undo the wrapper pointer added when the payload is memory + // allocated (e.g. a nested >=2-variant enum), so that the copy + // below reads the payload data rather than the wrapper pointer. + let payload = if payload_ty.is_memory_allocated(registry)? { + *payload.cast::>().as_ref() + } else { + payload + }; + let (layout, tag_layout, variant_layouts) = crate::types::r#enum::get_layout_for_variants( registry, diff --git a/test_data/programs/nested_enum_arg.cairo b/test_data/programs/nested_enum_arg.cairo new file mode 100644 index 000000000..3054eebbf --- /dev/null +++ b/test_data/programs/nested_enum_arg.cairo @@ -0,0 +1,29 @@ +// Both `Inner` and `Outer` are 2-variant enums, so both are memory-allocated. +// Passing an `Outer` as an argument forces `to_ptr` to serialize a nested +// memory-allocated enum. The return value depends on both discriminants and the +// payload, so any corruption of the nested value changes the result. + +#[derive(Drop)] +enum Inner { + A: felt252, + B: felt252, +} + +#[derive(Drop)] +enum Outer { + X: Inner, + Y: Inner, +} + +fn run_test(x: Outer) -> felt252 { + match x { + Outer::X(inner) => match inner { + Inner::A(v) => v, + Inner::B(v) => v + 1, + }, + Outer::Y(inner) => match inner { + Inner::A(v) => v + 2, + Inner::B(v) => v + 3, + }, + } +} diff --git a/tests/tests/enums.rs b/tests/tests/enums.rs index 0d824c890..8720ea936 100644 --- a/tests/tests/enums.rs +++ b/tests/tests/enums.rs @@ -50,3 +50,52 @@ fn single_variant_enum_in_array_matches_vm() { ) .expect("single-variant enum in array must agree between VM and native"); } + +#[test] +fn nested_enum_argument_matches_vm() { + let program = &load_program_and_runner("programs/nested_enum_arg"); + + // (outer_tag, inner_tag). Both enums have 2 variants, so the tag equals the + // variant index (no selector encoding is needed). + for (outer_tag, inner_tag) in [(0, 0), (0, 1), (1, 0), (1, 1)] { + let payload = Felt::from(0x1234); + + let result_vm = run_vm_program( + program, + "run_test", + vec![ + Arg::Value(Felt::from(outer_tag as u64)), + Arg::Value(Felt::from(inner_tag as u64)), + Arg::Value(payload), + ], + Some(DEFAULT_GAS as usize), + ) + .unwrap(); + + let result_native = run_native_program( + program, + "run_test", + &[Value::Enum { + tag: outer_tag, + value: Box::new(Value::Enum { + tag: inner_tag, + value: Box::new(Value::Felt252(payload)), + debug_name: None, + }), + debug_name: None, + }], + Some(DEFAULT_GAS), + Option::::None, + ); + + compare_outputs( + &program.1, + &program.2.find_function("run_test").unwrap().id, + &result_vm, + &result_native, + ) + .unwrap_or_else(|e| { + panic!("nested enum (outer={outer_tag}, inner={inner_tag}) mismatch: {e:?}") + }); + } +}