/*
 * trigger_oobfree.c - alist out-of-domain free discipline triggers
 * (DF-2798 candidate).  -DNDEBUG emulates a production kernel
 * (KKASSERT no-op).  Geometry: blocks=100 -> root meta (radix 512,
 * skip 16), leaves at slots 1..4 cover blocks 0..127 but only 0..99
 * are in domain; slots 5..16 exist (rootblks=17) yet are never
 * initialized -- we pre-poison them to prove they are never touched.
 *
 * T1: alist_free(bl, 128, 32): 128+32 > bl_blocks.
 *     - INVARIANTS kernel: KKASSERT at alist_free:286 panics.
 *     - production kernel: KKASSERT compiled out; the free lands in
 *       alst_meta_free's FULL-COVER branch (:745) which has NO
 *       bl_blocks check (the "beyond limit" panic :784 only guards
 *       the PARTIAL branch) -> parent bits for child 4 silently set
 *       to ALL-FREE, bl_free += 32 (132 > capacity!).
 *     Then alist_alloc(bl, 0, 32) -> alst_meta_alloc DIRECT path
 *     (:552, also no bl_blocks check) matches the poisoned bits and
 *     RETURNS BLOCK 128 -- out of domain.
 *     For the in-kernel consumer this would be:
 *       PHYS_TO_VM_PAGE(128 << PAGE_SHIFT) -> beyond vm_page_array.
 * T2: alist_free(bl, 120, 4): partial child beyond the limit -> hits
 *     the unconditional panic at :784 ("attempt to free block beyond
 *     limit") -- shows the asymmetry (partial=loud, full=silent).
 * T3: poison persistence dump: slots 5..16 never written (the
 *     corruption is purely in the parent's meta bits).
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/wait.h>

#define main alist_debug_main
#include "kern/subr_alist.c"
#undef main

#define POISON32	0xAAAAAAAAU

static struct alist bl;
static almeta_t records[64];

static void
t1_silent_oob(void)
{
	alist_blk_t r;
	int i, dirty = 0;

	memset(records, POISON32, sizeof(records));
	alist_init(&bl, 100, records, 64);
	printf("    geometry: blocks=%u radix=%u skip=%u rootblks=%u\n",
	       bl.bl_blocks, bl.bl_radix, bl.bl_skip, bl.bl_rootblks);
	alist_free(&bl, 0, 100);
	printf("    after free-all: bl_free=%u root bitmap=%08x\n",
	       bl.bl_free, bl.bl_root->bm_bitmap);

	/* caller slip: free 32 blocks at 128 (out of domain) */
	alist_free(&bl, 128, 32);
	printf("    after alist_free(bl,128,32): bl_free=%u (capacity=%u!) "
	       "root bitmap=%08x  <-- NO PANIC, bits 8..9 set (child4=11)\n",
	       bl.bl_free, bl.bl_blocks, bl.bl_root->bm_bitmap);

	/* exhaust the 3 in-domain all-free children (blocks 0..95) */
	for (i = 0; i < 3; ++i)
		r = alist_alloc(&bl, 0, 32);
	printf("    3x alist_alloc(bl,0,32) -> %u (in-domain exhausted)\n",
	       r);

	/* now the allocation is forced onto the poisoned child 4 */
	r = alist_alloc(&bl, 0, 32);
	printf("    alist_alloc(bl,0,32) returned %u  <-- bl_blocks=%u: "
	       "OUT-OF-DOMAIN block number handed out\n",
	       r, bl.bl_blocks);
	printf("    bl_free=%u (restored), root bitmap=%08x\n",
	       bl.bl_free, bl.bl_root->bm_bitmap);

	for (i = 5; i < (int)bl.bl_rootblks; ++i)
		if (records[i].bm_bitmap != POISON32 ||
		    records[i].bm_bighint != POISON32)
			dirty++;
	printf("    uninitialized slots 5..%d written: %d (corruption is "
	       "logical, in parent meta bits)\n",
	       (int)bl.bl_rootblks - 1, dirty);
}

static void
t2_partial_oob(void)
{
	memset(records, POISON32, sizeof(records));
	alist_init(&bl, 100, records, 64);
	alist_free(&bl, 0, 100);
	printf("    calling alist_free(bl, 120, 4) (partial child beyond "
	       "limit)...\n");
	fflush(stdout);
	alist_free(&bl, 120, 4);	/* panics via panic() at :784 */
	printf("    UNEXPECTEDLY returned\n");
}

int
main(void)
{
	pid_t p;
	int st;

	setvbuf(stdout, NULL, _IONBF, 0);
	printf("== T1 (silent full-cover OOB free -> out-of-domain alloc) "
	       "==\n");
	t1_silent_oob();

	printf("== T2 (partial OOB free -> loud panic :784) ==\n");
	p = fork();
	if (p == 0)
		t2_partial_oob();
	waitpid(p, &st, 0);
	printf("    child exit=%d (1 == panic'ed as expected)\n",
	       WEXITSTATUS(st));

	return 0;
}
