/*
 * DF-5691 — pass-2 differential + lifetime harness for
 * sys/libprop/prop_dictionary_util.c (the prop_dictionary_
 * {get,set}_{bool,u,}int{8,16,32,64}/cstring[_nocopy] helper family,
 * prop_dictionary_get_dict, prop_dictionary_set_and_rel).
 *
 * Modes:
 *   matrix        plist-XML -> prop_dictionary_internalize -> every typed
 *                 getter, differential against the DERIVED guard contract:
 *                   signed v:   intN accept iff INTN_MIN<=v<=INTN_MAX
 *                               uintN accept iff 0<=v<=INTN_MAX
 *                   unsigned u: intN accept iff u<=INTN_MAX
 *                               uintN accept iff u<=UINTN_MAX
 *                 (identical algebra to the array twins, DF-5690 sec.1);
 *                 plus setter roundtrips in BOTH directions (incl. the
 *                 DF-2240 differential: set_uint64(UINT64_MAX) roundtrips
 *                 here because set_uintN uses the UNSIGNED creator,
 *                 prop_dictionary_util.c:167 — the array add_uint broke),
 *                 wrong-type/missing-key/NULL-dict type-guard sweep, bool,
 *                 cstring(copy), get_dict, set_and_rel, immutable-dict
 *                 setter failures, externalize sanity.
 *   nolife        _nocopy lifetime hazards on the guard-page rig
 *                 (fork per case; SIGSEGV = deterministic trap):
 *                 T1  get_cstring_nocopy borrows a pointer into the
 *                     prop_string's sole-owned buffer; REPLACING the same
 *                     key in the same dict releases the old string
 *                     (prop_dictionary.c:988-993) -> use-after-free read.
 *                     This is the LIVE in-tree dm_dev_status_ioctl pattern
 *                     (dm_ioctl.c:397 borrow -> :409 key replace).
 *                 T1b the dm_table_deps_ioctl pattern: borrow name+uuid,
 *                     then prop_dictionary_set_cstring BOTH keys
 *                     (dm_ioctl.c:619-620) -> both borrows dangle; deref
 *                     name -> use-after-free read.
 *                 T2  dict release (last ref) while borrowed -> UAF read.
 *                 T2b set_cstring_nocopy keeps a raw pointer to the
 *                     caller's buffer forever (prop_string.c:188-200; free
 *                     skipped at prop_string.c:85-87); caller frees the
 *                     buffer; the pointer proplib still hands out is read
 *                     with no intervening allocation -> UAF read.
 *                 T2c same, but the caller REUSES the buffer; externalize
 *                     (the dm dict copyout analog) leaks the NEW buffer
 *                     content (re-read to the current NUL, ignoring the
 *                     frozen ps_size) — exit-code semantics:
 *                     exit0 = leak observed (baseline), exit3 = frozen
 *                     copy (fixed).
 *                 T3  ps_size frozen at set_cstring_nocopy time, but
 *                     prop_string_cstring()/get_cstring strcpy()s to the
 *                     CURRENT NUL of the caller's buffer
 *                     (prop_string.c:302-304) -> heap overflow into the
 *                     guard page (15 frozen vs 8190 current).
 *                 T4  control: copy-variant end-to-end -> no trap.
 *                 DML latent-today proof: the EXACT in-tree
 *                     dm_table_deps_ioctl sequence (borrow -> replace both
 *                     keys -> NO post-replace deref) must NOT trap and the
 *                     dict must stay consistent — documents that the
 *                     in-tree dangle is real but undereferenced today.
 *                 NCD alive-read sanity (borrow valid while parent alive).
 *   nolife-fixed  same battery with T2b/T2c/T3 requiring the FIXED
 *                 behavior (fix under test: kernel-build nocopy-create
 *                 copies instead of borrowing, prop_string.c); T1/T1b/T2
 *                 still trap (documented caller-side borrow contract).
 *
 * Build: see build.sh (same-source libprop rigs from /usr/src/sys/libprop).
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>

#include <libprop/proplib.h>

#ifdef USE_GA
#include "guardalloc.h"
#endif

static int checks, fails, knowns;

#define FAIL(...) do { printf("FAIL: " __VA_ARGS__); fails++; } while (0)
#define CHECK(cond, ...) do { checks++; if (!(cond)) { FAIL(__VA_ARGS__); } } while (0)

/* the live dm ioctl keys (sys/dev/disk/dm/netbsd-dm.h:190-191) */
#define DM_NAME_KEY	"name"
#define DM_UUID_KEY	"uuid"

/* ---------------- plist construction ---------------- */

static const char plist_hdr[] =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" "
"\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
"<plist version=\"1.0\">\n";
static const char plist_ftr[] = "</plist>\n";

static prop_dictionary_t
dict_from_body(const char *body)
{
	char xml[4096];

	snprintf(xml, sizeof(xml), "%s%s%s", plist_hdr, body, plist_ftr);
	return (prop_dictionary_internalize(xml));
}

