/* TEST_HEADER id = $Id$ summary = regression test for GitHub issue #61 language = c link = testlib.o newfmt.o parameters = END_HEADER */ #include "testlib.h" #if !defined(MPS_OS_W3) static void test(void *stack_pointer) { /* nothing to do on non-Windows systems */ } #else #include "mpsavm.h" #include "mpscamc.h" #include "newfmt.h" #include #include /* On Windows, the MPS uses a vectored exception handler to intercept * and handle protection errors. These handlers need to save and * restore the value inside GetLastError(), otherwise the mutator may * observe that GetLastError() has ben cleared or set to something * else (especially if the MPS executed a system call that failed). * * This test first sets up an AMC pool with the new format in * newfmt.h. Then it creates a single root node with room for 100 * elements, and populates them with nodes as well. Each of these * created nodes are populated with pointers from the root node to * give the MPS a reason to protect them. * * We then start looking for nodes residing in pages where the barrier * have been raised (both for reads and writes). We continuously * allocate new nodes for even indices in the root node. This will * trigger the MPS to do garbage collections, and only updating even * nodes will ensure that we will end up with pointers to both objects * in the nursery generation and in the older generation, increasing * the likelihood of us finding a page with the barrier active. * * When we find a page with the read and write barrier active, we * store that pointer and set up the test we want to perform. We want * to set up a situation where we can trigger a barrier hit, and that * the MPS will call a system call that fails in the exception handler * when responding to the hit. * * The tool we use here is a separate thread. We create and register * an additional thread with the MPS. Then we keep that thread alive * until we know that the next thing the MPS will do is the barrier * hit (if it fails to halt the thread once, it assumes the thread is * no longer alive and will no longer try to halt it again). As such, * we terminate the thread when we have found a page that will trigger * the hit. Then, we can call SetLastError() with som error code, read * from the memory with a barrier. This triggers the exception * handler, which will realize that it needs to scan the particular * page, and thus need to stop threads. In this case, we know that the * SuspendThread() call will fail and thus clobbers the value in * GetLastError(). Then we can verify that the main thread does not * observe this change. This serves as a good (and hopefully, * reliable) illustration of what could happen, even if it is * tecnically a bad thing to have a terminated thread registered with * the MPS. */ /* Error code we check for. We pick an error unlikely to happen in the * MPS. Typically we see error code 5 (ERROR_ACCESS_DENIED) from * SuspendThread() in this case, so anything but that should work. */ #define CHECK_ERROR_CODE ERROR_NETWORK_BUSY /* Number of cells to allocate. */ #define NUM_CELLS 100 /* Maximum attempts before giving up. It seems like 10 or so is enough * in most cases. */ #define MAX_ATTEMPTS 100 /* Get memory protection of the page containing the specified address. */ static DWORD memory_protection(void *ptr) { MEMORY_BASIC_INFORMATION info; if (VirtualQuery(ptr, &info, sizeof(info)) == 0) { printf("VirtualQuery failed\n"); fail(); } return info.Protect; } /* Find a cell linked to from 'root' that is in a read- and write-protected page. */ static mycell *any_protected(mycell *root) { int i; for (i = 0; i < NUM_CELLS; i++) { mycell *cell = getref(root, i); DWORD protection = memory_protection(cell); switch (protection) { case PAGE_EXECUTE: case PAGE_NOACCESS: return cell; } } return NULL; } /* Allocate an object and fill it with some pointers to encourage the * MPS to raise the read- and write-barrier for the object at some point. */ static mycell *alloc_cell(mps_ap_t ap, mycell *root) { int i; mycell *cell = allocone(ap, NUM_CELLS); for (i = 0; i < NUM_CELLS; i++) setref(cell, i, getref(root, i)); return cell; } /* Variables shared by the spawned thread and the main thread. */ static mps_arena_t arena; static mps_thr_t extra_thread; static HANDLE terminate_thread; /* Function for the extra thread. The thread registers itself with the * MPS, waits until the main thread asks it to terminate, and then * exits. */ static DWORD WINAPI thread_fn(LPVOID parameter) { mps_thread_reg(&extra_thread, arena); WaitForSingleObject(terminate_thread, INFINITE); return 0; } static void test(void *stack_pointer) { mps_fmt_t format; mps_pool_t pool; mps_thr_t thread; mps_root_t root; mps_chain_t chain; mps_ap_t ap; int i, j; mycell *root_cell; mycell *protected; HANDLE thread_handle; DWORD after_trip; mps_gen_param_s gen_params[2] = { /* Make the first generation fairly small to trigger the case we want. */ { 512, 0.9 }, { 10 * 1024, 0.5 } }; die(mps_arena_create_k(&arena, mps_arena_class_vm(), mps_args_none), "mps_arena_create_k"); die(mps_fmt_create_A(&format, arena, &fmtA), "create format"); die(mps_chain_create(&chain, arena, 2, gen_params), "mps_chain_create"); die(mps_thread_reg(&thread, arena), "mps_thread_reg"); die(mps_root_create_thread(&root, arena, thread, stack_pointer), "mps_root_create_thread"); MPS_ARGS_BEGIN(args) { MPS_ARGS_ADD(args, MPS_KEY_FORMAT, format); MPS_ARGS_ADD(args, MPS_KEY_CHAIN, chain); die(mps_pool_create_k(&pool, arena, mps_class_amc(), args), "mps_pool_create_k"); } MPS_ARGS_END(args); die(mps_ap_create_k(&ap, pool, mps_args_none), "mps_ap_create_k"); /* Launch the extra thread, keep it alive until we are ready. We * need to keep it alive since the MPS will stop trying to stop the * thread during collections as soon as it realizes that it has * terminated. Since this happens during collections, we don't want * it to terminate too early. */ terminate_thread = CreateSemaphore(NULL, 0, 1, NULL); thread_handle = CreateThread(NULL, 0, thread_fn, NULL, 0, NULL); /* Allocate a number of cells to start with. */ root_cell = allocone(ap, NUM_CELLS); for (i = 0; i < NUM_CELLS; i++) { setref(root_cell, i, alloc_cell(ap, root_cell)); } /* Now, continue to allocate things, forcing the MPS to do * incremental collections from time to time. We do this until we * find that the MPS is in the middle of a collection and has raised * the read- and write-barrier for some of the objects. */ for (i = 0; i < MAX_ATTEMPTS; i++) { /* Fill every other entry with new objects. This way, some will be * in the nursery generation, and others will be in higher generations. */ for (j = 0; j < NUM_CELLS; j += 2) setref(root_cell, j, alloc_cell(ap, root_cell)); /* Ask for some collection work (a very small amount). */ mps_arena_step(arena, 0.0001, 10000); /* See if we are done. */ protected = any_protected(root_cell); if (protected) break; } /* Make sure we actually triggered the situation we are looking for. */ asserts(protected != NULL, "Failed to find a protected page after many attempts."); /* Signal the semaphore and wait for the other thread to exit. In * this way, we know that the MPS will try (and fail) to stop the * thread when we hit the barrier later. */ ReleaseSemaphore(terminate_thread, 1, NULL); WaitForSingleObject(thread_handle, INFINITE); /* Now we have a page that will trip the barrier when we write to * it. So we do that and observe the value of GetLastError(). */ SetLastError(CHECK_ERROR_CODE); /* Read from it. In the current implementation, reads are handled by * scanning (which is what we want to trigger since that requires * suspending threads), which is enough. A write would also work. */ getref(protected, 0); /* Check so that the error code we set is preserved. Typically, * GetLastError() will be 5 if it is not preserved (that is what * SuspendThread() sets it to on failure). */ after_trip = GetLastError(); asserts(after_trip == CHECK_ERROR_CODE, "Last error was not properly preserved in the exception handler!"); /* Tear down MPS data structures. */ mps_arena_park(arena); mps_ap_destroy(ap); mps_root_destroy(root); mps_thread_dereg(thread); mps_thread_dereg(extra_thread); mps_pool_destroy(pool); mps_chain_destroy(chain); mps_fmt_destroy(format); mps_arena_destroy(arena); /* Other cleanup. */ CloseHandle(thread_handle); CloseHandle(terminate_thread); } #endif int main(void) { run_test(test); pass(); return 0; }