From 7aeba1e508dc44e4bcc1d0c4bf989b7321906f0d Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Sat, 28 Sep 2024 21:12:12 +0800 Subject: [PATCH] Fix undefined behavior in interpreter mixed union upcast (#15042) The interpreter upcasts a value to a mixed union by placing it on top of the stack, and then copying the data portion to a higher position to reserve space for the type ID. Hence, when the size of the value exceeds that of the type ID, the `copy_to` here would occur between two overlapping ranges: ```cr tmp_stack = stack stack_grow_by(union_size - from_size) (tmp_stack - from_size).copy_to(tmp_stack - from_size + type_id_bytesize, from_size) (tmp_stack - from_size).as(Int64*).value = type_id.to_i64! ``` This is undefined behavior in both ISO C and POSIX. Instead `move_to` must be used here (and most likely in a few other places too). This patch also changes the `move_to` in the tuple indexers to `move_from`, although in practice these don't exhibit unexpected behavior, because most `memcpy` implementations copy data from lower addresses to higher addresses, and these calls move data to a lower address. --- spec/compiler/interpreter/unions_spec.cr | 7 +++++++ src/compiler/crystal/interpreter/instructions.cr | 8 +++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/spec/compiler/interpreter/unions_spec.cr b/spec/compiler/interpreter/unions_spec.cr index 11bde229b44d..0fa82e8cbddb 100644 --- a/spec/compiler/interpreter/unions_spec.cr +++ b/spec/compiler/interpreter/unions_spec.cr @@ -36,6 +36,13 @@ describe Crystal::Repl::Interpreter do CRYSTAL end + it "returns large union type (#15041)" do + interpret(<<-CRYSTAL).should eq(4_i64) + a = {3_i64, 4_i64} || nil + a.is_a?(Tuple) ? a[1] : 5_i64 + CRYSTAL + end + it "put and remove from union in local var" do interpret(<<-CRYSTAL).should eq(3) a = 1 == 1 ? 2 : true diff --git a/src/compiler/crystal/interpreter/instructions.cr b/src/compiler/crystal/interpreter/instructions.cr index 6a38afd888d3..23428df03b90 100644 --- a/src/compiler/crystal/interpreter/instructions.cr +++ b/src/compiler/crystal/interpreter/instructions.cr @@ -1309,7 +1309,7 @@ require "./repl" code: begin tmp_stack = stack stack_grow_by(union_size - from_size) - (tmp_stack - from_size).copy_to(tmp_stack - from_size + type_id_bytesize, from_size) + (tmp_stack - from_size).move_to(tmp_stack - from_size + type_id_bytesize, from_size) (tmp_stack - from_size).as(Int64*).value = type_id.to_i64! end, disassemble: { @@ -1319,6 +1319,8 @@ require "./repl" put_reference_type_in_union: { operands: [union_size : Int32], code: begin + # `copy_to` here is valid only when `from_size <= type_id_bytesize`, + # which is always true from_size = sizeof(Pointer(UInt8)) reference = (stack - from_size).as(UInt8**).value type_id = @@ -1462,7 +1464,7 @@ require "./repl" tuple_indexer_known_index: { operands: [tuple_size : Int32, offset : Int32, value_size : Int32], code: begin - (stack - tuple_size).copy_from(stack - tuple_size + offset, value_size) + (stack - tuple_size).move_from(stack - tuple_size + offset, value_size) aligned_value_size = align(value_size) stack_shrink_by(tuple_size - value_size) stack_grow_by(aligned_value_size - value_size) @@ -1474,7 +1476,7 @@ require "./repl" }, tuple_copy_element: { operands: [tuple_size : Int32, old_offset : Int32, new_offset : Int32, element_size : Int32], - code: (stack - tuple_size + new_offset).copy_from(stack - tuple_size + old_offset, element_size), + code: (stack - tuple_size + new_offset).move_from(stack - tuple_size + old_offset, element_size), }, # >>> Tuples (3)