/* ---------------- matrix mode (plain rig only) ---------------- */

static int64_t  svals[] = { 0, 1, -1, 42, -42, INT8_MIN, INT8_MAX,
			    INT16_MIN, INT16_MAX, INT32_MIN, INT32_MAX,
			    INT64_MIN, INT64_MAX,
			    INT8_MIN + 1, INT8_MAX - 1, -100, 100, 200, 300 };
static uint64_t uvals[] = { 0, 1, 42, UINT8_MAX, UINT8_MAX - 1, 200, 255, 256,
			    UINT16_MAX, UINT16_MAX - 1, 65535, 65536,
			    UINT32_MAX, UINT32_MAX - 1,
			    (uint64_t)INT32_MAX + 1,
			    UINT64_MAX, UINT64_MAX - 1,
			    (uint64_t)INT64_MAX + 1, (uint64_t)INT64_MAX };

#define NSV ((int)(sizeof(svals) / sizeof(svals[0])))
#define NUV ((int)(sizeof(uvals) / sizeof(uvals[0])))

/* width-parameterized min/max tables + derived guard contract
 * (identical algebra to the array twins, DF-5690 sec.1) */
static int64_t  smin[5]  = {0, INT8_MIN,  INT16_MIN,  INT32_MIN,  INT64_MIN };
static int64_t  smax[5]  = {0, INT8_MAX,  INT16_MAX,  INT32_MAX,  INT64_MAX };
static uint64_t umax_[5] = {0, UINT8_MAX, UINT16_MAX, UINT32_MAX, UINT64_MAX};
static int
widx(int width) { return (width == 8 ? 1 : width == 16 ? 2 : width == 32 ? 3 : 4); }
static int64_t
intmin(int width) { return (smin[widx(width)]); }
static int64_t
intmax(int width) { return (smax[widx(width)]); }
static uint64_t
uintmax(int width) { return (umax_[widx(width)]); }

static int
accept_int(int width, int is_unsigned, uint64_t uval, int64_t sval)
{
	if (!is_unsigned)
		return (sval >= intmin(width) && sval <= intmax(width));
	return (uval <= (uint64_t)intmax(width));
}
static int
accept_uint(int width, int is_unsigned, uint64_t uval, int64_t sval)
{
	if (!is_unsigned)
		return (sval >= 0 && sval <= intmax(width));
	return (uval <= uintmax(width));
}

static void
check_int_getters(prop_dictionary_t dict, const char *key, int is_unsigned,
		  uint64_t uval, int64_t sval, int64_t expected64)
{
	int8_t   v8;  int16_t v16; int32_t v32; int64_t v64;
	uint8_t  w8;  uint16_t w16; uint32_t w32; uint64_t w64;
	int acc;

	acc = accept_int(8,  is_unsigned, uval, sval);
	CHECK(prop_dictionary_get_int8(dict, key, &v8)   == acc, "int8  acc %s\n", key);
	if (acc) CHECK(v8 == (int8_t)expected64, "int8 val %s\n", key);
	acc = accept_int(16, is_unsigned, uval, sval);
	CHECK(prop_dictionary_get_int16(dict, key, &v16) == acc, "int16 acc %s\n", key);
	if (acc) CHECK(v16 == (int16_t)expected64, "int16 val %s\n", key);
	acc = accept_int(32, is_unsigned, uval, sval);
	CHECK(prop_dictionary_get_int32(dict, key, &v32) == acc, "int32 acc %s\n", key);
	if (acc) CHECK(v32 == (int32_t)expected64, "int32 val %s\n", key);
	acc = accept_int(64, is_unsigned, uval, sval);
	CHECK(prop_dictionary_get_int64(dict, key, &v64) == acc, "int64 acc %s\n", key);
	if (acc) CHECK(v64 == (int64_t)expected64, "int64 val %s\n", key);

	acc = accept_uint(8,  is_unsigned, uval, sval);
	CHECK(prop_dictionary_get_uint8(dict, key, &w8)   == acc, "uint8  acc %s\n", key);
	if (acc) CHECK(w8 == (uint8_t)expected64, "uint8 val %s\n", key);
	acc = accept_uint(16, is_unsigned, uval, sval);
	CHECK(prop_dictionary_get_uint16(dict, key, &w16) == acc, "uint16 acc %s\n", key);
	if (acc) CHECK(w16 == (uint16_t)expected64, "uint16 val %s\n", key);
	acc = accept_uint(32, is_unsigned, uval, sval);
	CHECK(prop_dictionary_get_uint32(dict, key, &w32) == acc, "uint32 acc %s\n", key);
	if (acc) CHECK(w32 == (uint32_t)expected64, "uint32 val %s\n", key);
	acc = accept_uint(64, is_unsigned, uval, sval);
	CHECK(prop_dictionary_get_uint64(dict, key, &w64) == acc, "uint64 acc %s\n", key);
	if (acc) CHECK(w64 == (uint64_t)expected64, "uint64 val %s\n", key);
}

