#include <darwintest.h>
#include <darwintest_utils.h>

#include <mach/mach_init.h>
#include <mach/vm_map.h>

T_GLOBAL_META(
	T_META_NAMESPACE("xnu.vm"),
	T_META_RADAR_COMPONENT_NAME("xnu"),
	T_META_RADAR_COMPONENT_VERSION("VM"));

struct context1 {
	vm_size_t obj_size;
	vm_address_t e0;
	dispatch_semaphore_t running_sem;
	pthread_mutex_t mtx;
	bool done;
};

static void *
protect_thread(__unused void *arg)
{
	kern_return_t kr;
	struct context1 *ctx;

	ctx = (struct context1 *)arg;
	/* tell main thread we're ready to run */
	dispatch_semaphore_signal(ctx->running_sem);
	while (!ctx->done) {
		/* wait for main thread to be done setting things up */
		pthread_mutex_lock(&ctx->mtx);
		if (ctx->done) {
			break;
		}
		/* make 2nd target mapping (e0) read-only */
		kr = vm_protect(mach_task_self(),
		    ctx->e0,
		    ctx->obj_size,
		    FALSE,             /* set_maximum */
		    VM_PROT_READ);
		T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_protect() RO");
		/* wait a little bit */
		usleep(100);
		/* make it read-write again */
		kr = vm_protect(mach_task_self(),
		    ctx->e0,
		    ctx->obj_size,
		    FALSE,             /* set_maximum */
		    VM_PROT_READ | VM_PROT_WRITE);
		T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_protect() RW");
		/* tell main thread we're done changing protections */
		pthread_mutex_unlock(&ctx->mtx);
		usleep(100);
	}
	return NULL;
}

T_DECL(unaligned_write_to_cow_bypass,
    "Test that unaligned copy respects COW", T_META_TAG_VM_PREFERRED)
{
	pthread_t th = NULL;
	int ret;
	kern_return_t kr;
	time_t start, duration;
	mach_msg_type_number_t cow_read_size;
	vm_size_t copied_size;
	int loops;
	vm_address_t e1, e2, e5;
	struct context1 context1, *ctx;
	int kern_success = 0, kern_protection_failure = 0, kern_other = 0;

	ctx = &context1;
	ctx->obj_size = 256 * 1024;
	ctx->e0 = 0;
	ctx->running_sem = dispatch_semaphore_create(0);
	T_QUIET; T_ASSERT_NE(ctx->running_sem, NULL, "dispatch_semaphore_create");
	ret = pthread_mutex_init(&ctx->mtx, NULL);
	T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_mutex_init");
	ctx->done = false;

	pthread_mutex_lock(&ctx->mtx);

	/* start racing thread */
	ret = pthread_create(&th, NULL, protect_thread, (void *)ctx);
	T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_create");

	/* wait for racing thread to be ready to run */
	dispatch_semaphore_wait(ctx->running_sem, DISPATCH_TIME_FOREVER);

	duration = 10; /* 10 seconds */
	T_LOG("Testing for %ld seconds...", duration);
	for (start = time(NULL), loops = 0;
	    time(NULL) < start + duration;
	    loops++) {
		/* reserve space for our 2 contiguous allocations */
		e2 = 0;
		kr = vm_allocate(mach_task_self(),
		    &e2,
		    2 * ctx->obj_size,
		    VM_FLAGS_ANYWHERE);
		T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate to reserve e2+e0");

		/* make 1st allocation in our reserved space */
		kr = vm_allocate(mach_task_self(),
		    &e2,
		    ctx->obj_size,
		    VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(240));
		T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e2");
		/* initialize to 'B' */
		memset((char *)e2, 'B', ctx->obj_size);

		/* make 2nd allocation in our reserved space */
		ctx->e0 = e2 + ctx->obj_size;
		kr = vm_allocate(mach_task_self(),
		    &ctx->e0,
		    ctx->obj_size,
		    VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(241));
		T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e0");
		memset((char *)ctx->e0, 'A', ctx->obj_size);
		/* initialize to 'A' */

		/* make a COW copy of e0 */
		e1 = 0;
		kr = vm_read(mach_task_self(),
		    ctx->e0,
		    ctx->obj_size,
		    &e1,
		    &cow_read_size);
		T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_read e0->e1");

		/* allocate a source buffer */
		kr = vm_allocate(mach_task_self(),
		    &e5,
		    ctx->obj_size,
		    VM_FLAGS_ANYWHERE);
		T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e5");
		/* initialize to 'C' */
		memset((char *)e5, 'C', ctx->obj_size);

		/* let the racing thread go */
		pthread_mutex_unlock(&ctx->mtx);

		/* trigger copy_unaligned while racing with other thread */
		kr = vm_read_overwrite(mach_task_self(),
		    e5,
		    ctx->obj_size,
		    e2 + 1,
		    &copied_size);
		T_QUIET; T_ASSERT_TRUE(kr == KERN_SUCCESS || kr == KERN_PROTECTION_FAILURE,
		    "vm_read_overwrite kr %d", kr);
		switch (kr) {
		case KERN_SUCCESS:
			/* the target as RW */
			kern_success++;
			break;
		case KERN_PROTECTION_FAILURE:
			/* the target was RO */
			kern_protection_failure++;
			break;
		default:
			/* should not happen */
			kern_other++;
			break;
		}

		/* check that the COW copy of e0 (at e1) was not modified */
		T_QUIET; T_ASSERT_EQ(*(char *)e1, 'A', "COW mapping was modified");

		/* tell racing thread to stop toggling protections */
		pthread_mutex_lock(&ctx->mtx);

		/* clean up before next loop */
		vm_deallocate(mach_task_self(), ctx->e0, ctx->obj_size);
		ctx->e0 = 0;
		vm_deallocate(mach_task_self(), e1, ctx->obj_size);
		e1 = 0;
		vm_deallocate(mach_task_self(), e2, ctx->obj_size);
		e2 = 0;
		vm_deallocate(mach_task_self(), e5, ctx->obj_size);
		e5 = 0;
	}

	ctx->done = true;
	pthread_mutex_unlock(&ctx->mtx);
	pthread_join(th, NULL);

	T_LOG("vm_read_overwrite: KERN_SUCCESS:%d KERN_PROTECTION_FAILURE:%d other:%d",
	    kern_success, kern_protection_failure, kern_other);
	T_PASS("Ran %d times in %ld seconds with no failure", loops, duration);
}
