#include "test/jemalloc_test.h"
#include "test/arena_util.h"
#include "test/san.h"

#include "jemalloc/internal/san.h"

static void
verify_extent_guarded(tsdn_t *tsdn, void *ptr) {
	expect_true(
	    extent_is_guarded(tsdn, ptr), "All extents should be guarded.");
}

#define MAX_SMALL_ALLOCATIONS 4096
void *small_alloc[MAX_SMALL_ALLOCATIONS];

/*
 * This test allocates page sized slabs and checks that every two slabs have
 * at least one page in between them. That page is supposed to be the guard
 * page.
 */
TEST_BEGIN(test_guarded_small) {
	test_skip_if(opt_prof);

	tsdn_t  *tsdn = tsd_tsdn(tsd_fetch());
	unsigned npages = 16, pages_found = 0, ends_found = 0;
	VARIABLE_ARRAY(uintptr_t, pages, npages);

	/* Allocate to get sanitized pointers. */
	size_t   slab_sz = PAGE;
	size_t   sz = slab_sz / 8;
	unsigned n_alloc = 0;
	while (n_alloc < MAX_SMALL_ALLOCATIONS) {
		void *ptr = malloc(sz);
		expect_ptr_not_null(ptr, "Unexpected malloc() failure");
		small_alloc[n_alloc] = ptr;
		verify_extent_guarded(tsdn, ptr);
		if ((uintptr_t)ptr % PAGE == 0) {
			assert_u_lt(pages_found, npages,
			    "Unexpectedly large number of page aligned allocs");
			pages[pages_found++] = (uintptr_t)ptr;
		}
		if (((uintptr_t)ptr + (uintptr_t)sz) % PAGE == 0) {
			ends_found++;
		}
		n_alloc++;
		if (pages_found == npages && ends_found == npages) {
			break;
		}
	}
	/* Should found the ptrs being checked for overflow and underflow. */
	expect_u_eq(pages_found, npages, "Could not found the expected pages.");
	expect_u_eq(ends_found, npages, "Could not found the expected pages.");

	/* Verify the pages are not continuous, i.e. separated by guards. */
	for (unsigned i = 0; i < npages - 1; i++) {
		for (unsigned j = i + 1; j < npages; j++) {
			uintptr_t ptr_diff = pages[i] > pages[j]
			    ? pages[i] - pages[j]
			    : pages[j] - pages[i];
			expect_zu_ge((size_t)ptr_diff, slab_sz + PAGE,
			    "There should be at least one pages between "
			    "guarded slabs");
		}
	}

	for (unsigned i = 0; i < n_alloc + 1; i++) {
		free(small_alloc[i]);
	}
}
TEST_END

TEST_BEGIN(test_guarded_large) {
	tsdn_t  *tsdn = tsd_tsdn(tsd_fetch());
	unsigned nlarge = 32;
	VARIABLE_ARRAY(uintptr_t, large, nlarge);

	/* Allocate to get sanitized pointers. */
	size_t large_sz = SC_LARGE_MINCLASS;
	for (unsigned i = 0; i < nlarge; i++) {
		void *ptr = malloc(large_sz);
		verify_extent_guarded(tsdn, ptr);
		expect_ptr_not_null(ptr, "Unexpected malloc() failure");
		large[i] = (uintptr_t)ptr;
	}

	/* Verify the pages are not continuous, i.e. separated by guards. */
	for (unsigned i = 0; i < nlarge; i++) {
		for (unsigned j = i + 1; j < nlarge; j++) {
			uintptr_t ptr_diff = large[i] > large[j]
			    ? large[i] - large[j]
			    : large[j] - large[i];
			expect_zu_ge((size_t)ptr_diff, large_sz + 2 * PAGE,
			    "There should be at least two pages between "
			    " guarded large allocations");
		}
	}

	for (unsigned i = 0; i < nlarge; i++) {
		free((void *)large[i]);
	}
}
TEST_END

static void
verify_pdirty(unsigned arena_ind, uint64_t expected) {
	uint64_t pdirty = get_arena_pdirty(arena_ind);
	expect_u64_eq(pdirty, expected / PAGE, "Unexpected dirty page amount.");
}