static int
run_matrix(void)
{
	prop_dictionary_t dict, sub, dp;
	char *cp;
	const char *ncp;
	int8_t v8; uint8_t w8; int16_t v16; uint16_t w16;
	int32_t v32; uint32_t w32; int64_t v64; uint64_t w64;
	bool b;
	int i;
	char val[64];

	/* ---- fixture: attacker-style plist with signed, unsigned, string,
	 *      bool, array, dict, empty-string keys ---- */
	dict = dict_from_body(
	    "<dict>"
	    "<key>n1</key><integer>-100</integer>"	/* size-8 signed neg */
	    "<key>n2</key><integer>127</integer>"
	    "<key>n3</key><integer>128</integer>"	/* > INT8_MAX */
	    "<key>n4</key><integer>300</integer>"
	    "<key>n5</key><integer>-129</integer>"
	    "<key>nu1</key><integer>0xff</integer>"	/* 255: uint8 ok */
	    "<key>nu2</key><integer>0x100</integer>"	/* 256 */
	    "<key>nu3</key><integer>0x7f</integer>"
	    "<key>nu4</key><integer>0x8000000000000000</integer>"
	    "<key>nu5</key><integer>0xffffffffffffffff</integer>"
	    "<key>str</key><string>hello-dict</string>"
	    "<key>estr</key><string></string>"
	    "<key>bt</key><true/>"
	    "<key>bf</key><false/>"
	    "<key>arr</key><array><integer>1</integer></array>"
	    "<key>sub</key><dict><key>k</key><string>v</string></dict>"
	    "</dict>");
	CHECK(dict != NULL, "internalize fixture\n");
	if (dict == NULL) { printf("FATAL: fixture\n"); return (1); }

	/* derived-contract sweep over the numeric keys */
	{
		int64_t  fx_s[] = { -100, 127, 128, 300, -129 };
		uint64_t fx_u[] = { 0xffULL, 0x100ULL, 0x7fULL,
				    0x8000000000000000ULL, 0xffffffffffffffffULL };
		const char *fk_s[] = { "n1","n2","n3","n4","n5" };
		const char *fk_u[] = { "nu1","nu2","nu3","nu4","nu5" };
		for (i = 0; i < 5; i++)
			check_int_getters(dict, fk_s[i], 0, 0, fx_s[i], fx_s[i]);
		for (i = 0; i < 5; i++)
			check_int_getters(dict, fk_u[i], 1, fx_u[i], 0,
			    (int64_t)fx_u[i]);
	}

	/* wrong-type sweep: each getter only against keys of OTHER types */
	{
		/* strings (bt/bf are bools, arr array, sub dict, n1 number,
		 * absent missing) — nothing here is right-typed for the
		 * numeric getters */
		const char *ntnum[] = { "str", "bt", "bf", "arr", "sub",
					"absent", NULL };
		const char *ntbool[] = { "str", "arr", "sub", "n1", "absent", NULL };
		const char *ntstr[]  = { "bt", "bf", "arr", "sub", "n1",
					 "absent", NULL };
		const char *ntdict[] = { "str", "bt", "bf", "arr", "n1",
					 "absent", NULL };
		for (i = 0; ntnum[i] != NULL; i++) {
			CHECK(prop_dictionary_get_int8(dict, ntnum[i], &v8) == false,
			    "int8 wrong-type %s\n", ntnum[i]);
			CHECK(prop_dictionary_get_uint8(dict, ntnum[i], &w8) == false,
			    "uint8 wrong-type %s\n", ntnum[i]);
			CHECK(prop_dictionary_get_int16(dict, ntnum[i], &v16) == false,
			    "int16 wrong-type %s\n", ntnum[i]);
			CHECK(prop_dictionary_get_uint16(dict, ntnum[i], &w16) == false,
			    "uint16 wrong-type %s\n", ntnum[i]);
			CHECK(prop_dictionary_get_int32(dict, ntnum[i], &v32) == false,
			    "int32 wrong-type %s\n", ntnum[i]);
			CHECK(prop_dictionary_get_uint32(dict, ntnum[i], &w32) == false,
			    "uint32 wrong-type %s\n", ntnum[i]);
			CHECK(prop_dictionary_get_int64(dict, ntnum[i], &v64) == false,
			    "int64 wrong-type %s\n", ntnum[i]);
			CHECK(prop_dictionary_get_uint64(dict, ntnum[i], &w64) == false,
			    "uint64 wrong-type %s\n", ntnum[i]);
		}
		for (i = 0; ntbool[i] != NULL; i++)
			CHECK(prop_dictionary_get_bool(dict, ntbool[i], &b) == false,
			    "bool wrong-type %s\n", ntbool[i]);
		for (i = 0; ntstr[i] != NULL; i++) {
			CHECK(prop_dictionary_get_cstring(dict, ntstr[i], &cp) == false,
			    "cstring wrong-type %s\n", ntstr[i]);
			CHECK(prop_dictionary_get_cstring_nocopy(dict, ntstr[i], &ncp) == false,
			    "cnocopy wrong-type %s\n", ntstr[i]);
		}
		for (i = 0; ntdict[i] != NULL; i++)
			CHECK(prop_dictionary_get_dict(dict, ntdict[i], &dp) == false,
			    "getdict wrong-type %s\n", ntdict[i]);
		/* NULL dict: every getter must fail cleanly, no crash */
		CHECK(prop_dictionary_get_int8(NULL, "n1", &v8) == false, "int8 NULL dict\n");
		CHECK(prop_dictionary_get_uint64(NULL, "nu1", &w64) == false, "uint64 NULL dict\n");
		CHECK(prop_dictionary_get_bool(NULL, "bt", &b) == false, "bool NULL dict\n");
		CHECK(prop_dictionary_get_cstring(NULL, "str", &cp) == false, "cstring NULL dict\n");
		CHECK(prop_dictionary_get_dict(NULL, "sub", &dp) == false, "getdict NULL dict\n");
	}

	/* bool + string + dict semantics */
	CHECK(prop_dictionary_get_bool(dict, "bt", &b) == true && b == true, "bool true\n");
	CHECK(prop_dictionary_get_bool(dict, "bf", &b) == true && b == false, "bool false\n");
	CHECK(prop_dictionary_get_cstring(dict, "str", &cp) == true && cp != NULL &&
	    strcmp(cp, "hello-dict") == 0, "cstring copy\n");
	free(cp);
	CHECK(prop_dictionary_get_cstring(dict, "estr", &cp) == true &&
	    cp != NULL && cp[0] == '\0', "cstring empty\n");
	free(cp);
	CHECK(prop_dictionary_get_cstring_nocopy(dict, "str", &ncp) == true &&
	    strcmp(ncp, "hello-dict") == 0, "cnocopy alive-read\n");
	CHECK(prop_dictionary_get_dict(dict, "sub", &dp) == true && dp != NULL,
	    "getdict nested\n");
	CHECK(prop_dictionary_get_cstring(dp, "k", &cp) == true &&
	    strcmp(cp, "v") == 0, "nested content\n");
	free(cp);

	/* setter roundtrips — signed and unsigned, every width, both ways */
	for (i = 0; i < NSV; i++) {
		snprintf(val, sizeof(val), "t%d", i);
		CHECK(prop_dictionary_set_int8(dict, val, (int8_t)svals[i]) == true,
		    "set_int8 %d\n", i);
		check_int_getters(dict, val, 0, 0,
		    (int64_t)(int8_t)svals[i], (int64_t)(int8_t)svals[i]);
		snprintf(val, sizeof(val), "t%d", i);
		CHECK(prop_dictionary_set_int16(dict, val, (int16_t)svals[i]) == true,
		    "set_int16 %d\n", i);
		check_int_getters(dict, val, 0, 0,
		    (int64_t)(int16_t)svals[i], (int64_t)(int16_t)svals[i]);
		snprintf(val, sizeof(val), "t%d", i);
		CHECK(prop_dictionary_set_int32(dict, val, (int32_t)svals[i]) == true,
		    "set_int32 %d\n", i);
		check_int_getters(dict, val, 0, 0,
		    (int64_t)(int32_t)svals[i], (int64_t)(int32_t)svals[i]);
		snprintf(val, sizeof(val), "t%d", i);
		CHECK(prop_dictionary_set_int64(dict, val, svals[i]) == true,
		    "set_int64 %d\n", i);
		check_int_getters(dict, val, 0, 0, svals[i], svals[i]);
	}
	for (i = 0; i < NUV; i++) {
		snprintf(val, sizeof(val), "q%d", i);
		CHECK(prop_dictionary_set_uint8(dict, val, (uint8_t)uvals[i]) == true,
		    "set_uint8 %d\n", i);
		check_int_getters(dict, val, 1, (uint64_t)(uint8_t)uvals[i], 0,
		    (int64_t)(uint8_t)uvals[i]);
		snprintf(val, sizeof(val), "q%d", i);
		CHECK(prop_dictionary_set_uint16(dict, val, (uint16_t)uvals[i]) == true,
		    "set_uint16 %d\n", i);
		check_int_getters(dict, val, 1, (uint64_t)(uint16_t)uvals[i], 0,
		    (int64_t)(uint16_t)uvals[i]);
		snprintf(val, sizeof(val), "q%d", i);
		CHECK(prop_dictionary_set_uint32(dict, val, (uint32_t)uvals[i]) == true,
		    "set_uint32 %d\n", i);
		check_int_getters(dict, val, 1, (uint64_t)(uint32_t)uvals[i], 0,
		    (int64_t)(uint32_t)uvals[i]);
		snprintf(val, sizeof(val), "q%d", i);
		CHECK(prop_dictionary_set_uint64(dict, val, uvals[i]) == true,
		    "set_uint64 %d\n", i);
		{
			/* full 64-bit unsigned: stored unsigned, read back
			 * exactly — the DF-2240 differential (array
			 * add_uint64(UINT64_MAX) stored it SIGNED and broke
			 * the roundtrip; the dict set_uint64 creator is
			 * prop_number_create_unsigned_integer, line 167) */
			uint64_t rr;
			CHECK(prop_dictionary_get_uint64(dict, val, &rr) == true,
			    "DF2240-diff get %d\n", i);
			if (uvals[i] == UINT64_MAX) {
				CHECK(rr == uvals[i], "DF2240-diff val %d: %" PRIu64 " != %" PRIu64 "\n",
				    i, rr, uvals[i]);
			}
		}
		/* expected64 is a bit-preserving cast: the uint64 getter row
		 * compares (uint64_t)expected64, so full-range uvals come
		 * back identical */
		check_int_getters(dict, val, 1, uvals[i], 0, (int64_t)uvals[i]);
	}

	/* DF-2240 headline row: UINT64_MAX roundtrips exactly */
	CHECK(prop_dictionary_set_uint64(dict, "umax", UINT64_MAX) == true,
	    "set UINT64_MAX\n");
	CHECK(prop_dictionary_get_uint64(dict, "umax", &w64) == true &&
	    w64 == UINT64_MAX, "UINT64_MAX roundtrip (DF-2240 differential)\n");
	knowns++;	/* documented differential-good row */

	/* bool/cstring setters */
	CHECK(prop_dictionary_set_bool(dict, "b1", true) == true, "set_bool t\n");
	CHECK(prop_dictionary_get_bool(dict, "b1", &b) == true && b == true, "rt bool t\n");
	CHECK(prop_dictionary_set_bool(dict, "b1", false) == true, "set_bool f\n");
	CHECK(prop_dictionary_get_bool(dict, "b1", &b) == true && b == false, "rt bool f\n");
	CHECK(prop_dictionary_set_cstring(dict, "cs", "set-me") == true, "set_cstring\n");
	CHECK(prop_dictionary_get_cstring(dict, "cs", &cp) == true &&
	    strcmp(cp, "set-me") == 0, "rt cstring\n");
	free(cp);
	CHECK(prop_dictionary_set_cstring(dict, "cs", "") == true, "set empty\n");
	CHECK(prop_dictionary_get_cstring_nocopy(dict, "cs", &ncp) == true &&
	    ncp[0] == '\0', "rt empty nocopy\n");

	/* set_and_rel: success, NULL-po, immutable-fail */
	sub = prop_dictionary_create();
	CHECK(sub != NULL, "sub create\n");
	CHECK(prop_dictionary_set_cstring(sub, "x", "y") == true, "sub fill\n");
	prop_object_retain(sub);			/* keep alive past rel */
	CHECK(prop_dictionary_set_and_rel(dict, "sar", sub) == true, "sar ok\n");
	CHECK(prop_dictionary_get_dict(dict, "sar", &dp) == true &&
	    prop_object_equals(dp, sub) == true, "sar stored\n");
	prop_object_release(sub);			/* our extra ref */
	sub = prop_dictionary_create();
	prop_object_retain(sub);			/* survived the rel below */
	CHECK(prop_dictionary_set_and_rel(dict, "sar2", NULL) == false, "sar NULL\n");
	prop_dictionary_make_immutable(dict);
	CHECK(prop_dictionary_set_and_rel(dict, "sar3", sub) == false,
	    "sar immutable\n");
	prop_object_release(sub);	/* would UAF/double-free if sar released it too */
	/* immutable dict: every setter must refuse */
	CHECK(prop_dictionary_set_int32(dict, "imm", 1) == false, "imm set_int32\n");
	CHECK(prop_dictionary_set_uint32(dict, "imm", 1) == false, "imm set_uint32\n");
	CHECK(prop_dictionary_set_bool(dict, "imm", true) == false, "imm set_bool\n");
	CHECK(prop_dictionary_set_cstring(dict, "imm", "x") == false, "imm set_cstring\n");
	/* and getters still work on the immutable dict */
	CHECK(prop_dictionary_get_cstring_nocopy(dict, "str", &ncp) == true &&
	    strcmp(ncp, "hello-dict") == 0, "immutable read\n");

	/* externalize sanity on the fully-populated dict */
	cp = prop_dictionary_externalize(dict);
	CHECK(cp != NULL && strstr(cp, "hello-dict") != NULL, "externalize\n");
	free(cp);

	/* set-side NULL dict rows */
	CHECK(prop_dictionary_set_int32(NULL, "x", 1) == false, "set_int32 NULL\n");
	CHECK(prop_dictionary_set_cstring(NULL, "x", "y") == false, "set_cstring NULL\n");
	CHECK(prop_dictionary_set_cstring_nocopy(NULL, "x", "y") == false, "set_cstr_nc NULL\n");

	prop_object_release(dict);
	printf("MATRIX: %d checks, %d failures, %d known(DF-2240-diff) rows\n",
	    checks, fails, knowns);
	return (fails == 0 ? 0 : 1);
}

