From 5e24d294da00519ef05b7fce88995d4f6cfbf487 Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Thu, 26 Mar 2026 00:32:51 +0100 Subject: [PATCH 1/4] tests: add regression test for variadic interface chain forwarding (#26760) Add test covering the pattern where interface arrays are passed through variadic parameter chains (caller -> Transaction.execute -> Statement.execute) using arrays.concat and the spread operator. This pattern was reported to cause parameter value corruption in certain environments. Ref #26760 Co-Authored-By: Claude Opus 4.6 (1M context) --- ...variadic_interface_chain_forwarding_test.v | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 vlib/v/tests/fns/variadic_interface_chain_forwarding_test.v diff --git a/vlib/v/tests/fns/variadic_interface_chain_forwarding_test.v b/vlib/v/tests/fns/variadic_interface_chain_forwarding_test.v new file mode 100644 index 00000000000000..0542d23d30bf83 --- /dev/null +++ b/vlib/v/tests/fns/variadic_interface_chain_forwarding_test.v @@ -0,0 +1,138 @@ +import arrays + +// Regression test for https://github.com/vlang/v/issues/26760 +// Interface values passed through variadic parameter chains must preserve +// their type tags and data pointers. + +interface Value {} + +fn process_params(params []Value) string { + mut result := []string{} + for i := 0; i < params.len; i++ { + param := params[i] + match param { + string { + result << 'string:${param}' + } + i32 { + result << 'i32:${param}' + } + []u8 { + result << '[]u8:${param.len}' + } + else { + result << 'unknown' + } + } + } + return result.join(', ') +} + +struct Statement { +mut: + handle int +} + +struct Transaction { +mut: + handle int +} + +fn (mut stmt Statement) execute(params ...Value) !string { + return process_params(params) +} + +fn (stmt Statement) close() ! {} + +fn (mut t Transaction) prepare(query string) !Statement { + return Statement{ handle: 1 } +} + +fn (mut t Transaction) execute(query string, params ...Value) !string { + mut stmt := t.prepare(query)! + result := stmt.execute(...params)! + stmt.close()! + return result +} + +struct ListParams { + email ?string + role ?string + offset i32 + fetch i32 +} + +fn get_conditions(p ListParams) (string, []Value) { + mut conditions := []string{} + mut params := []Value{} + + if email := p.email { + conditions = arrays.concat(conditions, 'email = ?') + params = arrays.concat(params, email) + } + + if role := p.role { + conditions = arrays.concat(conditions, 'role = ?') + params = arrays.concat(params, role) + } + + return conditions.join(' AND '), params +} + +fn test_variadic_interface_forwarding_with_match() ! { + p := ListParams{ + email: 'info@peony.com' + offset: 0 + fetch: 10 + } + + conditions, mut params := get_conditions(p) + params = arrays.concat(params, p.offset, p.fetch) + + mut tx := Transaction{ handle: 1 } + result := tx.execute('SELECT * FROM users WHERE ${conditions}', ...params)! + assert result == 'string:info@peony.com, i32:0, i32:10', 'got: ${result}' +} + +fn test_variadic_interface_forwarding_single_param() ! { + p := ListParams{ + email: 'info@peony.com' + offset: 0 + fetch: 10 + } + + conditions, params := get_conditions(p) + + mut tx := Transaction{ handle: 1 } + result := tx.execute('SELECT COUNT(*) FROM users WHERE ${conditions}', ...params)! + assert result == 'string:info@peony.com', 'got: ${result}' +} + +fn test_variadic_interface_forwarding_with_byte_arrays() ! { + user_id_bin := [u8(1), 2, 3, 4] + email := 'info@peony.com' + + mut params := [Value(user_id_bin), email] + + mut tx := Transaction{ handle: 1 } + result := tx.execute('INSERT INTO users', ...params)! + assert result == '[]u8:4, string:info@peony.com', 'got: ${result}' +} + +fn test_variadic_interface_forwarding_repeated() ! { + for _ in 0 .. 100 { + p := ListParams{ + email: 'info@peony.com' + role: 'admin' + offset: 5 + fetch: 20 + } + + conditions, mut params := get_conditions(p) + params = arrays.concat(params, p.offset, p.fetch) + + mut tx := Transaction{ handle: 1 } + result := tx.execute('SELECT * FROM users WHERE ${conditions}', ...params)! + assert result == 'string:info@peony.com, string:admin, i32:5, i32:20', 'got: ${result}' + } +} From 899fc7de57fba3f6e1442de4ee8f4e023b96fc5f Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Thu, 26 Mar 2026 11:26:49 +0100 Subject: [PATCH 2/4] fix: assert []u8 contents and fix vfmt formatting Address review feedback: check byte-array payload not just length, to catch pointer corruption that preserves array size. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...variadic_interface_chain_forwarding_test.v | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/vlib/v/tests/fns/variadic_interface_chain_forwarding_test.v b/vlib/v/tests/fns/variadic_interface_chain_forwarding_test.v index 0542d23d30bf83..ce1cac9e5b80e3 100644 --- a/vlib/v/tests/fns/variadic_interface_chain_forwarding_test.v +++ b/vlib/v/tests/fns/variadic_interface_chain_forwarding_test.v @@ -18,7 +18,7 @@ fn process_params(params []Value) string { result << 'i32:${param}' } []u8 { - result << '[]u8:${param.len}' + result << '[]u8:${param}' } else { result << 'unknown' @@ -45,7 +45,9 @@ fn (mut stmt Statement) execute(params ...Value) !string { fn (stmt Statement) close() ! {} fn (mut t Transaction) prepare(query string) !Statement { - return Statement{ handle: 1 } + return Statement{ + handle: 1 + } } fn (mut t Transaction) execute(query string, params ...Value) !string { @@ -89,7 +91,9 @@ fn test_variadic_interface_forwarding_with_match() ! { conditions, mut params := get_conditions(p) params = arrays.concat(params, p.offset, p.fetch) - mut tx := Transaction{ handle: 1 } + mut tx := Transaction{ + handle: 1 + } result := tx.execute('SELECT * FROM users WHERE ${conditions}', ...params)! assert result == 'string:info@peony.com, i32:0, i32:10', 'got: ${result}' } @@ -103,7 +107,9 @@ fn test_variadic_interface_forwarding_single_param() ! { conditions, params := get_conditions(p) - mut tx := Transaction{ handle: 1 } + mut tx := Transaction{ + handle: 1 + } result := tx.execute('SELECT COUNT(*) FROM users WHERE ${conditions}', ...params)! assert result == 'string:info@peony.com', 'got: ${result}' } @@ -114,9 +120,11 @@ fn test_variadic_interface_forwarding_with_byte_arrays() ! { mut params := [Value(user_id_bin), email] - mut tx := Transaction{ handle: 1 } + mut tx := Transaction{ + handle: 1 + } result := tx.execute('INSERT INTO users', ...params)! - assert result == '[]u8:4, string:info@peony.com', 'got: ${result}' + assert result == '[]u8:&[1, 2, 3, 4], string:info@peony.com', 'got: ${result}' } fn test_variadic_interface_forwarding_repeated() ! { @@ -131,7 +139,9 @@ fn test_variadic_interface_forwarding_repeated() ! { conditions, mut params := get_conditions(p) params = arrays.concat(params, p.offset, p.fetch) - mut tx := Transaction{ handle: 1 } + mut tx := Transaction{ + handle: 1 + } result := tx.execute('SELECT * FROM users WHERE ${conditions}', ...params)! assert result == 'string:info@peony.com, string:admin, i32:5, i32:20', 'got: ${result}' } From f4b032ab0f557943f5d1847e2eac7a1bf47625dd Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Thu, 26 Mar 2026 15:48:50 +0100 Subject: [PATCH 3/4] fix: heap-allocate interface values in array/variadic contexts When casting lvalues to interface types inside array literals or variadic argument packing, the generated C code used stack pointers (&local_var) for the interface object. These pointers become dangling when the stack frame ends, causing parameter corruption on Linux (confirmed on CI: string values read as empty or pointed to wrong stack data like the query string). Fix: set inside_cast_in_heap when generating interface/sumtype array elements and variadic args so that call_cfn_for_casting_expr uses HEAP() instead of bare &. This does not affect direct interface assignments (mut ii := II(aa)) which correctly share memory. Fixes #26760 Co-Authored-By: Claude Opus 4.6 (1M context) --- vlib/v/gen/c/array.v | 6 ++++++ vlib/v/gen/c/cgen.v | 9 ++++++++- vlib/v/gen/c/fn.v | 8 ++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/vlib/v/gen/c/array.v b/vlib/v/gen/c/array.v index b2d0f74e639546..65ab730ac02b6e 100644 --- a/vlib/v/gen/c/array.v +++ b/vlib/v/gen/c/array.v @@ -59,6 +59,9 @@ fn (mut g Gen) array_init(node ast.ArrayInit, var_name string) { g.write('\t\t') } is_iface_or_sumtype := elem_sym.kind in [.sum_type, .interface] + if is_iface_or_sumtype { + g.inside_cast_in_heap++ + } for i, expr in node.exprs { expr_type := if node.expr_types.len > i { node.expr_types[i] } else { node.elem_type } if expr_type == ast.string_type @@ -91,6 +94,9 @@ fn (mut g Gen) array_init(node ast.ArrayInit, var_name string) { } } } + if is_iface_or_sumtype { + g.inside_cast_in_heap-- + } g.write('}))') if g.is_shared { g.write('}, sizeof(${shared_styp}))') diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 2b69d19e19fdd1..4c90c178467cf7 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -3143,10 +3143,17 @@ fn (mut g Gen) call_cfn_for_casting_expr(fname string, expr ast.Expr, exp ast.Ty false } + // When casting an lvalue to an interface inside an array context + // (e.g. variadic args, array init), the interface stores a pointer + // that may outlive the source variable. Heap-allocate to prevent + // dangling pointers. (fixes #26760) + is_interface_in_heap_context := fname.contains('_to_Interface_') + && g.inside_cast_in_heap > 0 + if !is_cast_fixed_array_init && (is_comptime_variant || !expr.is_lvalue() || (expr is ast.Ident && (expr.obj.is_simple_define_const() || (expr.obj is ast.Var && expr.obj.is_index_var))) - || is_primitive_to_interface || is_fn_arg) { + || is_primitive_to_interface || is_fn_arg || is_interface_in_heap_context) { // Note: the `_to_sumtype_` family of functions do call memdup internally, making // another duplicate with the HEAP macro is redundant, so use ADDR instead: if expr.is_as_cast() { diff --git a/vlib/v/gen/c/fn.v b/vlib/v/gen/c/fn.v index a298a27a7125fb..247deeb2a47f28 100644 --- a/vlib/v/gen/c/fn.v +++ b/vlib/v/gen/c/fn.v @@ -2862,6 +2862,11 @@ fn (mut g Gen) call_args(node ast.CallExpr) { g.writeln('${g.styp(varg_type)} ${tmp_var};') g.write('builtin___option_ok((${base_type}[]) {') } + elem_sym := g.table.sym(arr_info.elem_type) + is_iface_or_sumtype := elem_sym.kind in [.sum_type, .interface] + if is_iface_or_sumtype { + g.inside_cast_in_heap++ + } g.write('builtin__new_array_from_c_array${noscan}(${variadic_count}, ${variadic_count}, sizeof(${elem_type}), _MOV((${elem_type}[${variadic_count}]){') for j in arg_nr .. args.len { g.ref_or_deref_arg(args[j], arr_info.elem_type, node.language, @@ -2871,6 +2876,9 @@ fn (mut g Gen) call_args(node ast.CallExpr) { } } g.write('}))') + if is_iface_or_sumtype { + g.inside_cast_in_heap-- + } if is_option { g.writeln(' }, (${option_name}*)&${tmp_var}, sizeof(${base_type}));') g.write(tmp) From 01a595baea2e52b31c644c002352eae7493716a7 Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Wed, 1 Apr 2026 19:02:28 +0200 Subject: [PATCH 4/4] fix: restrict inside_cast_in_heap to interface types only The original fix set inside_cast_in_heap for both sum types and interfaces in array/variadic contexts. This broke self-compilation because struct.v uses inside_cast_in_heap > 0 to add memdup wrapping for struct inits, producing invalid C for sum type arrays like []ast.Expr used pervasively in the compiler. Restrict the counter to .interface only: - array.v: keep is_iface_or_sumtype for string->cast check, use is_iface for counter increment only - fn.v: use is_iface for counter increment Also format cgen.v for vfmt compliance. --- vlib/v/gen/c/array.v | 5 +++-- vlib/v/gen/c/cgen.v | 3 ++- vlib/v/gen/c/fn.v | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/vlib/v/gen/c/array.v b/vlib/v/gen/c/array.v index 65ab730ac02b6e..e9e94e1e7eaf2c 100644 --- a/vlib/v/gen/c/array.v +++ b/vlib/v/gen/c/array.v @@ -59,7 +59,8 @@ fn (mut g Gen) array_init(node ast.ArrayInit, var_name string) { g.write('\t\t') } is_iface_or_sumtype := elem_sym.kind in [.sum_type, .interface] - if is_iface_or_sumtype { + is_iface := elem_sym.kind == .interface + if is_iface { g.inside_cast_in_heap++ } for i, expr in node.exprs { @@ -94,7 +95,7 @@ fn (mut g Gen) array_init(node ast.ArrayInit, var_name string) { } } } - if is_iface_or_sumtype { + if is_iface { g.inside_cast_in_heap-- } g.write('}))') diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 4c90c178467cf7..916b590c0f27ff 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -3153,7 +3153,8 @@ fn (mut g Gen) call_cfn_for_casting_expr(fname string, expr ast.Expr, exp ast.Ty if !is_cast_fixed_array_init && (is_comptime_variant || !expr.is_lvalue() || (expr is ast.Ident && (expr.obj.is_simple_define_const() || (expr.obj is ast.Var && expr.obj.is_index_var))) - || is_primitive_to_interface || is_fn_arg || is_interface_in_heap_context) { + || is_primitive_to_interface || is_fn_arg + || is_interface_in_heap_context) { // Note: the `_to_sumtype_` family of functions do call memdup internally, making // another duplicate with the HEAP macro is redundant, so use ADDR instead: if expr.is_as_cast() { diff --git a/vlib/v/gen/c/fn.v b/vlib/v/gen/c/fn.v index 247deeb2a47f28..e1095f6e00bf8c 100644 --- a/vlib/v/gen/c/fn.v +++ b/vlib/v/gen/c/fn.v @@ -2863,8 +2863,8 @@ fn (mut g Gen) call_args(node ast.CallExpr) { g.write('builtin___option_ok((${base_type}[]) {') } elem_sym := g.table.sym(arr_info.elem_type) - is_iface_or_sumtype := elem_sym.kind in [.sum_type, .interface] - if is_iface_or_sumtype { + is_iface := elem_sym.kind == .interface + if is_iface { g.inside_cast_in_heap++ } g.write('builtin__new_array_from_c_array${noscan}(${variadic_count}, ${variadic_count}, sizeof(${elem_type}), _MOV((${elem_type}[${variadic_count}]){') @@ -2876,7 +2876,7 @@ fn (mut g Gen) call_args(node ast.CallExpr) { } } g.write('}))') - if is_iface_or_sumtype { + if is_iface { g.inside_cast_in_heap-- } if is_option {