/*
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 <stdio.h>
#include <Windows.h>
/* 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;
}