From 0af4a75ce7f5d80a13c04e4fd54669daeda882ea Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 15 Apr 2026 15:09:37 +0300 Subject: [PATCH] cgen: add runtime guards for unsafe math operations (fixes #4261) --- vlib/v/gen/c/assign.v | 23 ++++++++-- vlib/v/gen/c/cgen.v | 12 ++++- vlib/v/gen/c/infix.v | 5 ++- ...division_by_zero_runtime_guard.c.must_have | 5 +++ .../division_by_zero_runtime_guard.vv | 12 +++++ vlib/v/tests/division_by_zero_runtime_test.v | 45 +++++++++++++++++++ 6 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 vlib/v/gen/c/testdata/division_by_zero_runtime_guard.c.must_have create mode 100644 vlib/v/gen/c/testdata/division_by_zero_runtime_guard.vv create mode 100644 vlib/v/tests/division_by_zero_runtime_test.v diff --git a/vlib/v/gen/c/assign.v b/vlib/v/gen/c/assign.v index 962ea26fb..9c7cce041 100644 --- a/vlib/v/gen/c/assign.v +++ b/vlib/v/gen/c/assign.v @@ -747,9 +747,12 @@ fn (mut g Gen) assign_stmt(node_ ast.AssignStmt) { } mut cur_indexexpr := -1 consider_int_overflow := g.do_int_overflow_checks && g.unwrap_generic(var_type).is_int() + consider_int_div_mod := g.table.final_sym(g.unwrap_generic(var_type)).is_int() is_safe_add_assign := node.op == .plus_assign && consider_int_overflow is_safe_sub_assign := node.op == .minus_assign && consider_int_overflow is_safe_mul_assign := node.op == .mult_assign && consider_int_overflow + is_safe_div_assign := node.op == .div_assign && consider_int_div_mod + is_safe_mod_assign := node.op == .mod_assign && consider_int_div_mod initial_left_sym := g.table.sym(g.unwrap_generic(var_type)) is_va_list = initial_left_sym.language == .c && initial_left_sym.name == 'C.va_list' if mut left is ast.Ident { @@ -1702,23 +1705,35 @@ fn (mut g Gen) assign_stmt(node_ ast.AssignStmt) { } } else if !var_type.has_flag(.option_mut_param_t) && cur_indexexpr == -1 && !str_add && !op_overloaded && !is_safe_add_assign && !is_safe_sub_assign - && !is_safe_mul_assign && !is_safe_shift_assign { + && !is_safe_mul_assign && !is_safe_div_assign && !is_safe_mod_assign + && !is_safe_shift_assign { g.write(' ${op} ') } else if (str_add || op_overloaded) && !is_safe_add_assign && !is_safe_sub_assign - && !is_safe_mul_assign { + && !is_safe_mul_assign && !is_safe_div_assign && !is_safe_mod_assign { g.write(', ') } else if is_safe_shift_assign { g.write(' = ${safe_shift_fn_name}(') g.expr(left) g.write(', (u64)') - } else if is_safe_add_assign || is_safe_sub_assign || is_safe_mul_assign { + } else if is_safe_add_assign || is_safe_sub_assign || is_safe_mul_assign + || is_safe_div_assign || is_safe_mod_assign { overflow_styp := g.styp(get_overflow_fn_type(var_type)) + div_mod_styp := + g.styp(g.unwrap_generic(var_type).clear_flag(.shared_f).clear_flag(.atomic_f)) vsafe_fn_name := match true { is_safe_add_assign { 'builtin__overflow__add_${overflow_styp}' } is_safe_sub_assign { 'builtin__overflow__sub_${overflow_styp}' } is_safe_mul_assign { 'builtin__overflow__mul_${overflow_styp}' } + is_safe_div_assign { 'VSAFE_DIV_${div_mod_styp}' } + is_safe_mod_assign { 'VSAFE_MOD_${div_mod_styp}' } else { '' } } + if is_safe_div_assign || is_safe_mod_assign { + g.vsafe_arithmetic_ops[vsafe_fn_name] = VSafeArithmeticOp{ + typ: g.unwrap_generic(var_type).clear_flag(.shared_f).clear_flag(.atomic_f) + op: token.assign_op_to_infix_op(node.op) + } + } g.write(' = ${vsafe_fn_name}(') g.expr(left) g.write(',') @@ -2005,7 +2020,7 @@ fn (mut g Gen) assign_stmt(node_ ast.AssignStmt) { } } if str_add || op_overloaded || is_safe_add_assign || is_safe_sub_assign - || is_safe_mul_assign { + || is_safe_mul_assign || is_safe_div_assign || is_safe_mod_assign { g.write(')') } else if is_safe_shift_assign { g.write(')') diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index dc73383c9..c3a722353 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -748,9 +748,17 @@ pub fn gen(files []&ast.File, mut table ast.Table, pref_ &pref.Preferences) GenO for vsafe_fn_name, val in g.vsafe_arithmetic_ops { styp := g.styp(val.typ) if val.op == .div { - b.writeln('static inline ${styp} ${vsafe_fn_name}(${styp} x, ${styp} y) { if (_unlikely_(0 == y)) { return 0; } else { return x / y; } }') + if g.pref.div_by_zero_is_zero { + b.writeln('static inline ${styp} ${vsafe_fn_name}(${styp} x, ${styp} y) { if (_unlikely_(0 == y)) { return 0; } else { return x / y; } }') + } else { + b.writeln('static inline ${styp} ${vsafe_fn_name}(${styp} x, ${styp} y) { if (_unlikely_(0 == y)) { builtin___v_panic(_S("division by zero")); } return x / y; }') + } } else { - b.writeln('static inline ${styp} ${vsafe_fn_name}(${styp} x, ${styp} y) { if (_unlikely_(0 == y)) { return x; } else { return x % y; } }') + if g.pref.div_by_zero_is_zero { + b.writeln('static inline ${styp} ${vsafe_fn_name}(${styp} x, ${styp} y) { if (_unlikely_(0 == y)) { return x; } else { return x % y; } }') + } else { + b.writeln('static inline ${styp} ${vsafe_fn_name}(${styp} x, ${styp} y) { if (_unlikely_(0 == y)) { builtin___v_panic(_S("modulo by zero")); } return x % y; }') + } } } } diff --git a/vlib/v/gen/c/infix.v b/vlib/v/gen/c/infix.v index 9bc2bbe3f..aea5fba54 100644 --- a/vlib/v/gen/c/infix.v +++ b/vlib/v/gen/c/infix.v @@ -1903,8 +1903,9 @@ fn (mut g Gen) gen_plain_infix_expr(node ast.InfixExpr) { is_safe_add := checkoverflow_op && node.op == .plus is_safe_sub := checkoverflow_op && node.op == .minus is_safe_mul := checkoverflow_op && node.op == .mul - is_safe_div := node.op == .div && g.pref.div_by_zero_is_zero && typ.is_int() - is_safe_mod := node.op == .mod && g.pref.div_by_zero_is_zero && typ.is_int() + is_integer_div_mod := g.table.final_sym(g.unwrap_generic(typ)).is_int() + is_safe_div := node.op == .div && is_integer_div_mod + is_safe_mod := node.op == .mod && is_integer_div_mod if resolved_left_type.is_ptr() && node.left.is_auto_deref_var() && !resolved_right_type.is_pointer() { g.write('*') diff --git a/vlib/v/gen/c/testdata/division_by_zero_runtime_guard.c.must_have b/vlib/v/gen/c/testdata/division_by_zero_runtime_guard.c.must_have new file mode 100644 index 000000000..cdc584e61 --- /dev/null +++ b/vlib/v/gen/c/testdata/division_by_zero_runtime_guard.c.must_have @@ -0,0 +1,5 @@ +int c = VSAFE_DIV_int(a , z); +c = VSAFE_DIV_int(c,z); +int d = VSAFE_MOD_int(a , z); +d = VSAFE_MOD_int(d,z); +f32 fc = fa / fz; diff --git a/vlib/v/gen/c/testdata/division_by_zero_runtime_guard.vv b/vlib/v/gen/c/testdata/division_by_zero_runtime_guard.vv new file mode 100644 index 000000000..28fc2a843 --- /dev/null +++ b/vlib/v/gen/c/testdata/division_by_zero_runtime_guard.vv @@ -0,0 +1,12 @@ +a := 42 +z := 0 +mut c := a / z +c /= z +mut d := a % z +d %= z +fa := f32(42) +fz := f32(0) +fc := fa / fz +println(c) +println(d) +println(fc) diff --git a/vlib/v/tests/division_by_zero_runtime_test.v b/vlib/v/tests/division_by_zero_runtime_test.v new file mode 100644 index 000000000..c78f0c0d5 --- /dev/null +++ b/vlib/v/tests/division_by_zero_runtime_test.v @@ -0,0 +1,45 @@ +import os + +const vexe = @VEXE +const vtmp_folder = os.join_path(os.vtmp_dir(), 'division_by_zero_runtime_tests') + +@[markused] +const turn_off_vcolors = os.setenv('VCOLORS', 'never', true) + +fn test_indirect_integer_division_by_zero_panics_in_v() { + output := + compile_and_run_program('fn divide(a int, b int) int {\n\treturn a / b\n}\n\nfn main() {\n\tprintln(divide(1, 0))\n}\n') + assert_valid_panic_output(output, 'division by zero') +} + +fn test_indirect_integer_modulo_by_zero_panics_in_v() { + output := + compile_and_run_program('fn modulo(a int, b int) int {\n\treturn a % b\n}\n\nfn main() {\n\tprintln(modulo(3, 0))\n}\n') + assert_valid_panic_output(output, 'modulo by zero') +} + +fn assert_valid_panic_output(output string, message string) { + normalized := output.replace('\r', '') + assert normalized.contains('V panic: ${message}'), normalized + assert !normalized.to_lower().contains('runtime error:'), normalized +} + +fn compile_and_run_program(source string) string { + os.mkdir_all(vtmp_folder) or {} + defer { + os.rmdir_all(vtmp_folder) or {} + } + source_path := os.join_path(vtmp_folder, 'division_by_zero_program.v') + exe_path := os.join_path(vtmp_folder, 'division_by_zero_test${exe_suffix()}') + os.write_file(source_path, source) or { panic(err) } + compile_cmd := '${os.quoted_path(vexe)} -o ${os.quoted_path(exe_path)} ${os.quoted_path(source_path)}' + compilation := os.execute(compile_cmd) + assert compilation.exit_code == 0, 'compilation failed: ${compilation.output}' + result := os.execute(os.quoted_path(exe_path)) + assert result.exit_code != 0, 'program unexpectedly succeeded' + return result.output +} + +fn exe_suffix() string { + return if os.user_os() == 'windows' { '.exe' } else { '' } +} -- 2.39.5