/*
 * DF-5789 / DF-5790 — pass-3 subr_bus.c verification rig.
 *
 * Part A (DF-5789): devclass_delete_driver() (sys/kern/subr_bus.c:888-897)
 *   detaches devices one-by-one and RETURNS on the first detach veto,
 *   after already having irreversibly detached the earlier units of the
 *   same driver.  Driver "dfveto" on nexus creates two units:
 *     unit 0: detach always succeeds
 *     unit 1: detach returns EBUSY until hw.df5789.cooperate is set
 *   `kldunload` therefore leaves dfveto0 permanently dead (DS_NOTPRESENT,
 *   driver NULL, softc freed, sysctl tree freed) while the module stays
 *   loaded forever — the unload can never succeed again for unit 1, and
 *   dfveto0 can never be re-attached.  This is the in-subr_bus root of
 *   the detach-veto family (DF-5544/5562/5635 are the driver-side
 *   instances that swallow this same error).
 *
 * Part B (DF-5790): device_delete_child() on a probed-but-unattached
 *   (DS_ALIVE) device leaks the softc.  device_detach() short-circuits
 *   (subr_bus.c:2136-2137, state != DS_ATTACHED -> return 0) without
 *   calling device_set_driver(dev, NULL), so the softc allocated by
 *   device_set_driver() during probe (subr_bus.c:1953) is never freed;
 *   kobj_delete(child) (subr_bus.c:1306) frees only the device struct.
 *   hw.df5789.leak=N drives N add->probe->delete cycles with driver
 *   "dfleak" whose softc size is 8192; each cycle leaks 8192 bytes of
 *   M_BUS.  hw.df5789.leakctl=N is the control (no probe -> no softc).
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/sysctl.h>
#include <sys/malloc.h>
#include <sys/types.h>

/* ------------------------------------------------------------------ */
/* Part A: DF-5789                                                     */
/* ------------------------------------------------------------------ */

static int	 dfveto_cooperate;

static void dfveto_identify(driver_t *driver, device_t parent);
static int	 dfveto_probe(device_t dev);
static int	 dfveto_attach(device_t dev);
static int	 dfveto_detach(device_t dev);

static device_method_t dfveto_methods[] = {
	DEVMETHOD(device_identify, dfveto_identify),
	DEVMETHOD(device_probe,	 dfveto_probe),
	DEVMETHOD(device_attach, dfveto_attach),
	DEVMETHOD(device_detach, dfveto_detach),
	DEVMETHOD_END
};

static driver_t dfveto_driver = {
	"dfveto", dfveto_methods, 64,
};
static devclass_t dfveto_devclass;

DRIVER_MODULE(dfveto, nexus, dfveto_driver, dfveto_devclass, NULL, NULL);

static void
dfveto_identify(driver_t *driver, device_t parent)
{
	/*
	 * Add exactly two rendezvous children (wired units) if not already
	 * present.  Must go through BUS_ADD_CHILD so nexus allocates the
	 * nexus_device ivars (nexus_print_child dereferences them).
	 */
	if (device_find_child(parent, "dfveto", 0) != NULL)
		return;
	BUS_ADD_CHILD(parent, parent, 0, "dfveto", 0);	/* detachable */
	BUS_ADD_CHILD(parent, parent, 0, "dfveto", 1);	/* vetoes */
}

static int
dfveto_probe(device_t dev)
{
	device_set_desc(dev, "DF-5789 half-unload rig");
	return (0);
}

static int
dfveto_attach(device_t dev)
{
	device_printf(dev, "DF5789: attached (detach will %s)\n",
	    device_get_unit(dev) == 1 ? "VETO with EBUSY" : "succeed");
	return (0);
}

static int
dfveto_detach(device_t dev)
{
	if (device_get_unit(dev) == 1 && !dfveto_cooperate) {
		device_printf(dev, "DF5789: DETACH VETO (EBUSY): "
		    "devclass_delete_driver() already killed dfveto0\n");
		return (EBUSY);
	}
	device_printf(dev, "DF5789: detaching OK\n");
	return (0);
}

/* ------------------------------------------------------------------ */
/* Part B: DF-5790                                                     */
/* ------------------------------------------------------------------ */

static int	 dfleak_probe(device_t dev);

static device_method_t dfleak_methods[] = {
	DEVMETHOD(device_probe, dfleak_probe),
	DEVMETHOD_END
};

/* softc size deliberately large so the leak is loud in vmstat -m */
static driver_t dfleak_driver = {
	"dfleak", dfleak_methods, 8192,
};
static devclass_t dfleak_devclass;

