/*
 * DF-5678 — userland differential against the SAME libprop source that is
 * compiled into the DragonFly kernel (/usr/src/sys/libprop).
 *
 * Defect under test: prop_array_set() / prop_array_remove() enforce the
 * idx < pa_count bound ONLY via _PROP_ASSERT:
 *
 *   prop_array.c:648   _PROP_ASSERT(idx < pa->pa_count);   (prop_array_set)
 *   prop_array.c:701   _PROP_ASSERT(idx < pa->pa_count);   (prop_array_remove)
 *
 * and then immediately:
 *   :650  opo = pa->pa_array[idx];          OOB READ  (8-byte kernel pointer)
 *   :654  pa->pa_array[idx] = po;           OOB WRITE (8-byte kernel pointer)
 *   :657  prop_object_release(opo);         release of OOB-read pointer
 *   :709  po = pa->pa_array[idx];           OOB READ
 *   :719  prop_object_release(po);          release of OOB-read pointer
 *
 * _PROP_ASSERT == KKASSERT in the kernel (sys/sys/systm.h:94-122): active
 * only when the kernel is built with "options INVARIANTS" (X86_64_GENERIC
 * has it; a fully supported production kernel without it compiles the check
 * to nothing -> silent heap OOB).  In userland _PROP_ASSERT == assert(),
 * killed by -DNDEBUG.  The rig exploits that 1:1 correspondence:
 *
 *   assert build  (~ stock INVARIANTS kernel)  -> abort at prop_array.c:648/:701
 *   -DNDEBUG     (~ non-INVARIANTS kernel)     -> set() returns TRUE, silent
 *   -DNDEBUG+guard (guard-page allocator)      -> SIGSEGV exactly at :650/:654/:709
 *
 * usage: ./df5678_userland <scenario>
 *   scenario: control | seteqcnt | set | remove
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>

#include <libprop/proplib.h>

static void
segv_handler(int sig, siginfo_t *si, void *uc __unused)
{
	/*
	 * Guard rig: the fault address is inside the PROT_NONE page that
	 * follows the prop_array backing store; for the 'set' scenario the
	 * first touched byte is pa_array_base + 512*8 == block_end + 0,
	 * for 'remove' it is pa_array_base + 600*8.
	 */
	fprintf(stderr, "SIGSEGV si_addr=%p (guard page after prop_array "
	    "backing store — OOB access reached)\n", si->si_addr);
	_exit(139);
}

/*
 * capacity 510 -> backing calloc(1, 510*8=4080).  In the guarded rig the
 * block is placed so byte 4080 is the last byte of a mapped page: index 512
 * (byte offset 4096) is past the rear guard page.
 */
#define	CAP	510
#define	BAD_SET_IDX	512	/* 4096 bytes in: first OOB slot */
#define	BAD_REM_IDX	600	/* 4800 bytes in: unambiguous OOB */

static void
fill(prop_array_t pa, prop_object_t po)
{
	unsigned int i;

	for (i = 0; i < CAP; i++) {
		if (prop_array_add(pa, po) == false) {
			fprintf(stderr, "FATAL: add failed at %u\n", i);
			exit(3);
		}
	}
}

int
main(int argc, char **argv)
{
	const char *sc = argc > 1 ? argv[1] : "set";
	struct sigaction sa;
	prop_array_t pa;
	prop_object_t po;
	int rv;

	memset(&sa, 0, sizeof(sa));
	sa.sa_sigaction = segv_handler;
	sa.sa_flags = SA_SIGINFO;
	sigaction(SIGSEGV, &sa, NULL);
	sigaction(SIGBUS, &sa, NULL);

	po = prop_string_create_cstring("victim");
	if (po == NULL)
		return (3);

	/* control: in-bounds set + remove must always be clean */
	if (strcmp(sc, "control") == 0) {
		pa = prop_array_create_with_capacity(CAP);
		fill(pa, po);
		rv = prop_array_set(pa, 5, po);		/* in-bounds replace */
		printf("control: set(5) rv=%d count=%u cap=%u\n",
		    rv, prop_array_count(pa), prop_array_capacity(pa));
		prop_array_remove(pa, 5);		/* in-bounds remove */
		printf("control: remove(5) count=%u\n", prop_array_count(pa));
		prop_object_release(pa);
		prop_object_release(po);
		printf("control: done, no fault\n");
		return (0);
	}

	/* seteqcnt: idx == count is the documented append path — must work */
	if (strcmp(sc, "seteqcnt") == 0) {
		pa = prop_array_create_with_capacity(CAP);
		fill(pa, po);
		rv = prop_array_set(pa, CAP, po);	/* == count -> _prop_array_add */
		printf("seteqcnt: set(%u) rv=%d count=%u cap=%u\n",
		    CAP, rv, prop_array_count(pa), prop_array_capacity(pa));
		prop_object_release(pa);
		prop_object_release(po);
		printf("seteqcnt: done, no fault\n");
		return (0);
	}

	if (strcmp(sc, "set") == 0) {
		pa = prop_array_create_with_capacity(CAP);
		fill(pa, po);
		printf("set: pre  count=%u cap=%u, calling set(%u)...\n",
		    prop_array_count(pa), prop_array_capacity(pa), BAD_SET_IDX);
		fflush(stdout);
		/*
		 * idx > count: on the assert build this aborts at
		 * prop_array.c:648; on NDEBUG builds it falls through to
		 * OOB read (:650), OOB write (:654) and release(opo) (:657).
		 */
		rv = prop_array_set(pa, BAD_SET_IDX, po);
		printf("set: SURVIVED, rv=%d (out-of-range index accepted%s)\n",
		    rv, rv ? " — assert compiled out" : "");
		prop_object_release(pa);
		prop_object_release(po);
		printf("set: released cleanly\n");
		return (0);
	}

	if (strcmp(sc, "remove") == 0) {
		pa = prop_array_create_with_capacity(CAP);
		fill(pa, po);
		printf("remove: pre  count=%u cap=%u, calling remove(%u)...\n",
		    prop_array_count(pa), prop_array_capacity(pa), BAD_REM_IDX);
		fflush(stdout);
		/*
		 * idx >= count: assert build aborts at :701; NDEBUG builds
		 * OOB-read (:709), decrement count, release garbage (:719).
		 */
		prop_array_remove(pa, BAD_REM_IDX);
		printf("remove: SURVIVED count=%u (assert compiled out)\n",
		    prop_array_count(pa));
		prop_object_release(pa);
		prop_object_release(po);
		printf("remove: released cleanly\n");
		return (0);
	}

	fprintf(stderr, "unknown scenario %s\n", sc);
	return (2);
}
