diff --git a/src/cli/test/fx_test_specs.zig b/src/cli/test/fx_test_specs.zig index e46ca2f4ef..9fb9a200cc 100644 --- a/src/cli/test/fx_test_specs.zig +++ b/src/cli/test/fx_test_specs.zig @@ -229,6 +229,16 @@ pub const io_spec_tests = [_]TestSpec{ .io_spec = "1>ok", .description = "Regression test: List.first with function syntax", }, + .{ + .roc_file = "test/fx/stdin_while_uaf.roc", + .io_spec = "0<123456789012345678901234|1>123456789012345678901234|0<|1>", + .description = "Regression test: Stdin.line! in while loop with 24 char input (heap-allocated string)", + }, + .{ + .roc_file = "test/fx/stdin_while_uaf.roc", + .io_spec = "0short|0<|1>", + .description = "Regression test: Stdin.line! in while loop with short input (small string optimization)", + }, }; /// Get the total number of IO spec tests diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index 27d080e013..4b8be2ae4e 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -14217,11 +14217,9 @@ pub const Interpreter = struct { var args = [1]StackValue{operand}; const result = try self.callLowLevelBuiltin(low_level.op, &args, roc_ops, null); - // Decref operand based on ownership semantics - const arg_ownership = low_level.op.getArgOwnership(); - if (arg_ownership.len > 0 and arg_ownership[0] == .borrow) { - operand.decref(&self.runtime_layout_store, roc_ops); - } + // Note: We do NOT decref the operand here. + // The defer statement at the top of unary_op_apply already handles decrefing. + // Decrefing here too would cause a double-free bug. self.env = saved_env; try value_stack.push(result); @@ -14578,14 +14576,11 @@ pub const Interpreter = struct { var args = [2]StackValue{ lhs, rhs }; var result = try self.callLowLevelBuiltin(low_level.op, &args, roc_ops, null); - // Decref arguments based on ownership semantics - const arg_ownership = low_level.op.getArgOwnership(); - for (args, 0..) |arg, arg_idx| { - const ownership = if (arg_idx < arg_ownership.len) arg_ownership[arg_idx] else .borrow; - if (ownership == .borrow) { - arg.decref(&self.runtime_layout_store, roc_ops); - } - } + // Note: We do NOT decref arguments here for borrow semantics. + // The defer statements at the top of binop_apply already handle decrefing + // lhs and rhs. Decrefing here too would cause a double-free bug. + // For consume semantics, the low-level builtin takes ownership, so we + // also don't decref - the builtin is responsible for the memory. // Decref the method closure (for low-level, we handle it here) method_func.decref(&self.runtime_layout_store, roc_ops); diff --git a/test/fx/stdin_while_uaf.roc b/test/fx/stdin_while_uaf.roc new file mode 100644 index 0000000000..a1ce390be2 --- /dev/null +++ b/test/fx/stdin_while_uaf.roc @@ -0,0 +1,18 @@ +app [main!] { pf: platform "./platform/main.roc" } + +import pf.Stdin +import pf.Stdout + +main! = || { + # Read lines until empty string + var $count = 0 + var $continue = True + while $continue { + line = Stdin.line!() + Stdout.line!(line) + $count = $count + 1 + if line == "" { + $continue = False + } + } +}