DRIVER_MODULE(dfleak, nexus, dfleak_driver, dfleak_devclass, NULL, NULL);

static int
dfleak_probe(device_t dev)
{
	device_set_desc(dev, "DF-5790 softc-leak probe");
	return (0);
}

static device_t
nexus0(void)
{
	devclass_t dc = devclass_find("nexus");

	if (dc == NULL)
		return (NULL);
	return (devclass_get_device(dc, 0));
}

static int	 df5790_cycles;
static int	 df5790_probe_failures;
static void	*df5790_last_softc;
static int	 df5790_last_state;

static int
sysctl_df5790_leak(SYSCTL_HANDLER_ARGS)
{
	device_t bus, child;
	int error, n, i;

	error = sysctl_handle_int(oidp, &n, 0, req);
	if (error || req->newptr == NULL)
		return (error);
	if (n <= 0 || n > 100000)
		return (EINVAL);

	bus = nexus0();
	if (bus == NULL)
		return (ENXIO);

	for (i = 0; i < n; i++) {
		child = device_add_child(bus, "dfleak", -1);
		if (child == NULL)
			break;
		error = device_probe_child(bus, child);
		if (error) {
			/* unexpected: driver is registered */
			df5790_probe_failures++;
			device_delete_child(bus, child);
			continue;
		}
		/*
		 * Child is now DS_ALIVE with driver == dfleak_driver and a
		 * freshly allocated 8192-byte softc.  Deleting it from this
		 * state exercises the device_detach() short-circuit.
		 */
		df5790_last_state = device_get_state(child);
		df5790_last_softc = device_get_softc(child);
		error = device_delete_child(bus, child);
		if (error)
			break;
	}
	df5790_cycles += i;
	kprintf("DF5790: %d add/probe/delete cycles (total %d), "
	    "last child state=%d softc=%p (LEAKED, never kfree'd)\n",
	    i, df5790_cycles, df5790_last_state, df5790_last_softc);
	return (0);
}

static int
sysctl_df5790_leakctl(SYSCTL_HANDLER_ARGS)
{
	device_t bus, child;
	int error, n, i;

	error = sysctl_handle_int(oidp, &n, 0, req);
	if (error || req->newptr == NULL)
		return (error);
	if (n <= 0 || n > 100000)
		return (EINVAL);

	bus = nexus0();
	if (bus == NULL)
		return (ENXIO);

	/* control: identical churn WITHOUT the probe -> no softc exists */
	for (i = 0; i < n; i++) {
		child = device_add_child(bus, "dfleak", -1);
		if (child == NULL)
			break;
		error = device_delete_child(bus, child);
		if (error)
			break;
	}
	kprintf("DF5790: control: %d add/delete cycles (no probe, no softc)\n", i);
	return (0);
}

static int
sysctl_df5789_status(SYSCTL_HANDLER_ARGS)
{
	int error, val = dfveto_cooperate;

	/* report-only; also dump the zombie status of dfveto unit 0 */
	error = sysctl_handle_int(oidp, &val, 0, req);
	if (error == 0 && req->newptr != NULL)
		dfveto_cooperate = val;
	{
		devclass_t dc = devclass_find("dfveto");
		device_t d0 = dc ? devclass_get_device(dc, 0) : NULL;

		kprintf("DF5789: dfveto0=%p state=%d driver=%p nameunit='%s' "
		    "softc=%p (state 0=NOTPRESENT 3=ATTACHED)\n",
		    d0, d0 ? device_get_state(d0) : -1,
		    d0 ? (void *)device_get_driver(d0) : NULL,
		    d0 && device_get_nameunit(d0) ? device_get_nameunit(d0) : "(null)",
		    d0 ? device_get_softc(d0) : NULL);
	}
	return (error);
}

SYSCTL_NODE(_hw, OID_AUTO, df5789, CTLFLAG_RW, 0, "DF-5789/5790 rig");

SYSCTL_PROC(_hw_df5789, OID_AUTO, cooperate,
    CTLTYPE_INT | CTLFLAG_RW, 0, 0, sysctl_df5789_status, "I",
    "allow dfveto unit1 to detach (status dump on read)");

SYSCTL_PROC(_hw_df5789, OID_AUTO, leak,
    CTLTYPE_INT | CTLFLAG_RW, 0, 0, sysctl_df5790_leak, "I",
    "N add/probe/delete cycles leaking the probe softc");

SYSCTL_PROC(_hw_df5789, OID_AUTO, leakctl,
    CTLTYPE_INT | CTLFLAG_RW, 0, 0, sysctl_df5790_leakctl, "I",
    "N add/delete control cycles without probe");
