From 9ed1f526278b9049727d45d7e4ff7ac18a2cad8e Mon Sep 17 00:00:00 2001 From: kichikuou Date: Tue, 31 Mar 2026 15:09:32 +0900 Subject: [PATCH] VM: Keep struct page alive during method calls for AIN v6.1+ This fixes heap use-after-free in the music mode in Rance 9. Hold a reference to the struct page (`this` pointer) for the duration of a method call. Without this, if the sole external reference to an object is dropped during one of its method calls (e.g. an event handler that destroys its own parent), the object and its members are freed immediately, causing use-after-free when the VM tries to access them afterward. This matches the behavior of the original VM, which was introduced in Rance9.exe and later titles. The fix is gated on AIN v6.1+ (AIN v6 with a MSG1 section) to match the original. Applying it unconditionally would change the heap refcount state visible to resume saves, potentially breaking existing saves for older games. --- src/vm.c | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/vm.c b/src/vm.c index 249f836..a43dbb4 100644 --- a/src/vm.c +++ b/src/vm.c @@ -294,11 +294,26 @@ static int alloc_scenario_page(const char *fname) return slot; } +static void set_struct_page(int slot) +{ + call_stack[call_stack_ptr-1].struct_page = slot; + // Keep `this` alive during the call (from Rance9 onwards). + if (AIN_VERSION_GTE(ain, 6, 1)) + heap_ref(slot); +} + +static void unref_call_frame(struct function_call *frame) +{ + if (frame->struct_page >= 0 && AIN_VERSION_GTE(ain, 6, 1)) + heap_unref(frame->struct_page); + heap_unref(frame->page_slot); +} + static void scenario_jump(int address) { // flush call stack for (int i = call_stack_ptr - 1; i >= 0; i--) { - heap_unref(call_stack[i].page_slot); + unref_call_frame(&call_stack[i]); } call_stack_ptr = 0; instr_ptr = address; @@ -309,7 +324,7 @@ static void scenario_call(int slot) int fno = heap[slot].page->index; // flush call stack for (int i = call_stack_ptr - 1; i >= 0; i--) { - heap_unref(call_stack[i].page_slot); + unref_call_frame(&call_stack[i]); } call_stack[0] = (struct function_call) { .fno = fno, @@ -378,7 +393,7 @@ static void method_call(int fno, int return_address) { function_call(fno, return_address); int struct_page = stack_pop().i; - call_stack[call_stack_ptr-1].struct_page = struct_page; + set_struct_page(struct_page); heap[call_stack[call_stack_ptr-1].page_slot].page->local.struct_ptr = struct_page; } @@ -411,7 +426,7 @@ static void delegate_call(int dg_no, int return_address) heap[slot].page->values[i] = vm_copy(arg, dg->variables[i].type.data); } - call_stack[call_stack_ptr-1].struct_page = obj; + set_struct_page(obj); } else { // call finished: clean up stack and jump to return address union vm_value r; @@ -453,7 +468,7 @@ void vm_call(int fno, int struct_page) static void function_return(void) { - heap_unref(call_stack[call_stack_ptr-1].page_slot); + unref_call_frame(&call_stack[call_stack_ptr-1]); instr_ptr = call_stack[call_stack_ptr-1].return_address; call_stack_ptr--; } @@ -1907,7 +1922,7 @@ static enum opcode execute_instruction(enum opcode opcode) case SH_STRUCTREF_CALLMETHOD_NO_PARAM: { int memb_page = member_get(get_argument(0)).i; function_call(get_argument(1), instr_ptr + instruction_width(SH_STRUCTREF_CALLMETHOD_NO_PARAM)); - call_stack[call_stack_ptr-1].struct_page = memb_page; + set_struct_page(memb_page); break; } case SH_STRUCTREF2: { @@ -1931,7 +1946,7 @@ static enum opcode execute_instruction(enum opcode opcode) int memb1 = member_get(get_argument(0)).i; int memb2 = page_get_var(heap_get_page(memb1), get_argument(1)).i; function_call(get_argument(2), instr_ptr + instruction_width(SH_STRUCTREF2_CALLMETHOD_NO_PARAM)); - call_stack[call_stack_ptr-1].struct_page = memb2; + set_struct_page(memb2); break; } case SH_IF_STRUCTREF_Z: { @@ -1966,7 +1981,7 @@ static enum opcode execute_instruction(enum opcode opcode) case THISCALLMETHOD_NOPARAM: { int this_page = struct_page_slot(); function_call(get_argument(0), instr_ptr + instruction_width(THISCALLMETHOD_NOPARAM)); - call_stack[call_stack_ptr-1].struct_page = this_page; + set_struct_page(this_page); break; } case SH_IF_LOC_NE_IMM: { @@ -2373,6 +2388,8 @@ static void vm_free(void) exit_libraries(); // flush call stack for (int i = call_stack_ptr - 1; i >= 0; i--) { + if (call_stack[i].struct_page >= 0 && AIN_VERSION_GTE(ain, 6, 1)) + exit_unref(call_stack[i].struct_page); exit_unref(call_stack[i].page_slot); } // free globals