static void
verify_pmuzzy(unsigned arena_ind, uint64_t expected) {
	uint64_t pmuzzy = get_arena_pmuzzy(arena_ind);
	expect_u64_eq(pmuzzy, expected / PAGE, "Unexpected muzzy page amount.");
}

TEST_BEGIN(test_guarded_decay) {
	unsigned arena_ind = do_arena_create(-1, -1);
	do_decay(arena_ind);
	do_purge(arena_ind);

	verify_pdirty(arena_ind, 0);
	verify_pmuzzy(arena_ind, 0);

	/* Verify that guarded extents as dirty. */
	size_t sz1 = PAGE, sz2 = PAGE * 2;
	/* W/o maps_coalesce, guarded extents are unguarded eagerly. */
	size_t add_guard_size = maps_coalesce ? 0 : SAN_PAGE_GUARDS_SIZE;
	generate_dirty(arena_ind, sz1);
	verify_pdirty(arena_ind, sz1 + add_guard_size);
	verify_pmuzzy(arena_ind, 0);

	/* Should reuse the first extent. */
	generate_dirty(arena_ind, sz1);
	verify_pdirty(arena_ind, sz1 + add_guard_size);
	verify_pmuzzy(arena_ind, 0);

	/* Should not reuse; expect new dirty pages. */
	generate_dirty(arena_ind, sz2);
	verify_pdirty(arena_ind, sz1 + sz2 + 2 * add_guard_size);
	verify_pmuzzy(arena_ind, 0);

	tsdn_t *tsdn = tsd_tsdn(tsd_fetch());
	int     flags = MALLOCX_ARENA(arena_ind) | MALLOCX_TCACHE_NONE;

	/* Should reuse dirty extents for the two mallocx. */
	void *p1 = do_mallocx(sz1, flags);
	verify_extent_guarded(tsdn, p1);
	verify_pdirty(arena_ind, sz2 + add_guard_size);

	void *p2 = do_mallocx(sz2, flags);
	verify_extent_guarded(tsdn, p2);
	verify_pdirty(arena_ind, 0);
	verify_pmuzzy(arena_ind, 0);

	dallocx(p1, flags);
	verify_pdirty(arena_ind, sz1 + add_guard_size);
	dallocx(p2, flags);
	verify_pdirty(arena_ind, sz1 + sz2 + 2 * add_guard_size);
	verify_pmuzzy(arena_ind, 0);

	do_purge(arena_ind);
	verify_pdirty(arena_ind, 0);
	verify_pmuzzy(arena_ind, 0);

	if (config_stats) {
		expect_u64_eq(get_arena_npurge(arena_ind), 1,
		    "Expected purging to occur");
		expect_u64_eq(get_arena_dirty_npurge(arena_ind), 1,
		    "Expected purging to occur");
		expect_u64_eq(get_arena_dirty_purged(arena_ind),
		    (sz1 + sz2 + 2 * add_guard_size) / PAGE,
		    "Expected purging to occur");
		expect_u64_eq(get_arena_muzzy_npurge(arena_ind), 0,
		    "Expected purging to occur");
	}

	if (opt_retain) {
		/*
		 * With retain, guarded extents are not mergable and will be
		 * cached in ecache_retained.  They should be reused.
		 */
		void *new_p1 = do_mallocx(sz1, flags);
		verify_extent_guarded(tsdn, p1);
		expect_ptr_eq(p1, new_p1, "Expect to reuse p1");

		void *new_p2 = do_mallocx(sz2, flags);
		verify_extent_guarded(tsdn, p2);
		expect_ptr_eq(p2, new_p2, "Expect to reuse p2");

		dallocx(new_p1, flags);
		verify_pdirty(arena_ind, sz1 + add_guard_size);
		dallocx(new_p2, flags);
		verify_pdirty(arena_ind, sz1 + sz2 + 2 * add_guard_size);
		verify_pmuzzy(arena_ind, 0);
	}

	do_arena_destroy(arena_ind);
}
TEST_END

int
main(void) {
	return test(test_guarded_small, test_guarded_large, test_guarded_decay);
}
