Test module references created by resolving forwarded exports.
The following diagram (best viewed in light mode due to fixed colors) explains what happens in bug #52094 under the hood:
```mermaid graph TB subgraph state_00["Initial"] direction LR s00_ida[ida64.dll] ~~~ s00_idapy[idapython64.dll] -. "(import)" .-> s00_py3[python3.dll] -. "(forwards to)" .-> s00_py311[python311.dll] end state_00 -. "1. <strong><code><span style='color:#28f'>Load</span>Library("</code></strong>configured Python version (e.g., python311.dll)<strong><code>")</code></strong>" .-> state_01 subgraph state_01["python311.dll loaded"] direction LR s01_ida[ida64.dll] ~~~ s01_idapy[idapython64.dll] -. "(import)" .-> s01_py3[python3.dll] -. "(forwards to)" .-> s01_py311[python311.dll] s01_ida -- "<em>dynamic</em> ref" --> s01_py311 end state_01 -. "2. <strong><code><span style='color:#28f'>Load</span>Library("</code></strong>idapython64.dll<strong><code>")</code></strong>" .-> state_02 subgraph state_02["idapython64.dll loaded"] direction LR s02_ida[ida64.dll] -- "<em>dynamic</em> ref" --> s02_idapy[idapython64.dll] -- "<em><strong>static</strong></em> ref" --> s02_py3[python3.dll] -. "(forwards to)" .-> s02_py311[python311.dll] s02_ida -- "<em>dynamic</em> ref" --> s02_py311 s02_idapy -- "<span style='color:red'><em><strong>static forwarded</strong></em> ref</span>" --> s02_py311 end state_02 -. "3. <strong><code><span style='color:#d70'>Free</span>Library("</code></strong>configured Python version (e.g., python311.dll)<strong><code>")</code></strong>" .-> state_03 subgraph state_03["python311.dll reference removed"] direction LR style s03_py311 color:#f00,stroke:#f00 s03_ida[ida64.dll] -- "<em>dynamic</em> ref" --> s03_idapy[idapython64.dll] -- "<em><strong>static</strong></em> ref" --> s03_py3[python3.dll] -. "(forwards to)" .-> s03_py311[python311.dll] s03_ida -. "<del><em>dynamic</em> ref (deleted)</del>" .-> s03_py311 s03_idapy -- "<span style='color:red'><em><strong>static forwarded</strong></em> ref</span>" --> s03_py311 end ```
On Windows, the <em><strong>static forwarded</strong></em> ref keeps `python311.dll` from being unloaded; however, Wine is missing this behavior, causing it to be unloaded and segfault immediately (since further initialization happens just after step 3 FreeLibrary). The tests aim to reproduce this condition.
-- v9: kernel32/tests: Test module refcounting with forwarded exports.
From: Jinoh Kang jinoh.kang.kr@gmail.com
--- dlls/kernel32/tests/loader.c | 401 +++++++++++++++++++++++++++++++++++ 1 file changed, 401 insertions(+)
diff --git a/dlls/kernel32/tests/loader.c b/dlls/kernel32/tests/loader.c index 8f418ef09a9..de3fdd1af84 100644 --- a/dlls/kernel32/tests/loader.c +++ b/dlls/kernel32/tests/loader.c @@ -2409,6 +2409,406 @@ static void test_import_resolution(void) } }
+static HANDLE gen_forward_chain_testdll( char testdll_path[MAX_PATH], const char source_dll[MAX_PATH], + BOOL is_export, BOOL is_import, + DWORD *exp_func_base_rva, + DWORD *imp_thunk_base_rva ) +{ + DWORD text_rva = page_size; + DWORD text_size = page_size; + DWORD edata_rva = text_rva + text_size; + DWORD edata_size = page_size; + DWORD idata_rva = edata_rva + text_size; + DWORD idata_size = page_size; + DWORD eof_rva = idata_rva + edata_size; + const IMAGE_SECTION_HEADER sections[3] = { + { + .Name = ".text", + .Misc = { .VirtualSize = text_size }, + .VirtualAddress = text_rva, + .SizeOfRawData = text_size, + .PointerToRawData = text_rva, + .Characteristics = IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE, + }, + { + .Name = ".edata", + .Misc = { .VirtualSize = edata_size }, + .VirtualAddress = edata_rva, + .SizeOfRawData = edata_size, + .PointerToRawData = edata_rva, + .Characteristics = IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ, + }, + { + .Name = ".idata", + .Misc = { .VirtualSize = edata_size }, + .VirtualAddress = idata_rva, + .SizeOfRawData = idata_size, + .PointerToRawData = idata_rva, + .Characteristics = IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE, + }, + }; + struct expdesc + { + IMAGE_EXPORT_DIRECTORY dir; + DWORD functions[2]; + DWORD names[2]; + WORD name_ords[2]; + char str_forward_test_func[32]; + char str_forward_test_func2[32]; + char dll_name[MAX_PATH]; + char strpool[2][MAX_PATH + 16]; + } expdesc = { + .dir = { + .Characteristics = 0, + .TimeDateStamp = 0x12345678, + .Name = edata_rva + offsetof(struct expdesc, dll_name), + .Base = 1, + .NumberOfFunctions = ARRAY_SIZE(expdesc.functions), + .NumberOfNames = ARRAY_SIZE(expdesc.names), + .AddressOfFunctions = edata_rva + offsetof(struct expdesc, functions), + .AddressOfNames = edata_rva + offsetof(struct expdesc, names), + .AddressOfNameOrdinals = edata_rva + offsetof(struct expdesc, name_ords), + }, + .functions = { + text_rva + 0x4, + text_rva + 0x8, + }, + .names = { + edata_rva + offsetof(struct expdesc, str_forward_test_func), + edata_rva + offsetof(struct expdesc, str_forward_test_func2), + }, + .name_ords = { + 0, + 1, + }, + .str_forward_test_func = "forward_test_func", + .str_forward_test_func2 = "forward_test_func2", + }; + struct impdesc + { + IMAGE_IMPORT_DESCRIPTOR descr[2]; + IMAGE_THUNK_DATA original_thunks[3]; + IMAGE_THUNK_DATA thunks[3]; + struct { WORD hint; char name[32]; } impname_forward_test_func; + char module[MAX_PATH]; + } impdesc = { + .descr = { + { + .OriginalFirstThunk = idata_rva + offsetof(struct impdesc, original_thunks), + .TimeDateStamp = 0, + .ForwarderChain = -1, + .Name = idata_rva + offsetof(struct impdesc, module), + .FirstThunk = idata_rva + offsetof(struct impdesc, thunks), + }, + {{ 0 }}, + }, + .original_thunks = { + {{ idata_rva + offsetof(struct impdesc, impname_forward_test_func) }}, + {{ IMAGE_ORDINAL_FLAG | 2 }}, + {{ 0 }}, + }, + .thunks = { + {{ idata_rva + offsetof(struct impdesc, impname_forward_test_func) }}, + {{ IMAGE_ORDINAL_FLAG | 2 }}, + {{ 0 }}, + }, + .impname_forward_test_func = { 0, "forward_test_func" }, + }; + IMAGE_NT_HEADERS nt_header; + char temp_path[MAX_PATH]; + HANDLE file, file_w; + LARGE_INTEGER qpc; + DWORD outlen; + BOOL ret; + int res; + + QueryPerformanceCounter( &qpc ); + res = snprintf( expdesc.dll_name, ARRAY_SIZE(expdesc.dll_name), + "ldr%05lx.dll", qpc.LowPart & 0xfffffUL ); + ok( res > 0 && res < ARRAY_SIZE(expdesc.dll_name), "snprintf failed\n" ); + + outlen = GetTempPathA( ARRAY_SIZE(temp_path), temp_path ); + ok( outlen > 0 && outlen < ARRAY_SIZE(temp_path), "GetTempPathA() err=%lu\n", GetLastError() ); + + res = snprintf( testdll_path, MAX_PATH, "%s\%s", temp_path, expdesc.dll_name ); + ok( res > 0 && res < MAX_PATH, "snprintf failed\n" ); + + if (source_dll) + { + const char *export_names[2] = { + "forward_test_func", + "#2", + }; + char *backslash = strrchr( source_dll, '\' ); + const char *dllname = backslash ? backslash + 1 : source_dll; + const char *dot = strrchr( dllname, '.' ); + size_t ext_start = dot ? dot - dllname : strlen(dllname); + size_t i; + + res = snprintf( impdesc.module, ARRAY_SIZE(impdesc.module), "%s", dllname ); + ok( res > 0 && res < ARRAY_SIZE(impdesc.module), "snprintf() failed\n" ); + + for (i = 0; i < ARRAY_SIZE(export_names); i++) + { + char *buf; + size_t buf_size; + + assert( i < ARRAY_SIZE(expdesc.strpool) ); + buf = expdesc.strpool[i]; + buf_size = ARRAY_SIZE(expdesc.strpool[i]); + + assert( ext_start < buf_size ); + memcpy( buf, dllname, ext_start ); + buf += ext_start; + buf_size -= ext_start; + + res = snprintf( buf, buf_size, ".%s", export_names[i] ); + ok( res > 0 && res < buf_size, "snprintf() failed\n" ); + + assert( i < ARRAY_SIZE(expdesc.functions) ); + expdesc.functions[i] = edata_rva + (expdesc.strpool[i] - (char *)&expdesc); + } + } + + nt_header = nt_header_template; + nt_header.FileHeader.TimeDateStamp = 0x12345678; + nt_header.FileHeader.NumberOfSections = ARRAY_SIZE(sections); + nt_header.FileHeader.SizeOfOptionalHeader = sizeof(IMAGE_OPTIONAL_HEADER); + + nt_header.OptionalHeader.SizeOfCode = text_size; + nt_header.OptionalHeader.SectionAlignment = page_size; + nt_header.OptionalHeader.FileAlignment = page_size; + nt_header.OptionalHeader.SizeOfHeaders = sizeof(dos_header) + sizeof(nt_header) + sizeof(sections); + nt_header.OptionalHeader.SizeOfImage = eof_rva; + if (is_export) + { + nt_header.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress = edata_rva; + nt_header.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size = sizeof(expdesc); + } + nt_header.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress = idata_rva; + nt_header.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size = is_import ? sizeof(impdesc) : sizeof(IMAGE_IMPORT_DESCRIPTOR); + + /* Open file handle that will be deleted on close or process termination */ + file = CreateFileA( testdll_path, + DELETE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + CREATE_NEW, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE, + NULL ); + ok( file != INVALID_HANDLE_VALUE, "CreateFile(%s) for delete returned error %lu\n", + wine_dbgstr_a( testdll_path ), GetLastError() ); + + /* Open file again with write access */ + file_w = CreateFileA( testdll_path, + GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL ); + ok( file_w != INVALID_HANDLE_VALUE, "CreateFile(%s) for write returned error %lu\n", + wine_dbgstr_a( testdll_path ), GetLastError() ); + + ret = WriteFile( file_w, &dos_header, sizeof(dos_header), &outlen, NULL ); + ok( ret && outlen == sizeof(dos_header), "write dos_header: err=%lu outlen=%lu\n", GetLastError(), outlen ); + + ret = WriteFile( file_w, &nt_header, sizeof(nt_header), &outlen, NULL ); + ok( ret && outlen == sizeof(nt_header), "write nt_header: err=%lu outlen=%lu\n", GetLastError(), outlen ); + + ret = WriteFile( file_w, sections, sizeof(sections), &outlen, NULL ); + ok( ret && outlen == sizeof(sections), "write sections: err=%lu outlen=%lu\n", GetLastError(), outlen ); + + if (is_export) + { + SetFilePointer( file_w, edata_rva, NULL, FILE_BEGIN ); + ret = WriteFile( file_w, &expdesc, sizeof(expdesc), &outlen, NULL ); + ok( ret && outlen == sizeof(expdesc), "write expdesc: err=%lu outlen=%lu\n", GetLastError(), outlen ); + } + + if (is_import) + { + SetFilePointer( file_w, idata_rva, NULL, FILE_BEGIN ); + ret = WriteFile( file_w, &impdesc, sizeof(impdesc), &outlen, NULL ); + ok( ret && outlen == sizeof(impdesc), "write impdesc: err=%lu outlen=%lu\n", GetLastError(), outlen ); + } + + ret = SetFilePointer( file_w, eof_rva, NULL, FILE_BEGIN ); + ok( ret, "%lu\n", GetLastError() ); + ret = SetEndOfFile( file_w ); + ok( ret, "%lu\n", GetLastError() ); + + ret = CloseHandle( file_w ); + ok( ret, "%lu\n", GetLastError() ); + + if (exp_func_base_rva) + { + *exp_func_base_rva = is_export ? edata_rva + ((char *)&expdesc.functions - (char *)&expdesc) : 0; + } + + if (imp_thunk_base_rva) + { + *imp_thunk_base_rva = is_import ? idata_rva + ((char *)&impdesc.thunks - (char *)&impdesc) : 0; + } + + return file; +} + +static void subtest_export_forwarder_module_dep( size_t num_export_modules, size_t exporter_index, BOOL static_import ) +{ + size_t num_modules = num_export_modules + !!static_import; + size_t importer_index = static_import ? num_modules - 1 : 0; + DWORD imp_thunk_base_rva, exp_func_base_rva; + size_t i, ultimate_depender_index = 0; + char temp_paths[4][MAX_PATH]; + HANDLE temp_files[4]; + HMODULE modules[4]; + UINT_PTR exports[2]; + BOOL res; + + assert(num_modules > 1 && num_modules <= ARRAY_SIZE(modules)); + assert(exporter_index < num_modules); + + if (winetest_debug > 1) + trace( "Generate test DLL fixtures\n" ); + + for (i = 0; i < num_modules; i++) + { + temp_files[i] = gen_forward_chain_testdll( temp_paths[i], + i >= 1 ? temp_paths[i - 1] : NULL, + i < num_export_modules, + importer_index && i == importer_index, + i == 0 ? &exp_func_base_rva : NULL, + i == importer_index ? &imp_thunk_base_rva : NULL ); + } + + if (winetest_debug > 1) + trace( "Load test DLLs\n" ); + + for (i = 0; i < num_modules; i++) + { + ok( !GetModuleHandleA( temp_paths[i] ), "%s already loaded\n", + wine_dbgstr_a( temp_paths[i] ) ); + + modules[i] = LoadLibraryA( temp_paths[i] ); + ok( !!modules[i], "LoadLibraryA(temp_paths[%Iu] = %s) err=%lu\n", + i, wine_dbgstr_a( temp_paths[i] ), GetLastError() ); + } + + if (winetest_debug > 1) + trace( "Get address of exported functions\n" ); + + for (i = 0; i < ARRAY_SIZE(exports); i++) + { + char *mod_base = (char *)modules[0]; + exports[i] = (UINT_PTR)(mod_base + ((DWORD *)(mod_base + exp_func_base_rva))[i]); + } + + if (winetest_debug > 1) + trace( "Check import address table, if any\n" ); + + if (importer_index) + { + UINT_PTR *imp_thunk_base = (UINT_PTR *)((char *)modules[importer_index] + imp_thunk_base_rva); + for (i = 0; i < ARRAY_SIZE(exports); i++) + { + ok( imp_thunk_base[i] == exports[i], "import thunk mismatch [%Iu]: (%#Ix, %#Ix)\n", + i, imp_thunk_base[i], exports[i] ); + } + + ultimate_depender_index = max(ultimate_depender_index, importer_index); + } + + if (winetest_debug > 1) + trace( "Call GetProcAddress() on the ultimate dependant exporter DLL, if any\n" ); + + if (exporter_index) + { + UINT_PTR proc; + + proc = (UINT_PTR)GetProcAddress( modules[exporter_index], "forward_test_func" ); + ok( proc == exports[0], "GetProcAddress mismatch [0]: (%#Ix, %#Ix)\n", proc, exports[0] ); + + proc = (UINT_PTR)GetProcAddress( modules[exporter_index], (LPSTR)2 ); + ok( proc == exports[1], "GetProcAddress mismatch [1]: (%#Ix, %#Ix)\n", proc, exports[1] ); + + ultimate_depender_index = max(ultimate_depender_index, exporter_index); + } + + if (winetest_debug > 1) + trace( "Unreference modules except the ultimate dependant DLL\n" ); + + for (i = 0; i < ultimate_depender_index; i++) + { + res = FreeLibrary( modules[i] ); + ok( res, "FreeLibrary(modules[%Iu]) err=%lu\n", i, GetLastError() ); + } + + if (winetest_debug > 1) + trace( "The ultimate dependant DLL should keep other DLLs from being unloaded\n" ); + + for (i = 0; i < num_modules; i++) + { + HMODULE module = GetModuleHandleA( temp_paths[i] ); + + todo_wine_if(i < ultimate_depender_index && i + 1 != importer_index) + ok( module == modules[i], "modules[%Iu] expected %p, got %p (unloaded?)\n", + i, modules[i], module ); + } + + if (winetest_debug > 1) + trace( "Unreference the remaining modules (including the dependant DLL)\n" ); + + for (i = ultimate_depender_index; i < num_modules; i++) + { + res = FreeLibrary( modules[i] ); + ok( res, "FreeLibrary(modules[%Iu]) err=%lu\n", i, GetLastError() ); + + /* FreeLibrary() should now unload the DLL immediately */ + ok( !GetModuleHandleA( temp_paths[i] ), "modules[%Iu] unexpectedly kept loaded (2)\n", i ); + } + + if (winetest_debug > 1) + trace( "All modules should be unloaded\n" ); + + for (i = 0; i < num_modules; i++) + { + ok( !GetModuleHandleA( temp_paths[i] ), "modules[%Iu] unexpectedly kept loaded (3)\n", i ); + } + + if (winetest_debug > 1) + trace( "Close and delete temp files\n" ); + + for (i = 0; i < num_modules; i++) + { + /* handles are delete-on-close */ + CloseHandle( temp_files[i] ); + } +} + +static void test_export_forwarder_module_dep(void) +{ + winetest_push_context( "export forwarder does not introduce dependency" ); + subtest_export_forwarder_module_dep( 2, FALSE, 0 ); + winetest_pop_context(); + + winetest_push_context( "static import of export forwarder (depth 1)" ); + subtest_export_forwarder_module_dep( 2, TRUE, 0 ); + winetest_pop_context(); + + winetest_push_context( "static import of export forwarder (depth 2)" ); + subtest_export_forwarder_module_dep( 3, TRUE, 0 ); + winetest_pop_context(); + + winetest_push_context( "dynamic import of export forwarder (depth 1)" ); + subtest_export_forwarder_module_dep( 2, FALSE, 1 ); + winetest_pop_context(); + + winetest_push_context( "dynamic import of export forwarder (depth 2)" ); + subtest_export_forwarder_module_dep( 3, FALSE, 2 ); + winetest_pop_context(); +} + #define MAX_COUNT 10 static HANDLE attached_thread[MAX_COUNT]; static DWORD attached_thread_count; @@ -4282,6 +4682,7 @@ START_TEST(loader) test_ImportDescriptors(); test_section_access(); test_import_resolution(); + test_export_forwarder_module_dep(); test_ExitProcess(); test_InMemoryOrderModuleList(); test_LoadPackagedLibrary();