From 3896c62c04c211dc62dfccd154223f72dfaeeada Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 10 Jun 2026 10:10:47 +0000 Subject: [PATCH] test: cover native VM call argument packing Co-authored-by: Tim Fox --- src/qcommon/vm.c | 7 +--- src/qcommon/vm_native_module.c | 21 ++++++++++ src/qcommon/vm_native_module.h | 10 +++++ tests/README.md | 2 +- tests/unit/test_vm_native_module.c | 65 ++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 7 deletions(-) diff --git a/src/qcommon/vm.c b/src/qcommon/vm.c index 64daf86413..fb52f0a22d 100644 --- a/src/qcommon/vm.c +++ b/src/qcommon/vm.c @@ -2077,15 +2077,10 @@ intptr_t QDECL VM_Call( vm_t *vm, int nargs, int callnum, ... ) // if we have a dll loaded, call it directly if ( vm->entryPoint ) { - /* Pass three arg slots: native vmMain only receives nargs from varargs; zero the rest - * so e.g. UI_GETAPIVERSION (nargs=0) does not read stack garbage. */ int32_t args[MAX_VMMAIN_CALL_ARGS-1]; - Com_Memset( args, 0, sizeof( args ) ); va_list ap; va_start( ap, callnum ); - for ( i = 0; i < nargs; i++ ) { - args[i] = va_arg( ap, int32_t ); - } + VM_BuildNativeModuleCallArgs( nargs, args, (int)ARRAY_LEN( args ), ap ); va_end( ap ); // add more arguments if you're changed MAX_VMMAIN_CALL_ARGS: diff --git a/src/qcommon/vm_native_module.c b/src/qcommon/vm_native_module.c index be2f9d7b78..a901fe79aa 100644 --- a/src/qcommon/vm_native_module.c +++ b/src/qcommon/vm_native_module.c @@ -1,5 +1,6 @@ #include "vm_native_module.h" #include +#include int VM_BuildNativeModuleCandidates( const char *moduleName, char out[][MAX_QPATH], int maxCandidates ) { int count = 0; @@ -24,3 +25,23 @@ int VM_BuildNativeModuleCandidates( const char *moduleName, char out[][MAX_QPATH return count; } + +void VM_BuildNativeModuleCallArgs( int nargs, int32_t *out, int maxArgs, va_list ap ) { + int i; + int copyCount; + + if ( !out || maxArgs <= 0 ) { + return; + } + + memset( out, 0, (size_t)maxArgs * sizeof( out[0] ) ); + + copyCount = nargs; + if ( copyCount > maxArgs ) { + copyCount = maxArgs; + } + + for ( i = 0; i < copyCount; i++ ) { + out[i] = va_arg( ap, int32_t ); + } +} diff --git a/src/qcommon/vm_native_module.h b/src/qcommon/vm_native_module.h index e8bd630f99..31a8a2fbd5 100644 --- a/src/qcommon/vm_native_module.h +++ b/src/qcommon/vm_native_module.h @@ -1,6 +1,9 @@ #ifndef VM_NATIVE_MODULE_H #define VM_NATIVE_MODULE_H +#include +#include + #include "q_shared.h" /* @@ -11,4 +14,11 @@ */ int VM_BuildNativeModuleCandidates( const char *moduleName, char out[][MAX_QPATH], int maxCandidates ); +/* + * Packs native vmMain argument slots from VM_Call varargs. Native vmMain takes + * exactly three integer argument slots after the command; missing slots must be + * zero so low-arity calls such as UI_GETAPIVERSION cannot observe stack junk. + */ +void VM_BuildNativeModuleCallArgs( int nargs, int32_t *out, int maxArgs, va_list ap ); + #endif diff --git a/tests/README.md b/tests/README.md index f155d19aa4..d1902e2372 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,5 +1,5 @@ # Tests -- **Unit** (`BUILD_UNIT_TESTS=ON`): `unit_macros`, `unit_qmath`, `unit_surfaceflags`, `unit_qhelpers`, `unit_crc`, `unit_pathutil`, `unit_msg`, `unit_info`, `unit_cm_bounds`, `unit_parse`, `unit_endian` (CRC, COM path, `Info_*`, `COM_Parse*`, and endian tests use the same minimal `stub_qcommon_min.c` + `q_shared.c` + `q_math.c` link as `unit_qhelpers`; `unit_msg` links `msg.c` + `huffman_static.c` with `stub_qcommon_min.c`, `stub_msg_cvar.c`, and `-DDEDICATED`; `unit_cm_bounds` links `cm_bounds.c` + `q_math.c` only) — run `ctest -R unit_` or `./unit_*` from the build directory. +- **Unit** (`BUILD_UNIT_TESTS=ON`): `unit_macros`, `unit_qmath`, `unit_surfaceflags`, `unit_qhelpers`, `unit_crc`, `unit_pathutil`, `unit_msg`, `unit_info`, `unit_cm_bounds`, `unit_parse`, `unit_endian`, `unit_vm_native_module` (CRC, COM path, `Info_*`, `COM_Parse*`, endian, and native VM module tests use the same minimal `stub_qcommon_min.c` + `q_shared.c` + `q_math.c` link as `unit_qhelpers` where needed; `unit_msg` links `msg.c` + `huffman_static.c` with `stub_qcommon_min.c`, `stub_msg_cvar.c`, and `-DDEDICATED`; `unit_cm_bounds` links `cm_bounds.c` + `q_math.c` only; `unit_vm_native_module` links `vm_native_module.c` only) — run `ctest -R unit_` or `./unit_*` from the build directory. - **Script regression tests**: `test_botlib_bounded_strings` (botlib string invariants). Run with `ctest -R test_botlib_bounded_strings` from the build directory. - **Validation**: `smoke_test`, `renderer_regression_check`, `check_artifacts`, `test_run_vulkan_script`, `test_compile_engine_lto`, `test_demo_game_pk3`, `test_vk_vegetation_dispatch_order`, `test_vulkan_mesh_shader_opt_in`, `test_vulkan_runtime_regressions`, `test_botlib_chat_message_bounds`, `test_vulkan_renderer_guards`, `test_vulkan_regression_source_guards`, `test_gltf_opengl_regressions`, `test_botlib_bounded_strings` — see `scripts/`, `tests/scripts/`, and `docs/RENDERER_CONFIDENCE.md`. diff --git a/tests/unit/test_vm_native_module.c b/tests/unit/test_vm_native_module.c index 51ee8d9035..6b835bcaa6 100644 --- a/tests/unit/test_vm_native_module.c +++ b/tests/unit/test_vm_native_module.c @@ -2,6 +2,8 @@ * Unit test: VM native module candidate naming * Run: ctest -R unit_vm_native_module */ +#include +#include #include #include @@ -64,6 +66,60 @@ static int test_candidate_limit(void) { return 0; } +static void build_call_args(int nargs, int32_t out[3], ...) { + va_list ap; + + va_start(ap, out); + VM_BuildNativeModuleCallArgs(nargs, out, 3, ap); + va_end(ap); +} + +static int test_native_call_args_zero_fill(void) { + int32_t out[3] = { 0x11111111, 0x22222222, 0x33333333 }; + + build_call_args(0, out); + ASSERT(out[0] == 0, "zero-arg call should clear arg0"); + ASSERT(out[1] == 0, "zero-arg call should clear arg1"); + ASSERT(out[2] == 0, "zero-arg call should clear arg2"); + + build_call_args(1, out, 1234); + ASSERT(out[0] == 1234, "one-arg call should copy arg0"); + ASSERT(out[1] == 0, "one-arg call should clear arg1"); + ASSERT(out[2] == 0, "one-arg call should clear arg2"); + + return 0; +} + +static int test_native_call_args_copy_all_slots(void) { + int32_t out[3] = { 0 }; + + build_call_args(3, out, -1, 0x12345678, 42); + ASSERT(out[0] == -1, "three-arg call should copy arg0"); + ASSERT(out[1] == 0x12345678, "three-arg call should copy arg1"); + ASSERT(out[2] == 42, "three-arg call should copy arg2"); + + return 0; +} + +static void build_call_args_limited(int nargs, int32_t out[3], int maxArgs, ...) { + va_list ap; + + va_start(ap, maxArgs); + VM_BuildNativeModuleCallArgs(nargs, out, maxArgs, ap); + va_end(ap); +} + +static int test_native_call_args_respect_limit(void) { + int32_t out[3] = { 77, 88, 99 }; + + build_call_args_limited(3, out, 2, 10, 20, 30); + ASSERT(out[0] == 10, "limited native args should copy arg0"); + ASSERT(out[1] == 20, "limited native args should copy arg1"); + ASSERT(out[2] == 99, "limited native args should not write past maxArgs"); + + return 0; +} + int main(void) { if (test_empty_inputs() != 0) { return 1; @@ -74,6 +130,15 @@ int main(void) { if (test_candidate_limit() != 0) { return 1; } + if (test_native_call_args_zero_fill() != 0) { + return 1; + } + if (test_native_call_args_copy_all_slots() != 0) { + return 1; + } + if (test_native_call_args_respect_limit() != 0) { + return 1; + } printf("PASS: unit_vm_native_module\n"); return 0;