Skip to content

Commit c22527c

Browse files
fix(values): un-box memory-allocated enum payload in Value::to_ptr (#1650)
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) <noreply@anthropic.com>
1 parent 5dd367b commit c22527c

3 files changed

Lines changed: 88 additions & 0 deletions

File tree

src/values.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,18 @@ impl Value {
327327
native_assert!(*tag < info.variants.len(), "Variant index out of range.");
328328

329329
let payload_type_id = &info.variants[*tag];
330+
let payload_ty = registry.get_type(payload_type_id)?;
330331
let payload = value.to_ptr(arena, registry, payload_type_id)?;
331332

333+
// Undo the wrapper pointer added when the payload is memory
334+
// allocated (e.g. a nested >=2-variant enum), so that the copy
335+
// below reads the payload data rather than the wrapper pointer.
336+
let payload = if payload_ty.is_memory_allocated(registry)? {
337+
*payload.cast::<NonNull<()>>().as_ref()
338+
} else {
339+
payload
340+
};
341+
332342
let (layout, tag_layout, variant_layouts) =
333343
crate::types::r#enum::get_layout_for_variants(
334344
registry,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Both `Inner` and `Outer` are 2-variant enums, so both are memory-allocated.
2+
// Passing an `Outer` as an argument forces `to_ptr` to serialize a nested
3+
// memory-allocated enum. The return value depends on both discriminants and the
4+
// payload, so any corruption of the nested value changes the result.
5+
6+
#[derive(Drop)]
7+
enum Inner {
8+
A: felt252,
9+
B: felt252,
10+
}
11+
12+
#[derive(Drop)]
13+
enum Outer {
14+
X: Inner,
15+
Y: Inner,
16+
}
17+
18+
fn run_test(x: Outer) -> felt252 {
19+
match x {
20+
Outer::X(inner) => match inner {
21+
Inner::A(v) => v,
22+
Inner::B(v) => v + 1,
23+
},
24+
Outer::Y(inner) => match inner {
25+
Inner::A(v) => v + 2,
26+
Inner::B(v) => v + 3,
27+
},
28+
}
29+
}

tests/tests/enums.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,52 @@ fn single_variant_enum_in_array_matches_vm() {
5050
)
5151
.expect("single-variant enum in array must agree between VM and native");
5252
}
53+
54+
#[test]
55+
fn nested_enum_argument_matches_vm() {
56+
let program = &load_program_and_runner("programs/nested_enum_arg");
57+
58+
// (outer_tag, inner_tag). Both enums have 2 variants, so the tag equals the
59+
// variant index (no selector encoding is needed).
60+
for (outer_tag, inner_tag) in [(0, 0), (0, 1), (1, 0), (1, 1)] {
61+
let payload = Felt::from(0x1234);
62+
63+
let result_vm = run_vm_program(
64+
program,
65+
"run_test",
66+
vec![
67+
Arg::Value(Felt::from(outer_tag as u64)),
68+
Arg::Value(Felt::from(inner_tag as u64)),
69+
Arg::Value(payload),
70+
],
71+
Some(DEFAULT_GAS as usize),
72+
)
73+
.unwrap();
74+
75+
let result_native = run_native_program(
76+
program,
77+
"run_test",
78+
&[Value::Enum {
79+
tag: outer_tag,
80+
value: Box::new(Value::Enum {
81+
tag: inner_tag,
82+
value: Box::new(Value::Felt252(payload)),
83+
debug_name: None,
84+
}),
85+
debug_name: None,
86+
}],
87+
Some(DEFAULT_GAS),
88+
Option::<DummySyscallHandler>::None,
89+
);
90+
91+
compare_outputs(
92+
&program.1,
93+
&program.2.find_function("run_test").unwrap().id,
94+
&result_vm,
95+
&result_native,
96+
)
97+
.unwrap_or_else(|e| {
98+
panic!("nested enum (outer={outer_tag}, inner={inner_tag}) mismatch: {e:?}")
99+
});
100+
}
101+
}

0 commit comments

Comments
 (0)