/* ---------------- nolife mode (guard-page rig) ---------------- */

#ifdef USE_GA

static volatile const char *keep;
static volatile char sink;

/* in-tree dm-style input dicts (what dmioctl() internalizes from the
 * libdevmapper request plist) */
#define DM_IN_XML_BODY \
	"<dict><key>name</key><string>myvol</string>" \
	"<key>uuid</key><string>uuid-AAAA-1111</string></dict>"

static void
die(int code, const char *what)
{
	printf("%s: %s failed\n", "case", what);
	fflush(stdout);
	_exit(code);
}

static void
do_case(int id)
{
	prop_dictionary_t d;
	const char *name, *uuid, *cp;
	char *owner, *xml, *copy;

	switch (id) {
	case 1:	/* T1: dm_dev_status_ioctl pattern — uuid borrow, then the
		 * ioctl replaces the UUID key (dm_ioctl.c:397 -> :409) */
		d = dict_from_body(DM_IN_XML_BODY);
		if (d == NULL) die(9, "internalize");
		name = uuid = NULL;
		if (prop_dictionary_get_cstring_nocopy(d, DM_NAME_KEY, &name) != true)
			die(9, "get name");
		if (prop_dictionary_get_cstring_nocopy(d, DM_UUID_KEY, &uuid) != true)
			die(9, "get uuid");
		printf("T1: borrowed uuid=%p into string storage\n", (void *)uuid);
		fflush(stdout);
		/* dm_ioctl.c:409: replace UUID key -> releases the borrowed-from
		 * prop_string (prop_dictionary.c:988-993) */
		if (prop_dictionary_set_cstring(d, DM_UUID_KEY, "kernel-uuid") != true)
			die(9, "replace uuid");
		fflush(stdout);
		keep = uuid;
		sink = *keep;		/* baseline: freed page -> SIGSEGV */
		printf("T1: NO TRAP reading freed uuid[0]='%c'\n", (int)sink);
		fflush(stdout);
		_exit(0);
	case 2:	/* T1b: dm_table_deps_ioctl pattern — name+uuid borrowed
		 * (dm_ioctl.c:596-597), then the ioctl replaces the keys
		 * (:619-620).  The name borrow dangles at :619 already; read
		 * it there (no intervening allocation before the read —
		 * reading after BOTH replaces lets the second set_cstring's
		 * own mmap recycle the just-freed page, which DF-5690 also
		 * observed). */
		d = dict_from_body(DM_IN_XML_BODY);
		if (d == NULL) die(9, "internalize");
		name = uuid = NULL;
		if (prop_dictionary_get_cstring_nocopy(d, DM_NAME_KEY, &name) != true)
			die(9, "get name");
		if (prop_dictionary_get_cstring_nocopy(d, DM_UUID_KEY, &uuid) != true)
			die(9, "get uuid");
		printf("T1b: borrowed name=%p uuid=%p\n", (void *)name, (void *)uuid);
		fflush(stdout);
		/* dm_ioctl.c:619: replace NAME key -> releases the borrowed-from
		 * prop_string (prop_dictionary.c:988-993); :620 does the same
		 * to uuid immediately after */
		if (prop_dictionary_set_cstring(d, DM_NAME_KEY, "kernel-name") != true)
			die(9, "replace name");
		fflush(stdout);
		keep = name;
		sink = *keep;		/* baseline: freed page -> SIGSEGV */
		printf("T1b: NO TRAP reading freed name[0]='%c'\n", (int)sink);
		fflush(stdout);
		_exit(0);
	case 3:	/* T2: dict release (last ref) while borrowed */
		d = prop_dictionary_create();
		if (d == NULL) die(9, "create");
		if (prop_dictionary_set_cstring(d, DM_UUID_KEY, "uuid-CCCC-3333") != true)
			die(9, "set");
		uuid = NULL;
		if (prop_dictionary_get_cstring_nocopy(d, DM_UUID_KEY, &uuid) != true)
			die(9, "get");
		printf("T2: borrowed uuid=%p; releasing dict (last ref)\n", (void *)uuid);
		fflush(stdout);
		prop_object_release(d);
		fflush(stdout);
		keep = uuid;
		sink = *keep;		/* freed page -> SIGSEGV */
		printf("T2: NO TRAP reading freed uuid[0]='%c'\n", (int)sink);
		fflush(stdout);
		_exit(0);
	case 4:	/* T2b: set_cstring_nocopy borrows owner; owner FREES it; the
		 * pointer proplib hands back is read with no intervening
		 * allocation */
		owner = ga_malloc(64);
		if (owner == NULL) die(9, "owner");
		strcpy(owner, "transient-buffer");
		d = prop_dictionary_create();
		if (d == NULL) die(9, "create");
		if (prop_dictionary_set_cstring_nocopy(d, "k", owner) != true)
			die(9, "set nocopy");
		printf("T2b: string borrows owner=%p; caller FREES it\n", (void *)owner);
		fflush(stdout);
		ga_free(owner);		/* kernel caller frees its buffer */
		cp = NULL;
		if (prop_dictionary_get_cstring_nocopy(d, "k", &cp) != true || cp == NULL)
			die(9, "get nocopy");
		fflush(stdout);
		keep = cp;
		sink = *keep;		/* baseline: freed page -> SIGSEGV */
		/* fixed: string owns its copy -> intact content */
		if (strcmp(cp, "transient-buffer") == 0) {
			printf("T2b: NO TRAP, intact content \"%s\"\n", cp);
			fflush(stdout);
			_exit(0);
		}
		printf("T2b: NO TRAP but wrong content \"%s\"\n", cp);
		fflush(stdout);
		_exit(5);
	case 5:	/* T2c: set_cstring_nocopy borrows owner; owner REUSES the
		 * buffer; externalize (dm dict copyout analog) leaks the NEW
		 * buffer content — exit0=leak(baseline), exit3=frozen(fixed) */
		owner = ga_malloc(64);
		if (owner == NULL) die(9, "owner");
		strcpy(owner, "transient-buffer");
		d = prop_dictionary_create();
		if (d == NULL) die(9, "create");
		if (prop_dictionary_set_cstring_nocopy(d, "k", owner) != true)
			die(9, "set nocopy");
		printf("T2c: string borrows owner=%p; caller REUSES the buffer\n",
		    (void *)owner);
		fflush(stdout);
		strcpy(owner, "PWNED-BY-BUFFER-REUSE");
		xml = prop_dictionary_externalize(d);
		printf("T2c: externalized:\n%s", xml ? xml : "(null)");
		fflush(stdout);
		if (xml != NULL && strstr(xml,
		    "<string>PWNED-BY-BUFFER-REUSE</string>") != NULL)
			_exit(0);
		if (xml != NULL && strstr(xml,
		    "<string>transient-buffer</string>") != NULL)
			_exit(3);
		_exit(6);
	case 6:	/* T3: ps_size frozen at set time; get_cstring strcpy()s to the
		 * CURRENT NUL of the caller's buffer -> overflow */
		owner = ga_malloc(8192);
		if (owner == NULL) die(9, "owner");
		memset(owner, 'B', 8191);
		owner[15] = '\0';	/* 15 chars at set time */
		d = prop_dictionary_create();
		if (d == NULL) die(9, "create");
		if (prop_dictionary_set_cstring_nocopy(d, "k", owner) != true)
			die(9, "set nocopy");
		printf("T3: ps_size=15 frozen; owner buffer now lengthened\n");
		fflush(stdout);
		owner[15] = 'B';	/* caller lengthens in same buffer */
		owner[8191] = '\0';
		copy = NULL;
		if (prop_dictionary_get_cstring(d, "k", &copy) != true)
			die(9, "get_cstring");
		printf("T3: NO TRAP copy=%.16s... (len=%zu)\n", copy, strlen(copy));
		fflush(stdout);
		/* fixed: frozen 15-byte copy, no overrun */
		if (strlen(copy) == 15)
			_exit(0);
		_exit(5);
	case 7:	/* T4 control: copy-variant set + owner free -> no trap */
		owner = ga_malloc(64);
		if (owner == NULL) die(9, "owner");
		strcpy(owner, "control");
		d = prop_dictionary_create();
		if (d == NULL) die(9, "create");
		if (prop_dictionary_set_cstring(d, "k", owner) != true)
			die(9, "set cstring");
		ga_free(owner);
		xml = prop_dictionary_externalize(d);
		printf("T4(control): externalize=%s", xml ? xml : "(null)");
		fflush(stdout);
		if (xml != NULL && strstr(xml, "<string>control</string>"))
			_exit(0);
		_exit(4);
	case 8:	/* DML: EXACT in-tree dm_table_deps_ioctl sequence WITHOUT
		 * post-replace deref — latent-today proof */
		d = dict_from_body(DM_IN_XML_BODY);
		if (d == NULL) die(9, "internalize");
		name = uuid = NULL;
		if (prop_dictionary_get_cstring_nocopy(d, DM_NAME_KEY, &name) != true)
			die(9, "get name");
		if (prop_dictionary_get_cstring_nocopy(d, DM_UUID_KEY, &uuid) != true)
			die(9, "get uuid");
		printf("DML: borrowed name=%p uuid=%p (dm_ioctl.c:596-597)\n",
		    (void *)name, (void *)uuid);
		fflush(stdout);
		/* dm_ioctl.c:619-620: replace BOTH keys; borrows dangle but are
		 * NOT dereferenced afterwards in the in-tree code */
		if (prop_dictionary_set_cstring(d, DM_NAME_KEY, "kernel-name") != true)
			die(9, "replace name");
		if (prop_dictionary_set_cstring(d, DM_UUID_KEY, "kernel-uuid") != true)
			die(9, "replace uuid");
		xml = prop_dictionary_externalize(d);	/* dict must stay consistent */
		if (xml == NULL || strstr(xml, "kernel-uuid") == NULL) {
			printf("DML: dict INCONSISTENT after replace pattern\n");
			fflush(stdout);
			_exit(8);
		}
		printf("DML: dict consistent after in-tree replace pattern; "
		    "borrows %p/%p NOT dereferenced (latent)\n",
		    (void *)name, (void *)uuid);
		fflush(stdout);
		prop_object_release(d);	/* also proves no double-free at teardown */
		_exit(0);
	case 9:	/* NCD: alive-read sanity — borrow valid while parent alive */
		d = dict_from_body(
		    "<dict><key>name</key><string>alive</string></dict>");
		if (d == NULL) die(9, "internalize");
		name = NULL;
		if (prop_dictionary_get_cstring_nocopy(d, DM_NAME_KEY, &name) != true)
			die(9, "get name");
		keep = name;
		sink = *keep;		/* valid page: no trap */
		if (sink != 'a') {
			printf("NCD: wrong content\n");
			fflush(stdout);
			_exit(7);
		}
		prop_object_release(d);
		_exit(0);
	}
	_exit(3);
}

static int
run_forked_case(int id)
{
	pid_t pid;
	int st;

	fflush(stdout); fflush(stderr);
	pid = fork();
	if (pid < 0) { perror("fork"); return (-9); }
	if (pid == 0)
		do_case(id);		/* no return */
	waitpid(pid, &st, 0);
	if (WIFSIGNALED(st) && WTERMSIG(st) == SIGSEGV)
		return (1);
	if (WIFEXITED(st)) {
		if (WEXITSTATUS(st) == 0)
			return (0);
		return (-WEXITSTATUS(st));
	}
	return (-9);
}

static const char *
code_str(int r)
{
	static char buf[32];
	if (r == 1)  return ("TRAP(SIGSEGV)");
	if (r == 0)  return ("exit=0 OK");
	snprintf(buf, sizeof(buf), "exit=%d", -r);
	return (buf);
}

struct nolife_case {
	int id;
	const char *name;
	int want_base;		/* baseline expectation: 1=trap, 0=exit0, -3=exit3 */
	int want_fixed;		/* fixed expectation */
};

static int
run_nolife(int fixed)
{
	/* want codes: 1 = SIGSEGV trap; 0 = exit 0; -3 = exit 3; ... */
	struct nolife_case cases[] = {
		{ 1, "T1  dict-replace dangle (dm_dev_status pattern)",    1,  1 },
		{ 2, "T1b both-keys replace (dm_table_deps pattern)",      1,  1 },
		{ 3, "T2  dict-release dangle",                            1,  1 },
		{ 4, "T2b freed-buffer read via proplist ptr",             1,  0 },
		{ 5, "T2c buffer-reuse externalize leak",                  0, -3 },
		{ 6, "T3  frozen ps_size strcpy overrun",                  1,  0 },
		{ 7, "T4  control copy-variant",                           0,  0 },
		{ 8, "DML in-tree dm sequence w/o deref (latent today)",   0,  0 },
		{ 9, "NCD alive-read sanity",                              0,  0 },
	};
	int i, bad = 0, want;

	printf("== DF-5691 nolife (%s): trap=SIGSEGV on guard/freed page ==\n",
	    fixed ? "FIXED: nocopy-create copies" : "BASELINE: stock prop_string.c");
	fflush(stdout);
	for (i = 0; i < (int)(sizeof(cases) / sizeof(cases[0])); i++) {
		int r = run_forked_case(cases[i].id);
		want = fixed ? cases[i].want_fixed : cases[i].want_base;
		printf("%-46s -> %-16s %s\n", cases[i].name, code_str(r),
		    (r == want) ? "OK" : "*** MISMATCH ***");
		fflush(stdout);
		if (r != want) bad++;
	}
	printf("NOLIFE(%s): %s\n", fixed ? "fixed" : "base",
	    bad == 0 ? "all expectations met" : "EXPECTATION VIOLATIONS");
	fflush(stdout);
	return (bad == 0 ? 0 : 1);
}

#else /* !USE_GA */

static int
run_nolife(int fixed)
{
	(void)fixed;
	printf("nolife mode requires the guard-page rig (USE_GA)\n");
	return (2);
}

#endif /* USE_GA */

int
main(int argc, char **argv)
{
	if (argc < 2) {
		fprintf(stderr, "usage: %s matrix|nolife|nolife-fixed\n", argv[0]);
		return (2);
	}
	if (strcmp(argv[1], "matrix") == 0)
		return (run_matrix());
	if (strcmp(argv[1], "nolife") == 0)
		return (run_nolife(0));
	if (strcmp(argv[1], "nolife-fixed") == 0)
		return (run_nolife(1));
	fprintf(stderr, "bad mode\n");
	return (2);
}
