From 5b76abb360d895feccefe96b38906554d2903987 Mon Sep 17 00:00:00 2001
From: Ryan Zezeski <ryan@zinascii.com>
Date: Fri, 26 Jul 2024 16:34:30 -0600
Subject: [PATCH] 13918 Want kernel test facility (IPD 20) Reviewed by: C
 Fraire <cfraire@me.com> Reviewed by: Peter Tribble <peter.tribble@gmail.com>

---
 exception_lists/packaging                    |    6 +
 usr/src/cmd/Makefile                         |    1 +
 usr/src/cmd/devfsadm/misc_link.c             |    5 +-
 usr/src/cmd/ktest/Makefile                   |   48 +
 usr/src/cmd/ktest/ktest.c                    | 1650 ++++++++++++++++++
 usr/src/man/man8/Makefile                    |    1 +
 usr/src/man/man8/ktest.8                     |  272 +++
 usr/src/man/man9/Makefile                    |    6 +-
 usr/src/man/man9/ktest.9                     |  202 +++
 usr/src/man/man9f/Makefile                   |   60 +
 usr/src/man/man9f/ktest_create_module.9f     |  272 +++
 usr/src/man/man9f/ktest_get_fn.9f            |  212 +++
 usr/src/man/man9f/ktest_get_input.9f         |   70 +
 usr/src/man/man9f/ktest_result_pass.9f       |  417 +++++
 usr/src/pkg/manifests/driver-ktest.p5m       |   81 +
 usr/src/uts/Makefile.targ                    |    4 +
 usr/src/uts/Makefile.uts                     |    5 +
 usr/src/uts/common/Makefile.files            |   12 +
 usr/src/uts/common/Makefile.rules            |    4 +
 usr/src/uts/common/io/ktest/ktest.c          | 1384 +++++++++++++++
 usr/src/uts/common/io/ktest/ktest.conf       |   16 +
 usr/src/uts/common/io/ktest/ktest_selftest.c |  245 +++
 usr/src/uts/common/io/mac/mac_test.c         |  686 ++++++++
 usr/src/uts/common/io/stream_test.c          |  112 ++
 usr/src/uts/common/sys/Makefile              |    2 +
 usr/src/uts/common/sys/ktest.h               |  333 ++++
 usr/src/uts/common/sys/ktest_impl.h          |   73 +
 usr/src/uts/intel/Makefile.intel             |   12 +
 usr/src/uts/intel/ktest/Makefile             |   42 +
 usr/src/uts/intel/ktest_selftest/Makefile    |   41 +
 usr/src/uts/intel/mac_test/Makefile          |   42 +
 usr/src/uts/intel/stream_test/Makefile       |   41 +
 32 files changed, 6354 insertions(+), 3 deletions(-)
 create mode 100644 usr/src/cmd/ktest/Makefile
 create mode 100644 usr/src/cmd/ktest/ktest.c
 create mode 100644 usr/src/man/man8/ktest.8
 create mode 100644 usr/src/man/man9/ktest.9
 create mode 100644 usr/src/man/man9f/ktest_create_module.9f
 create mode 100644 usr/src/man/man9f/ktest_get_fn.9f
 create mode 100644 usr/src/man/man9f/ktest_get_input.9f
 create mode 100644 usr/src/man/man9f/ktest_result_pass.9f
 create mode 100644 usr/src/pkg/manifests/driver-ktest.p5m
 create mode 100644 usr/src/uts/common/io/ktest/ktest.c
 create mode 100644 usr/src/uts/common/io/ktest/ktest.conf
 create mode 100644 usr/src/uts/common/io/ktest/ktest_selftest.c
 create mode 100644 usr/src/uts/common/io/mac/mac_test.c
 create mode 100644 usr/src/uts/common/io/stream_test.c
 create mode 100644 usr/src/uts/common/sys/ktest.h
 create mode 100644 usr/src/uts/common/sys/ktest_impl.h
 create mode 100644 usr/src/uts/intel/ktest/Makefile
 create mode 100644 usr/src/uts/intel/ktest_selftest/Makefile
 create mode 100644 usr/src/uts/intel/mac_test/Makefile
 create mode 100644 usr/src/uts/intel/stream_test/Makefile

diff --git a/exception_lists/packaging b/exception_lists/packaging
index 044215095d..660c9dab9f 100644
--- a/exception_lists/packaging
+++ b/exception_lists/packaging
@@ -825,3 +825,9 @@ opt/SUNWonld/
 #
 opt/os-tests/tests/cores/libdumper.so
 opt/os-tests/tests/cores/amd64/libdumper.so	i386
+
+#
+# Private ktest definitions which are only needed by the ktest
+# implementation -- which is fully controlled by gate.
+#
+usr/include/sys/ktest_impl.h
diff --git a/usr/src/cmd/Makefile b/usr/src/cmd/Makefile
index 2afb0e43a3..89eb05fe1a 100644
--- a/usr/src/cmd/Makefile
+++ b/usr/src/cmd/Makefile
@@ -229,6 +229,7 @@ COMMON_SUBDIRS=		\
 	keyserv		\
 	killall		\
 	krb5		\
+	ktest		\
 	kvmstat		\
 	last		\
 	lastcomm	\
diff --git a/usr/src/cmd/devfsadm/misc_link.c b/usr/src/cmd/devfsadm/misc_link.c
index 7dc45f5281..43fd5a72d7 100644
--- a/usr/src/cmd/devfsadm/misc_link.c
+++ b/usr/src/cmd/devfsadm/misc_link.c
@@ -23,7 +23,7 @@
  * Copyright 2011 Nexenta Systems, Inc.  All rights reserved.
  * Copyright (c) 2015, Joyent, Inc. All rights reserved.
  * Copyright 2022 Garrett D'Amore <garrett@damore.org>
- * Copyright 2022 Oxide Computer Company
+ * Copyright 2023 Oxide Computer Company
  */
 
 #include <regex.h>
@@ -213,6 +213,9 @@ static devfsadm_create_t misc_cbt[] = {
 	{ "9p", "ddi_pseudo", "vio9p",
 	    TYPE_EXACT | DRV_EXACT, ILEVEL_0, vio9p_create,
 	},
+	{ "pseudo", "ddi_pseudo", "ktest",
+	    TYPE_EXACT | DRV_EXACT, ILEVEL_0, minor_name
+	},
 };
 
 DEVFSADM_CREATE_INIT_V0(misc_cbt);
diff --git a/usr/src/cmd/ktest/Makefile b/usr/src/cmd/ktest/Makefile
new file mode 100644
index 0000000000..673c7f97dd
--- /dev/null
+++ b/usr/src/cmd/ktest/Makefile
@@ -0,0 +1,48 @@
+#
+# This file and its contents are supplied under the terms of the
+# Common Development and Distribution License ("CDDL"), version 1.0.
+# You may only use this file in accordance with the terms of version
+# 1.0 of the CDDL.
+#
+# A full copy of the text of the CDDL should have accompanied this
+# source.  A copy of the CDDL is also available via the Internet at
+# http://www.illumos.org/license/CDDL.
+#
+
+#
+# Copyright 2023 Oxide Computer Company
+#
+
+PROG = ktest
+OBJS = ktest.o
+SRCS =  $(OBJS:%.o=%.c)
+
+CLEANFILES += $(OBJS)
+
+include ../Makefile.cmd
+$(BUILD64) include ../Makefile.cmd.64
+include ../Makefile.ctf
+
+CFLAGS += $(CCVERBOSE)
+CFLAGS64 += $(CCVERBOSE)
+
+LDLIBS += -L$(ROOT)/lib -lumem -lgen -lnvpair -lofmt -lcmdutils
+
+CSTD = $(CSTD_GNU99)
+
+all: $(PROG)
+
+$(PROG): $(OBJS)
+	$(LINK.c) -o $@ $(OBJS) $(LDLIBS)
+	$(POST_PROCESS)
+
+%.o: %.c
+	$(COMPILE.c) $<
+	$(POST_PROCESS_O)
+
+install: all $(ROOTPROG)
+
+clean:
+	-$(RM) $(CLEANFILES)
+
+include ../Makefile.targ
diff --git a/usr/src/cmd/ktest/ktest.c b/usr/src/cmd/ktest/ktest.c
new file mode 100644
index 0000000000..f15c0507f9
--- /dev/null
+++ b/usr/src/cmd/ktest/ktest.c
@@ -0,0 +1,1650 @@
+/*
+ * This file and its contents are supplied under the terms of the
+ * Common Development and Distribution License ("CDDL"), version 1.0.
+ * You may only use this file in accordance with the terms of version
+ * 1.0 of the CDDL.
+ *
+ * A full copy of the text of the CDDL should have accompanied this
+ * source.  A copy of the CDDL is also available via the Internet at
+ * http://www.illumos.org/license/CDDL.
+ */
+
+/*
+ * Copyright 2023 Oxide Computer Company
+ * Copyright 2024 Ryan Zezeski
+ */
+#include <sys/debug.h>
+#include <sys/ktest.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/list.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stropts.h>
+#include <stdio.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <err.h>
+#include <stdarg.h>
+#include <strings.h>
+#include <libgen.h>
+#include <libnvpair.h>
+#include <regex.h>
+#include <libcmdutils.h>
+#include <ofmt.h>
+#include <zone.h>
+
+#define	EXIT_USAGE		2
+#define	KTEST_CMD_SZ		24
+#define	KTEST_DEV_PATH		"/dev/ktest"
+
+static const char *ktest_prog;
+
+/* Print a horizontal rule. */
+static void
+ktest_print_hr(uint8_t cols)
+{
+	for (uint8_t i = 0; i < cols; i++) {
+		(void) putchar('-');
+	}
+
+	(void) putchar('\n');
+}
+
+/* An adapter to use errx with libofmt. */
+void
+ktest_ofmt_errx(const char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	verrx(EXIT_FAILURE, fmt, ap);
+}
+
+typedef enum ktest_fmt_fields {
+	KTEST_FMT_RESULT,
+	KTEST_FMT_MODULE,
+	KTEST_FMT_SUITE,
+	KTEST_FMT_TEST,
+	KTEST_FMT_INPUT_FLAG,
+	KTEST_FMT_INPUT_PATH,
+	KTEST_FMT_LINE,
+	KTEST_FMT_REASON,
+} ktest_fmt_fields_t;
+
+typedef struct ktest_list_ofmt {
+	char *klof_module;
+	char *klof_suite;
+	char *klof_test;
+	boolean_t klof_input;
+} ktest_list_ofmt_t;
+
+static boolean_t
+ktest_list_ofmt_cb(ofmt_arg_t *ofarg, char *buf, uint_t len)
+{
+	ktest_list_ofmt_t *klof = ofarg->ofmt_cbarg;
+
+	switch (ofarg->ofmt_id) {
+	case KTEST_FMT_MODULE:
+		if (snprintf(buf, len, "%s", klof->klof_module) >= len) {
+			return (B_FALSE);
+		}
+		break;
+
+	case KTEST_FMT_SUITE:
+		if (snprintf(buf, len, "%s", klof->klof_suite) >= len) {
+			return (B_FALSE);
+		}
+		break;
+
+	case KTEST_FMT_TEST:
+		if (snprintf(buf, len, "%s", klof->klof_test) >= len) {
+			return (B_FALSE);
+		}
+		break;
+
+	case KTEST_FMT_INPUT_FLAG: {
+		const char *flag = klof->klof_input ? "Y" : "N";
+
+		if (snprintf(buf, len, "%s", flag) >= len) {
+			return (B_FALSE);
+		}
+	}
+	}
+
+	return (B_TRUE);
+}
+
+#define	KTEST_LIST_CMD_DEF_FIELDS	"module,suite,test,input"
+
+static const ofmt_field_t ktest_list_ofmt[] = {
+	{ "MODULE", 12, KTEST_FMT_MODULE, ktest_list_ofmt_cb },
+	{ "SUITE", 16, KTEST_FMT_SUITE, ktest_list_ofmt_cb },
+	{ "TEST", 45, KTEST_FMT_TEST, ktest_list_ofmt_cb },
+	{ "INPUT", 7, KTEST_FMT_INPUT_FLAG, ktest_list_ofmt_cb },
+	{ NULL, 0, 0, NULL },
+};
+
+static const char *
+ktest_result_str(ktest_result_t *result)
+{
+	switch (result->kr_type) {
+	case KTEST_RESULT_NONE:
+		return ("NONE");
+	case KTEST_RESULT_PASS:
+		return ("PASS");
+	case KTEST_RESULT_FAIL:
+		return ("FAIL");
+	case KTEST_RESULT_SKIP:
+		return ("SKIP");
+	case KTEST_RESULT_ERROR:
+		return ("ERROR");
+	}
+
+	/* Make the compiler happy. */
+	return ("NONE");
+}
+
+static boolean_t
+ktest_run_ofmt_cb(ofmt_arg_t *ofarg, char *buf, uint_t len)
+{
+	ktest_run_op_t *op = ofarg->ofmt_cbarg;
+	ktest_result_t *res = &op->kro_result;
+
+	switch (ofarg->ofmt_id) {
+	case KTEST_FMT_RESULT:
+		if (snprintf(buf, len, "%s", ktest_result_str(res)) >= len) {
+			return (B_FALSE);
+		}
+		break;
+
+	case KTEST_FMT_MODULE:
+		if (snprintf(buf, len, "%s", op->kro_module) >= len) {
+			return (B_FALSE);
+		}
+		break;
+
+	case KTEST_FMT_SUITE:
+		if (snprintf(buf, len, "%s", op->kro_suite) >= len) {
+			return (B_FALSE);
+		}
+		break;
+
+	case KTEST_FMT_TEST:
+		if (snprintf(buf, len, "%s", op->kro_test) >= len) {
+			return (B_FALSE);
+		}
+		break;
+
+	case KTEST_FMT_INPUT_PATH: {
+		if (snprintf(buf, len, "%s", op->kro_input_path) >= len) {
+			return (B_FALSE);
+		}
+		break;
+	}
+
+	case KTEST_FMT_LINE:
+		if (snprintf(buf, len, "%d", op->kro_result.kr_line) >= len) {
+			return (B_FALSE);
+		}
+		break;
+
+	case KTEST_FMT_REASON: {
+		if (snprintf(buf, len, "%s%s", res->kr_msg_prepend,
+		    res->kr_msg) >= len) {
+			return (B_FALSE);
+		}
+		break;
+	}
+	}
+
+	return (B_TRUE);
+}
+
+/*
+ * The 'run' and 'run-file' commands share the same fields.
+ */
+#define	KTEST_RUN_CMD_DEF_FIELDS	"result,line,module,suite,test"
+
+/*
+ * The input column for the run command is for displaying the path to
+ * the input file, as opposed to the list command which indicates if
+ * the test requires input or not.
+ */
+static const ofmt_field_t ktest_run_ofmt[] = {
+	{ "RESULT", 7, KTEST_FMT_RESULT, ktest_run_ofmt_cb },
+	{ "MODULE", 12, KTEST_FMT_MODULE, ktest_run_ofmt_cb },
+	{ "SUITE", 16, KTEST_FMT_SUITE, ktest_run_ofmt_cb },
+	{ "TEST", 45, KTEST_FMT_TEST, ktest_run_ofmt_cb },
+	{ "INPUT", 48, KTEST_FMT_INPUT_PATH, ktest_run_ofmt_cb },
+	{ "LINE", 6, KTEST_FMT_LINE, ktest_run_ofmt_cb },
+	{ "REASON", 256, KTEST_FMT_REASON, ktest_run_ofmt_cb },
+	{ NULL, 0, 0, NULL },
+};
+
+typedef enum ktest_stat_type {
+	KTEST_STAT_MOD,
+	KTEST_STAT_SUITE,
+} ktest_stat_type_t;
+
+typedef struct ktest_stats {
+	list_node_t		ks_node;
+	ktest_stat_type_t	ks_type;
+	char			*ks_name;
+	uint32_t		ks_total;
+	uint32_t		ks_pass;
+	uint32_t		ks_fail;
+	uint32_t		ks_err;
+	uint32_t		ks_skip;
+	uint32_t		ks_none;
+} ktest_stats_t;
+
+static ktest_stats_t *
+ktest_stats_new(ktest_stat_type_t type, const char *name)
+{
+	ktest_stats_t *stats;
+
+	if ((stats = malloc(sizeof (ktest_stats_t))) == NULL) {
+		err(EXIT_FAILURE, "failed to allocate stats structure");
+	}
+
+	stats->ks_type = type;
+	stats->ks_name = strndup(name, KTEST_MAX_NAME_LEN);
+
+	if (stats->ks_name == NULL) {
+		err(EXIT_FAILURE, "failed to allocate stats name");
+	}
+
+	stats->ks_total = 0;
+	stats->ks_pass = 0;
+	stats->ks_fail = 0;
+	stats->ks_err = 0;
+	stats->ks_skip = 0;
+	stats->ks_none = 0;
+	return (stats);
+}
+
+static void
+ktest_record_stat(ktest_stats_t *mod, ktest_stats_t *suite,
+    const ktest_result_t *res)
+{
+	mod->ks_total++;
+	suite->ks_total++;
+
+	switch (res->kr_type) {
+	case KTEST_RESULT_NONE:
+		mod->ks_none++;
+		suite->ks_none++;
+		break;
+
+	case KTEST_RESULT_PASS:
+		mod->ks_pass++;
+		suite->ks_pass++;
+		break;
+
+	case KTEST_RESULT_FAIL:
+		mod->ks_fail++;
+		suite->ks_fail++;
+		break;
+
+	case KTEST_RESULT_SKIP:
+		mod->ks_skip++;
+		suite->ks_skip++;
+		break;
+
+	case KTEST_RESULT_ERROR:
+		mod->ks_err++;
+		suite->ks_err++;
+		break;
+	}
+}
+
+typedef enum ktest_fmt_stats {
+	KTEST_FMT_STATS_MS,
+	KTEST_FMT_STATS_TOTAL,
+	KTEST_FMT_STATS_PASS,
+	KTEST_FMT_STATS_FAIL,
+	KTEST_FMT_STATS_ERR,
+	KTEST_FMT_STATS_SKIP,
+	KTEST_FMT_STATS_NONE,
+} ktest_fmt_stats_t;
+
+static boolean_t
+ktest_stats_ofmt_cb(ofmt_arg_t *ofarg, char *buf, uint_t len)
+{
+	ktest_stats_t *stats = ofarg->ofmt_cbarg;
+
+	switch (ofarg->ofmt_id) {
+	case KTEST_FMT_STATS_MS: {
+		char *pre = (stats->ks_type == KTEST_STAT_MOD) ? "" : "  ";
+
+		if (snprintf(buf, len, "%s%s", pre, stats->ks_name) >= len) {
+			return (B_FALSE);
+		}
+		break;
+	}
+
+	case KTEST_FMT_STATS_TOTAL:
+		if (snprintf(buf, len, "%" PRIu32, stats->ks_total) >= len) {
+			return (B_FALSE);
+		}
+		break;
+
+	case KTEST_FMT_STATS_PASS:
+		if (snprintf(buf, len, "%" PRIu32, stats->ks_pass) >= len) {
+			return (B_FALSE);
+		}
+		break;
+
+	case KTEST_FMT_STATS_FAIL:
+		if (snprintf(buf, len, "%" PRIu32, stats->ks_fail) >= len) {
+			return (B_FALSE);
+		}
+		break;
+
+	case KTEST_FMT_STATS_ERR:
+		if (snprintf(buf, len, "%" PRIu32, stats->ks_err) >= len) {
+			return (B_FALSE);
+		}
+		break;
+
+	case KTEST_FMT_STATS_SKIP:
+		if (snprintf(buf, len, "%" PRIu32, stats->ks_skip) >= len) {
+			return (B_FALSE);
+		}
+		break;
+
+	case KTEST_FMT_STATS_NONE:
+		if (snprintf(buf, len, "%" PRIu32, stats->ks_none) >= len) {
+			return (B_FALSE);
+		}
+		break;
+	}
+
+	return (B_TRUE);
+}
+
+#define	KTEST_STATS_FIELDS	"module/suite,total,pass,fail,err,skip,none"
+
+static const ofmt_field_t ktest_stats_ofmt[] = {
+	{ "MODULE/SUITE", 40, KTEST_FMT_STATS_MS, ktest_stats_ofmt_cb },
+	{ "TOTAL", 6, KTEST_FMT_STATS_TOTAL, ktest_stats_ofmt_cb },
+	{ "PASS", 6, KTEST_FMT_STATS_PASS, ktest_stats_ofmt_cb },
+	{ "FAIL", 6, KTEST_FMT_STATS_FAIL, ktest_stats_ofmt_cb },
+	{ "ERR", 6, KTEST_FMT_STATS_ERR, ktest_stats_ofmt_cb },
+	{ "SKIP", 6, KTEST_FMT_STATS_SKIP, ktest_stats_ofmt_cb },
+	{ "NONE", 6, KTEST_FMT_STATS_NONE, ktest_stats_ofmt_cb },
+};
+
+static void
+ktest_usage(const char *fmt, ...)
+{
+	if (fmt != NULL) {
+		va_list ap;
+
+		va_start(ap, fmt);
+		vwarnx(fmt, ap);
+		va_end(ap);
+	}
+
+	(void) fprintf(stderr,
+	    "usage: %s <subcommand> [<opts>] [<args>]\n\n"
+	    "\tlist [-H] [[-p] -o field,...] [<triple> ...]: "
+	    "list registered tests\n"
+	    "\trun [-H] [[-p] -o field,...] [-i <file>] <triple> ...: "
+	    "run specified tests\n"
+	    "\trun-file [-H] [[-p] -o field,...] [<runfile>]: "
+	    "run tests specified in runfile\n",
+	    ktest_prog);
+}
+
+/*
+ * A user-specified test triple. The input path is set to the empty
+ * string if no input is provided. The line number provides useful
+ * error reporting when an error is encountered in the run file.
+ */
+typedef struct ktest_triple {
+	list_node_t	ktr_node;
+	char		ktr_module[KTEST_MAX_NAME_LEN];
+	char		ktr_suite[KTEST_MAX_NAME_LEN];
+	char		ktr_test[KTEST_MAX_NAME_LEN];
+	char		ktr_input_path[MAXPATHLEN];
+	uint32_t	ktr_lineno;
+} ktest_triple_t;
+
+/* The default triple matches all tests. */
+static ktest_triple_t *ktest_def_triple;
+
+/*
+ * A test description obtained from iterating the list tests nvlist.
+ */
+typedef struct ktest_test_desc {
+	char		*ktd_module;
+	char		*ktd_suite;
+	char		*ktd_test;
+	boolean_t	ktd_requires_input;
+} ktest_test_desc_t;
+
+static void
+ktest_test_desc_init(ktest_test_desc_t *desc)
+{
+	desc->ktd_module = NULL;
+	desc->ktd_suite = NULL;
+	desc->ktd_test = NULL;
+	desc->ktd_requires_input = B_FALSE;
+}
+
+static void
+ktest_free_triples(list_t *triples)
+{
+	ktest_triple_t *t = NULL;
+
+	while ((t = list_remove_head(triples)) != NULL) {
+		free(t);
+	}
+}
+
+/*
+ * Does the test descriptor match this triple? The descriptor will
+ * always have module name present, but the suite and test names may
+ * or may not be present.
+ */
+static boolean_t
+ktest_match_triple(const ktest_test_desc_t *desc, const ktest_triple_t *triple)
+{
+	/* Must at least specify the module. */
+	VERIFY(desc->ktd_module != NULL);
+
+	if (desc->ktd_suite != NULL && desc->ktd_test != NULL) {
+		return (gmatch(desc->ktd_module, triple->ktr_module) != 0 &&
+		    gmatch(desc->ktd_suite, triple->ktr_suite) != 0 &&
+		    gmatch(desc->ktd_test, triple->ktr_test) != 0);
+	} else if (desc->ktd_suite != NULL) {
+		return (gmatch(desc->ktd_module, triple->ktr_module) != 0 &&
+		    gmatch(desc->ktd_suite, triple->ktr_suite) != 0);
+	}
+
+	return (gmatch(desc->ktd_module, triple->ktr_module) != 0);
+}
+
+/*
+ * Does the test descriptor match any of the triples?
+ */
+static boolean_t
+ktest_match_any(const ktest_test_desc_t *desc, list_t *triples)
+{
+	for (ktest_triple_t *triple = list_head(triples); triple != NULL;
+	    triple = list_next(triples, triple)) {
+		if (ktest_match_triple(desc, triple)) {
+			return (B_TRUE);
+		}
+	}
+
+	return (B_FALSE);
+}
+
+typedef struct ktest_iter {
+	/*
+	 * The list of all modules and current module the iterator is on.
+	 */
+	nvlist_t	*ki_modules;
+	nvpair_t	*ki_module;
+
+	/*
+	 * The list of all suites in the current module and the
+	 * current suite the iterator is on.
+	 */
+	nvlist_t	*ki_suites;
+	nvpair_t	*ki_suite;
+
+	/*
+	 * The list of all tests in the current suite and the current
+	 * test the iterator is on.
+	 */
+	nvlist_t	*ki_tests;
+	nvpair_t	*ki_test;
+
+	ktest_test_desc_t	ki_desc;
+
+	/*
+	 * A list of ktest_triple_t used to filter the tests returned
+	 * by the iterator.
+	 */
+	list_t		*ki_triples;
+} ktest_iter_t;
+
+static char *
+ktest_module_name(nvpair_t *module)
+{
+	nvlist_t *desc = fnvpair_value_nvlist(module);
+	return (fnvlist_lookup_string(desc, KTEST_NAME_KEY));
+}
+
+static nvlist_t *
+ktest_module_suites(nvpair_t *module)
+{
+	nvlist_t *desc = fnvpair_value_nvlist(module);
+	return (fnvlist_lookup_nvlist(desc, KTEST_MODULE_SUITES_KEY));
+}
+
+static char *
+ktest_suite_name(nvpair_t *suite)
+{
+	nvlist_t *desc = fnvpair_value_nvlist(suite);
+	return (fnvlist_lookup_string(desc, KTEST_NAME_KEY));
+}
+
+static nvlist_t *
+ktest_suite_tests(nvpair_t *suite)
+{
+	nvlist_t *desc = fnvpair_value_nvlist(suite);
+	return (fnvlist_lookup_nvlist(desc, KTEST_SUITE_TESTS_KEY));
+}
+
+static char *
+ktest_test_name(nvpair_t *test)
+{
+	nvlist_t *desc = fnvpair_value_nvlist(test);
+	return (fnvlist_lookup_string(desc, KTEST_NAME_KEY));
+}
+
+static boolean_t
+ktest_test_requires_input(nvpair_t *test)
+{
+	nvlist_t *desc = fnvpair_value_nvlist(test);
+	return (fnvlist_lookup_boolean_value(desc, KTEST_TEST_INPUT_KEY));
+}
+
+static ktest_iter_t *
+ktest_iter(nvlist_t *tests, list_t *triples)
+{
+	ktest_iter_t *iter = malloc(sizeof (ktest_iter_t));
+
+	if (iter == NULL) {
+		err(EXIT_FAILURE, "failed to allocate test iterator");
+	}
+
+	iter->ki_modules = tests;
+	iter->ki_module = nvlist_next_nvpair(tests, NULL);
+
+	iter->ki_suites = NULL;
+	iter->ki_suite = NULL;
+
+	iter->ki_tests = NULL;
+	iter->ki_test = NULL;
+
+	ktest_test_desc_init(&iter->ki_desc);
+
+	iter->ki_triples = triples;
+	return (iter);
+}
+
+#define	KT_NEXT_TEST(iter)						\
+	(nvlist_next_nvpair((iter)->ki_tests, (iter)->ki_test))
+
+static boolean_t
+ktest_iter_tests(ktest_iter_t *iter, ktest_test_desc_t *desc)
+{
+	if (iter->ki_test == NULL) {
+		iter->ki_test = KT_NEXT_TEST(iter);
+	}
+
+	for (; iter->ki_test != NULL; iter->ki_test = KT_NEXT_TEST(iter)) {
+		iter->ki_desc.ktd_test = ktest_test_name(iter->ki_test);
+		iter->ki_desc.ktd_requires_input =
+		    ktest_test_requires_input(iter->ki_test);
+
+		/*
+		 * We found a match and are returning control to the
+		 * ktest_iter_next() caller; but we first need to copy
+		 * the matching descriptor and move the iterator to
+		 * the next test in preparation for the next call to
+		 * ktest_iter_next().
+		 */
+		if (ktest_match_any(&iter->ki_desc, iter->ki_triples)) {
+			*desc = iter->ki_desc;
+			iter->ki_test = KT_NEXT_TEST(iter);
+			return (B_TRUE);
+		}
+	}
+
+	return (B_FALSE);
+}
+
+#define	KT_NEXT_SUITE(iter)						\
+	(nvlist_next_nvpair((iter)->ki_suites, (iter)->ki_suite))
+
+static boolean_t
+ktest_iter_suites(ktest_iter_t *iter, ktest_test_desc_t *desc)
+{
+	if (iter->ki_suite == NULL) {
+		iter->ki_suite = KT_NEXT_SUITE(iter);
+	}
+
+	for (; iter->ki_suite != NULL; iter->ki_suite = KT_NEXT_SUITE(iter)) {
+		iter->ki_desc.ktd_suite = ktest_suite_name(iter->ki_suite);
+
+		if (!ktest_match_any(&iter->ki_desc, iter->ki_triples)) {
+			continue;
+		}
+
+		iter->ki_tests = ktest_suite_tests(iter->ki_suite);
+
+		if (ktest_iter_tests(iter, desc)) {
+			/*
+			 * We've iterated all tests in the suite, move
+			 * to the next one.
+			 */
+			if (iter->ki_test == NULL) {
+				iter->ki_suite = KT_NEXT_SUITE(iter);
+			}
+
+			return (B_TRUE);
+		}
+	}
+
+	return (B_FALSE);
+}
+
+#define	KT_NEXT_MOD(iter)						\
+	(nvlist_next_nvpair((iter)->ki_modules, (iter)->ki_module))
+
+static boolean_t
+ktest_iter_next(ktest_iter_t *iter, ktest_test_desc_t *desc)
+{
+	for (; iter->ki_module != NULL; iter->ki_module = KT_NEXT_MOD(iter)) {
+		ktest_test_desc_init(&iter->ki_desc);
+		iter->ki_desc.ktd_module = ktest_module_name(iter->ki_module);
+
+		if (!ktest_match_any(&iter->ki_desc, iter->ki_triples)) {
+			continue;
+		}
+
+		iter->ki_suites = ktest_module_suites(iter->ki_module);
+
+		if (ktest_iter_suites(iter, desc)) {
+			/*
+			 * We've iterated all suites in the module,
+			 * move to the next one.
+			 */
+			if (iter->ki_suite == NULL) {
+				iter->ki_module = KT_NEXT_MOD(iter);
+			}
+
+			return (B_TRUE);
+		}
+	}
+
+	return (B_FALSE);
+}
+
+/*
+ * Get a list of tests from the in-kernel ktest registry.
+ */
+static nvlist_t *
+ktest_list_tests(int dev)
+{
+	int ret = 0;
+	nvlist_t *tests = NULL;
+	boolean_t retry = B_FALSE;
+	ktest_list_op_t klo;
+	size_t resp_len = 1 * 1024 * 1024;
+	char *resp = NULL;
+	uint64_t vsn = 0;
+
+	if ((resp = malloc(resp_len)) == NULL) {
+		err(EXIT_FAILURE, "failed to allocate response buffer");
+	}
+
+	bzero(resp, resp_len);
+	klo.klo_resp = resp;
+	klo.klo_resp_len = resp_len;
+
+retry:
+	ret = ioctl(dev, KTEST_IOCTL_LIST_TESTS, &klo);
+
+	if (ret == -1 && errno == ENOBUFS && !retry) {
+		free(resp);
+		resp_len = klo.klo_resp_len;
+
+		if ((resp = malloc(resp_len)) == NULL) {
+			err(EXIT_FAILURE, "failed to allocate response buffer");
+		}
+
+		bzero(resp, resp_len);
+		retry = B_TRUE;
+		goto retry;
+	} else if (ret == -1) {
+		err(EXIT_FAILURE, "list ioctl failed");
+	}
+
+	resp_len = klo.klo_resp_len;
+
+	if ((ret = nvlist_unpack(resp, resp_len, &tests, 0)) != 0) {
+		errx(EXIT_FAILURE, "failed to unpack list response: %s",
+		    strerror(ret));
+	}
+
+	free(resp);
+
+	/*
+	 * Verify that the response is marked with the expected
+	 * serialization format. Remove the nvpair so that only the
+	 * modules remain.
+	 */
+	if (nvlist_lookup_uint64(tests, KTEST_SER_FMT_KEY, &vsn) != 0) {
+		errx(EXIT_FAILURE, "invalid list response, missing %s key\n",
+		    KTEST_SER_FMT_KEY);
+	}
+
+	if (vsn != KTEST_SER_FMT_VSN) {
+		errx(EXIT_FAILURE,
+		    "invalid serialization format version: %" PRIu64 "\n", vsn);
+	}
+
+	fnvlist_remove(tests, KTEST_SER_FMT_KEY);
+	return (tests);
+}
+
+static void
+ktest_print_tests(nvlist_t *tests, list_t *triples, ofmt_handle_t ofmt)
+{
+	ktest_iter_t *iter = ktest_iter(tests, triples);
+	ktest_test_desc_t desc;
+
+	while (ktest_iter_next(iter, &desc)) {
+		ktest_list_ofmt_t klof;
+
+		klof.klof_module = desc.ktd_module;
+		klof.klof_suite = desc.ktd_suite;
+		klof.klof_test = desc.ktd_test;
+		klof.klof_input = desc.ktd_requires_input;
+		ofmt_print(ofmt, &klof);
+	}
+
+	free(iter);
+}
+
+static void
+ktest_print_stats(list_t *stats)
+{
+	ktest_stats_t *stat;
+	ofmt_handle_t stats_ofmt;
+	ofmt_status_t oferr;
+	boolean_t first = B_FALSE;
+
+	oferr = ofmt_open(KTEST_STATS_FIELDS, ktest_stats_ofmt, 0, 0,
+	    &stats_ofmt);
+	ofmt_check(oferr, B_FALSE, stats_ofmt, ktest_ofmt_errx, warnx);
+
+	for (stat = list_head(stats); stat != NULL;
+	    stat = list_next(stats, stat)) {
+		if (!first && stat->ks_type == KTEST_STAT_MOD) {
+			printf("\n");
+		}
+
+		ofmt_print(stats_ofmt, stat);
+
+		if (stat->ks_type == KTEST_STAT_MOD) {
+			first = B_FALSE;
+			ktest_print_hr(74);
+		}
+	}
+
+	ofmt_close(stats_ofmt);
+}
+
+/*
+ * Read file at path into the byte array. The byte array is allocated
+ * as part of this function and ownership is given to the caller via
+ * the bytes argument along with its length returned by 'len'. If an
+ * error occurs while reading, the 'err' string is filled in with the
+ * appropriate error message.
+ *
+ * It might be nice to replace this with kobj_{open,read}_file() in
+ * the ktest kernel module to avoid shuffling bytes between user and
+ * kernel (see devcache which uses these private APIs for the purpose
+ * of reading serialized nvlists).
+ */
+static boolean_t
+ktest_read_file(const char *path, uchar_t **bytes, uint64_t *len, char *err)
+{
+	FILE *f;
+	struct stat stats;
+	uchar_t *tmp_bytes;
+	uint64_t tmp_len;
+
+	*bytes = NULL;
+	*len = 0;
+
+	if ((f = fopen(path, "r")) == NULL) {
+		(void) strcpy(err, "failed to open input file");
+		return (B_FALSE);
+	}
+
+	if (fstat(fileno(f), &stats) == -1) {
+		(void) fclose(f);
+		(void) strcpy(err, "failed to stat input file");
+		return (B_FALSE);
+	}
+
+	tmp_len = (uint64_t)stats.st_size;
+
+	if ((tmp_bytes = malloc(tmp_len)) == NULL) {
+		(void) fclose(f);
+		(void) strcpy(err, "failed to allocate byte array of size");
+		return (B_FALSE);
+	}
+
+	if (fread(tmp_bytes, sizeof (*tmp_bytes), tmp_len, f) != tmp_len) {
+		(void) fclose(f);
+		(void) snprintf(err, KTEST_MAX_LOG_LEN,
+		    "failed to read %u bytes from input file", tmp_len);
+		return (B_FALSE);
+	}
+
+	*bytes = tmp_bytes;
+	*len = tmp_len;
+	return (B_TRUE);
+}
+
+static boolean_t
+ktest_run_test(int dev, const ktest_test_desc_t *desc, const char *input_path,
+    ktest_stats_t *mod_stats, ktest_stats_t *suite_stats, ofmt_handle_t ofmt)
+{
+	ktest_run_op_t kro;
+	char err_msg[KTEST_MAX_LOG_LEN];
+
+	/*
+	 * It is up to the caller to ensure that an input path is
+	 * specified when a test requires it.
+	 */
+	if (desc->ktd_requires_input) {
+		VERIFY(input_path != NULL);
+	}
+
+	bzero(&err_msg, KTEST_MAX_LOG_LEN);
+	bzero(&kro, sizeof (kro));
+
+	/*
+	 * The module/suite/test come from the kernel's list tests
+	 * nvlist which we know contain properly sized strings.
+	 */
+	(void) strlcpy(kro.kro_module, desc->ktd_module, KTEST_MAX_NAME_LEN);
+	(void) strlcpy(kro.kro_suite, desc->ktd_suite, KTEST_MAX_NAME_LEN);
+	(void) strlcpy(kro.kro_test, desc->ktd_test, KTEST_MAX_NAME_LEN);
+
+	if (input_path != NULL) {
+		uchar_t *bytes = NULL;
+		uint64_t len = 0;
+
+		/*
+		 * The input_path came from the ktest_triple_t which
+		 * we know contains a properly sized string.
+		 */
+		(void) strlcpy(kro.kro_input_path, input_path,
+		    sizeof (kro.kro_input_path));
+
+		/*
+		 * We treat a failure to read the input file as a test
+		 * error.
+		 */
+		if (!ktest_read_file(input_path, &bytes, &len, err_msg)) {
+			kro.kro_result.kr_type = KTEST_RESULT_ERROR;
+			(void) strlcpy(kro.kro_result.kr_msg, err_msg,
+			    KTEST_MAX_LOG_LEN);
+			ktest_record_stat(mod_stats, suite_stats,
+			    &kro.kro_result);
+			ofmt_print(ofmt, &kro);
+			return (B_FALSE);
+		}
+
+		/*
+		 * The input stream must contain at least 1 byte.
+		 */
+		if (len == 0) {
+			kro.kro_result.kr_type = KTEST_RESULT_ERROR;
+			(void) strcpy(kro.kro_result.kr_msg,
+			    "zero-length input stream");
+			ktest_record_stat(mod_stats, suite_stats,
+			    &kro.kro_result);
+			ofmt_print(ofmt, &kro);
+			return (B_FALSE);
+		}
+
+		kro.kro_input_len = len;
+		kro.kro_input_bytes = bytes;
+	}
+
+	if (ioctl(dev, KTEST_IOCTL_RUN_TEST, &kro) == -1) {
+		if (input_path != NULL) {
+			err(EXIT_FAILURE, "failed to run test %s:%s:%s with "
+			    "input %s", desc->ktd_module, desc->ktd_suite,
+			    desc->ktd_test, input_path);
+		} else {
+			err(EXIT_FAILURE, "failed to run test %s:%s:%s",
+			    desc->ktd_module, desc->ktd_suite, desc->ktd_test);
+		}
+	}
+
+	ktest_record_stat(mod_stats, suite_stats, &kro.kro_result);
+	ofmt_print(ofmt, &kro);
+	return (kro.kro_result.kr_type == KTEST_RESULT_PASS ||
+	    kro.kro_result.kr_type == KTEST_RESULT_SKIP);
+}
+
+/*
+ * Run all tests specified in the run list and print the result of
+ * each test. If print_stats is true, the result statistics are
+ * printed as well. A return of true indicates all tests passed. A
+ * return of false indicates one or more tests produced an ERROR or
+ * FAIL result.
+ */
+static boolean_t
+ktest_run_tests(int dev, nvlist_t *tests, list_t *run_list, ofmt_handle_t ofmt,
+    boolean_t print_stats)
+{
+	ktest_iter_t *iter = ktest_iter(tests, run_list);
+	ktest_test_desc_t desc;
+	ktest_stats_t *mod_stats = NULL;
+	ktest_stats_t *suite_stats = NULL;
+	list_t stats;
+	ktest_stats_t *stat = NULL;
+	boolean_t all_pass = B_TRUE;
+
+	ktest_test_desc_init(&desc);
+	list_create(&stats, sizeof (ktest_stats_t),
+	    offsetof(ktest_stats_t, ks_node));
+
+	while (ktest_iter_next(iter, &desc)) {
+		/*
+		 * Either this is our first matching test or we are
+		 * transitioning to a new module and/or suite. In
+		 * either case, create new stats structures and add
+		 * them to the list.
+		 */
+		if (mod_stats == NULL ||
+		    strcmp(mod_stats->ks_name, desc.ktd_module) != 0) {
+			mod_stats = ktest_stats_new(KTEST_STAT_MOD,
+			    desc.ktd_module);
+			list_insert_tail(&stats, mod_stats);
+		}
+
+		if (suite_stats == NULL ||
+		    strcmp(suite_stats->ks_name, desc.ktd_suite) != 0) {
+			suite_stats = ktest_stats_new(KTEST_STAT_SUITE,
+			    desc.ktd_suite);
+			list_insert_tail(&stats, suite_stats);
+		}
+
+		/*
+		 * A test that does not require input only has to run
+		 * once.
+		 */
+		if (!desc.ktd_requires_input) {
+			if (!ktest_run_test(dev, &desc, NULL, mod_stats,
+			    suite_stats, ofmt)) {
+				all_pass = B_FALSE;
+			}
+			continue;
+		}
+
+		/*
+		 * A test that requires input may have more than one
+		 * matching triple. This feature allows a user to
+		 * specify multiple input streams for the same test
+		 * where each input stream is a separate run of the
+		 * test. We iterate the run list; running the test
+		 * for each triple that matches and has an input path
+		 * specified.
+		 */
+		for (ktest_triple_t *triple = list_head(run_list);
+		    triple != NULL;
+		    triple = list_next(run_list, triple)) {
+			if (ktest_match_triple(&desc, triple) &&
+			    triple->ktr_input_path[0] != '\0') {
+				if (!ktest_run_test(dev, &desc,
+				    triple->ktr_input_path, mod_stats,
+				    suite_stats, ofmt)) {
+					all_pass = B_FALSE;
+				}
+			}
+		}
+	}
+
+	if (print_stats) {
+		printf("\n");
+		ktest_print_stats(&stats);
+	}
+
+	while ((stat = list_remove_head(&stats)) != NULL) {
+		free(stat);
+	}
+
+	list_destroy(&stats);
+	free(iter);
+	return (all_pass);
+}
+
+/*
+ * Attempt to parse the test triple string and return the resulting
+ * triple struct. This leaves the original triple string untouched.
+ *
+ * This function produces a warning when failing to parse a triple,
+ * this is on purpose. This allows the run file parser to produce an
+ * error that points out the line number with the bad triple.
+ */
+static ktest_triple_t *
+ktest_parse_triple(const char *tstr, uint32_t lineno)
+{
+	char *cp = NULL, *orig = NULL;
+	char *module = NULL;
+	char *suite = NULL;
+	char *test = NULL;
+	ktest_triple_t *triple = NULL;
+
+	if ((triple = calloc(1, sizeof (*triple))) == NULL) {
+		warn("failed to allocate triple");
+		return (NULL);
+	}
+
+	triple->ktr_lineno = lineno;
+
+	if (strnlen(tstr, KTEST_MAX_TRIPLE_LEN) >= KTEST_MAX_TRIPLE_LEN) {
+		warnx("triple is too long");
+		goto fail;
+	}
+
+	if ((cp = strndup(tstr, KTEST_MAX_TRIPLE_LEN)) == NULL) {
+		warn("failed to dup triple string");
+		goto fail;
+	}
+
+	orig = cp;
+	module = strsep(&cp, KTEST_SEPARATOR);
+
+	if (strnlen(module, KTEST_MAX_NAME_LEN) >= KTEST_MAX_NAME_LEN) {
+		warnx("module pattern too long: %s", module);
+		goto fail;
+	}
+
+	if (*module == '\0') {
+		module = "*";
+	}
+
+	if (cp == NULL) {
+		suite = "*";
+		test = "*";
+		goto copy;
+	}
+
+	suite = strsep(&cp, KTEST_SEPARATOR);
+
+	if (strnlen(suite, KTEST_MAX_NAME_LEN) >= KTEST_MAX_NAME_LEN) {
+		warnx("suite pattern too long: %s", suite);
+		goto fail;
+	}
+
+	if (*suite == '\0') {
+		suite = "*";
+	}
+
+	if (cp == NULL) {
+		test = "*";
+		goto copy;
+	}
+
+	test = cp;
+
+	if (strstr(cp, KTEST_SEPARATOR) != NULL) {
+		warnx("malformed triple, unexpected ':' in test pattern: %s",
+		    test);
+		goto fail;
+	}
+
+	if (strnlen(test, KTEST_MAX_NAME_LEN) >= KTEST_MAX_NAME_LEN) {
+		warnx("test pattern too long: %s", test);
+		goto fail;
+	}
+
+	if (*test == '\0') {
+		test = "*";
+	}
+
+copy:
+	/* We've checked the string lengths, but just in case. */
+	(void) strlcpy(triple->ktr_module, module, sizeof (triple->ktr_module));
+	(void) strlcpy(triple->ktr_suite, suite, sizeof (triple->ktr_suite));
+	(void) strlcpy(triple->ktr_test, test, sizeof (triple->ktr_test));
+	free(orig);
+	return (triple);
+
+fail:
+	free(orig);
+	free(triple);
+	return (NULL);
+}
+
+/*
+ * Attempt to load the run file specified and decode it into a run
+ * list. Use stdin as the content of the runfile when use_stdin is
+ * true.
+ *
+ * Currently all input files must either be relative to the working
+ * directory or an absolute path. In the future it would be nice to
+ * support something like glob(3C) to perform tilde expansion for the
+ * input file path. Another idea would be to add a search path for
+ * input files, allowing us to more easily constrain where on the
+ * filesystem these files are searched for.
+ */
+static void
+ktest_load_run_file(const char *path, boolean_t use_stdin, nvlist_t *tests,
+    list_t *run_list)
+{
+	FILE *f;
+	char *line = NULL;
+	size_t cap = 0;
+	ssize_t len;
+	uint32_t lineno = 0;
+	boolean_t one_line = B_FALSE; /* At least one valid line? */
+
+	if (use_stdin) {
+		f = stdin;
+	} else {
+		if ((f = fopen(path, "r")) == NULL) {
+			err(EXIT_FAILURE, "failed to open run file %s", path);
+		}
+	}
+
+	while ((len = getline(&line, &cap, f)) != -1) {
+		char *input, *lasts, *tstr;
+		ktest_triple_t *triple;
+
+		lineno++;
+		/* A line is always at least one character: newline. */
+		VERIFY3S(len, >=, 1);
+		/* Skip the newline. */
+		line[len - 1] = '\0';
+
+		/* Skip empty lines. */
+		if (line[0] == '\0') {
+			continue;
+		}
+
+		/*
+		 * A valid line consists of either a test triple on
+		 * its own or a test triple and an input file
+		 * separated by whitespace.
+		 */
+		tstr = strtok_r(line, " \t", &lasts);
+		triple = ktest_parse_triple(tstr, lineno);
+
+		if (triple == NULL) {
+			errx(EXIT_FAILURE, "failed to parse triple %s at line "
+			    "%u", tstr, lineno);
+		}
+
+		input = strtok_r(NULL, " \t", &lasts);
+
+		if (input != NULL) {
+			size_t len = strlcpy(triple->ktr_input_path, input,
+			    sizeof (triple->ktr_input_path));
+			if (len >= sizeof (triple->ktr_input_path)) {
+				err(EXIT_FAILURE, "input path at line %u too "
+				    "long: %s\n", lineno, input);
+			}
+		}
+
+		list_insert_tail(run_list, triple);
+		one_line = B_TRUE;
+	}
+
+	/*
+	 * If we broke from the loop for a reason other than EOF, then
+	 * assume we do not have a full run list and exit.
+	 */
+	if (ferror(f)) {
+		err(EXIT_FAILURE, "failed to read entire runfile");
+	}
+
+	if (!use_stdin) {
+		(void) fclose(f);
+	}
+
+	free(line);
+
+	if (!one_line) {
+		errx(EXIT_FAILURE, "no tests specified in: %s", path);
+	}
+}
+
+/*
+ * Is this test triple fully-qualified?
+ *
+ * A fully-qualified triple is one where the module, suite, and test
+ * use no glob characters with the intent that it refers to a single,
+ * unique test.
+ */
+static boolean_t
+ktest_is_fqt(const ktest_triple_t *triple)
+{
+	return (strpbrk(triple->ktr_module, KTEST_GMATCH_CHARS) == NULL &&
+	    strpbrk(triple->ktr_suite, KTEST_GMATCH_CHARS) == NULL &&
+	    strpbrk(triple->ktr_test, KTEST_GMATCH_CHARS) == NULL);
+}
+
+/*
+ * Does this fully-qualified triple refer to a test which requires
+ * input?
+ */
+static boolean_t
+ktest_fqt_requires_input(ktest_triple_t *triple, nvlist_t *tests)
+{
+	list_t filter;
+	ktest_iter_t *iter = NULL;
+	ktest_test_desc_t desc;
+	/*
+	 * Need a local copy of the triple in order to build a filter
+	 * list for ktest_iter() because the argument is already a
+	 * part of the run list and reusing it would clobber its node
+	 * link.
+	 */
+	ktest_triple_t cp = *triple;
+
+	VERIFY(ktest_is_fqt(triple));
+
+	list_create(&filter, sizeof (ktest_triple_t),
+	    offsetof(ktest_triple_t, ktr_node));
+	list_insert_head(&filter, &cp);
+	iter = ktest_iter(tests, &filter);
+
+	if (!ktest_iter_next(iter, &desc)) {
+		return (B_FALSE);
+	}
+
+	return (desc.ktd_requires_input);
+}
+
+/*
+ * Check if the fully-qualified triple has an input path when it
+ * should. Return true if the entry is okay and false if there is a
+ * mismatch between the test descriptor the the triple entry.
+ */
+static boolean_t
+ktest_check_fqt_entry(nvlist_t *tests, ktest_triple_t *triple)
+{
+	boolean_t requires_input = ktest_fqt_requires_input(triple, tests);
+
+	if (requires_input && strlen(triple->ktr_input_path) == 0) {
+		warnx("fully-qualified triple %s:%s:%s at line %u missing "
+		    "input for test that requires input", triple->ktr_module,
+		    triple->ktr_suite, triple->ktr_test, triple->ktr_lineno);
+		return (B_FALSE);
+	} else if (!requires_input && strlen(triple->ktr_input_path) != 0) {
+		warnx("fully-qualified triple %s:%s:%s at line %u specifies "
+		    "input for test that does not require it",
+		    triple->ktr_module, triple->ktr_suite, triple->ktr_test,
+		    triple->ktr_lineno);
+		return (B_FALSE);
+	}
+
+	return (B_TRUE);
+}
+
+/*
+ * When a test is fully-qualified it could be for the purpose of
+ * specifying an input stream. We provide this check to catch missing
+ * input (or input on a test that does not require it) as a benefit to
+ * the user.
+ *
+ * We do not exit immediately upon finding a bad entry; but instead
+ * print a warning for each bad triple and then exit with failure.
+ */
+static void
+ktest_check_fqt_entries(nvlist_t *tests, list_t *run_list)
+{
+	boolean_t bad_triple = B_FALSE;
+
+	for (ktest_triple_t *triple = list_head(run_list); triple != NULL;
+	    triple = list_next(run_list, triple)) {
+		if (ktest_is_fqt(triple)) {
+			if (!ktest_check_fqt_entry(tests, triple)) {
+				bad_triple = B_TRUE;
+			}
+		}
+	}
+
+	if (bad_triple) {
+		errx(EXIT_FAILURE, "one or more incorrect triples");
+	}
+}
+
+/*
+ * Verify that the run list is acceptable.
+ */
+static void
+ktest_verify_run_list(nvlist_t *tests, list_t *run_list)
+{
+	ktest_check_fqt_entries(tests, run_list);
+}
+
+static boolean_t
+ktest_run_cmd(int argc, char *argv[], int ktdev)
+{
+	char c;
+	nvlist_t *tests = NULL;
+	list_t run_list;
+	char *input_path = NULL;
+	boolean_t parsable = B_FALSE;
+	boolean_t fields_set = B_FALSE;
+	boolean_t print_stats = B_TRUE;
+	char *fields = KTEST_RUN_CMD_DEF_FIELDS;
+	uint_t oflags = 0;
+	ofmt_handle_t ofmt = NULL;
+	ofmt_status_t oferr;
+	boolean_t all_pass = B_FALSE;
+
+	while ((c = getopt(argc, argv, ":Ho:pi:")) != -1) {
+		switch (c) {
+		case 'H':
+			oflags |= OFMT_NOHEADER;
+			break;
+		case 'o':
+			fields = optarg;
+			fields_set = B_TRUE;
+			break;
+		case 'p':
+			parsable = B_TRUE;
+			print_stats = B_FALSE;
+			oflags |= OFMT_PARSABLE;
+			break;
+		case 'i':
+			if (input_path != NULL) {
+				ktest_usage("cannot specify -i more than once");
+				exit(EXIT_USAGE);
+			}
+
+			input_path = optarg;
+
+			if (strnlen(input_path, MAXPATHLEN) >= MAXPATHLEN) {
+				err(EXIT_FAILURE, "input path too long");
+			}
+
+			break;
+		case ':':
+			ktest_usage("missing argument to -%c", optopt);
+			exit(EXIT_USAGE);
+
+		case '?':
+			ktest_usage("unknown run option: -%c", optopt);
+			exit(EXIT_USAGE);
+		}
+	}
+
+	if (parsable && !fields_set) {
+		ktest_usage("must specify -o with -p");
+		exit(EXIT_USAGE);
+	}
+
+	oferr = ofmt_open(fields, ktest_run_ofmt, oflags, 0, &ofmt);
+	ofmt_check(oferr, parsable, ofmt, ktest_ofmt_errx, warnx);
+
+	argc -= optind;
+	argv += optind;
+
+	/*
+	 * We don't run all tests by default. We assume that as the
+	 * library of test modules grows we want to be sure the user
+	 * actually wants to run all tests by forcing them to at least
+	 * specify the `*` glob.
+	 */
+	if (argc < 1) {
+		ktest_usage("must specify at least one triple");
+		exit(EXIT_USAGE);
+	}
+
+	list_create(&run_list, sizeof (ktest_triple_t),
+	    offsetof(ktest_triple_t, ktr_node));
+
+	for (uint_t i = 0; i < argc; i++) {
+		ktest_triple_t *triple = ktest_parse_triple(argv[i], 0);
+
+		if (triple == NULL) {
+			errx(EXIT_FAILURE, "failed to parse triple: %s",
+			    argv[i]);
+		}
+
+		if (input_path != NULL) {
+			/*
+			 * The path length was checked during option
+			 * parsing.
+			 */
+			(void) strcpy(triple->ktr_input_path, input_path);
+		}
+
+		list_insert_tail(&run_list, triple);
+	}
+
+	tests = ktest_list_tests(ktdev);
+	ktest_verify_run_list(tests, &run_list);
+	all_pass = ktest_run_tests(ktdev, tests, &run_list, ofmt, print_stats);
+	ofmt_close(ofmt);
+	ktest_free_triples(&run_list);
+	list_destroy(&run_list);
+	nvlist_free(tests);
+	return (all_pass);
+}
+
+static boolean_t
+ktest_run_file_cmd(int argc, char *argv[], int ktdev)
+{
+	char c;
+	nvlist_t *tests = NULL;
+	list_t run_list;
+	char *run_file = NULL;
+	boolean_t use_stdin = B_FALSE;
+	boolean_t parsable = B_FALSE;
+	boolean_t fields_set = B_FALSE;
+	boolean_t print_stats = B_TRUE;
+	char *fields = KTEST_RUN_CMD_DEF_FIELDS;
+	uint_t oflags = 0;
+	ofmt_handle_t ofmt = NULL;
+	ofmt_status_t oferr;
+	boolean_t all_pass = B_FALSE;
+
+	while ((c = getopt(argc, argv, ":Ho:p")) != -1) {
+		switch (c) {
+		case 'H':
+			oflags |= OFMT_NOHEADER;
+			break;
+		case 'o':
+			fields = optarg;
+			fields_set = B_TRUE;
+			break;
+		case 'p':
+			parsable = B_TRUE;
+			print_stats = B_FALSE;
+			oflags |= OFMT_PARSABLE;
+			break;
+		case ':':
+			ktest_usage("missing argument to -%c", optopt);
+			exit(EXIT_USAGE);
+		case '?':
+			ktest_usage("unknown option: -%c", optopt);
+			exit(EXIT_USAGE);
+		}
+	}
+
+	if (parsable && !fields_set) {
+		ktest_usage("must specify -o with -p");
+		exit(EXIT_USAGE);
+	}
+
+	oferr = ofmt_open(fields, ktest_run_ofmt, oflags, 0, &ofmt);
+	ofmt_check(oferr, parsable, ofmt, ktest_ofmt_errx, warnx);
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc > 1) {
+		ktest_usage("must specify only one run file");
+		exit(EXIT_USAGE);
+	}
+
+	/*
+	 * Use stdin as the run file when no run file argument is
+	 * specified.
+	 */
+	if (argc == 0) {
+		use_stdin = B_TRUE;
+	}
+
+	run_file = argv[0];
+	tests = ktest_list_tests(ktdev);
+	list_create(&run_list, sizeof (ktest_triple_t),
+	    offsetof(ktest_triple_t, ktr_node));
+	ktest_load_run_file(run_file, use_stdin, tests, &run_list);
+	ktest_verify_run_list(tests, &run_list);
+	all_pass = ktest_run_tests(ktdev, tests, &run_list, ofmt, print_stats);
+	ofmt_close(ofmt);
+	ktest_free_triples(&run_list);
+	list_destroy(&run_list);
+	nvlist_free(tests);
+	return (all_pass);
+}
+
+static void
+ktest_list_cmd(int argc, char *argv[], int dev)
+{
+	char c;
+	list_t triples;
+	nvlist_t *tests = NULL;
+	boolean_t parsable = B_FALSE;
+	boolean_t fields_set = B_FALSE;
+	char *fields = KTEST_LIST_CMD_DEF_FIELDS;
+	uint_t oflags = 0;
+	ofmt_handle_t ofmt = NULL;
+	ofmt_status_t oferr;
+
+	list_create(&triples, sizeof (ktest_triple_t),
+	    offsetof(ktest_triple_t, ktr_node));
+
+	while ((c = getopt(argc, argv, ":Ho:p")) != -1) {
+		switch (c) {
+		case 'H':
+			oflags |= OFMT_NOHEADER;
+			break;
+		case 'o':
+			fields = optarg;
+			fields_set = B_TRUE;
+			break;
+		case 'p':
+			parsable = B_TRUE;
+			oflags |= OFMT_PARSABLE;
+			break;
+		case ':':
+			ktest_usage("missing argument to -%c", optopt);
+			exit(EXIT_USAGE);
+		case '?':
+			ktest_usage("unknown option: -%c", optopt);
+			exit(EXIT_USAGE);
+		}
+	}
+
+	if (parsable && !fields_set) {
+		ktest_usage("must specify -o with -p");
+		exit(EXIT_USAGE);
+	}
+
+	oferr = ofmt_open(fields, ktest_list_ofmt, oflags, 0, &ofmt);
+	ofmt_check(oferr, parsable, ofmt, ktest_ofmt_errx, warnx);
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc == 0) {
+		list_insert_tail(&triples, ktest_def_triple);
+	} else {
+		for (uint_t i = 0; i < argc; i++) {
+			ktest_triple_t *triple = ktest_parse_triple(argv[i], 0);
+
+			if (triple == NULL) {
+				errx(EXIT_FAILURE, "failed to parse triple: %s",
+				    argv[i]);
+			}
+
+			list_insert_tail(&triples, triple);
+		}
+	}
+
+	tests = ktest_list_tests(dev);
+	ktest_print_tests(tests, &triples, ofmt);
+	ofmt_close(ofmt);
+	nvlist_free(tests);
+	ktest_free_triples(&triples);
+	list_destroy(&triples);
+}
+
+static void
+ktest_alloc_def_triple()
+{
+	ktest_def_triple = ktest_parse_triple(KTEST_DEF_TRIPLE, 0);
+
+	if (ktest_def_triple == NULL) {
+		err(EXIT_FAILURE, "failed to initialize default triple");
+	}
+}
+
+int
+main(int argc, char *argv[])
+{
+	int fd;
+	const char *cmd;
+
+	ktest_prog = basename(argv[0]);
+
+	if (getzoneid() != GLOBAL_ZONEID || getuid() != 0) {
+		errx(EXIT_FAILURE, "can only be used by root from"
+		    " the global zone");
+	}
+
+	if (argc < 2) {
+		ktest_usage("no command specified");
+		exit(EXIT_USAGE);
+	}
+
+	/*
+	 * Peel off program name and command.
+	 */
+	cmd = argv[1];
+	argc -= 2;
+	argv += 2;
+	optind = 0;
+
+	if ((fd = open(KTEST_DEV_PATH, O_RDONLY, 0)) == -1) {
+		err(EXIT_FAILURE, "failed to open %s", KTEST_DEV_PATH);
+	}
+
+	ktest_alloc_def_triple();
+
+	if (strncasecmp("list", cmd, KTEST_CMD_SZ) == 0) {
+		ktest_list_cmd(argc, argv, fd);
+	} else if (strncasecmp("run", cmd, KTEST_CMD_SZ) == 0) {
+		if (!ktest_run_cmd(argc, argv, fd)) {
+			errx(EXIT_FAILURE, "one or more tests did not pass");
+		}
+	} else if (strncasecmp("run-file", cmd, KTEST_CMD_SZ) == 0) {
+		if (!ktest_run_file_cmd(argc, argv, fd)) {
+			errx(EXIT_FAILURE, "one or more tests did not pass");
+		}
+	} else if (strncasecmp("help", cmd, KTEST_CMD_SZ) == 0) {
+		ktest_usage(NULL);
+	} else {
+		ktest_usage("unknown command: %s", cmd);
+		exit(EXIT_USAGE);
+	}
+
+	(void) close(fd);
+	return (0);
+}
diff --git a/usr/src/man/man8/Makefile b/usr/src/man/man8/Makefile
index ef34b38ff7..be8003622a 100644
--- a/usr/src/man/man8/Makefile
+++ b/usr/src/man/man8/Makefile
@@ -250,6 +250,7 @@ _MANFILES=	6to4relay.8		\
 		kproplog.8		\
 		krb5kdc.8		\
 		kstat.8			\
+		ktest.8			\
 		ktkt_warnd.8		\
 		labelit.8		\
 		labelit_hsfs.8		\
diff --git a/usr/src/man/man8/ktest.8 b/usr/src/man/man8/ktest.8
new file mode 100644
index 0000000000..77373eb145
--- /dev/null
+++ b/usr/src/man/man8/ktest.8
@@ -0,0 +1,272 @@
+.\"
+.\" This file and its contents are supplied under the terms of the
+.\" Common Development and Distribution License ("CDDL"), version 1.0.
+.\" You may only use this file in accordance with the terms of version
+.\" 1.0 of the CDDL.
+.\"
+.\" A full copy of the text of the CDDL should have accompanied this
+.\" source.  A copy of the CDDL is also available via the Internet at
+.\" http://www.illumos.org/license/CDDL.
+.\"
+.\"
+.\" Copyright 2023 Oxide Computer Company
+.\"
+.Dd February 13, 2023
+.Dt KTEST 8
+.Os
+.Sh NAME
+.Nm ktest
+.Nd list and run kernel tests
+.Sh SYNOPSIS
+.Nm ktest Cm help
+.Pp
+.Nm ktest Cm list
+.Op Fl H
+.Op Oo Fl p Oc Fl o Ar field Ns Op ,...
+.Op Ar triple...
+.Pp
+.Nm ktest Cm run
+.Op Fl H
+.Op Oo Fl p Oc Fl o Ar field Ns Op ,...
+.Op -i Ar input
+.Ar triple...
+.Pp
+.Nm ktest Cm run-file
+.Op Fl H
+.Op Oo Fl p Oc Fl o Ar field Ns Op ,...
+.Op Ar runfile
+.Sh DESCRIPTION
+The
+.Nm
+facility provides in situ kernel testing by running test code in
+kernel context; essentially unit tests for the kernel.
+The
+.Nm
+command is used to list or run registered kernel tests.
+The
+.Nm
+command interacts with the in-kernel ktest facility by way of the
+.Pa /dev/ktest
+pseudo device.
+A process must run in the Global Zone and have the
+.Sy PRIV_SYS_DEVICES
+privilege in order to access the device.
+.Ss OUTPUT FIELDS
+The following is a list of output fields used by the
+.Nm
+command along with their meanings.
+.Bl -tag -offset 4m -width 8m
+.It Sy MODULE
+The module name of the test.
+This is typically the same name as the module-under-test.
+.It Sy SUITE
+The suite name of the test.
+.It Sy TEST
+The name of the test.
+.It Sy INPUT
+For the
+.Sy list
+command this indicates if the test requires an input stream: [Y]es or [N]o.
+For the
+.Sy run
+and
+.Sy run-file
+commands it is the path to the input stream.
+.It Sy LINE
+The source code line number that produced the test
+.Sy RESULT .
+All
+.Sy PASS
+and
+.Sy NONE
+results have a value of 0.
+Upon detection of multiple results this value indicates the line that
+overwrote the previous result and the
+.Sy RESULT
+field contains a message with the line number of the previous result.
+This field applies only to the
+.Sy run
+and
+.Sy run-file
+commands.
+.It Sy RESULT
+The test result.
+The
+.Sy TEST RESULTS
+section explains the different types of results.
+This field applies only to the
+.Sy run
+and
+.Sy run-file
+commands.
+.It Sy REASON
+Additional context when the test result is one of
+.Sy FAIL ,
+.Sy ERROR ,
+or
+.Sy SKIP .
+This field applies only to the
+.Sy run
+and
+.Sy run-file
+commands.
+.El
+.Ss TEST RESULTS
+The different types of results that a test can produce.
+.Bl -tag -offset 4m -width 8m
+.It Sy PASS
+The test passed.
+.It Sy FAIL
+The test had a logical failure (one of its assertions failed).
+.It Sy ERROR
+The test had an unexpected, non-logical error.
+This is typically caused by a failure to acquire resources or an error
+attempting some operation that is tangential to the logic under test.
+.It Sy SKIP
+The test decided to skip itself because it lacks the context required to run
+the test.
+For example, some type of missing resource or hardware.
+.It Sy NONE
+The test failed to produce a result.
+This is a bug in the test and should be fixed.
+.El
+.Ss OPTIONS
+The following is a list of options shared across all subcommands.
+This field applies only to the
+.Sy run
+and
+.Sy run-file
+commands.
+.Pp
+.Fl H
+.Bd -ragged -offset 2m
+Omit column headers when printing output.
+.Ed
+.Pp
+.Fl o Ar field Ns Op ,...
+.Bd -ragged -offset 2m
+A case-insensitive, comma-separated list of output fields to display.
+.Ed
+.Pp
+.Fl p
+.Bd -ragged -offset 2m
+Display in a machine-parsable format and elide test stats output when
+executing the
+.Sy run
+or
+.Sy run-file
+commands.
+The
+.Fl o
+option is required with
+.Fl p .
+.Ed
+.Ss SUBCOMMANDS
+.Nm ktest help
+.Bd -ragged -offset 2m
+Print command usage.
+.Ed
+.Pp
+.Nm ktest list
+.Op Fl H
+.Op Oo Fl p Oc Fl o Ar field Ns Op ,...
+.Op Ar triple...
+.Bd -ragged -offset 2m
+List all registered tests.
+One or more triples may be specified to narrow the listing.
+.Pp
+.Ed
+.Pp
+.Nm ktest run
+.Op Fl H
+.Op Oo Fl p Oc Fl o Ar field Ns Op ,...
+.Op Fl i Ar file
+.Ar triple...
+.Bd -ragged -offset 2m
+Run registered tests.
+One or more triples may be specified to limit which tests are run.
+At least one triple must be specified.
+.Pp
+.Fl i Ar file
+.Ed
+.Bd -ragged -offset 4m
+Specify a
+.Ar file
+to act as the input stream for all tests requiring one.
+.Ed
+.Pp
+.\" ktest run-file
+.Nm
+run-file
+.Op Fl H
+.Op Oo Fl p Oc Fl o Ar field Ns Op ,...
+.Op Ar runfile
+.Bd -ragged -offset 2m
+Run the tests specified by the
+.Ar runfile .
+If no
+.Ar runfile
+is specified, then use stdin as the contents of the
+.Ar runfile .
+.Ed
+.Sh EXIT STATUS
+The following exit values are returned:
+.Bl -tag -width indent
+.It 0
+Successful completion of the command.
+For the
+.Sy run
+and
+.Sy run-file
+commands this also indicates that all tests produced a PASS or SKIP result.
+.It 1
+An error occurred while executing the command.
+For the
+.Sy run
+and
+.Sy run-file
+commands this also indicates that one or more tests produced
+a result other than PASS or SKIP.
+.It 2
+An invalid combination of command line options or arguments were
+specified.
+.El
+.Sh EXAMPLES
+.Ss List all Tests
+List all tests registered with the ktest framework.
+.Bd -literal -offset ident
+ktest list
+.Ed
+.Ss Run all Tests
+Run all tests registered with the ktest framework.
+.Bd -literal -offset ident
+ktest run '*'
+.Ed
+.Ss Run specific test module
+This example runs only tests registered under the
+.Sy mac
+test module.
+This includes all suites under the
+.Sy mac
+module and all tests under each suite.
+.Bd -literal -offset ident
+ktest run 'mac::'
+.Ed
+.Ss Execute a run file
+Executes the run file named
+.Sy mac.runfile
+and outputs the results in parsable format to stdout.
+This is useful for integrating ktest into a larger testing framework.
+.Bd -literal -offset ident
+ktest run-file -Hpo result,module,suite,test,input,line,reason mac.runfile
+.Ed
+.Ss Run a test with an input file
+Execute a specific test which requires an input stream.
+.Bd -literal -offset ident
+ktest run -i data.pcap mac:checksum:mac_sw_cksum_ipv4_snoop_test
+.Ed
+.Sh INTERFACE STABILITY
+The command syntax is
+.Sy Uncommitted .
+The output format is
+.Sy Uncommitted .
diff --git a/usr/src/man/man9/Makefile b/usr/src/man/man9/Makefile
index 103a1e6b28..c02846e807 100644
--- a/usr/src/man/man9/Makefile
+++ b/usr/src/man/man9/Makefile
@@ -10,15 +10,17 @@
 #
 
 #
+# Copyright 2023 Oxide Computer Company
 # Copyright 2017, Richard Lowe
 # Copyright 2013 Nexenta Systems, Inc.  All rights reserved.
 #
 
 include		$(SRC)/Makefile.master
 
-MANSECT= 	9
+MANSECT=	9
 MANFILES=	Intro.9	\
 		iport.9 \
+		ktest.9 \
 		vmem.9
 
 
@@ -36,4 +38,4 @@ tgtmap.9	:= LINKSRC = iport.9
 
 include		$(SRC)/man/Makefile.man
 
-install: 	$(ROOTMANFILES) $(ROOTMANLINKS)
+install:	$(ROOTMANFILES) $(ROOTMANLINKS)
diff --git a/usr/src/man/man9/ktest.9 b/usr/src/man/man9/ktest.9
new file mode 100644
index 0000000000..ab4fcdf915
--- /dev/null
+++ b/usr/src/man/man9/ktest.9
@@ -0,0 +1,202 @@
+.\"
+.\" This file and its contents are supplied under the terms of the
+.\" Common Development and Distribution License ("CDDL"), version 1.0.
+.\" You may only use this file in accordance with the terms of version
+.\" 1.0 of the CDDL.
+.\"
+.\" A full copy of the text of the CDDL should have accompanied this
+.\" source.  A copy of the CDDL is also available via the Internet at
+.\" http://www.illumos.org/license/CDDL.
+.\"
+.\"
+.\" Copyright 2023 Oxide Computer Company
+.\"
+.Dd February 17, 2023
+.Dt KTEST 9
+.Os
+.Sh NAME
+.Nm ktest
+.Nd kernel test facility
+.Sh DESCRIPTION
+The ktest facility provides the means for writing and executing kernel
+test modules.
+A kernel test module is a kernel module that contains test functions
+designed to execute and verify some aspect of the kernel.
+You can test any aspect of the kernel for which you can create the
+requisite context, whether that be its local arguments or shared
+state.
+.Pp
+The key design idea behind ktest is to put the test in the same
+context as the code it's testing: kernel context.
+By writing tests as kernel functions it allows one to more thoroughly test
+particular aspects of the kernel.
+The ktest facility complements user-space testing by allowing targeted
+testing of the kernel that is not as easily exercised via user-space.
+.Pp
+The ktest facility provides a kernel API for creating tests as well as
+a character device for interacting with the facility from user-space.
+The latter of which should be done by way of the
+.Xr ktest 8
+command.
+.Pp
+This page concerns itself with the various kernel-side concepts of the
+ktest facility and acts as a guide to the various 9F APIs.
+.Ss The Test Triple
+A test is uniquely identified by its test triple.
+The triple is a three-level namespace comprising of the module name,
+suite name, and test name.
+Each part of the namespace is separated by the ':' character.
+.Bd -literal -offset 4m
+<module>:<suite>:<test>
+.Ed
+.Bl -tag -width 6m
+.It module
+The top of the namespace.
+If the test module is primarily focused on testing one kernel module,
+then its module name is typically named after the module-under-test.
+For example, a test module designed to test all aspects of the
+.Sy mac
+kernel module might use "mac" as the module name.
+This is merely a convention; at the end of the day the module name is
+simply a string meant to represent the general theme of its underlying
+tests.
+.It suite
+Each module consists of one or more suites.
+The suite name provides a second level of organization for grouping
+related tests.
+For example, your "mac" test module may have several tests for
+checksum-related routines.
+In that case it might help to organize all those tests under the
+"checksum" suite.
+.It test
+The name of the test.
+This can be any name you find descriptive of the test.
+For tests that focus on a single function you could use the name of
+the function-under-test with the "_test" suffix attached.
+For tests that exercise many functions do your best to make the name
+descriptive.
+The test functions are typically suffixed with "_test" to visually
+separate them from test-helper functions; and the test name is often
+the same as the test function.
+.El
+.Pp
+For a name to be considered valid it must meet the following criteria.
+.Bl -enum
+.It
+It must be 63 characters or less (the 64th byte must hold the
+.Sy NUL
+character).
+.It
+It must contain only the following ASCII
+characters: 'A-Z', 'a-z', '0-9', '.', '_'.
+.El
+.Ss The Context Handle
+The context handle provides the means through which the test
+communicates with the ktest facility.
+All test functions must conform to the following prototype.
+.Bd -literal -offset 4m
+typedef void (*ktest_fn_t)(ktest_ctx_hdl_t *ctx);
+.Ed
+.Pp
+The test interacts with
+.Fa ctx
+through various ktest routines documented in their requisite sections.
+.Ss Setting Test Results
+The ultimate goal of a test function is to return a result.
+See
+.Xr ktest_result_pass 9f
+for the complete guide to setting test results.
+.Ss Test Input
+Some tests require an input stream to test against.
+The input stream is provided at runtime by the user.
+The user may run the same test against one or more different input
+streams.
+.Pp
+This is useful when a test applies to many different scenarios with
+the same general logic.
+For example, a test that verifies a checksum routine applies to any
+byte stream; or a test that verifies TCP header parsing applies to any
+byte stream as well.
+.Pp
+Writing a test against an input stream provides several benefits.
+.Bl -enum
+.It
+It allows one test to verify innumerable scenarios, reducing the need
+to replicate test logic.
+.It
+It avoids the need to hard-code test input in the test module itself.
+.It
+With the ability to change the input stream at runtime it allows
+testing new scenarios on the spot, avoiding the need for writing a new
+test.
+.El
+.Pp
+A test that requires input must specify the
+.Sy KTEST_FLAG_INPUT
+flag when calling
+.Xr ktest_add_test 9f .
+To retrieve the input stream the test uses the
+.Xr ktest_get_input 9f
+function.
+.Ss Testing Private Functions
+A test facility that can't test private (
+.Ft static
+) functions is going to be pretty limited.
+Some of the most useful functions to test are those that act as
+"helpers" to other, larger functions.
+These functions are often declared
+.Ft static
+and not directly accessible to the test module which is built as its
+own compilation unit.
+.Pp
+One solution would be to alter ktest to also allow declaring test
+functions inside the regular kernel modules themselves, but it
+currently does not offer that capability.
+Instead, ktest provides the
+.Xr ktest_get_fn 9f
+function for obtaining a handle to private functions.
+See that page for more detail.
+.Ss Registering Tests
+Only registered tests are accessible to the user.
+See
+.Xr ktest_create_module 9f
+for more information.
+.Ss Test Module Conventions
+There are several conventions to follow when creating a new test
+module.
+.Bl -enum
+.It
+The test module should be a miscellaneous-type module using the
+.Xr modlmisc 9S
+linkage structure.
+.It
+Test functions are prefixed with their test module name.
+.It
+Test functions are suffixed with "_test" to distinguish them from test
+helper functions.
+.It
+Test registration should happen in
+.Xr _init 9E .
+.It
+Test deregistration should happen in
+.Xr _fini 9E .
+.El
+.Sh SEE ALSO
+.Xr KT_ASSERT 9F ,
+.Xr KT_ERROR 9F ,
+.Xr KT_FAIL 9F ,
+.Xr KT_PASS 9F ,
+.Xr KT_SKIP 9F ,
+.Xr ktest_add_suite 9F ,
+.Xr ktest_add_test 9F ,
+.Xr ktest_create_module 9F ,
+.Xr ktest_create_suite 9F ,
+.Xr ktest_get_fn 9F ,
+.Xr ktest_get_input 9F ,
+.Xr ktest_hold_mod 9F ,
+.Xr ktest_msg_clear 9F ,
+.Xr ktest_msg_prepend 9F ,
+.Xr ktest_register_module 9F ,
+.Xr ktest_release_mod 9F ,
+.Xr ktest_unregister_module 9F ,
+.Xr modlmisc 9S
diff --git a/usr/src/man/man9f/Makefile b/usr/src/man/man9f/Makefile
index 3b90b10129..5cb353f6de 100644
--- a/usr/src/man/man9f/Makefile
+++ b/usr/src/man/man9f/Makefile
@@ -308,6 +308,10 @@ MANFILES=	ASSERT.9f				\
 		kstat_install.9f			\
 		kstat_named_init.9f			\
 		kstat_queue.9f				\
+		ktest_create_module.9f			\
+		ktest_get_fn.9f				\
+		ktest_get_input.9f			\
+		ktest_result_pass.9f			\
 		ldi_add_event_handler.9f		\
 		ldi_aread.9f				\
 		ldi_devmap.9f				\
@@ -567,6 +571,20 @@ MANFILES=	ASSERT.9f				\
 
 MANLINKS=	AVL_NEXT.9f					\
 		AVL_PREV.9f					\
+		KT_ASSERT.9f					\
+		KT_ASSERT0.9f					\
+		KT_ASSERT3P.9f					\
+		KT_ASSERT3S.9f					\
+		KT_ASSERT3U.9f					\
+		KT_ASSERTG.9f					\
+		KT_ASSERT0G.9f					\
+		KT_ASSERT3PG.9f					\
+		KT_ASSERT3SG.9f					\
+		KT_ASSERT3UG.9f					\
+		KT_ERROR.9f					\
+		KT_FAIL.9f					\
+		KT_PASS.9f					\
+		KT_SKIP.9f					\
 		CRED.9f						\
 		SIZEOF_PTR.9f					\
 		SIZEOF_STRUCT.9f				\
@@ -959,6 +977,18 @@ MANLINKS=	AVL_NEXT.9f					\
 		kstat_waitq_enter.9f				\
 		kstat_waitq_exit.9f				\
 		kstat_waitq_to_runq.9f				\
+		ktest_add_suite.9f				\
+		ktest_add_test.9f				\
+		ktest_free_module.9f				\
+		ktest_hold_mod.9f				\
+		ktest_msg_clear.9f				\
+		ktest_msg_prepend.9f				\
+		ktest_register_module.9f			\
+		ktest_release_mod.9f				\
+		ktest_result_error.9f				\
+		ktest_result_fail.9f				\
+		ktest_result_skip.9f				\
+		ktest_unregister_module.9f			\
 		ldi_awrite.9f					\
 		ldi_close.9f					\
 		ldi_get_devid.9f				\
@@ -1780,6 +1810,36 @@ kstat_waitq_enter.9f			:= LINKSRC = kstat_queue.9f
 kstat_waitq_exit.9f			:= LINKSRC = kstat_queue.9f
 kstat_waitq_to_runq.9f			:= LINKSRC = kstat_queue.9f
 
+ktest_add_suite.9f			:= LINKSRC = ktest_create_module.9f
+ktest_add_test.9f			:= LINKSRC = ktest_create_module.9f
+ktest_free_module.9f			:= LINKSRC = ktest_create_module.9f
+ktest_register_module.9f		:= LINKSRC = ktest_create_module.9f
+ktest_unregister_module.9f		:= LINKSRC = ktest_create_module.9f
+
+ktest_hold_mod.9f			:= LINKSRC = ktest_get_fn.9f
+ktest_release_mod.9f			:= LINKSRC = ktest_get_fn.9f
+
+KT_ASSERT.9f				:= LINKSRC = ktest_result_pass.9f
+KT_ASSERT0.9f				:= LINKSRC = ktest_result_pass.9f
+KT_ASSERT3P.9f				:= LINKSRC = ktest_result_pass.9f
+KT_ASSERT3S.9f				:= LINKSRC = ktest_result_pass.9f
+KT_ASSERT3U.9f				:= LINKSRC = ktest_result_pass.9f
+KT_ASSERTG.9f				:= LINKSRC = ktest_result_pass.9f
+KT_ASSERT0G.9f				:= LINKSRC = ktest_result_pass.9f
+KT_ASSERT3PG.9f				:= LINKSRC = ktest_result_pass.9f
+KT_ASSERT3SG.9f				:= LINKSRC = ktest_result_pass.9f
+KT_ASSERT3UG.9f				:= LINKSRC = ktest_result_pass.9f
+KT_ERROR.9f				:= LINKSRC = ktest_result_pass.9f
+KT_FAIL.9f				:= LINKSRC = ktest_result_pass.9f
+KT_PASS.9f				:= LINKSRC = ktest_result_pass.9f
+KT_SKIP.9f				:= LINKSRC = ktest_result_pass.9f
+
+ktest_msg_clear.9f			:= LINKSRC = ktest_result_pass.9f
+ktest_msg_prepend.9f			:= LINKSRC = ktest_result_pass.9f
+ktest_result_error.9f			:= LINKSRC = ktest_result_pass.9f
+ktest_result_fail.9f			:= LINKSRC = ktest_result_pass.9f
+ktest_result_skip.9f			:= LINKSRC = ktest_result_pass.9f
+
 ldi_awrite.9f				:= LINKSRC = ldi_aread.9f
 
 ldi_get_devid.9f			:= LINKSRC = ldi_get_dev.9f
diff --git a/usr/src/man/man9f/ktest_create_module.9f b/usr/src/man/man9f/ktest_create_module.9f
new file mode 100644
index 0000000000..cf9616750d
--- /dev/null
+++ b/usr/src/man/man9f/ktest_create_module.9f
@@ -0,0 +1,272 @@
+.\"
+.\" This file and its contents are supplied under the terms of the
+.\" Common Development and Distribution License ("CDDL"), version 1.0.
+.\" You may only use this file in accordance with the terms of version
+.\" 1.0 of the CDDL.
+.\"
+.\" A full copy of the text of the CDDL should have accompanied this
+.\" source.  A copy of the CDDL is also available via the Internet at
+.\" http://www.illumos.org/license/CDDL.
+.\"
+.\"
+.\" Copyright 2023 Oxide Computer Company
+.\" Copyright 2024 Ryan Zezeski
+.\"
+.Dd February 8, 2024
+.Dt KTEST_CREATE_MODULE 9F
+.Os
+.Sh NAME
+.Nm ktest_create_module ,
+.Nm ktest_add_suite ,
+.Nm ktest_add_test ,
+.Nm ktest_register_module ,
+.Nm ktest_unregister_module ,
+.Nm ktest_free_module
+.Nd create and register ktest test modules
+.Sh SYNOPSIS
+.In sys/ktest.h
+.Ft int
+.Fo ktest_create_module
+.Fa "const char *name"
+.Fa "ktest_module_hdl_t **km_hdl"
+.Fc
+.Ft int
+.Fo ktest_add_suite
+.Fa "ktest_module_hdl_t *km_hdl"
+.Fa "const char *name"
+.Fa "ktest_suite_hdl_t **ks_hdl"
+.Fc
+.Ft int
+.Fo ktest_add_test
+.Fa "ktest_suite_hdl_t *ks_hdl"
+.Fa "const char *name"
+.Fa "ktest_fn_t fn"
+.Fa "ktest_test_flags_t flags"
+.Fc
+.Ft int
+.Fo ktest_register_module
+.Fa "ktest_module_hdl_t *km_hdl"
+.Fc
+.Ft void
+.Fo ktest_unregister_module
+.Fa "const char *name"
+.Fc
+.Ft void
+.Fo ktest_free_module
+.Fa "ktest_module_hdl_t *km_hdl"
+.Fc
+.Sh INTERFACE LEVEL
+.Sy Volatile -
+This interface is still evolving in illumos.
+API and ABI stability is not guaranteed.
+.Sh PARAMETERS
+.Bl -tag -width Fa
+.It Fa name
+The name of the module, suite, or test.
+See the "Names" section below.
+.It Fa km_hdl
+The handle to a ktest module.
+.It Fa ks_hdl
+The handle to a ktest suite.
+.It Fa fn
+A pointer to the test function.
+.It Fa flags
+The set of test flags.
+See the
+.Xr ktest 9
+page for a description of the valid flags.
+.El
+.Sh DESCRIPTION
+A ktest test module is created by building up a module object and
+registering it with the ktest facility.
+The module object consists of a name and one or more suite objects.
+The suite object consists of a name and one or more test objects.
+Each test object consists of a name, a function pointer to the test
+function, and a set of test flags.
+.Pp
+Test module object creation and registration should happen as part of
+the test module's
+.Xr _init 9E
+routine.
+The sequence of calls inside a test module's
+.Xr _init 9E
+should proceed in the following order.
+.Bl -enum -offset 4m
+.It
+.Fn ktest_create_module
+.It
+.Fn ktest_add_suite
+.It
+.Fn ktest_add_test ,
+1 or more times
+.It
+back to step 2 if more than one suite
+.It
+.Fn ktest_register_module
+.El
+.Pp
+Conversely, the test module should unregister its test module object
+as part of its
+.Xr _fini 9E
+routine.
+.Ss Names
+The
+.Fa name
+is the string used to reference a particular module, suite, or test.
+Any given test is uniquely identified by the combination of its
+module, suite, and test name -- also referred to as its "triple".
+This triple is how the user identifies a test when making use of the
+.Xr ktest 8
+command.
+.Pp
+The module name may be the same as the module-under-test, but that
+isn't a requirement.
+At the end of the day, the test module's name is simply a string used
+to organize suites of tests in some logical manner.
+In some cases it makes sense to name the test module something other
+than the module-under-test.
+For example, when the module-under-test is large, such as genunix.
+Or when the test module is testing a property of the larger system
+that spans more than a single module.
+.Pp
+Module names must be unique.
+Suite names must be unique for a given module.
+Test names must be unique for a given suite.
+That implies that suite and test names may be reused so long as they
+are unique within their given namespace.
+.Ss Test Flags
+.Bl -tag -width 4m
+.It Sy KTEST_FLAG_NONE
+No flags specified.
+.It Sy KTEST_FLAG_INPUT
+This test requires an input stream.
+.El
+.Ss Functions
+.Bl -tag -width 2m
+.It Sy ktest_create_module()
+Create a module object.
+Return failure if the name is not valid, or if the object could not be
+allocated.
+.It Sy ktest_add_suite()
+Create a suite object and add it to the module object referenced by
+.Fa km_hdl .
+Return failure if the name is already in use, the name is not valid,
+or if the object could not be allocated.
+.It Sy ktest_add_test()
+Create a test object and add it to the suite object referenced by
+.Fa ks_hdl .
+The
+.Fa fn
+should be a pointer to the test function and
+.Fa flags
+should contain a value derived from a bitwise-OR of one or more test flag
+values.
+Return failure if the name is already in use, the name is not valid,
+or if the object could not be allocated.
+.It Sy ktest_register_module()
+Register the test module object referenced by
+.Fa km_hdl .
+Return failure if the module name already exists.
+.It Sy ktest_unregister_module()
+Unregister the test module object that maps to the
+.Fa name .
+.It Sy ktest_free_module()
+Used to free the module along with its contained suites and tests in
+the event that the module failed to register.
+.El
+.Sh RETURN VALUES
+The
+.Fn ktest_create_module ,
+.Fn ktest_add_suite ,
+.Fn ktest_add_test ,
+and
+.Fn ktest_register_module
+functions return 0 on success or an error value for failure.
+.Sh EXAMPLES
+The following example provides a template for how one would typically
+organize the source code of a test module.
+.Bd -literal
+#include <sys/ktest.h>
+
+void
+foo_test1(ktest_ctx_hdl_t *ctx)
+{
+	...
+}
+
+void
+foo_test2(ktest_ctx_hdl_t *ctx)
+{
+	...
+}
+
+void
+foo_test3(ktest_ctx_hdl_t *ctx)
+{
+	...
+}
+
+static struct modlmisc foo_test_modlmisc = {
+	.misc_modops = &mod_miscops,
+	.misc_linkinfo = "foo ktest module"
+};
+
+static struct modlinkage foo_test_modlinkage = {
+	.ml_rev = MODREV_1,
+	.ml_linkage = { &foo_test_modlmisc, NULL }
+};
+
+int
+_init()
+{
+	int ret;
+	ktest_module_hdl_t *km = NULL;
+	ktest_suite_hdl_t *ks = NULL;
+
+	VERIFY0(ktest_create_module("foo", &km));
+	VERIFY0(ktest_add_suite("suite1", &ks));
+	VERIFY0(ktest_add_test(ks, "foo_test1", foo_test1,
+	    KTEST_FLAG_NONE));
+	VERIFY0(ktest_add_test(ks, "foo_test2", foo_test2,
+	    KTEST_FLAG_NONE));
+	VERIFY0(ktest_add_test(ks, "foo_test3", foo_test3,
+	    KTEST_FLAG_INPUT));
+
+	if ((ret = ktest_register_module(km)) != 0) {
+		ktest_free_module(km);
+		return (ret);
+	}
+
+	if ((ret = mod_install(&foo_test_modlinkage)) != 0) {
+		ktest_unregister_module("foo");
+		return (ret);
+	}
+
+	return (0);
+}
+
+int
+_fini(void)
+{
+	ktest_unregister_module("foo");
+	return (mod_remove(&mac_test_modlinkage));
+}
+.Ed
+.Sh ERRORS
+.Bl -tag -width 4m
+.It Er EEXIST
+The name already exists.
+.It Er EINVAL
+The name contains illegal characters.
+.It Er ENOMEM
+The module, suite, or test object could not be allocated.
+.It Er EOVERFLOW
+The
+.Fa name
+value is too long.
+.El
+.Sh SEE ALSO
+.Xr ktest 8 ,
+.Xr ktest 9 ,
+.Xr _fini 9E ,
+.Xr _init 9E
diff --git a/usr/src/man/man9f/ktest_get_fn.9f b/usr/src/man/man9f/ktest_get_fn.9f
new file mode 100644
index 0000000000..f261dd0671
--- /dev/null
+++ b/usr/src/man/man9f/ktest_get_fn.9f
@@ -0,0 +1,212 @@
+.\"
+.\" This file and its contents are supplied under the terms of the
+.\" Common Development and Distribution License ("CDDL"), version 1.0.
+.\" You may only use this file in accordance with the terms of version
+.\" 1.0 of the CDDL.
+.\"
+.\" A full copy of the text of the CDDL should have accompanied this
+.\" source.  A copy of the CDDL is also available via the Internet at
+.\" http://www.illumos.org/license/CDDL.
+.\"
+.\"
+.\" Copyright 2023 Oxide Computer Company
+.\"
+.Dd February 15, 2023
+.Dt KTEST_GET_FN 9F
+.Os
+.Sh NAME
+.Nm ktest_get_fn ,
+.Nm ktest_hold_mod ,
+.Nm ktest_release_mod
+.Nd get a pointer to a static function
+.Sh SYNOPSIS
+.In sys/ktest.h
+.Ft int
+.Fo ktest_get_fn
+.Fa "ddi_modhandle_t hdl"
+.Fa "const char *fn_name"
+.Fa "void **fn"
+.Fc
+.Ft int
+.Fo ktest_hold_mod
+.Fa "const char *module"
+.Fa "ddi_modhandle_t *hdl"
+.Fc
+.Ft void
+.Fo ktest_release_mod
+.Fa "ddi_modhandle_t hdl"
+.Fc
+.Sh INTERFACE LEVEL
+.Sy Volatile -
+This interface is still evolving in illumos.
+API and ABI stability is not guaranteed.
+.Sh PARAMETERS
+.Bl -tag -width Fa
+.It Fa hdl
+A handle to the module.
+.It Fa module
+The name of the module which contains the
+.Ft static
+function you wish to call.
+.It Fa fn_name
+The name of the function.
+.It Fa fn
+A pointer to the function.
+.El
+.Sh DESCRIPTION
+The
+.Fn ktest_get_fn
+function provides the means for accessing private functions (those
+declared with the
+.Ft static
+modifier).
+This is needed when a test module wants to directly test a private
+function.
+.Pp
+In oreder to properly use
+.Fn ktest_get_fn
+there are four steps you must take.
+.Bl -enum
+.It
+Declare a local function pointer with the same type signature as the
+private function you wish to call.
+.It
+Call
+.Fn ktest_hold_mod
+to get a handle to the module containing the private function.
+.It
+Call
+.Fn ktest_get_fn
+to get a pointer to the private function.
+.It
+Make sure to call
+.Fn ktest_release_mod
+as part of test cleanup.
+.El
+.Pp
+First, use a
+.Sy typedef
+to declare a function pointer type with the same signature as the
+private function.
+For example, if the private function has a type signature as follows:
+.Bd -literal -offset 4m
+static uint64_t omnislash(uint64_t level);
+.Ed
+.Pp
+then you should declare a local
+.Sy typedef
+as follows:
+.Bd -literal -offset 4m
+typedef int (*omnislash_t)(uint64_t);
+.Ed
+.Pp
+Notice the use of the "_t" suffix for the typedef.
+With the
+.Sy typedef
+in place you can now easily declare a local function pointer as so:
+.Bd -literal -offset 4m
+omnislash_t omnislash = NULL;
+.Ed
+.Pp
+Next you must call
+.Fn ktest_hold_mod
+to get a handle to the module.
+This also places a hold on the module which acts as an additional
+safety that the function symbol will not go away while the hold
+is outstanding.
+.Pp
+At this point you can now use the module handle to call
+.Fn ktest_get_fn
+to resolve the symbol and fill in the function pointer with the
+correct address.
+Now you can call your local
+.Fn omnislash
+as if it was defined in the test module.
+.Pp
+Finally, you'll want to release the hold as part of test cleanup by
+calling the
+.Fn ktest_release_mod
+function.
+After its completion the function pointer should be considered
+invalid.
+.Pp
+One downside of this approach is that you must be vigilant to
+modifying type signatures of private functions accessed this way.
+The compiler cannot catch a discrepancy in the type signature because
+they are not compiled as a unit.
+It is up to you to make sure the type signature in your test matches
+that of the private function you are calling.
+.Sh RETURN VALUES
+Upon success the
+.Fn ktest_get_fn
+and
+.Fn ktest_hold_mod
+functions return 0.
+Otherwise, these functions return a non-zero value to indicate
+failure.
+The error values returned are not documented because the underlying
+.Xr ddi_modopen 9F
+does not document them.
+.Pp
+The
+.Fn ktest_hold_mod
+function returns the module handle via the
+.Fa hdl
+parameter.
+.Pp
+The
+.Fn ktest_get_fn
+function returns the function pointer via the
+.Fa fn
+parameter.
+.Sh EXAMPLES
+Here's an example of accessing the private function
+.Fn mac_sw_cksum_ipv4 .
+.Bd -literal
+/*
+ * We must declare a local typedef of the private function for
+ * the compiler to generate the call site correctly.
+ */
+typedef boolean_t (*mac_sw_cksum_ipv4_t)(mblk_t *, uint32_t, ipha_t *,
+    const char **);
+
+void
+private_fn_test(ktest_ctx_hdl_t *ctx)
+{
+	ddi_modhandle_t hdl = NULL;
+	mac_sw_cksum_ipv4_t mac_sw_cksum_ipv4 = NULL;
+
+	<... other test state ...>
+
+	/*
+	 * Get a handle to the module and place a hold on it.
+	 */
+	if (ktest_hold_mod("mac", &hdl) != 0) {
+		KT_ERROR(ctx, "failed to hold 'mac' module");
+		return;
+	}
+
+	/*
+	 * Use the module handle to get a pointer to the private
+	 * function.
+	 */
+	if (ktest_get_fn(hdl, "mac_sw_cksum_ipv4",
+	    (void **)&mac_sw_cksum_ipv4) != 0) {
+		KT_ERROR(ctx, "failed to resolve symbol "
+		    "mac`mac_sw_cksum_ipv4");
+		goto cleanup;
+	}
+
+	<... test logic ...>
+
+	KT_ASSERT0G(mac_sw_cksum_ipv4(...), ctx, cleanup);
+	KT_PASS(ctx);
+
+cleanup:
+	if (hdl != NULL) {
+		ktest_release_mod(hdl);
+	}
+}
+.Ed
+.Sh SEE ALSO
+.Xr ddi_modopen 9F
diff --git a/usr/src/man/man9f/ktest_get_input.9f b/usr/src/man/man9f/ktest_get_input.9f
new file mode 100644
index 0000000000..73ad799d3a
--- /dev/null
+++ b/usr/src/man/man9f/ktest_get_input.9f
@@ -0,0 +1,70 @@
+.\"
+.\" This file and its contents are supplied under the terms of the
+.\" Common Development and Distribution License ("CDDL"), version 1.0.
+.\" You may only use this file in accordance with the terms of version
+.\" 1.0 of the CDDL.
+.\"
+.\" A full copy of the text of the CDDL should have accompanied this
+.\" source.  A copy of the CDDL is also available via the Internet at
+.\" http://www.illumos.org/license/CDDL.
+.\"
+.\"
+.\" Copyright 2023 Oxide Computer Company
+.\"
+.Dd February 15, 2023
+.Dt KTEST_GET_INPUT 9F
+.Os
+.Sh NAME
+.Nm ktest_get_input
+.Nd get the input stream
+.Sh SYNOPSIS
+.In sys/ktest.h
+.Ft void
+.Fo ktest_get_input
+.Fa "const ktest_ctx_hdl_t *ctx"
+.Fa "uchar_t **bytes"
+.Fa "size_t *len"
+.Fc
+.Sh INTERFACE LEVEL
+.Sy Volatile -
+This interface is still evolving in illumos.
+API and ABI stability is not guaranteed.
+.Sh PARAMETERS
+.Bl -tag -width Fa
+.It Fa ctx
+A handle to the test context.
+This handle is passed as argument to the test function by the ktest facility.
+.It Fa bytes
+A return parameter used to provide the caller with a pointer to the
+bytes of the input stream.
+.It Fa len
+A return parameter used to provide the caller with the number of bytes
+in the input stream.
+.El
+.Sh DESCRIPTION
+The
+.Fn ktest_get_input
+function is used to get a handle to the input stream passed to the
+test along with its length.
+.Pp
+This function panics when called on a context with no input stream.
+.Sh EXAMPLES
+This examples shows a test that verifies all bytes in the input stream
+are lowercase ASCII.
+.Bd -literal
+void
+is_lower_ascii_test(ktest_ctx_hdl_t *ctx)
+{
+	uchar_t *bytes = NULL;
+	size_t len = 0;
+
+	ktest_get_input(ctx, &bytes, &len);
+
+	for (size_t i = 0; i < len; i++) {
+		KT_ASSERT3U(bytes[i], >=, 'a', ctx);
+		KT_ASSERT3U(bytes[i], <=, 'z', ctx);
+	}
+
+	KT_PASS(ctx);
+}
+.Ed
diff --git a/usr/src/man/man9f/ktest_result_pass.9f b/usr/src/man/man9f/ktest_result_pass.9f
new file mode 100644
index 0000000000..41dd7a77a3
--- /dev/null
+++ b/usr/src/man/man9f/ktest_result_pass.9f
@@ -0,0 +1,417 @@
+.\"
+.\" This file and its contents are supplied under the terms of the
+.\" Common Development and Distribution License ("CDDL"), version 1.0.
+.\" You may only use this file in accordance with the terms of version
+.\" 1.0 of the CDDL.
+.\"
+.\" A full copy of the text of the CDDL should have accompanied this
+.\" source.  A copy of the CDDL is also available via the Internet at
+.\" http://www.illumos.org/license/CDDL.
+.\"
+.\"
+.\" Copyright 2023 Oxide Computer Company
+.\"
+.Dd February 15, 2023
+.Dt KTEST_RESULT_PASS 9F
+.Os
+.Sh NAME
+.Nm KT_PASS ,
+.Nm KT_FAIL ,
+.Nm KT_ERROR ,
+.Nm KT_SKIP ,
+.Nm KT_ASSERT ,
+.Nm KT_ASSERT0 ,
+.Nm KT_ASSERT3S ,
+.Nm KT_ASSERT3U ,
+.Nm KT_ASSERT3P ,
+.Nm KT_ASSERTG ,
+.Nm KT_ASSERT0G ,
+.Nm KT_ASSERT3SG ,
+.Nm KT_ASSERT3UG ,
+.Nm KT_ASSERT3PG ,
+.Nm ktest_result_pass ,
+.Nm ktest_result_fail ,
+.Nm ktest_result_error ,
+.Nm ktest_result_skip ,
+.Nm ktest_msg_prepend ,
+.Nm ktest_msg_clear
+.Nd set test result, assert test conditions, add failure context
+.Sh SYNOPSIS
+.In sys/ktest.h
+.Ft void
+.Fo ktest_result_pass
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fa "int line"
+.Fc
+.Ft void
+.Fo ktest_result_fail
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fa "int line"
+.Fa "const char *msg"
+.Fa "..."
+.Fc
+.Ft void
+.Fo ktest_result_error
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fa "int line"
+.Fa "const char *msg"
+.Fa "..."
+.Fc
+.Ft void
+.Fo ktest_result_skip
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fa "int line"
+.Fa "const char *msg"
+.Fa "..."
+.Fc
+.Ft void
+.Fo ktest_msg_prepend
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fa "const char *msg"
+.Fa "..."
+.Fc
+.Ft void
+.Fo ktest_msg_clear
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fc
+.Ft void
+.Fo KT_PASS
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fc
+.Ft void
+.Fo KT_FAIL
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fa "const char *msg"
+.Fa "..."
+.Fc
+.Ft void
+.Fo KT_ERROR
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fa "const char *msg"
+.Fa "..."
+.Fc
+.Ft void
+.Fo KT_SKIP
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fa "const char *msg"
+.Fa "..."
+.Fc
+.Ft void
+.Fo KT_ASSERT
+.Fa "exp"
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fc
+.Ft void
+.Fo KT_ASSERT0
+.Fa "exp"
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fc
+.Ft void
+.Fo KT_ASSERT3S
+.Fa "int64_t left"
+.Fa "op"
+.Fa "int64_t right"
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fc
+.Ft void
+.Fo KT_ASSERT3U
+.Fa "uint64_t left"
+.Fa "op"
+.Fa "uint64_t right"
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fc
+.Ft void
+.Fo KT_ASSERT3P
+.Fa "uintptr_t left"
+.Fa "op"
+.Fa "uintptr_t right"
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fc
+.Ft void
+.Fo KT_ASSERTG
+.Fa "exp"
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fa "label"
+.Fc
+.Ft void
+.Fo KT_ASSERT0G
+.Fa "exp"
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fa "label"
+.Fc
+.Ft void
+.Fo KT_ASSERT3SG
+.Fa "int64_t left"
+.Fa "op"
+.Fa "int64_t right"
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fa "label"
+.Fc
+.Ft void
+.Fo KT_ASSERT3UG
+.Fa "uint64_t left"
+.Fa "op"
+.Fa "uint64_t right"
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fa "label"
+.Fc
+.Ft void
+.Fo KT_ASSERT3PG
+.Fa "uintptr_t left"
+.Fa "op"
+.Fa "uintptr_t right"
+.Fa "ktest_ctx_hdl_t *ctx"
+.Fa "label"
+.Fc
+.Sh INTERFACE LEVEL
+.Sy Volatile -
+This interface is still evolving in illumos.
+API and ABI stability is not guaranteed.
+.Sh PARAMETERS
+.Bl -tag -width Fa
+.It Fa ctx
+A handle to the test context.
+This handle is passed as argument to the test function by the ktest facility.
+.It Fa exp
+A test condition expression.
+.It Fa left
+The left side value of the test condition.
+This may be an expression.
+.It Fa right
+The right side value of the test condition.
+This may be an expression.
+.It Fa op
+The operator used to compare the
+.Fa left
+and
+.Fa right
+side values.
+.It Fa label
+The source code label to jump to if the test condition is false.
+.It Fa line
+The source line number where the result is set.
+This should always be the
+.Sy __LINE__
+macro.
+.It Fa msg
+A message giving additional context on why a test did not pass.
+.El
+.Sh DESCRIPTION
+These functions and macros are used to set the result of a test function.
+.Ss Result Macros
+These are convenience macros for setting the test result, providing an
+alternative to the verbose result functions.
+In general, you should only need to use the
+.Fn KT_PASS
+and
+.Fn KT_SKIP
+macros.
+For most test assertions it's more convenient to use the "KTest ASSERT
+Macros" described below.
+These macros do not cause a
+.Sy return .
+.Bl -tag -width 2m
+.It Fn KT_PASS
+Set a passing result.
+.It Fn KT_FAIL
+Set a failure result along with the failure message.
+.It Fn KT_ERROR
+Set an error result along with the error message.
+.It Fn KT_SKIP
+Set a skip result along with the skip message.
+.El
+.Ss KTest ASSERT Macros
+These macros evaluate their test condition expression and verify it's true.
+They take care of building a failure message based on the expression
+and calling the
+.Fn ktest_result_fail
+function with the appropriate line number.
+They provide a convenient way to express test conditions and
+automatically build failure messages on the caller's behalf.
+They are essentially the same as the traditional
+.Sy ASSERT3
+family of macros with three exceptions:
+.Bl -enum
+.It
+They all require the additional
+.Fa ctx
+argument in order to set the failure result when the assert trips.
+.It
+They do not panic but instead build a failure message, call
+.Fn ktest_result_fail ,
+and cause an immediate return of the test function.
+.It
+The "goto" variations of these macros provide the ability to cleanup
+test state instead of returning immediately.
+.El
+.Pp
+There are two variations of these macros.
+.Bl -tag -width 6m
+.It Sy KT_ASSERT*
+Essentially the same as the traditional
+.Sy ASSERT3
+family of macros, with the exception that they all take the
+.Fa ctx
+as an additional argument.
+This assert returns from the test function.
+.It Sy KT_ASSERT*G
+Assert the condition or
+.Sy goto
+.Fa label .
+.El
+.Ss KTest Error ASSERT Macros
+These macros are the same as the
+.Fn KT_ASSERT*
+macros with the only exception being that they call the
+.Fn ktest_result_error
+function to indicate an error condition.
+These macros use the same names as the
+.Fn KT_ASSERT*
+macros but prefixed with the character 'E', like so:
+.Fn KT_EASSERT* .
+This is a convenience for checking conditions which are indicative of
+a test error rather than failure.
+For example, for most tests a failure to acquire memory is considered
+an error, not a test failure.
+In that case one could use the following assert to raise a test error.
+.Bd -literal
+	mblk_t *mp = allocb(len, 0);
+	KT_EASSERT(mp != NULL, ctx);
+.Ed
+.Ss Additional Message Context
+Sometimes the failure message generated by the
+.Fn KT_ASSERT*
+macros is not enough.
+You may find the need to prepend additional information to the message
+to disambiguate the reason for failure.
+For example, you might find yourself asserting an invariant against an
+array of values and in order to disambiguate the failure you need to
+know the index of the value which tripped the assert.
+The
+.Fn ktest_msg_prepend
+function provides this ability.
+.Bl -tag -width 4m
+.It Nm ktest_msg_prepend
+Append the given format string to the failure message.
+This overwrites the prepended string of any previous call making it
+more convenient to use in a
+.Sy for
+loop.
+.It Nm ktest_msg_clear
+Clear the prepend buffer.
+This is equivalent to
+.Sy ktest_msg_prepend("") .
+.El
+.Ss Multiple Results
+Given the nature of ktest's design it is trivial to accidentally write
+a test that can produce multiple results for a given execution of its
+code.
+For example, placing the
+.Nm KT_PASS
+call in a cleanup label would overwrite a failure result with a pass
+result.
+This is the unavoidable nature of ktest's implementation: unlike
+typical testing frameworks we are executing in kernel context and it
+would be annoying if test failure was reported by way of host panic.
+.Pp
+To avoid incorrect test results the ktest facility itself checks for
+this scenario.
+For each call to the result API, ktest first checks if a result has
+already been set.
+If no result is present, then it stores the result along with its line
+number.
+However, if a result already exists, it generates an error result
+describing the line number of the overriding result along with the
+line number of the original result.
+.Sh EXAMPLES
+.Ss Test Without Cleanup
+This example shows the basic skeleton of a contrived test for a fictional
+.Ft object_t
+type.
+As there is no allocation or resource acquisition, there is no need
+for cleanup.
+.Bd -literal
+void
+no_cleanup_test(ktest_ctx_hdl_t *ctx)
+{
+	object_t obj;
+
+	obj.obj_value = 7777;
+	obj.obj_state = OBJ_STATE_FIRST;
+
+	if (!check_for_condition_x()) {
+		KT_SKIP(ctx, "condition X was not met");
+		return;
+	}
+
+	KT_ASSERT3U(obj.obj_state, ==, OBJ_STATE_FIRST, ctx);
+	next_state(&obj);
+	KT_ASSERT3U(obj.obj_state, ==, OBJ_STATE_SECOND, ctx);
+
+	<... more obj manipulation and assertions ...>
+
+	KT_PASS(ctx);
+}
+.Ed
+.Ss Test With Cleanup
+It's more likely that your test will require some amount of allocation and
+thus will need to make use of the
+.Fn KT_ASSERTG*
+macros.
+In this scenario
+.Fn KT_PASS
+must come before the
+.Sy cleanup
+label.
+Calling it after the
+.Sy cleanup
+label produces a multiple-result bug when one of the assertions trips.
+The ktest facility automatically catches this type of bug as explained
+in the "Multiple Results" section.
+.Bd -literal
+void
+test_with_cleanup(ktest_ctx_hdl_t *ctx)
+{
+	mblk_t *mp = allocb(74, 0);
+
+	/*
+	 * Failure to allocate is an error, not a test failure.
+	 */
+	KT_EASSERT(mp != NULL, ctx);
+
+	/*
+	 * If any of these assertions trips, a failure result is set
+	 * and execution jumps to the 'cleanup' label.
+	 */
+	KT_ASSERT3UG(msgsize(mp), ==, 0, ctx, cleanup);
+	KT_ASSERT3PG(mp->b_rptr, ==, mp->b_wptr, ctx, cleanup);
+	KT_ASSERT3PG(mp->b_next, ==, NULL, ctx, cleanup);
+	KT_ASSERT3PG(mp->b_cont, ==, NULL, ctx, cleanup);
+
+	/*
+	 * All assertions passed; mark the test a success and let
+	 * execution fall into the 'cleanup' label.
+	 */
+	KT_PASS(ctx);
+
+cleanup:
+	freeb(mp);
+}
+.Ed
+.Ss Additional Failure Context
+This example shows how to prepend additional context to the failure
+message.
+The
+.Fn ktest_msg_clear
+call after the loop is important; otherwise any subsequent assert failure
+would pick up the prepended message from the last iteration of the loop.
+.Bd -literal
+for (uint_t i = 0; i < num_objs; i++) {
+        obj_t *obj = &objs[i];
+
+        ktest_msg_prepend(ctx, "objs[%d]: ", i);
+        KT_ASSERT3U(obj->o_state, ==, EXPECTED_STATE, ctx);
+}
+
+ktest_msg_clear(ctx);
+.Ed
diff --git a/usr/src/pkg/manifests/driver-ktest.p5m b/usr/src/pkg/manifests/driver-ktest.p5m
new file mode 100644
index 0000000000..273def49b6
--- /dev/null
+++ b/usr/src/pkg/manifests/driver-ktest.p5m
@@ -0,0 +1,81 @@
+#
+# This file and its contents are supplied under the terms of the
+# Common Development and Distribution License ("CDDL"), version 1.0.
+# You may only use this file in accordance with the terms of version
+# 1.0 of the CDDL.
+#
+# A full copy of the text of the CDDL should have accompanied this
+# source.  A copy of the CDDL is also available via the Internet at
+# http://www.illumos.org/license/CDDL.
+#
+
+#
+# Copyright 2023 Oxide Computer Company
+# Copyright 2024 Ryan Zezeski
+#
+
+<include global_zone_only_component>
+set name=pkg.fmri value=pkg:/driver/ktest@$(PKGVERS)
+set name=pkg.summary value="Kernel Test Driver"
+set name=pkg.description value="Kernel Test Driver"
+set name=info.classification \
+    value=org.opensolaris.category.2008:Development/System
+set name=variant.arch value=i386
+dir  path=kernel group=sys
+dir  path=kernel/drv group=sys
+dir  path=kernel/drv/$(ARCH64) group=sys
+file path=kernel/drv/$(ARCH64)/ktest group=sys mode=0755
+file path=kernel/drv/ktest.conf group=sys
+dir  path=usr/bin
+file path=usr/bin/ktest mode=0555
+dir  path=usr/include
+dir  path=usr/include/sys
+file path=usr/include/sys/ktest.h mode=0644
+dir  path=usr/kernel/misc group=sys
+dir  path=usr/kernel/misc/ktest group=sys
+dir  path=usr/kernel/misc/ktest/$(ARCH64) group=sys
+file path=usr/kernel/misc/ktest/$(ARCH64)/ktest_selftest group=sys mode=0755
+file path=usr/kernel/misc/ktest/$(ARCH64)/mac_test group=sys mode=0755
+file path=usr/kernel/misc/ktest/$(ARCH64)/stream_test group=sys mode=0755
+dir  path=usr/lib/devfsadm group=sys
+dir  path=usr/lib/devfsadm/linkmod group=sys
+dir  path=usr/share/man
+dir  path=usr/share/man/man8
+file path=usr/share/man/man8/ktest.8
+dir  path=usr/share/man/man9
+file path=usr/share/man/man9/ktest.9
+dir  path=usr/share/man/man9f
+link path=usr/share/man/man9f/KT_ASSERT.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/KT_ASSERT0.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/KT_ASSERT0G.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/KT_ASSERT3P.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/KT_ASSERT3PG.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/KT_ASSERT3S.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/KT_ASSERT3SG.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/KT_ASSERT3U.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/KT_ASSERT3UG.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/KT_ASSERTG.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/KT_ERROR.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/KT_FAIL.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/KT_PASS.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/KT_SKIP.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/ktest_add_suite.9f target=ktest_create_module.9f
+link path=usr/share/man/man9f/ktest_add_test.9f target=ktest_create_module.9f
+file path=usr/share/man/man9f/ktest_create_module.9f
+link path=usr/share/man/man9f/ktest_free_module.9f target=ktest_create_module.9f
+file path=usr/share/man/man9f/ktest_get_fn.9f
+file path=usr/share/man/man9f/ktest_get_input.9f
+link path=usr/share/man/man9f/ktest_hold_mod.9f target=ktest_get_fn.9f
+link path=usr/share/man/man9f/ktest_msg_clear.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/ktest_msg_prepend.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/ktest_register_module.9f \
+    target=ktest_create_module.9f
+link path=usr/share/man/man9f/ktest_release_mod.9f target=ktest_get_fn.9f
+link path=usr/share/man/man9f/ktest_result_error.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/ktest_result_fail.9f target=ktest_result_pass.9f
+file path=usr/share/man/man9f/ktest_result_pass.9f
+link path=usr/share/man/man9f/ktest_result_skip.9f target=ktest_result_pass.9f
+link path=usr/share/man/man9f/ktest_unregister_module.9f \
+    target=ktest_create_module.9f
+driver name=ktest
+license lic_CDDL license=lic_CDDL
diff --git a/usr/src/uts/Makefile.targ b/usr/src/uts/Makefile.targ
index 896ae47415..6fdfdd930f 100644
--- a/usr/src/uts/Makefile.targ
+++ b/usr/src/uts/Makefile.targ
@@ -24,6 +24,7 @@
 # Copyright 2016 Hans Rosenfeld <rosenfeld@grumpf.hope-2000.org>
 # Copyright (c) 2017 by Delphix. All rights reserved.
 # Copyright 2019 Joyent, Inc.
+# Copyright 2021 Oxide Computer Company
 #
 #	This Makefiles contains the common targets and definitions for
 #	all kernels. It is to be included in the Makefiles for specific
@@ -176,6 +177,9 @@ $(USR_SYS_DIR)/%:	$(OBJS_DIR)/% $(USR_SYS_DIR) FRC
 $(USR_MISC_DIR)/%:	$(OBJS_DIR)/% $(USR_MISC_DIR) FRC
 	$(INS.file)
 
+$(USR_KTEST_DIR)/%:	$(OBJS_DIR)/% $(USR_KTEST_DIR) FRC
+	$(INS.file)
+
 $(USR_DACF_DIR)/%:	$(OBJS_DIR)/% $(USR_DACF_DIR) FRC
 	$(INS.file)
 
diff --git a/usr/src/uts/Makefile.uts b/usr/src/uts/Makefile.uts
index ffb6e0a376..88460db6bd 100644
--- a/usr/src/uts/Makefile.uts
+++ b/usr/src/uts/Makefile.uts
@@ -26,6 +26,7 @@
 # Copyright (c) 2013 Andrew Stormont.  All rights reserved.
 # Copyright 2016 Hans Rosenfeld <rosenfeld@grumpf.hope-2000.org>
 # Copyright 2019 Joyent, Inc.
+# Copyright 2021 Oxide Computer Company
 #
 
 #
@@ -484,6 +485,7 @@ USR_SOCK_DIR_32		= $(USR_MOD_DIR)/socketmod
 USR_STRMOD_DIR_32	= $(USR_MOD_DIR)/strmod
 USR_SYS_DIR_32		= $(USR_MOD_DIR)/sys
 USR_MISC_DIR_32		= $(USR_MOD_DIR)/misc
+USR_KTEST_DIR_32	= $(USR_MOD_DIR)/misc/ktest
 USR_DACF_DIR_32		= $(USR_MOD_DIR)/dacf
 USR_PCBE_DIR_32		= $(USR_MOD_DIR)/pcbe
 USR_DTRACE_DIR_32	= $(USR_MOD_DIR)/dtrace
@@ -497,6 +499,7 @@ USR_SOCK_DIR_64		= $(USR_MOD_DIR)/socketmod/$(SUBDIR64)
 USR_STRMOD_DIR_64	= $(USR_MOD_DIR)/strmod/$(SUBDIR64)
 USR_SYS_DIR_64		= $(USR_MOD_DIR)/sys/$(SUBDIR64)
 USR_MISC_DIR_64		= $(USR_MOD_DIR)/misc/$(SUBDIR64)
+USR_KTEST_DIR_64	= $(USR_MOD_DIR)/misc/ktest/$(SUBDIR64)
 USR_DACF_DIR_64		= $(USR_MOD_DIR)/dacf/$(SUBDIR64)
 USR_PCBE_DIR_64		= $(USR_MOD_DIR)/pcbe/$(SUBDIR64)
 USR_DTRACE_DIR_64	= $(USR_MOD_DIR)/dtrace/$(SUBDIR64)
@@ -510,6 +513,7 @@ USR_SOCK_DIR		= $(USR_SOCK_DIR_$(CLASS))
 USR_STRMOD_DIR		= $(USR_STRMOD_DIR_$(CLASS))
 USR_SYS_DIR		= $(USR_SYS_DIR_$(CLASS))
 USR_MISC_DIR		= $(USR_MISC_DIR_$(CLASS))
+USR_KTEST_DIR		= $(USR_KTEST_DIR_$(CLASS))
 USR_DACF_DIR		= $(USR_DACF_DIR_$(CLASS))
 USR_PCBE_DIR		= $(USR_PCBE_DIR_$(CLASS))
 USR_DTRACE_DIR		= $(USR_DTRACE_DIR_$(CLASS))
@@ -519,6 +523,7 @@ USR_MOD_DIRS_32		= $(USR_DRV_DIR_32) $(USR_EXEC_DIR_32)
 USR_MOD_DIRS_32		+= $(USR_FS_DIR_32) $(USR_SCHED_DIR_32)
 USR_MOD_DIRS_32		+= $(USR_STRMOD_DIR_32) $(USR_SYS_DIR_32)
 USR_MOD_DIRS_32		+= $(USR_MISC_DIR_32) $(USR_DACF_DIR_32)
+USR_MOD_DIRS_32		+= $(USR_KTEST_DIR_32)
 USR_MOD_DIRS_32		+= $(USR_PCBE_DIR_32)
 USR_MOD_DIRS_32		+= $(USR_DTRACE_DIR_32) $(USR_BRAND_DIR_32)
 USR_MOD_DIRS_32		+= $(USR_SOCK_DIR_32)
diff --git a/usr/src/uts/common/Makefile.files b/usr/src/uts/common/Makefile.files
index d80cb65e01..dd1b94fe6b 100644
--- a/usr/src/uts/common/Makefile.files
+++ b/usr/src/uts/common/Makefile.files
@@ -2347,3 +2347,15 @@ ENA_OBJS +=	ena.o ena_admin.o ena_dma.o ena_aenq.o ena_gld.o ena_hw.o \
 
 IGC_CORE_OBJS += igc_api.o igc_base.o igc_i225.o igc_mac.o igc_nvm.o igc_phy.o
 IGC_OBJS += $(IGC_CORE_OBJS) igc.o igc_osdep.o igc_gld.o igc_ring.o igc_stat.o
+
+#
+# The Kernel Test driver.
+#
+KTEST_OBJS += ktest.o
+
+#
+# Kernel test modules included with the system.
+#
+MAC_TEST_OBJS += mac_test.o
+KTEST_SELFTEST_OBJS += ktest_selftest.o
+STREAM_TEST_OBJS += stream_test.o
diff --git a/usr/src/uts/common/Makefile.rules b/usr/src/uts/common/Makefile.rules
index 920ae4bdff..9c48db4035 100644
--- a/usr/src/uts/common/Makefile.rules
+++ b/usr/src/uts/common/Makefile.rules
@@ -938,6 +938,10 @@ $(OBJS_DIR)/%.o:		$(UTSBASE)/common/io/ksocket/%.c
 	$(COMPILE.c) -o $@ $<
 	$(CTFCONVERT_O)
 
+$(OBJS_DIR)/%.o:		$(UTSBASE)/common/io/ktest/%.c
+	$(COMPILE.c) -o $@ $<
+	$(CTFCONVERT_O)
+
 $(OBJS_DIR)/%.o:		$(UTSBASE)/common/io/aggr/%.c
 	$(COMPILE.c) -o $@ $<
 	$(CTFCONVERT_O)
diff --git a/usr/src/uts/common/io/ktest/ktest.c b/usr/src/uts/common/io/ktest/ktest.c
new file mode 100644
index 0000000000..7e11cadba0
--- /dev/null
+++ b/usr/src/uts/common/io/ktest/ktest.c
@@ -0,0 +1,1384 @@
+/*
+ * This file and its contents are supplied under the terms of the
+ * Common Development and Distribution License ("CDDL"), version 1.0.
+ * You may only use this file in accordance with the terms of version
+ * 1.0 of the CDDL.
+ *
+ * A full copy of the text of the CDDL should have accompanied this
+ * source.  A copy of the CDDL is also available via the Internet at
+ * http://www.illumos.org/license/CDDL.
+ */
+
+/*
+ * Copyright 2023 Oxide Computer Company
+ * Copyright 2024 Ryan Zezeski
+ */
+
+/*
+ * The Kernel Test Facility
+ * ------------------------
+ *
+ * The kernel test facility, otherwise known as "ktest", provides a
+ * means for in situ kernel testing. It allows one to write kernel
+ * modules whose purpose is to test other kernel modules (or the
+ * kernel at large). While much can be tested from userland, there are
+ * some cases where there is no substitute for running the test code
+ * in kernel context, right next to the code it's testing. In many
+ * cases it's the only way to efficiently test specific execution
+ * paths, by avoiding the brittleness of action from afar which relies
+ * on subtle interactions between userland and kernel. For these
+ * cases, and many more, ktest gives you the best chance at directly
+ * testing kernel code (short of injecting DEBUG invariant checks
+ * inline with the code itself).
+ *
+ * The kernel test facility provides the following.
+ *
+ * - The ktest kernel module (this file), which acts as a central
+ *   location for all administration and execution of test modules.
+ *
+ * - The ktest(9) API, which provides the tools to write tests and
+ *   register them with the ktest module.
+ *
+ * - The ktest pseudo device, which presents a control surface to
+ *   userspace in the form of your typical ioctl interface.
+ *
+ * - A ktest(8) user command, which provides a user interface to the
+ *   ktest facility.
+ *
+ * Ktest Architecture
+ * ------------------
+ *
+ * ## The Test Triple
+ *
+ * Ktest provides a three-level namespace for organizing tests,
+ * referred to as the "test triple". It consists of the module name,
+ * suite name, and test name, written as '<module>:<suite>:<test>'.
+ *
+ * - Module: The top of the namespace, typically named after the
+ *   module-under-test (MUT). The convention is to append the '_test'
+ *   suffix to the module-under-test. For example, the 'mac' module
+ *   might have a 'mac_test' test module. However, there is no hard
+ *   rule that a test module must be named after its
+ *   module-under-test, it’s merely a suggestion. Such a convention is
+ *   a bit unwieldy for large modules like genunix. In those cases it
+ *   makes sense to break from the norm.
+ *
+ * - Suite: Each module consists of one or more suites. A suite groups
+ *   tests of related functionality. For example, you may have several
+ *   tests that verify checksum routines for which you might name the
+ *   suite 'checksum'.
+ *
+ * - Test: Each suite consists of one of more tests. The name of the
+ *   test can be any string which you find descriptive of the test. A
+ *   unit test for a single, small function will often use the name of
+ *   the function-under-test with a _test suffix added. But for
+ *   testing a series of function calls, or a larger function, it may
+ *   make sense to abandon this convention.
+ *
+ * A test triple can be fully or partially-qualified, depending on the
+ * context. A fully-qualified triple is one that names one test by
+ * specifying each level of the namespace and using no glob characters
+ * -- it’s unambiguous. A partially-qualified triple, on the other
+ * hand, can be ambiguous; it only names some of the namespace or
+ * makes use of glob characters.
+ *
+ * Fully qualified:
+ *
+ *   'mac:checksum:mac_sw_cksum_ipv4_tcp_test'
+ *
+ * Partially qualified
+ *
+ *   '*'
+ *   '*:*:*'
+ *   'mac:'
+ *   'mac:checksum'
+ *   'mac:*:mac_sw*'
+ *
+ * ## The Context Handle
+ *
+ * All communication between ktest and the individual test happens via
+ * the "context object". This object cannot be accessed directly.
+ * Instead, ktest provides a context handle to be accessed via its
+ * ktest(9) API. A test must conform to the ktest_fn_t prototype.
+ *
+ * ## Setting Test Results
+ *
+ * A test conveys its result using one of the result ktest(9) APIs. A
+ * result is typically pass or fail, but a test may also be skipped or
+ * may encounter an unforeseen error. See ktest_result_type_t for a
+ * description of the types of results. All test results should
+ * include the associated source line by way of the __LINE__ macro.
+ * The fail, error, and skip results should also include a message
+ * giving further context on the result.
+ *
+ * ktest_result_pass(ktest_ctx_hdl_t *, int)
+ *
+ *   The test ran as expected and all conditions were met. The result
+ *   value is set to KTEST_RESULT_PASS.
+ *
+ * ktest_result_fail(ktest_ctx_hdl_t *, int, const char *, ...)
+ *
+ *   One of the test conditions was violated. The test should use the
+ *   format string and arguments to create a message describing which
+ *   condition failed and why. The result value is set to KTEST_RESULT_FAIL.
+ *
+ * ktest_result_error(ktest_ctx_hdl_t *, int, const char *, ...)
+ *
+ *   The test encountered an unexpected error, one that is not
+ *   directly related to the logic under test. For example, failure to
+ *   acquire memory is often outside of the test parameters for most
+ *   tests. These types of errors are often encountered when
+ *   interacting with the kernel at large and when acquiring resources
+ *   for test setup. Perhaps most importantly, it indicates the lack
+ *   of a pass/fail determination for this test. The result value is
+ *   set to KTEST_RESULT_ERROR.
+ *
+ * ktest_result_skip(ktest_ctx_hdl_t *, int, const char *, ...)
+ *
+ *   The test lacks the required context to execute, typically for
+ *   lack of resources or specific hardware under test. Like the error
+ *   result, this lacks a pass/fail determination. The result value is
+ *   set to KTEST_RESULT_SKIP.
+ *
+ * ## Result Macros
+ *
+ * Using the API above is cumbersome, requiring the repetitive use of
+ * the __LINE__ macro. The following macros are provided for ease of
+ * use.
+ *
+ *     - KT_PASS(ktest_ctx_hdl_t *ctx)
+ *     - KT_FAIL(ktest_ctx_hdl_t *ctx, char *msg, ...)
+ *     - KT_ERROR(ktest_ctx_hdl_t *ctx, char *msg, ...)
+ *     - KT_SKIP(ktest_ctx_hdl_t *ctx, char *msg, ...)
+ *
+ * ## KTest ASSERT Macros
+ *
+ * Even with the help of the result macros, writing test assertions
+ * requires quite a bit of verbosity and boilerplate; requiring an if
+ * statement, a KT_* call, and the failure message arguments. The
+ * KTest ASSERT macros provide an ASSERT3-like family of macros to
+ * reduce the boilerplate and make test writing feel more natural.
+ * However, they are different from the ASSERT3 family in two major
+ * ways.
+ *
+ * 1. They don't panic. The point is to report test failure, not
+ *    preserve system state leading up to an invalid condition.
+ *    However, for particularly difficult-to-debug test failures you
+ *    could use DTrace to invoke a panic upon entry to
+ *    ktest_result_error.
+ *
+ * 2. Following from (1), there may be test state to cleanup such as
+ *    freeing memory or other resources. This cleanup needs to happen
+ *    as a consequence of the assertion triggering, before returning
+ *    from the test function.
+ *
+ * There are three types of KT_ASSERT macros: KTest ASSERT, KTest
+ * ASSERT Goto, and KTest ASSERT Block. The first type of assert is
+ * the closest match to the standard ASSERT macros: they provide no
+ * state cleanup, but require the context handle is passed as final
+ * argument. The goto versions allow for cleanup via a jump to a
+ * label. The block versions allow for cleanup via an attached block,
+ * much like an if statement, but requires an additional
+ * KT_ASSERTB_END to indicate the end of the block. What follows is a
+ * list of the various KT_ASSERT macros and their arguments. For each
+ * macro listed below, there is a corresponding KTEST_EASSERT macro.
+ * These later macros set a KTEST_ERROR result when tripped.
+ *
+ * KTest ASSERT (no cleanup)
+ *
+ *   KTEST_ASSERT3S(left, op, right, ctx)
+ *   KTEST_ASSERT3U(left, op, right, ctx)
+ *   KTEST_ASSERT3P(left, op, right, ctx)
+ *   KTEST_ASSERT(exp, ctx)
+ *   KTEST_ASSERT0(exp, ctx)
+ *
+ * KTest ASSERT Goto (cleanup via label)
+ *
+ *   KT_ASSERT3SG(left, op, right, ctx, label)
+ *   KT_ASSERT3UG(left, op, right, ctx, label)
+ *   KT_ASSERT3PG(left, op, right, ctx, label)
+ *   KT_ASSERTG(exp, ctx, label)
+ *   KT_ASSERT0G(exp, ctx, label)
+ *
+ * KTest ASSERT Block (cleanup via block)
+ *
+ *   KT_ASSERT*B(left, op, right, ctx) {
+ *      <... cleanup goes here ...>
+ *   }
+ *   KT_ASSERTB_END
+ *
+ * ## Additional Failure/Error Context
+ *
+ * Sometimes the failure message generated by the KT_ASSERT macro is
+ * not enough. You might want to prepend some information to the
+ * message to provide additional context about the failure. This would
+ * require using the ktest result API manually, which defeats the
+ * purpose of the KT_ASSERT macros. Instead, ktest offers the
+ * ktest_msg_{prepend,clear}(9F) API; allowing you to prepend
+ * additional context to the failure message (if the assertion should
+ * trip) while still using the KT_ASSERT macros.
+ *
+ * For example, if you were asserting an invariant on an array of
+ * objects, and you wanted the failure message to include the index of
+ * the object which tripped the assert, you could write something like
+ * the following.
+ *
+ * ----
+ * for (int i = 0; i < num_objs; i++) {
+ *         obj_t *obj = &objs[i];
+ *
+ *         ktest_msg_prepend(ctx, "objs[%d]: ", i);
+ *         KT_ASSERT3P(obj->o_state, !=, NULL, ctx);
+ * }
+ *
+ * ktest_msg_clear(ctx);
+ * ----
+ *
+ * The ktest_msg_prepend() call is not additive; it always overwrites
+ * the contents of the prepend buffer.
+ *
+ * ## Test Input
+ *
+ * A test has the option to require input. The input is always in the
+ * form of a byte stream. The interpretation of those bytes is left to
+ * the test; the ktest facility at large treats the input stream as
+ * opaque. It is legal to have an input stream of zero bytes. The test
+ * retrieves its byte stream with the ktest_get_input(9F) API.
+ *
+ * ## Testing Private Functions
+ *
+ * A test module often needs to test static (private) functions.
+ * However, as the test module and module-under-test are two different
+ * modules, and a static function's linkage is local, there is no way
+ * to easily access them. Ktest works around this by offering a set of
+ * APIs to dynamically load the the function object into the test module.
+ *
+ *   ktest_hold_mod(9F)
+ *   ktest_get_fn(9F)
+ *   ktest_release_mod(9F)
+ *
+ * The test modules must perform four steps when accessing a static
+ * function.
+ *
+ *   1. Recreate the function prototype, typically in the form of a
+ *      typedef. This is then used to declare the function pointer to
+ *      the static function.
+ *
+ *   2. Get a handle to the module-under-test via ktest_hold_mod(9F).
+ *
+ *   3. Fill in the function pointer with ktest_get_fn(9F), after
+ *      which it can be called just as it would in the
+ *      module-under-test.
+ *
+ *   4. At completion of the test release the module handle via
+ *      ktest_release_mod(9F).
+ *
+ * ## Registering Tests
+ *
+ * For a test to be run it first needs to be registered with the ktest
+ * facility. This is done via the ktest(9) APIs described below. The
+ * test module should be a 'modlmisc' module and perform all test
+ * registration/unregistration in its '_init' and '_fini' callbacks.
+ * Internally, ktest tracks all registered tests via the ktest_modules
+ * list.
+ *
+ * ktest_create_module(9F)
+ * ktest_add_test(9F)
+ * ktest_add_suite(9F)
+ * ktest_register_module(9F)
+ * ktest_unregister_module(9F)
+ *
+ * The creation and registration of tests is typically done in the
+ * following order.
+ *
+ *   1. Create a new module with ktest_create_module(9F).
+ *
+ *   2. Add a new suite with ktest_add_suite(9F).
+ *
+ *   3. Add one or more tests to the suite with ktest_add_test(9F).
+ *
+ *   4. Go back to step (2) if more suites are needed.
+ *
+ *   5. Register the module with ktest_register_module(9F).
+ *
+ * For unregistering your test module it's a simple matter of calling
+ * ktest_unregister_module(9F).
+ *
+ * The ktest_add_test(9F) API does provide a flags argument for
+ * providing additional information about the test, see
+ * ktest_test_flags_t for more information.
+ */
+#include <sys/stddef.h>
+#include <sys/conf.h>
+#include <sys/file.h>
+#include <sys/stat.h>
+#include <sys/modctl.h>
+#include <sys/ktest_impl.h>
+#include <sys/ddi.h>
+#include <sys/sunddi.h>
+
+#define	KTEST_CTL_MINOR	0
+
+dev_info_t	*ktest_dip;
+kmutex_t	ktest_lock;
+
+/*
+ * The global list of registered ktest modules. A module must call
+ * ktest_register_module() to register itself with the ktest framework.
+ *
+ * Protected by ktest_lock.
+ *
+ * List modules in MDB
+ * -------------------
+ *
+ * > ktest_modules::walk list |::print ktest_module_t
+ */
+list_t ktest_modules;
+
+/*
+ * Determine if the name is valid. This is probably overly
+ * restrictive, but it's easier to add additional characters later
+ * than to remove them. We want to avoid:
+ *
+ * - KTEST_SEPARATOR and KTEST_GMATCH_CHARS, as it causes ambiguity.
+ *
+ * - Characters that make it harder to use the ktest command in an
+ *   interactive shell, such as whitespace and special characters like '&'.
+ */
+static int
+ktest_valid_name(const char *name)
+{
+	size_t len = strnlen(name, KTEST_MAX_NAME_LEN);
+
+	if (len >= KTEST_MAX_NAME_LEN) {
+		return (EOVERFLOW);
+	}
+
+	for (uint_t i = 0; i < len; i++) {
+		char c = name[i];
+		boolean_t good_char = c == '.' || c == '_' ||
+		    (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
+		    (c >= '0' && c <= '9');
+
+		if (!good_char) {
+			return (EINVAL);
+		}
+	}
+
+	return (0);
+}
+
+static ktest_module_t *
+ktest_find_module(const char *module)
+{
+	ktest_module_t *km = NULL;
+
+	VERIFY(MUTEX_HELD(&ktest_lock));
+
+	for (km = list_head(&ktest_modules); km != NULL;
+	    km = list_next(&ktest_modules, km)) {
+		if (strncmp(km->km_name, module, KTEST_MAX_NAME_LEN) == 0) {
+			return (km);
+		}
+	}
+
+	return (NULL);
+}
+
+static ktest_suite_t *
+ktest_find_suite(ktest_module_t *km, const char *suite)
+{
+	ktest_suite_t *ks = NULL;
+
+	for (ks = list_head(&km->km_suites); ks != NULL;
+	    ks = list_next(&km->km_suites, ks)) {
+		if (strncmp(ks->ks_name, suite, KTEST_MAX_NAME_LEN) == 0) {
+			return (ks);
+		}
+	}
+
+	return (NULL);
+}
+
+static ktest_test_t *
+ktest_find_test(ktest_suite_t *ks, const char *test)
+{
+	ktest_test_t *kt = NULL;
+
+	for (kt = list_head(&ks->ks_tests); kt != NULL;
+	    kt = list_next(&ks->ks_tests, kt)) {
+		if (strncmp(kt->kt_name, test, KTEST_MAX_NAME_LEN) == 0) {
+			return (kt);
+		}
+	}
+
+	return (NULL);
+}
+
+/*
+ * Return a pointer to the test that matches the fully-qualified
+ * triple. Return NULL if no match is found.
+ */
+static ktest_test_t *
+ktest_get_test(const char *module, const char *suite, const char *test)
+{
+	ktest_module_t *km = NULL;
+	ktest_suite_t *ks = NULL;
+
+	VERIFY(module != NULL);
+	VERIFY(suite != NULL);
+	VERIFY(test != NULL);
+	VERIFY(MUTEX_HELD(&ktest_lock));
+
+	if ((km = ktest_find_module(module)) == NULL) {
+		return (NULL);
+	}
+
+	if ((ks = ktest_find_suite(km, suite)) == NULL) {
+		return (NULL);
+	}
+
+	return (ktest_find_test(ks, test));
+}
+
+/*
+ * Create a new test module object named 'name'. The test module name
+ * may be the same as the module-under-test, but this isn't required.
+ *
+ * Zero indicates success and a handle to the module object is
+ * returned via 'km_hdl'.
+ *
+ * See ktest_create_module(9F).
+ */
+int
+ktest_create_module(const char *name, ktest_module_hdl_t **km_hdl)
+{
+	int ret = 0;
+	ktest_module_t *km = NULL;
+
+	if ((ret = ktest_valid_name(name)) != 0) {
+		return (ret);
+	}
+
+	if ((km = kmem_zalloc(sizeof (*km), KM_NOSLEEP)) == NULL) {
+		return (ENOMEM);
+	}
+
+	list_create(&km->km_suites, sizeof (ktest_suite_t),
+	    offsetof(ktest_suite_t, ks_node));
+	/* The length was already checked by ktest_valid_name(). */
+	(void) strlcpy(km->km_name, name, sizeof (km->km_name));
+	*km_hdl = (ktest_module_hdl_t *)km;
+	return (0);
+}
+
+/*
+ * Create a new suite object named 'name' and add it to the module.
+ *
+ * Zero indicates success and a handle to the suite object is returned
+ * via 'ks_hdl'.
+ *
+ * See ktest_add_suite(9F).
+ */
+int
+ktest_add_suite(ktest_module_hdl_t *km_hdl, const char *name,
+    ktest_suite_hdl_t **ks_hdl)
+{
+	int ret = 0;
+	ktest_module_t *km = (ktest_module_t *)km_hdl;
+	ktest_suite_t *ks = NULL;
+
+	if ((ret = ktest_valid_name(name)) != 0) {
+		return (ret);
+	}
+
+	if (ktest_find_suite(km, name) != NULL) {
+		return (EEXIST);
+	}
+
+	if ((ks = kmem_zalloc(sizeof (*ks), KM_NOSLEEP)) == NULL) {
+		return (ENOMEM);
+	}
+
+	list_create(&ks->ks_tests, sizeof (ktest_test_t),
+	    offsetof(ktest_test_t, kt_node));
+	/* The length was already checked by ktest_valid_name(). */
+	(void) strlcpy(ks->ks_name, name, sizeof (ks->ks_name));
+	ks->ks_module = km;
+	list_insert_tail(&km->km_suites, ks);
+	km->km_num_suites++;
+	km->km_num_tests += ks->ks_num_tests;
+	*ks_hdl = (ktest_suite_hdl_t *)ks;
+	return (0);
+}
+
+static int
+ktest_create_test(ktest_test_t **test_out, ktest_suite_t *ks, const char *name,
+    ktest_fn_t fn, ktest_test_flags_t flags)
+{
+	int ret = 0;
+	ktest_test_t *kt = NULL;
+	boolean_t requires_input = B_FALSE;
+
+	if ((ret = ktest_valid_name(name)) != 0) {
+		return (ret);
+	}
+
+	if ((kt = kmem_zalloc(sizeof (*kt), KM_NOSLEEP)) == NULL) {
+		return (ENOMEM);
+	}
+
+	if ((flags & KTEST_FLAG_INPUT) != 0) {
+		requires_input = B_TRUE;
+	}
+
+	/* The length was already checked by ktest_valid_name(). */
+	(void) strlcpy(kt->kt_name, name, sizeof (kt->kt_name));
+	kt->kt_fn = fn;
+	kt->kt_suite = ks;
+	kt->kt_requires_input = requires_input;
+	*test_out = kt;
+	return (0);
+}
+
+/*
+ * Add a test function to the suite specified by 'ks_hdl'. The test is
+ * registered under the 'name' pseudonym and refers to the 'fn'
+ * function. While the name is often the same as the function symbol,
+ * this is merely a convention and not enforced. The 'flags' argument
+ * may specify additional information about the function -- see the
+ * ktest_test_flags_t definition.
+ *
+ * This function creates a new test object on the caller's behalf and
+ * registers it with the specified suite. Zero indicates success.
+ *
+ * See ktest_add_test(9F).
+ */
+int
+ktest_add_test(ktest_suite_hdl_t *ks_hdl, const char *name, ktest_fn_t fn,
+    ktest_test_flags_t flags)
+{
+	ktest_suite_t *ks = (ktest_suite_t *)ks_hdl;
+	ktest_test_t *test;
+	int ret;
+
+	if (ktest_find_test(ks, name) != NULL) {
+		return (EEXIST);
+	}
+
+	if ((ret = ktest_create_test(&test, ks, name, fn, flags)) != 0) {
+		return (ret);
+	}
+
+	list_insert_tail(&ks->ks_tests, test);
+	ks->ks_num_tests++;
+	return (0);
+}
+
+/*
+ * Register the test module specified by 'km_hdl' with the ktest
+ * facility.
+ *
+ * See ktest_register_module(9F).
+ */
+int
+ktest_register_module(ktest_module_hdl_t *km_hdl)
+{
+	ktest_module_t *km = (ktest_module_t *)km_hdl;
+
+	mutex_enter(&ktest_lock);
+
+	if (ktest_find_module(km->km_name) != NULL) {
+		mutex_exit(&ktest_lock);
+		cmn_err(CE_NOTE, "test module already exists: %s", km->km_name);
+		return (EEXIST);
+	}
+
+	list_insert_tail(&ktest_modules, km);
+	mutex_exit(&ktest_lock);
+	return (0);
+}
+
+static void
+ktest_free_test(ktest_test_t *test)
+{
+	kmem_free(test, sizeof (*test));
+}
+
+static void
+ktest_free_suite(ktest_suite_t *ks)
+{
+	ktest_test_t *kt = NULL;
+
+	while ((kt = list_remove_head(&ks->ks_tests)) != NULL) {
+		ktest_free_test(kt);
+	}
+
+	list_destroy(&ks->ks_tests);
+	kmem_free(ks, sizeof (*ks));
+}
+
+void
+ktest_free_module(ktest_module_hdl_t *km_hdl)
+{
+	ktest_module_t *km = (ktest_module_t *)km_hdl;
+	ktest_suite_t *ks = NULL;
+
+	while ((ks = list_remove_head(&km->km_suites)) != NULL) {
+		ktest_free_suite(ks);
+	}
+
+	list_destroy(&km->km_suites);
+	kmem_free(km, sizeof (*km));
+}
+
+/*
+ * Unregister the test module named by 'name'. This walks all suites
+ * and tests registered under this module, removing them and freeing
+ * their resources.
+ *
+ * See ktest_unregister_module(9F).
+ */
+void
+ktest_unregister_module(const char *name)
+{
+	mutex_enter(&ktest_lock);
+
+	for (ktest_module_t *km = list_head(&ktest_modules);
+	    km != NULL;
+	    km = list_next(&ktest_modules, km)) {
+		if (strncmp(name, km->km_name, KTEST_MAX_NAME_LEN) == 0) {
+			list_remove(&ktest_modules, km);
+			ktest_free_module((ktest_module_hdl_t *)km);
+			break;
+		}
+	}
+
+	mutex_exit(&ktest_lock);
+}
+
+static void
+ktest_unregister_all()
+{
+	ktest_module_t *km;
+	mutex_enter(&ktest_lock);
+
+	while ((km = list_remove_head(&ktest_modules)) != NULL) {
+		ktest_free_module((ktest_module_hdl_t *)km);
+	}
+
+	mutex_exit(&ktest_lock);
+}
+
+/*
+ * Get a function pointer to the function with symbol 'fn_name'. This
+ * function must be a symbol in the module referenced by 'hdl',
+ * otherwise an error is returned. It's up to the caller to make sure
+ * that the 'fn' pointer is declared correctly.
+ *
+ * Zero indicates success.
+ *
+ * See ktest_get_fn(9F).
+ */
+int
+ktest_get_fn(ddi_modhandle_t hdl, const char *fn_name, void **fn)
+{
+	int err;
+
+	if ((*fn = ddi_modsym(hdl, fn_name, &err)) == NULL) {
+		return (err);
+	}
+
+	return (0);
+}
+
+/*
+ * Get the input stream from the context handle. The contract for this
+ * API guarantees that if it is called, then there MUST be an input
+ * stream. It does this by VERIFYing that a) the test's
+ * 'kt_requires_input' flag is set, and b) that the 'ktc_input' is
+ * non-NULL. This means that failure to set an input stream on a test
+ * which requires it will result in a kernel panic. That may seem
+ * extreme, however, consider that this is meant to be discovered
+ * during development, and that the ktest cmd also takes steps to
+ * ensure that any test which requires input has an input stream
+ * specified. The impetus for this contract is to avoid checking for
+ * valid input in every test -- it allows the test to assume the input
+ * is there and categorically catch any case where it is not.
+ *
+ * This contract does not preclude the possibility of a 0-byte stream,
+ * which may be a valid test case for some tests. It only precludes a
+ * non-existent stream.
+ *
+ * See ktest_get_input(9F).
+ */
+void
+ktest_get_input(const ktest_ctx_hdl_t *hdl, uchar_t **input, size_t *len)
+{
+	ktest_ctx_t *ctx = (ktest_ctx_t *)hdl;
+	VERIFY(ctx->ktc_test->kt_requires_input == B_TRUE);
+	VERIFY3P(ctx->ktc_input, !=, NULL);
+	*len = ctx->ktc_input_len;
+	*input = ctx->ktc_input;
+}
+
+/*
+ * Grab a hold on 'module' and return it in 'hdl'. Meant to be used
+ * with ktest_get_fn(). Zero indicates success.
+ *
+ * Remember, 'ddi_modhandle_t' is a pointer, so 'hdl' is pointer to
+ * pointer.
+ *
+ * See ktest_hold_mod(9F).
+ */
+int
+ktest_hold_mod(const char *module, ddi_modhandle_t *hdl)
+{
+	int err;
+
+	if ((*hdl = ddi_modopen(module, KRTLD_MODE_FIRST, &err)) == NULL) {
+		return (err);
+	}
+
+	return (0);
+}
+
+/*
+ * The opposite of ktest_hold_mod().
+ *
+ * See ktest_release_mod(9F).
+ */
+void
+ktest_release_mod(ddi_modhandle_t hdl)
+{
+	(void) ddi_modclose(hdl);
+}
+
+/*
+ * Check if the result is already set. Setting the result more than
+ * once is a bug in the test. This check catches the bug and produces
+ * an error result with a message indicating the line number of the
+ * original result which was overwritten. It replaces 'ktc_res_line'
+ * with the line number of the overwriting result.
+ *
+ * Return true when an existing result was found.
+ */
+static boolean_t
+ktest_result_check(ktest_ctx_t *ctx, int line)
+{
+	if (ctx->ktc_res->kr_type != KTEST_RESULT_NONE) {
+		char *msg = ctx->ktc_res->kr_msg;
+		int first_line = ctx->ktc_res->kr_line;
+
+		ctx->ktc_res->kr_type = KTEST_RESULT_ERROR;
+		ctx->ktc_res->kr_line = line;
+
+		/* We know the string is within max length. */
+		(void) snprintf(msg, KTEST_MAX_LOG_LEN, "multiple results: "
+		    "prev result at line %d", first_line);
+
+		return (B_TRUE);
+	}
+
+	return (B_FALSE);
+}
+
+/*
+ * Set result if and only if one has not already been set. Return true
+ * if the result was set. Return false if it was already set.
+ */
+static boolean_t
+ktest_set_result(ktest_ctx_hdl_t *hdl, ktest_result_type_t rt, int line)
+{
+	ktest_ctx_t *ctx = (ktest_ctx_t *)hdl;
+
+	/* Overwriting a previous result is not allowed. */
+	if (ktest_result_check(ctx, line)) {
+		return (B_FALSE);
+	}
+
+	ctx->ktc_res->kr_type = rt;
+	ctx->ktc_res->kr_line = line;
+	return (B_TRUE);
+}
+
+static void
+ktest_set_msg(ktest_ctx_hdl_t *hdl, const char *format, va_list args)
+{
+	ktest_ctx_t *ctx = (ktest_ctx_t *)hdl;
+	char *msg = ctx->ktc_res->kr_msg;
+	size_t written = 0;
+
+	written = vsnprintf(msg, KTEST_MAX_LOG_LEN, format, args);
+
+	/* Subtract one to account for the implicit NULL byte. */
+	if (written > (KTEST_MAX_LOG_LEN - 1)) {
+		const ktest_test_t *test = ctx->ktc_test;
+		ktest_suite_t *suite = test->kt_suite;
+		ktest_module_t *mod = suite->ks_module;
+
+		cmn_err(CE_NOTE, "result message truncated: %s:%s:%s [%d]",
+		    mod->km_name, suite->ks_name, test->kt_name,
+		    ctx->ktc_res->kr_line);
+	}
+}
+
+void
+ktest_result_skip(ktest_ctx_hdl_t *hdl, int line, const char *format, ...)
+{
+	if (ktest_set_result(hdl, KTEST_RESULT_SKIP, line)) {
+		va_list adx;
+
+		va_start(adx, format);
+		ktest_set_msg(hdl, format, adx);
+		va_end(adx);
+	}
+}
+
+void
+ktest_result_fail(ktest_ctx_hdl_t *hdl, int line, const char *format, ...)
+{
+	if (ktest_set_result(hdl, KTEST_RESULT_FAIL, line)) {
+		va_list adx;
+
+		va_start(adx, format);
+		ktest_set_msg(hdl, format, adx);
+		va_end(adx);
+	}
+}
+
+void
+ktest_result_error(ktest_ctx_hdl_t *hdl, int line, const char *format, ...)
+{
+	if (ktest_set_result(hdl, KTEST_RESULT_ERROR, line)) {
+		va_list adx;
+
+		va_start(adx, format);
+		ktest_set_msg(hdl, format, adx);
+		va_end(adx);
+	}
+}
+
+void
+ktest_result_pass(ktest_ctx_hdl_t *hdl, int line)
+{
+	(void) ktest_set_result(hdl, KTEST_RESULT_PASS, line);
+}
+
+/*
+ * Clear the prepend message, undoing any message set by ktest_msg_prepend().
+ *
+ * See ktest_msg_clear(9F).
+ */
+void
+ktest_msg_clear(ktest_ctx_hdl_t *hdl)
+{
+	ktest_ctx_t *ctx = (ktest_ctx_t *)hdl;
+	ctx->ktc_res->kr_msg_prepend[0] = '\0';
+}
+
+/*
+ * Prepend formatted text to the result message. This is useful in
+ * cases where the KT_ASSERT macro's generated message doesn't convey
+ * enough context to determine the precise cause of the failure. By
+ * prepending the formatted text you can add additional context while
+ * still using the KT_ASSERT macros (and not having to reimplement
+ * them yourself). This overwrites any existing prepend text.
+ *
+ * See ktest_msg_prepend(9F).
+ */
+void
+ktest_msg_prepend(ktest_ctx_hdl_t *hdl, const char *format, ...)
+{
+	ktest_ctx_t *ctx = (ktest_ctx_t *)hdl;
+	char *msg = ctx->ktc_res->kr_msg_prepend;
+	size_t written;
+	va_list adx;
+
+	va_start(adx, format);
+	written = vsnprintf(msg, KTEST_MAX_LOG_LEN, format, adx);
+	/* Subtract one to account for the implicit NULL byte. */
+	if (written > (KTEST_MAX_LOG_LEN - 1)) {
+		const ktest_test_t *test = ctx->ktc_test;
+		ktest_suite_t *suite = test->kt_suite;
+		ktest_module_t *mod = suite->ks_module;
+
+		cmn_err(CE_NOTE, "prepend message truncated: %s:%s:%s",
+		    mod->km_name, suite->ks_name, test->kt_name);
+	}
+	va_end(adx);
+}
+
+/*
+ * Each `{:}` represents an nvpair, each `[,]` represents an nvlist.
+ *
+ * Test nvlist
+ * -----------
+ *
+ * [{"name":"<test_name>"},
+ *  {"input_required":boolean_t}]
+ *
+ * Tests nvlist
+ * ------------
+ *
+ * [{"test1":<test1_nvlist>},
+ *  {"test2":<test2_nvlist>"},
+ *  ...]
+ *
+ * Suite nvlist
+ * ------------
+ *
+ * [{"name":"<ks->ks_name>"},
+ *  {"tests":<tests_nvlist>}]
+ *
+ * Suites nvlist
+ * -------------
+ *
+ * [{"suite1":<suite1_nvlist>},
+ *  {"suite2":<suite2_nvlist>},
+ *  ...]
+ *
+ * Module nvlist
+ * -------------
+ *
+ * [{"name":"<km->km_name>"},
+ *  {"suites":<suites_nvlist>}]
+ *
+ * Modules nvlist
+ * --------------
+ *
+ * [{"ser_fmt_version":1},
+ *  {"module1":<module1_nvlist>},
+ *  {"module2":<module2_nvlist>},
+ *  ...]
+ *
+ */
+int
+ktest_list_tests(ktest_list_op_t *klo, int mode)
+{
+	nvlist_t *modules = fnvlist_alloc();
+	char *buf = NULL;
+	size_t len = 0;
+	int ret = 0;
+
+	/*
+	 * The first thing we add is a uint64_t ser_fmt_version field.
+	 * This field allows any consumer of this nvlist (namely the
+	 * ktest cmd) to know which serialization format it is in.
+	 * Specifically, the format version tells the consumer which
+	 * fields to expect and how they are laid out. Given that the
+	 * ktest kernel facility and its user command are delivered in
+	 * gate, this should never be needed. However, including a
+	 * versioned format now keeps the future flexible, and costs
+	 * us little.
+	 */
+	fnvlist_add_uint64(modules, "ser_fmt_version", KTEST_SER_FMT_VSN);
+	mutex_enter(&ktest_lock);
+
+	for (ktest_module_t *km = list_head(&ktest_modules);
+	    km != NULL;
+	    km = list_next(&ktest_modules, km)) {
+		nvlist_t *module = fnvlist_alloc();
+		nvlist_t *suites = fnvlist_alloc();
+
+		for (ktest_suite_t *ks = list_head(&km->km_suites);
+		    ks != NULL;
+		    ks = list_next(&km->km_suites, ks)) {
+			nvlist_t *suite = fnvlist_alloc();
+			nvlist_t *tests = fnvlist_alloc();
+
+			for (ktest_test_t *kt = list_head(&ks->ks_tests);
+			    kt != NULL;
+			    kt = list_next(&ks->ks_tests, kt)) {
+				nvlist_t *test = fnvlist_alloc();
+
+				fnvlist_add_string(test, KTEST_NAME_KEY,
+				    kt->kt_name);
+				fnvlist_add_boolean_value(test,
+				    KTEST_TEST_INPUT_KEY,
+				    kt->kt_requires_input);
+				fnvlist_add_nvlist(tests, kt->kt_name, test);
+				nvlist_free(test);
+			}
+
+			if (nvlist_empty(tests)) {
+				nvlist_free(tests);
+				nvlist_free(suite);
+				continue;
+			}
+
+			fnvlist_add_string(suite, KTEST_NAME_KEY, ks->ks_name);
+			fnvlist_add_nvlist(suite, KTEST_SUITE_TESTS_KEY, tests);
+			fnvlist_add_nvlist(suites, ks->ks_name, suite);
+			nvlist_free(tests);
+			nvlist_free(suite);
+		}
+
+		if (nvlist_empty(suites)) {
+			nvlist_free(suites);
+			nvlist_free(module);
+			continue;
+		}
+
+		fnvlist_add_string(module, KTEST_NAME_KEY, km->km_name);
+		fnvlist_add_nvlist(module, KTEST_MODULE_SUITES_KEY, suites);
+		fnvlist_add_nvlist(modules, km->km_name, module);
+		nvlist_free(suites);
+		nvlist_free(module);
+	}
+
+	mutex_exit(&ktest_lock);
+	buf = fnvlist_pack(modules, &len);
+
+	/*
+	 * The userspace response buffer is not large enough. Fill in
+	 * the amount needed and return ENOBUFS so that the command
+	 * may retry.
+	 */
+	if (klo->klo_resp_len < len) {
+		klo->klo_resp_len = len;
+		nvlist_free(modules);
+		ret = ENOBUFS;
+		goto out;
+	}
+
+	klo->klo_resp_len = len;
+
+	if (ddi_copyout(buf, klo->klo_resp, len, mode) != 0) {
+		ret = EFAULT;
+		goto out;
+	}
+
+out:
+	nvlist_free(modules);
+	kmem_free(buf, len);
+	return (ret);
+}
+
+static void
+ktest_run_test(const ktest_test_t *kt, uchar_t *input, uint64_t input_len,
+    ktest_result_t *res)
+{
+	ktest_ctx_t ctx;
+
+	bzero(&ctx, sizeof (ctx));
+	res->kr_type = KTEST_RESULT_NONE;
+	ctx.ktc_test = kt;
+	ctx.ktc_res = res;
+	ctx.ktc_input = input;
+	ctx.ktc_input_len = input_len;
+	kt->kt_fn((ktest_ctx_hdl_t *)&ctx);
+}
+
+static int
+ktest_getinfo(dev_info_t *dip, ddi_info_cmd_t cmd, void *arg, void **resultp)
+{
+	switch (cmd) {
+	case DDI_INFO_DEVT2DEVINFO:
+		*resultp = ktest_dip;
+		break;
+	case DDI_INFO_DEVT2INSTANCE:
+		*resultp = (void *)0;
+		break;
+	default:
+		return (DDI_FAILURE);
+	}
+
+	return (DDI_SUCCESS);
+}
+
+static int
+ktest_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
+{
+	if (cmd != DDI_ATTACH) {
+		return (DDI_FAILURE);
+	}
+
+	if (ddi_create_minor_node(dip, "ktest", S_IFCHR, KTEST_CTL_MINOR,
+	    DDI_PSEUDO, 0) != DDI_SUCCESS) {
+		return (DDI_FAILURE);
+	}
+
+	ktest_dip = dip;
+	ddi_report_dev(dip);
+	return (DDI_SUCCESS);
+}
+
+static int
+ktest_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
+{
+	if (cmd != DDI_DETACH) {
+		return (DDI_FAILURE);
+	}
+
+	ddi_remove_minor_node(dip, NULL);
+	ktest_dip = NULL;
+	return (DDI_SUCCESS);
+}
+
+static int
+ktest_open(dev_t *devp, int flag, int otype, cred_t *credp)
+{
+	if (otype != OTYP_CHR) {
+		return (EINVAL);
+	}
+
+	/* Make sure attach(9E) has completed. */
+	if (ktest_dip == NULL) {
+		return (ENXIO);
+	}
+
+	if (getminor(*devp) != KTEST_CTL_MINOR) {
+		return (ENXIO);
+	}
+
+	if (flag & FWRITE) {
+		return (EACCES);
+	}
+
+	if (flag & FEXCL) {
+		return (ENOTSUP);
+	}
+
+	/*
+	 * Access to the ktest facility requires the utmost respect:
+	 * test modules have full access to the kernel address space
+	 * and the user executing ktest can pipe in any arbitrary
+	 * stream of bytes to any test which takes an input stream.
+	 * Given this liability, and the fact the test facility should
+	 * mostly be used for development quality assurance or
+	 * production pre-flight checklists or healthchecks, it makes
+	 * sense to restrict the loading, listing, and execution of
+	 * tests to those with the highest of privilege: the root
+	 * role/user in the Global Zone.
+	 */
+	if (drv_priv(credp) != 0 || crgetzoneid(credp) != GLOBAL_ZONEID) {
+		return (EPERM);
+	}
+
+	return (0);
+}
+
+static int
+ktest_close(dev_t dev, int flags, int otype, cred_t *credp)
+{
+	return (0);
+}
+
+static int
+ktest_ioctl_run_test(intptr_t arg, int mode)
+{
+	int ret = 0;
+	ktest_run_op_t kro;
+	uchar_t *input_bytes = NULL;
+	ktest_test_t *kt = NULL;
+
+	bzero(&kro, sizeof (kro));
+	if (ddi_copyin((void *)arg, &kro, sizeof (kro), mode) != 0) {
+		return (EFAULT);
+	}
+
+	if (kro.kro_input_len > KTEST_IOCTL_MAX_LEN) {
+		return (EINVAL);
+	}
+
+	/*
+	 * If there is input, copy it into KAS.
+	 */
+	if (kro.kro_input_len > 0) {
+		input_bytes = kmem_zalloc(kro.kro_input_len, KM_SLEEP);
+		ret = ddi_copyin((void *)kro.kro_input_bytes, input_bytes,
+		    kro.kro_input_len, mode);
+
+		if (ret != 0) {
+			ret = EFAULT;
+			goto done;
+		}
+	}
+
+	mutex_enter(&ktest_lock);
+	kt = ktest_get_test(kro.kro_module, kro.kro_suite, kro.kro_test);
+
+	/*
+	 * We failed to find a matching test. The ktest command should
+	 * always send down a valid fully-qualified triple; but it's
+	 * good hygiene to check for this case.
+	 */
+	if (kt == NULL) {
+		ret = ENOENT;
+		goto done;
+	}
+
+	/*
+	 * The test requires input but none was provided. The ktest
+	 * command should not send down such a request; but it's good
+	 * hygiene to check for it.
+	 */
+	if (kt->kt_requires_input && kro.kro_input_len == 0) {
+		ret = EINVAL;
+		goto done;
+	}
+
+	ktest_run_test(kt, input_bytes, kro.kro_input_len, &kro.kro_result);
+
+done:
+	mutex_exit(&ktest_lock);
+	kmem_free(input_bytes, kro.kro_input_len);
+
+	if (ret == 0 &&
+	    ddi_copyout(&kro, (void *)arg, sizeof (kro), mode) != 0) {
+		ret = EFAULT;
+	}
+
+	return (ret);
+}
+
+static int
+ktest_ioctl_list_tests(intptr_t arg, int mode)
+{
+	int ret = 0;
+	ktest_list_op_t klo;
+
+	bzero(&klo, sizeof (klo));
+	if (ddi_copyin((void *)arg, &klo, sizeof (klo), mode) != 0) {
+		return (EFAULT);
+	}
+
+	if ((ret = ktest_list_tests(&klo, mode)) == 0) {
+		if (ddi_copyout(&klo, (void *)arg, sizeof (klo), mode) != 0) {
+			return (EFAULT);
+		}
+	}
+
+	return (ret);
+}
+
+static int
+ktest_ioctl(dev_t dev, int cmd, intptr_t arg, int mode, cred_t *credp,
+    int *rvalp)
+{
+	int ret = 0;
+
+	/*
+	 * We make two assumptions:
+	 *
+	 *  1. That only the ktest command interacts with the ktest driver.
+	 *
+	 *  2. The the ktest command is 64-bit.
+	 */
+	if (ddi_model_convert_from(mode) != DDI_MODEL_NONE) {
+		return (ENOSYS);
+	}
+
+	switch (cmd) {
+	case KTEST_IOCTL_RUN_TEST:
+		ret = ktest_ioctl_run_test(arg, mode);
+		break;
+
+	case KTEST_IOCTL_LIST_TESTS:
+		ret = ktest_ioctl_list_tests(arg, mode);
+		break;
+
+	default:
+		ret = EINVAL;
+		break;
+	}
+
+	return (ret);
+}
+
+static struct cb_ops ktest_cb_ops = {
+	.cb_open = ktest_open,
+	.cb_close = ktest_close,
+	.cb_strategy = nodev,
+	.cb_print = nodev,
+	.cb_dump = nodev,
+	.cb_read = nodev,
+	.cb_write = nodev,
+	.cb_ioctl = ktest_ioctl,
+	.cb_devmap = nodev,
+	.cb_mmap = nodev,
+	.cb_segmap = nodev,
+	.cb_chpoll = nochpoll,
+	.cb_prop_op = ddi_prop_op,
+	.cb_flag = D_MP | D_64BIT,
+	.cb_rev = CB_REV,
+	.cb_aread = nodev,
+	.cb_awrite = nodev,
+	.cb_str = NULL
+};
+
+static struct dev_ops ktest_dev_ops = {
+	.devo_rev = DEVO_REV,
+	.devo_refcnt = 0,
+	.devo_getinfo = ktest_getinfo,
+	.devo_identify = nulldev,
+	.devo_probe = nulldev,
+	.devo_attach = ktest_attach,
+	.devo_detach = ktest_detach,
+	.devo_reset = nodev,
+	.devo_power = NULL,
+	.devo_quiesce = ddi_quiesce_not_supported,
+	.devo_cb_ops = &ktest_cb_ops,
+	.devo_bus_ops = NULL
+};
+
+static struct modldrv ktest_modldrv = {
+	.drv_modops = &mod_driverops,
+	.drv_linkinfo = "Kernel Test Driver v1",
+	.drv_dev_ops = &ktest_dev_ops
+};
+
+static struct modlinkage ktest_modlinkage = {
+	.ml_rev = MODREV_1,
+	.ml_linkage = { &ktest_modldrv, NULL }
+};
+
+static void
+ktest_fini()
+{
+	ktest_unregister_all();
+	list_destroy(&ktest_modules);
+	mutex_destroy(&ktest_lock);
+}
+
+/*
+ * This is a pseudo device driver with a single instance, therefore
+ * all state is allocated/freed during init/fini. We delay the
+ * creation of the taskq until attach, since tests cannot be executed
+ * until the driver is attached.
+ */
+int
+_init(void)
+{
+	int ret;
+
+	mutex_init(&ktest_lock, NULL, MUTEX_DRIVER, NULL);
+	list_create(&ktest_modules, sizeof (ktest_module_t),
+	    offsetof(ktest_module_t, km_node));
+	ret = mod_install(&ktest_modlinkage);
+
+	if (ret != DDI_SUCCESS) {
+		ktest_fini();
+	}
+
+	return (ret);
+}
+
+int
+_fini(void)
+{
+	int ret = mod_remove(&ktest_modlinkage);
+
+	if (ret == DDI_SUCCESS) {
+		ktest_fini();
+	}
+
+	return (ret);
+}
+
+int
+_info(struct modinfo *modinfop)
+{
+	return (mod_info(&ktest_modlinkage, modinfop));
+}
diff --git a/usr/src/uts/common/io/ktest/ktest.conf b/usr/src/uts/common/io/ktest/ktest.conf
new file mode 100644
index 0000000000..9d6046ffc6
--- /dev/null
+++ b/usr/src/uts/common/io/ktest/ktest.conf
@@ -0,0 +1,16 @@
+#
+# This file and its contents are supplied under the terms of the
+# Common Development and Distribution License ("CDDL"), version 1.0.
+# You may only use this file in accordance with the terms of version
+# 1.0 of the CDDL.
+#
+# A full copy of the text of the CDDL should have accompanied this
+# source.  A copy of the CDDL is also available via the Internet at
+# http://www.illumos.org/license/CDDL.
+#
+
+#
+# Copyright 2021 Oxide Computer Company
+#
+
+name="ktest" parent="pseudo" instance=0;
diff --git a/usr/src/uts/common/io/ktest/ktest_selftest.c b/usr/src/uts/common/io/ktest/ktest_selftest.c
new file mode 100644
index 0000000000..be07212ec5
--- /dev/null
+++ b/usr/src/uts/common/io/ktest/ktest_selftest.c
@@ -0,0 +1,245 @@
+/*
+ * This file and its contents are supplied under the terms of the
+ * Common Development and Distribution License ("CDDL"), version 1.0.
+ * You may only use this file in accordance with the terms of version
+ * 1.0 of the CDDL.
+ *
+ * A full copy of the text of the CDDL should have accompanied this
+ * source.  A copy of the CDDL is also available via the Internet at
+ * http://www.illumos.org/license/CDDL.
+ */
+
+/*
+ * Copyright 2023 Oxide Computer Company
+ * Copyright 2024 Ryan Zezeski
+ */
+
+/*
+ * The ktest self test. Make sure the basics work.
+ */
+#include <sys/ktest.h>
+
+void
+ktest_st_pass_test(ktest_ctx_hdl_t *ctx)
+{
+	KT_PASS(ctx);
+}
+
+static boolean_t
+ktest_st_is_even(int x)
+{
+	return ((x % 2) == 0);
+}
+
+void
+ktest_st_fail_test(ktest_ctx_hdl_t *ctx)
+{
+	KT_ASSERT(ktest_st_is_even(5), ctx);
+	KT_PASS(ctx);
+}
+
+static int
+ktest_st_pretend_func(int input)
+{
+	if (input == 42) {
+		return (0);
+	}
+
+	return (-1);
+}
+
+/*
+ * This test should report a NONE result as it never touches the
+ * context.
+ */
+void
+ktest_st_none_test(ktest_ctx_hdl_t *ctx)
+{
+	(void) ktest_st_pretend_func(7);
+}
+
+/*
+ * This test should report an ERROR result.
+ */
+void
+ktest_st_err_test(ktest_ctx_hdl_t *ctx)
+{
+	KT_EASSERT0(ktest_st_pretend_func(7), ctx);
+}
+
+/*
+ * This test should report a SKIP result.
+ */
+void
+ktest_st_skip_test(ktest_ctx_hdl_t *ctx)
+{
+	KT_SKIP(ctx, "This test should be skipped.");
+}
+
+/*
+ * This test should only run when given an input stream.
+ */
+void
+ktest_st_input_test(ktest_ctx_hdl_t *ctx)
+{
+	uchar_t *bytes;
+	size_t num_bytes = 0;
+
+	ktest_get_input(ctx, &bytes, &num_bytes);
+
+	if (num_bytes < 4) {
+		KT_ERROR(ctx, "expected 4 or more bytes, got %u", num_bytes);
+		return;
+	}
+
+	cmn_err(CE_WARN, "bytes (%lu): 0x%x 0x%x 0x%x 0x%x",
+	    num_bytes, bytes[0], bytes[1], bytes[2],
+	    bytes[3]);
+
+	KT_PASS(ctx);
+}
+
+/*
+ * Verify that ktest catches multiple results and returns an error to
+ * alert the user.
+ */
+void
+ktest_st_mult_result_test(ktest_ctx_hdl_t *ctx)
+{
+	KT_FAIL(ctx, "this is a fail result");
+	KT_PASS(ctx);
+}
+
+/*
+ * Verify the suite and test name uniqueness is enforced. Module name
+ * uniqueness is tested in _init().
+ */
+void
+ktest_st_unique_test(ktest_ctx_hdl_t *ctx)
+{
+	ktest_module_hdl_t *km = NULL;
+	ktest_suite_hdl_t *ks = NULL;
+
+	KT_ASSERT0(ktest_create_module("ktest", &km), ctx);
+	KT_ASSERT0G(ktest_add_suite(km, "selftest", &ks), ctx, cleanup);
+	KT_ASSERT3SG(ktest_add_suite(km, "selftest", &ks), ==, EEXIST, ctx,
+	    cleanup);
+	KT_ASSERT0G(ktest_add_test(ks, "ktest_st_pass_test", ktest_st_pass_test,
+	    KTEST_FLAG_NONE), ctx, cleanup);
+	KT_ASSERTG(ktest_add_test(ks, "ktest_st_pass_test", ktest_st_pass_test,
+	    KTEST_FLAG_NONE) == EEXIST, ctx, cleanup);
+
+	KT_PASS(ctx);
+
+cleanup:
+	ktest_free_module(km);
+}
+
+void
+ktest_st_name_test(ktest_ctx_hdl_t *ctx)
+{
+	ktest_module_hdl_t *km = NULL;
+
+	KT_ASSERT3SG(ktest_create_module("bad:name", &km), ==, EINVAL, ctx,
+	    cleanup);
+	KT_ASSERT3SG(ktest_create_module("bad/name", &km), ==, EINVAL, ctx,
+	    cleanup);
+	KT_ASSERT3SG(ktest_create_module("bad?name", &km), ==, EINVAL, ctx,
+	    cleanup);
+	KT_ASSERT3SG(ktest_create_module("bad name", &km), ==, EINVAL, ctx,
+	    cleanup);
+	KT_ASSERT3SG(ktest_create_module("bad>name", &km), ==, EINVAL, ctx,
+	    cleanup);
+	KT_ASSERT3SG(ktest_create_module("bad<name", &km), ==, EINVAL, ctx,
+	    cleanup);
+	KT_ASSERT3SG(ktest_create_module("bad&name", &km), ==, EINVAL, ctx,
+	    cleanup);
+	KT_ASSERT3SG(ktest_create_module("bad*name", &km), ==, EINVAL, ctx,
+	    cleanup);
+
+	KT_ASSERT0G(ktest_create_module("good_name", &km), ctx, cleanup);
+	ktest_free_module(km);
+	KT_ASSERT0G(ktest_create_module("good_name02", &km), ctx, cleanup);
+	ktest_free_module(km);
+	KT_ASSERT0G(ktest_create_module("good.name02", &km), ctx, cleanup);
+	ktest_free_module(km);
+	KT_ASSERT0G(ktest_create_module("_l33t.n4m3", &km), ctx, cleanup);
+
+	KT_PASS(ctx);
+
+cleanup:
+	ktest_free_module(km);
+}
+
+static struct modlmisc ktest_selftest_modlmisc = {
+	.misc_modops = &mod_miscops,
+	.misc_linkinfo = "ktest selftest module"
+};
+
+static struct modlinkage ktest_selftest_modlinkage = {
+	.ml_rev = MODREV_1,
+	.ml_linkage = { &ktest_selftest_modlmisc, NULL }
+};
+
+int
+_init()
+{
+	int ret;
+	ktest_module_hdl_t *km = NULL;
+	ktest_suite_hdl_t *ks = NULL;
+
+	VERIFY0(ktest_create_module("ktest", &km));
+	VERIFY0(ktest_add_suite(km, "selftest", &ks));
+	VERIFY0(ktest_add_test(ks, "ktest_st_none_test", ktest_st_none_test,
+	    KTEST_FLAG_NONE));
+	VERIFY0(ktest_add_test(ks, "ktest_st_pass_test", ktest_st_pass_test,
+	    KTEST_FLAG_NONE));
+	VERIFY0(ktest_add_test(ks, "ktest_st_fail_test", ktest_st_fail_test,
+	    KTEST_FLAG_NONE));
+	VERIFY0(ktest_add_test(ks, "ktest_st_err_test", ktest_st_err_test,
+	    KTEST_FLAG_NONE));
+	VERIFY0(ktest_add_test(ks, "ktest_st_skip_test", ktest_st_skip_test,
+	    KTEST_FLAG_NONE));
+	VERIFY0(ktest_add_test(ks, "ktest_st_input_test", ktest_st_input_test,
+	    KTEST_FLAG_INPUT));
+	VERIFY0(ktest_add_test(ks, "ktest_st_mult_result_test",
+	    ktest_st_mult_result_test, KTEST_FLAG_NONE));
+	VERIFY0(ktest_add_test(ks, "ktest_st_unique_test",
+	    ktest_st_unique_test, KTEST_FLAG_NONE));
+	VERIFY0(ktest_add_test(ks, "ktest_st_name_test",
+	    ktest_st_name_test, KTEST_FLAG_NONE));
+
+	if ((ret = ktest_register_module(km)) != 0) {
+		ktest_free_module(km);
+		return (ret);
+	}
+
+	/*
+	 * It would be nice to test this in ktest_st_test_unique(),
+	 * but we can't because this call grabs the ktest_lock, and
+	 * the lock is already held while a test is running. If you
+	 * see a panic here, check ktest_register_module() to make
+	 * sure it's enforcing module name uniqueness.
+	 */
+	VERIFY(ktest_register_module(km) == EEXIST);
+
+	if ((ret = mod_install(&ktest_selftest_modlinkage)) != 0) {
+		ktest_unregister_module("ktest");
+		return (ret);
+	}
+
+	return (0);
+}
+
+int
+_fini(void)
+{
+	ktest_unregister_module("ktest");
+	return (mod_remove(&ktest_selftest_modlinkage));
+}
+
+int
+_info(struct modinfo *modinfop)
+{
+	return (mod_info(&ktest_selftest_modlinkage, modinfop));
+}
diff --git a/usr/src/uts/common/io/mac/mac_test.c b/usr/src/uts/common/io/mac/mac_test.c
new file mode 100644
index 0000000000..beee510192
--- /dev/null
+++ b/usr/src/uts/common/io/mac/mac_test.c
@@ -0,0 +1,686 @@
+/*
+ * This file and its contents are supplied under the terms of the
+ * Common Development and Distribution License ("CDDL"), version 1.0.
+ * You may only use this file in accordance with the terms of version
+ * 1.0 of the CDDL.
+ *
+ * A full copy of the text of the CDDL should have accompanied this
+ * source.  A copy of the CDDL is also available via the Internet at
+ * http://www.illumos.org/license/CDDL.
+ */
+
+/*
+ * Copyright 2023 Oxide Computer Company
+ * Copyright 2024 Ryan Zezeski
+ */
+
+/*
+ * A test module for various mac routines.
+ */
+#include <inet/ip.h>
+#include <inet/ip_impl.h>
+#include <inet/tcp.h>
+#include <sys/dlpi.h>
+#include <sys/ethernet.h>
+#include <sys/ktest.h>
+
+static uint32_t
+mt_pseudo_sum(const uint8_t proto, ipha_t *ip)
+{
+	const uint32_t ip_hdr_sz = IPH_HDR_LENGTH(ip);
+	const ipaddr_t src = ip->ipha_src;
+	const ipaddr_t dst = ip->ipha_dst;
+	uint16_t len;
+	uint32_t sum = 0;
+
+	switch (proto) {
+	case IPPROTO_TCP:
+		sum = IP_TCP_CSUM_COMP;
+		break;
+
+	case IPPROTO_UDP:
+		sum = IP_UDP_CSUM_COMP;
+		break;
+	}
+
+	len = ntohs(ip->ipha_length) - ip_hdr_sz;
+	sum += (dst >> 16) + (dst & 0xFFFF) + (src >> 16) + (src & 0xFFFF);
+	sum += htons(len);
+	return (sum);
+}
+
+/*
+ * An implementation of the internet checksum inspired by RFC 1071.
+ * This implementation is as naive as possible. It serves as the
+ * reference point for testing the optimized versions in the rest of
+ * our stack. This is no place for optimization or cleverness.
+ *
+ * Arguments
+ *
+ *     initial: The initial sum value.
+ *
+ *     addr: Pointer to the beginning of the byte stream to sum.
+ *
+ *     len: The number of bytes to sum.
+ *
+ * Return
+ *
+ *     The resulting internet checksum.
+ */
+static uint32_t
+mt_rfc1071_sum(uint32_t initial, uint16_t *addr, size_t len)
+{
+	uint32_t sum = initial;
+
+	while (len > 1) {
+		sum += *addr;
+		addr++;
+		len -= 2;
+	}
+
+	if (len == 1) {
+		sum += *((uint8_t *)addr);
+	}
+
+	while ((sum >> 16) != 0) {
+		sum = (sum >> 16) + (sum & 0xFFFF);
+	}
+
+	return (~sum & 0xFFFF);
+}
+
+typedef boolean_t (*mac_sw_cksum_ipv4_t)(mblk_t *, uint32_t, ipha_t *,
+    const char **);
+
+/*
+ * Fill out a basic TCP header in the given mblk at the given offset.
+ * A TCP header should never straddle an mblk boundary.
+ */
+static tcpha_t *
+mt_tcp_basic_hdr(mblk_t *mp, uint16_t offset, uint16_t lport, uint16_t fport,
+    uint32_t seq, uint32_t ack, uint8_t flags, uint16_t win)
+{
+	tcpha_t *tcp = (tcpha_t *)(mp->b_rptr + offset);
+
+	VERIFY3U((uintptr_t)tcp + sizeof (*tcp), <=, mp->b_wptr);
+	tcp->tha_lport = htons(lport);
+	tcp->tha_fport = htons(fport);
+	tcp->tha_seq = htonl(seq);
+	tcp->tha_ack = htonl(ack);
+	tcp->tha_offset_and_reserved = 0x5 << 4;
+	tcp->tha_flags = flags;
+	tcp->tha_win = htons(win);
+	tcp->tha_sum = 0x0;
+	tcp->tha_urp = 0x0;
+
+	return (tcp);
+}
+
+static ipha_t *
+mt_ipv4_simple_hdr(mblk_t *mp, uint16_t offset, uint16_t datum_length,
+    uint16_t ident, uint8_t proto, char *src, char *dst)
+{
+	uint32_t srcaddr, dstaddr;
+	ipha_t *ip = (ipha_t *)(mp->b_rptr + offset);
+
+	VERIFY3U((uintptr_t)ip + sizeof (*ip), <=, mp->b_wptr);
+
+	VERIFY(inet_pton(AF_INET, src, &srcaddr));
+	VERIFY(inet_pton(AF_INET, dst, &dstaddr));
+	ip->ipha_version_and_hdr_length = IP_SIMPLE_HDR_VERSION;
+	ip->ipha_type_of_service = 0x0;
+	ip->ipha_length = htons(sizeof (*ip) + datum_length);
+	ip->ipha_ident = htons(ident);
+	ip->ipha_fragment_offset_and_flags = IPH_DF_HTONS;
+	ip->ipha_ttl = 255;
+	ip->ipha_protocol = proto;
+	ip->ipha_hdr_checksum = 0x0;
+	ip->ipha_src = srcaddr;
+	ip->ipha_dst = dstaddr;
+
+	return (ip);
+}
+
+static struct ether_header *
+mt_ether_hdr(mblk_t *mp, uint16_t offset, char *dst, char *src, uint16_t etype)
+{
+	char *byte = dst;
+	unsigned long tmp;
+	struct ether_header *eh = (struct ether_header *)(mp->b_rptr + offset);
+
+	VERIFY3U((uintptr_t)eh + sizeof (*eh), <=, mp->b_wptr);
+
+	/* No strtok in these here parts. */
+	for (uint_t i = 0; i < 6; i++) {
+		char *end = strchr(dst, ':');
+		VERIFY3P(end, !=, NULL);
+		VERIFY0(ddi_strtoul(byte, NULL, 16, &tmp));
+		VERIFY3U(tmp, <=, 255);
+		eh->ether_dhost.ether_addr_octet[i] = tmp;
+		byte = end + 1;
+	}
+
+	byte = src;
+	for (uint_t i = 0; i < 6; i++) {
+		char *end = strchr(dst, ':');
+		VERIFY3P(end, !=, NULL);
+		VERIFY0(ddi_strtoul(byte, NULL, 16, &tmp));
+		VERIFY3U(tmp, <=, 255);
+		eh->ether_shost.ether_addr_octet[i] = tmp;
+		byte = end + 1;
+	}
+
+	eh->ether_type = etype;
+	return (eh);
+}
+
+void
+mac_sw_cksum_ipv4_tcp_test(ktest_ctx_hdl_t *ctx)
+{
+	ddi_modhandle_t hdl = NULL;
+	mac_sw_cksum_ipv4_t mac_sw_cksum_ipv4 = NULL;
+	tcpha_t *tcp;
+	ipha_t *ip;
+	struct ether_header *eh;
+	mblk_t *mp = NULL;
+	char *msg = "...when it's not your turn";
+	size_t msglen = strlen(msg) + 1;
+	size_t mplen;
+	const char *err = "";
+	uint32_t sum;
+	size_t ehsz = sizeof (*eh);
+	size_t ipsz = sizeof (*ip);
+	size_t tcpsz = sizeof (*tcp);
+
+	if (ktest_hold_mod("mac", &hdl) != 0) {
+		KT_ERROR(ctx, "failed to hold 'mac' module");
+		return;
+	}
+
+	if (ktest_get_fn(hdl, "mac_sw_cksum_ipv4",
+	    (void **)&mac_sw_cksum_ipv4) != 0) {
+		KT_ERROR(ctx, "failed to resolve symbol %s`%s", "mac",
+		    "mac_sw_cksum_ipv4");
+		goto cleanup;
+	}
+
+	mplen = ehsz + ipsz + tcpsz + msglen;
+	mp = allocb(mplen, 0);
+	KT_EASSERT3P(mp, !=, NULL, ctx);
+	mp->b_wptr = mp->b_rptr + mplen;
+	tcp = mt_tcp_basic_hdr(mp, ehsz + ipsz, 2002, 2008, 1, 166, 0, 32000);
+	ip = mt_ipv4_simple_hdr(mp, ehsz, tcpsz + msglen, 410, IPPROTO_TCP,
+	    "192.168.2.4", "192.168.2.5");
+	eh = mt_ether_hdr(mp, 0, "f2:35:c2:72:26:57", "92:ce:5a:29:46:9d",
+	    ETHERTYPE_IP);
+
+	bcopy(msg, mp->b_rptr + ehsz + ipsz + tcpsz, msglen);
+
+	/*
+	 * It's important that we calculate the reference checksum
+	 * first, because mac_sw_cksum_ipv4() populates the checksum
+	 * field.
+	 */
+	sum = mt_pseudo_sum(IPPROTO_TCP, ip);
+	sum = mt_rfc1071_sum(sum, (uint16_t *)(mp->b_rptr + ehsz + ipsz),
+	    tcpsz + msglen);
+
+	/*
+	 * The internet checksum can never be 0xFFFF, as that would
+	 * indicate an input of all zeros.
+	 */
+	KT_ASSERT3UG(sum, !=, 0xFFFF, ctx, cleanup);
+	KT_ASSERTG(mac_sw_cksum_ipv4(mp, ehsz, ip, &err), ctx, cleanup);
+	KT_ASSERT3UG(tcp->tha_sum, !=, 0xFFFF, ctx, cleanup);
+	KT_ASSERT3UG(sum, ==, tcp->tha_sum, ctx, cleanup);
+	KT_PASS(ctx);
+
+cleanup:
+	if (hdl != NULL) {
+		ktest_release_mod(hdl);
+	}
+
+	if (mp != NULL) {
+		freeb(mp);
+	}
+}
+
+/*
+ * Verify that an unexpected IP protocol results in the expect
+ * failure.
+ */
+void
+mac_sw_cksum_ipv4_bad_proto_test(ktest_ctx_hdl_t *ctx)
+{
+	ddi_modhandle_t hdl = NULL;
+	mac_sw_cksum_ipv4_t mac_sw_cksum_ipv4 = NULL;
+	tcpha_t *tcp;
+	ipha_t *ip;
+	struct ether_header *eh;
+	mblk_t *mp = NULL;
+	char *msg = "...when it's not your turn";
+	size_t msglen = strlen(msg) + 1;
+	size_t mplen;
+	const char *err = "";
+	size_t ehsz = sizeof (*eh);
+	size_t ipsz = sizeof (*ip);
+	size_t tcpsz = sizeof (*tcp);
+
+	if (ktest_hold_mod("mac", &hdl) != 0) {
+		KT_ERROR(ctx, "failed to hold 'mac' module");
+		return;
+	}
+
+	if (ktest_get_fn(hdl, "mac_sw_cksum_ipv4",
+	    (void **)&mac_sw_cksum_ipv4) != 0) {
+		KT_ERROR(ctx, "failed to resolve symbol mac`mac_sw_cksum_ipv4");
+		goto cleanup;
+	}
+
+	mplen = ehsz + ipsz + tcpsz + msglen;
+	mp = allocb(mplen, 0);
+	KT_EASSERT3P(mp, !=, NULL, ctx);
+	mp->b_wptr = mp->b_rptr + mplen;
+	tcp = mt_tcp_basic_hdr(mp, ehsz + ipsz, 2002, 2008, 1, 166, 0, 32000);
+	ip = mt_ipv4_simple_hdr(mp, ehsz, tcpsz + msglen, 410, IPPROTO_ENCAP,
+	    "192.168.2.4", "192.168.2.5");
+	eh = mt_ether_hdr(mp, 0, "f2:35:c2:72:26:57", "92:ce:5a:29:46:9d",
+	    ETHERTYPE_IP);
+	bcopy(msg, mp->b_rptr + ehsz + ipsz + tcpsz, msglen);
+	KT_ASSERT0G(mac_sw_cksum_ipv4(mp, ehsz, ip, &err), ctx, cleanup);
+	KT_PASS(ctx);
+
+cleanup:
+	if (hdl != NULL) {
+		ktest_release_mod(hdl);
+	}
+
+	if (mp != NULL) {
+		freeb(mp);
+	}
+}
+
+typedef struct snoop_pkt_record_hdr {
+	uint32_t	spr_orig_len;
+	uint32_t	spr_include_len;
+	uint32_t	spr_record_len;
+	uint32_t	spr_cumulative_drops;
+	uint32_t	spr_ts_secs;
+	uint32_t	spr_ts_micros;
+} snoop_pkt_record_hdr_t;
+
+typedef struct snoop_pkt {
+	uchar_t *sp_bytes;
+	uint16_t sp_len;
+} snoop_pkt_t;
+
+typedef struct snoop_iter {
+	uchar_t *sic_input;	/* beginning of stream */
+	uintptr_t sic_end;	/* end of stream */
+	uchar_t *sic_pos;	/* current position in stream */
+	uint_t sic_pkt_num;	/* current packet number, 1-based */
+	snoop_pkt_record_hdr_t *sic_pkt_hdr; /* current packet record header */
+} snoop_iter_t;
+
+#define	PAST_END(itr, len)	\
+	(((uintptr_t)(itr)->sic_pos + len) > itr->sic_end)
+
+/*
+ * Get the next packet in the snoop stream iterator returned by
+ * mt_snoop_iter_get(). A copy of the packet is returned via the pkt
+ * pointer. The caller provides the snoop_pkt_t, and this function
+ * allocates a new buffer inside it to hold a copy of the packet's
+ * bytes. It is the responsibility of the caller to free the copy. It
+ * is recommended the caller make use of desballoc(9F) along with the
+ * snoop_pkt_free() callback. When all the packets in the stream have
+ * been read all subsequent calls to this function will set sp_bytes
+ * to NULL and sp_len to 0.
+ *
+ * The caller may optionally specify an rhdr argument in order to
+ * receive a pointer to the packet record header (unlike the packet
+ * bytes this is a pointer into the stream, not a copy).
+ */
+static int
+mt_snoop_iter_next(ktest_ctx_hdl_t *ctx, snoop_iter_t *itr, snoop_pkt_t *pkt,
+    snoop_pkt_record_hdr_t **rhdr)
+{
+	uchar_t *pkt_start;
+
+	/*
+	 * We've read exactly the number of bytes expected, this is
+	 * the end.
+	 */
+	if ((uintptr_t)(itr->sic_pos) == itr->sic_end) {
+		pkt->sp_bytes = NULL;
+		pkt->sp_len = 0;
+
+		if (rhdr != NULL)
+			*rhdr = NULL;
+
+		return (0);
+	}
+
+	/*
+	 * A corrupted record or truncated stream could point us past
+	 * the end of the stream.
+	 */
+	if (PAST_END(itr, sizeof (snoop_pkt_record_hdr_t))) {
+		KT_ERROR(ctx, "record corrupted or stream truncated, read past "
+		    "end of stream for record header #%d: 0x%p + %u > 0x%p",
+		    itr->sic_pkt_num, itr->sic_pos,
+		    sizeof (snoop_pkt_record_hdr_t), itr->sic_end);
+		return (EIO);
+	}
+
+	itr->sic_pkt_hdr = (snoop_pkt_record_hdr_t *)itr->sic_pos;
+	pkt_start = itr->sic_pos + sizeof (snoop_pkt_record_hdr_t);
+
+	/*
+	 * A corrupted record or truncated stream could point us past
+	 * the end of the stream.
+	 */
+	if (PAST_END(itr, ntohl(itr->sic_pkt_hdr->spr_record_len))) {
+		KT_ERROR(ctx, "record corrupted or stream truncated, read past "
+		    "end of stream for record #%d: 0x%p + %u > 0x%p",
+		    itr->sic_pkt_num, itr->sic_pos,
+		    ntohl(itr->sic_pkt_hdr->spr_record_len), itr->sic_end);
+		return (EIO);
+	}
+
+	pkt->sp_len = ntohl(itr->sic_pkt_hdr->spr_include_len);
+	pkt->sp_bytes = kmem_zalloc(pkt->sp_len, KM_SLEEP);
+	bcopy(pkt_start, pkt->sp_bytes, pkt->sp_len);
+	itr->sic_pos += ntohl(itr->sic_pkt_hdr->spr_record_len);
+	itr->sic_pkt_num++;
+
+	if (rhdr != NULL) {
+		*rhdr = itr->sic_pkt_hdr;
+	}
+
+	return (0);
+}
+
+/*
+ * Parse a snoop data stream (RFC 1761) provided by input and return
+ * a packet iterator to be used by mt_snoop_iter_next().
+ */
+static int
+mt_snoop_iter_get(ktest_ctx_hdl_t *ctx, uchar_t *input, const uint_t input_len,
+    snoop_iter_t **itr_out)
+{
+	const uchar_t id[8] = { 's', 'n', 'o', 'o', 'p', '\0', '\0', '\0' };
+	uint32_t version;
+	uint32_t datalink;
+	snoop_iter_t *itr;
+
+	*itr_out = NULL;
+
+	if (input_len < 16) {
+		KT_ERROR(ctx, "snoop stream truncated at file header: %u < %u ",
+		    input_len, 16);
+		return (ENOBUFS);
+	}
+
+	if (memcmp(input, &id, sizeof (id)) != 0) {
+		KT_ERROR(ctx, "snoop stream malformed identification: %x %x %x "
+		    "%x %x %x %x %x", input[0], input[1], input[2], input[3],
+		    input[4], input[5], input[6], input[7]);
+		return (EINVAL);
+	}
+
+	itr = kmem_zalloc(sizeof (*itr), KM_SLEEP);
+	itr->sic_input = input;
+	itr->sic_end = (uintptr_t)input + input_len;
+	itr->sic_pos = input + sizeof (id);
+	itr->sic_pkt_num = 1;
+	itr->sic_pkt_hdr = NULL;
+	version = ntohl(*(uint32_t *)itr->sic_pos);
+
+	if (version != 2) {
+		KT_ERROR(ctx, "snoop stream bad version: %u != %u", version, 2);
+		return (EINVAL);
+	}
+
+	itr->sic_pos += sizeof (version);
+	datalink = ntohl(*(uint32_t *)itr->sic_pos);
+
+	/* We expect only Ethernet. */
+	if (datalink != DL_ETHER) {
+		KT_ERROR(ctx, "snoop stream bad datalink type: %u != %u",
+		    datalink, DL_ETHER);
+		kmem_free(itr, sizeof (*itr));
+		return (EINVAL);
+	}
+
+	itr->sic_pos += sizeof (datalink);
+	*itr_out = itr;
+	return (0);
+}
+
+static void
+snoop_pkt_free(snoop_pkt_t *pkt)
+{
+	kmem_free(pkt->sp_bytes, pkt->sp_len);
+}
+
+/*
+ * Verify mac_sw_cksum_ipv4() against an arbitrary TCP stream read
+ * from the snoop capture given as input. In order to verify the
+ * checksum all TCP/IPv4 packets must be captured in full. The snoop
+ * capture may contain non-TCP/IPv4 packets, which will be skipped
+ * over. If not a single TCP/IPv4 packet is found, the test will
+ * report an error.
+ */
+void
+mac_sw_cksum_ipv4_snoop_test(ktest_ctx_hdl_t *ctx)
+{
+	ddi_modhandle_t hdl = NULL;
+	mac_sw_cksum_ipv4_t mac_sw_cksum_ipv4 = NULL;
+	uchar_t *bytes;
+	size_t num_bytes = 0;
+	uint_t pkt_num = 0;
+	tcpha_t *tcp;
+	ipha_t *ip;
+	struct ether_header *eh;
+	mblk_t *mp = NULL;
+	const char *err = "";
+	uint32_t csum;
+	size_t ehsz, ipsz, tcpsz, msglen;
+	snoop_iter_t *itr = NULL;
+	snoop_pkt_record_hdr_t *hdr = NULL;
+	boolean_t at_least_one = B_FALSE;
+	snoop_pkt_t pkt;
+	int ret;
+
+	if (ktest_hold_mod("mac", &hdl) != 0) {
+		KT_ERROR(ctx, "failed to hold 'mac' module");
+		return;
+	}
+
+	if (ktest_get_fn(hdl, "mac_sw_cksum_ipv4",
+	    (void **)&mac_sw_cksum_ipv4) != 0) {
+		KT_ERROR(ctx, "failed to resolve symbol mac`mac_sw_cksum_ipv4");
+		return;
+	}
+
+	ktest_get_input(ctx, &bytes, &num_bytes);
+	ret = mt_snoop_iter_get(ctx, bytes, num_bytes, &itr);
+	if (ret != 0) {
+		/* mt_snoop_iter_get() already set error context. */
+		goto cleanup;
+	}
+
+	bzero(&pkt, sizeof (pkt));
+
+	while ((ret = mt_snoop_iter_next(ctx, itr, &pkt, &hdr)) == 0) {
+		frtn_t frtn;
+
+		if (pkt.sp_len == 0) {
+			break;
+		}
+
+		pkt_num++;
+
+		/*
+		 * Prepend the packet record number to any
+		 * fail/skip/error message so the user knows which
+		 * record in the snoop stream to inspect.
+		 */
+		ktest_msg_prepend(ctx, "pkt #%u: ", pkt_num);
+
+		/* IPv4 only */
+		if (hdr->spr_include_len < (sizeof (*eh) + sizeof (*ip))) {
+			continue;
+		}
+
+		/* fully recorded packets only */
+		if (hdr->spr_include_len != hdr->spr_orig_len) {
+			continue;
+		}
+
+		frtn.free_func = snoop_pkt_free;
+		frtn.free_arg = (caddr_t)&pkt;
+		mp = desballoc(pkt.sp_bytes, pkt.sp_len, 0, &frtn);
+		KT_EASSERT3PG(mp, !=, NULL, ctx, cleanup);
+		mp->b_wptr += pkt.sp_len;
+		eh = (struct ether_header *)mp->b_rptr;
+		ehsz = sizeof (*eh);
+
+		/* IPv4 only */
+		if (ntohs(eh->ether_type) != ETHERTYPE_IP) {
+			freeb(mp);
+			mp = NULL;
+			continue;
+		}
+
+		ip = (ipha_t *)(mp->b_rptr + ehsz);
+		ipsz = sizeof (*ip);
+
+		if (ip->ipha_protocol == IPPROTO_TCP) {
+			tcp = (tcpha_t *)(mp->b_rptr + sizeof (*eh) +
+			    sizeof (*ip));
+			tcpsz = TCP_HDR_LENGTH(tcp);
+			msglen = ntohs(ip->ipha_length) - (ipsz + tcpsz);
+
+			/* Let's make sure we don't run off into space. */
+			if ((tcpsz + msglen) > (pkt.sp_len - (ehsz + ipsz))) {
+				KT_ERROR(ctx, "(tcpsz=%lu + msglen=%lu) > "
+				    "(pkt_len=%lu - (ehsz=%lu + ipsz=%lu))",
+				    tcpsz, msglen, pkt.sp_len, ehsz, ipsz);
+				goto cleanup;
+			}
+
+			/*
+			 * As we are reading a snoop input stream we
+			 * need to make sure to zero out any existing
+			 * checksum.
+			 */
+			tcp->tha_sum = 0;
+			csum = mt_pseudo_sum(IPPROTO_TCP, ip);
+			csum = mt_rfc1071_sum(csum,
+			    (uint16_t *)(mp->b_rptr + ehsz + ipsz),
+			    tcpsz + msglen);
+		} else {
+			freeb(mp);
+			mp = NULL;
+			continue;
+		}
+
+		/*
+		 * The internet checksum can never be 0xFFFF, as that
+		 * would indicate an input of all zeros.
+		 */
+		KT_ASSERT3UG(csum, !=, 0xFFFF, ctx, cleanup);
+		KT_ASSERTG(mac_sw_cksum_ipv4(mp, ehsz, ip, &err), ctx, cleanup);
+		KT_ASSERT3UG(tcp->tha_sum, !=, 0xFFFF, ctx, cleanup);
+		KT_ASSERT3UG(tcp->tha_sum, ==, csum, ctx, cleanup);
+		at_least_one = B_TRUE;
+		freeb(mp);
+		mp = NULL;
+
+		/*
+		 * Clear the prepended message for the iterator call
+		 * as it already includes the current record number
+		 * (and pkt_num is not incremented, thus incorrect,
+		 * until after a successful call).
+		 */
+		ktest_msg_clear(ctx);
+	}
+
+	if (ret != 0) {
+		/* mt_snoop_next() already set error context. */
+		goto cleanup;
+	}
+
+	if (at_least_one) {
+		KT_PASS(ctx);
+	} else {
+		ktest_msg_clear(ctx);
+		KT_ERROR(ctx, "at least one TCP/IPv4 packet expected");
+	}
+
+cleanup:
+	if (hdl != NULL) {
+		ktest_release_mod(hdl);
+	}
+
+	if (mp != NULL) {
+		freeb(mp);
+	}
+
+	if (itr != NULL) {
+		kmem_free(itr, sizeof (*itr));
+	}
+}
+
+static struct modlmisc mac_test_modlmisc = {
+	.misc_modops = &mod_miscops,
+	.misc_linkinfo = "mac ktest module"
+};
+
+static struct modlinkage mac_test_modlinkage = {
+	.ml_rev = MODREV_1,
+	.ml_linkage = { &mac_test_modlmisc, NULL }
+};
+
+int
+_init()
+{
+	int ret;
+	ktest_module_hdl_t *km = NULL;
+	ktest_suite_hdl_t *ks = NULL;
+
+	VERIFY0(ktest_create_module("mac", &km));
+	VERIFY0(ktest_add_suite(km, "checksum", &ks));
+	VERIFY0(ktest_add_test(ks, "mac_sw_cksum_ipv4_tcp_test",
+	    mac_sw_cksum_ipv4_tcp_test, KTEST_FLAG_NONE));
+	VERIFY0(ktest_add_test(ks, "mac_sw_cksum_ipv4_bad_proto_test",
+	    mac_sw_cksum_ipv4_bad_proto_test, KTEST_FLAG_NONE));
+	VERIFY0(ktest_add_test(ks, "mac_sw_cksum_ipv4_snoop_test",
+	    mac_sw_cksum_ipv4_snoop_test, KTEST_FLAG_INPUT));
+
+	if ((ret = ktest_register_module(km)) != 0) {
+		ktest_free_module(km);
+		return (ret);
+	}
+
+	if ((ret = mod_install(&mac_test_modlinkage)) != 0) {
+		ktest_unregister_module("mac");
+		return (ret);
+	}
+
+	return (0);
+}
+
+int
+_fini(void)
+{
+	ktest_unregister_module("mac");
+	return (mod_remove(&mac_test_modlinkage));
+}
+
+int
+_info(struct modinfo *modinfop)
+{
+	return (mod_info(&mac_test_modlinkage, modinfop));
+}
diff --git a/usr/src/uts/common/io/stream_test.c b/usr/src/uts/common/io/stream_test.c
new file mode 100644
index 0000000000..65b667c766
--- /dev/null
+++ b/usr/src/uts/common/io/stream_test.c
@@ -0,0 +1,112 @@
+/*
+ * This file and its contents are supplied under the terms of the
+ * Common Development and Distribution License ("CDDL"), version 1.0.
+ * You may only use this file in accordance with the terms of version
+ * 1.0 of the CDDL.
+ *
+ * A full copy of the text of the CDDL should have accompanied this
+ * source.  A copy of the CDDL is also available via the Internet at
+ * http://www.illumos.org/license/CDDL.
+ */
+
+/*
+ * Copyright 2023 Oxide Computer Company
+ * Copyright 2024 Ryan Zezeski
+ */
+#include <sys/modctl.h>
+#include <sys/strsun.h>
+#include <sys/ktest.h>
+
+/*
+ * Test the MBLKL macro.
+ */
+void
+mblkl_test(ktest_ctx_hdl_t *ctx)
+{
+	mblk_t *mp1 = allocb(64, 0);
+
+	KT_EASSERT3P(mp1, !=, NULL, ctx);
+	KT_ASSERT3UG(MBLKL(mp1), ==, 0, ctx, cleanup);
+	mp1->b_wptr += 14;
+	KT_ASSERT3UG(MBLKL(mp1), ==, 14, ctx, cleanup);
+	KT_PASS(ctx);
+
+cleanup:
+	freeb(mp1);
+}
+
+void
+msgsize_test(ktest_ctx_hdl_t *ctx)
+{
+	mblk_t *mp1 = allocb(14, 0);
+	mblk_t *mp2 = allocb(20, 0);
+
+	KT_EASSERT3P(mp1, !=, NULL, ctx);
+	KT_EASSERT3PG(mp2, !=, NULL, ctx, cleanup);
+	KT_ASSERT3UG(msgsize(mp1), ==, 0, ctx, cleanup);
+	KT_ASSERT3UG(msgsize(mp2), ==, 0, ctx, cleanup);
+	mp1->b_wptr += 14;
+	mp2->b_wptr += 20;
+	KT_ASSERT3UG(msgsize(mp1), ==, 14, ctx, cleanup);
+	KT_ASSERT3UG(msgsize(mp2), ==, 20, ctx, cleanup);
+	mp1->b_cont = mp2;
+	KT_ASSERT3UG(msgsize(mp1), ==, 34, ctx, cleanup);
+	KT_ASSERT3UG(msgsize(mp2), ==, 20, ctx, cleanup);
+	KT_PASS(ctx);
+
+cleanup:
+	freeb(mp1);
+
+	if (mp2 != NULL) {
+		freeb(mp2);
+	}
+}
+
+static struct modlmisc stream_test_modlmisc = {
+	.misc_modops = &mod_miscops,
+	.misc_linkinfo = "stream ktest module"
+};
+
+static struct modlinkage stream_test_modlinkage = {
+	.ml_rev = MODREV_1,
+	.ml_linkage = { &stream_test_modlmisc, NULL }
+};
+
+int
+_init()
+{
+	int ret;
+	ktest_module_hdl_t *km = NULL;
+	ktest_suite_hdl_t *ks = NULL;
+
+	VERIFY0(ktest_create_module("stream", &km));
+	VERIFY0(ktest_add_suite(km, "mblk", &ks));
+	VERIFY0(ktest_add_test(ks, "mblkl_test", mblkl_test, KTEST_FLAG_NONE));
+	VERIFY0(ktest_add_test(ks, "msgsize_test", msgsize_test,
+	    KTEST_FLAG_NONE));
+
+	if ((ret = ktest_register_module(km)) != 0) {
+		ktest_free_module(km);
+		return (ret);
+	}
+
+	if ((ret = mod_install(&stream_test_modlinkage)) != 0) {
+		ktest_unregister_module("stream");
+		return (ret);
+	}
+
+	return (0);
+}
+
+int
+_fini()
+{
+	ktest_unregister_module("stream");
+	return (mod_remove(&stream_test_modlinkage));
+}
+
+int
+_info(struct modinfo *modinfop)
+{
+	return (mod_info(&stream_test_modlinkage, modinfop));
+}
diff --git a/usr/src/uts/common/sys/Makefile b/usr/src/uts/common/sys/Makefile
index fa6ca6998c..9746ed83b5 100644
--- a/usr/src/uts/common/sys/Makefile
+++ b/usr/src/uts/common/sys/Makefile
@@ -350,6 +350,8 @@ CHKHDRS=			\
 	kstr.h			\
 	ksyms.h			\
 	ksynch.h		\
+	ktest.h			\
+	ktest_impl.h		\
 	ldterm.h		\
 	lgrp.h			\
 	lgrp_user.h		\
diff --git a/usr/src/uts/common/sys/ktest.h b/usr/src/uts/common/sys/ktest.h
new file mode 100644
index 0000000000..1e682e37cc
--- /dev/null
+++ b/usr/src/uts/common/sys/ktest.h
@@ -0,0 +1,333 @@
+/*
+ * This file and its contents are supplied under the terms of the
+ * Common Development and Distribution License ("CDDL"), version 1.0.
+ * You may only use this file in accordance with the terms of version
+ * 1.0 of the CDDL.
+ *
+ * A full copy of the text of the CDDL should have accompanied this
+ * source.  A copy of the CDDL is also available via the Internet at
+ * http://www.illumos.org/license/CDDL.
+ */
+
+/*
+ * Copyright 2023 Oxide Computer Company
+ * Copyright 2024 Ryan Zezeski
+ */
+
+/*
+ * This file defines both the userspace ioctl API/ABI as well as the
+ * ktest module API/ABI. The latter is everything hidden behind the
+ * _KERNEL guard.
+ */
+#ifndef	_SYS_KTEST_H
+#define	_SYS_KTEST_H
+
+#include <sys/ddi.h>
+#include <sys/sunddi.h>
+#include <sys/modctl.h>
+#include <sys/param.h>
+#include <sys/types.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define	KTEST_SEPARATOR		":"
+#define	KTEST_DEF_TRIPLE	"*:*:*"
+#define	KTEST_DEF_TRIPLE_SZ	6
+#define	KTEST_MAX_NAME_LEN	64
+#define	KTEST_MAX_TRIPLE_LEN	((KTEST_MAX_NAME_LEN * 3) + 3)
+#define	KTEST_MAX_LOG_LEN	4096
+#define	KTEST_NAME_KEY		"name"
+#define	KTEST_MODULE_KEY	"module"
+#define	KTEST_MODULE_SUITES_KEY	"suites"
+#define	KTEST_SUITE_KEY		"suite"
+#define	KTEST_SUITE_TESTS_KEY	"tests"
+#define	KTEST_TEST_KEY		"test"
+#define	KTEST_TEST_INPUT_KEY	"input_required"
+#define	KTEST_GMATCH_CHARS	"*?[]"
+
+#define	KTEST_SER_FMT_KEY	"ser_fmt_version"
+#define	KTEST_SER_FMT_VSN	1ULL
+
+/*
+ * The maximum amount of memory a user ioctl could require ktest to
+ * allocate in order to respond to the request. This doesn't include
+ * the actions of the test themselves. It's more to prevent a
+ * negligent ioctl directly causing ktest to kmem_alloc(9F) an
+ * arbitrarily large buffer.
+ *
+ * This also currently includes any input streams for a test, thus the
+ * cap is larger than you might expect it would be. In the future we
+ * should have the ktest driver directly read the file from kernel
+ * space, but for now we pass them as part of the packed nvlist via
+ * the ioctl interface.
+ */
+#define	KTEST_IOCTL_MAX_LEN	(64 * 1024 * 1024)
+
+#define	KTEST_IOCTL	(('k' << 16) | ('t' << 8))
+
+typedef enum ktest_ioctl {
+	KTEST_IOCTL_RUN_TEST	= (KTEST_IOCTL | 1),
+	KTEST_IOCTL_LIST_TESTS	= (KTEST_IOCTL | 2),
+} ktest_ioctl_t;
+
+/*
+ * Flags used to alter the behavior of ktest or convey additional
+ * information about the test. Passed as the final argument to
+ * ktest_add_test(9F).
+ *
+ * KTEST_FLAG_INPUT
+ *
+ *    This test requires an input stream.
+ *
+ */
+typedef enum ktest_test_flags {
+	KTEST_FLAG_NONE	= 0,
+	KTEST_FLAG_INPUT	= (1 << 0),
+} ktest_test_flags_t;
+
+/*
+ * See the ktest architecture comment in ktest.c for the semantics of
+ * these values. The KTEST_RESULT_NONE value indicates that the test
+ * failed to set a result and should be considered a bug in the test.
+ */
+typedef enum ktest_result_type {
+	KTEST_RESULT_NONE,
+	KTEST_RESULT_PASS,
+	KTEST_RESULT_FAIL,
+	KTEST_RESULT_SKIP,
+	KTEST_RESULT_ERROR
+} ktest_result_type_t;
+
+/* The result of a single test. */
+typedef struct ktest_result {
+	char			kr_msg_prepend[KTEST_MAX_LOG_LEN];
+	char			kr_msg[KTEST_MAX_LOG_LEN];
+	ktest_result_type_t	kr_type;
+	int			kr_line;
+} ktest_result_t;
+
+typedef struct ktest_run_op {
+	char		kro_module[KTEST_MAX_NAME_LEN];
+	char		kro_suite[KTEST_MAX_NAME_LEN];
+	char		kro_test[KTEST_MAX_NAME_LEN];
+	char		kro_input_path[MAXPATHLEN];
+	uchar_t		*kro_input_bytes;
+	uint64_t	kro_input_len;
+	ktest_result_t	kro_result;
+} ktest_run_op_t;
+
+typedef struct ktest_list_op {
+	char		*klo_resp;
+	size_t		klo_resp_len;
+} ktest_list_op_t;
+
+/*
+ * The following API/ABI is for the ktest modules.
+ */
+#ifdef _KERNEL
+
+typedef struct __ktest_module_hdl ktest_module_hdl_t;
+typedef struct __ktest_suite_hdl ktest_suite_hdl_t;
+typedef struct __ktest_test_hdl ktest_test_hdl_t;
+typedef struct __ktest_ctx_hdl ktest_ctx_hdl_t;
+
+typedef void (*ktest_fn_t)(ktest_ctx_hdl_t *);
+
+/*
+ * Module, suite, and test creation/registration.
+ */
+int ktest_create_module(const char *, ktest_module_hdl_t **);
+int ktest_add_suite(ktest_module_hdl_t *, const char *, ktest_suite_hdl_t **);
+int ktest_add_test(ktest_suite_hdl_t *, const char *, ktest_fn_t,
+    ktest_test_flags_t);
+int ktest_register_module(ktest_module_hdl_t *);
+void ktest_unregister_module(const char *);
+void ktest_free_module(ktest_module_hdl_t *);
+
+/*
+ * Utility for getting a handle to static functions.
+ */
+int ktest_hold_mod(const char *, ddi_modhandle_t *);
+void ktest_release_mod(ddi_modhandle_t);
+int ktest_get_fn(ddi_modhandle_t, const char *, void **);
+
+/*
+ * Retrieve the input stream for a test.
+ */
+void ktest_get_input(const ktest_ctx_hdl_t *, uchar_t **, size_t *);
+
+/*
+ * Set the test result.
+ */
+void ktest_result_skip(ktest_ctx_hdl_t *, int, const char *, ...);
+void ktest_result_fail(ktest_ctx_hdl_t *, int, const char *, ...);
+void ktest_result_error(ktest_ctx_hdl_t *, int, const char *, ...);
+void ktest_result_pass(ktest_ctx_hdl_t *, int);
+void ktest_msg_clear(ktest_ctx_hdl_t *);
+void ktest_msg_prepend(ktest_ctx_hdl_t *, const char *fmt, ...);
+
+/*
+ * Note: All the macros wrap the stringizing parameters in parentheses,
+ * otherwise make check complains "preprocessor statement not in column 1".
+ */
+
+#define	KT_PASS(ctx)	ktest_result_pass((ctx), __LINE__)
+
+#define	KT_FAIL(ctx, msg, ...)						\
+	ktest_result_fail((ctx), __LINE__, (msg), ##__VA_ARGS__)
+
+#define	KT_ERROR(ctx, msg, ...)						\
+	ktest_result_error((ctx), __LINE__, (msg), ##__VA_ARGS__)
+
+#define	KT_SKIP(ctx, msg, ...)						\
+	ktest_result_skip((ctx), __LINE__, (msg), ##__VA_ARGS__)
+
+/*
+ * KTest ASSERT
+ *
+ * If the expression fails, then stash the failure message in ctx and
+ * return from the calling function.
+ */
+#define	KT_ASSERT_IMPL(LEFT, OP, RIGHT, TYPE, ctx) do {			\
+	const TYPE __left = (TYPE)(LEFT);				\
+	const TYPE __right = (TYPE)(RIGHT);				\
+	const boolean_t __res = __left OP __right;			\
+	if (!__res) {							\
+		ktest_result_fail((ctx), __LINE__,			\
+		    "%s %s %s"						\
+		    " (0x%" PRIxMAX " %s 0x%" PRIxMAX ")",		\
+		    (#LEFT), (#OP), (#RIGHT),				\
+		    (uintmax_t)__left, (#OP), (uintmax_t)__right);	\
+		return;							\
+	}								\
+	_NOTE(CONSTCOND)  } while (0)
+
+#define	KT_ASSERT3S(l, op, r, ctx)			\
+	KT_ASSERT_IMPL(l, op, r, int64_t, ctx)
+
+#define	KT_ASSERT3U(l, op, r, ctx)			\
+	KT_ASSERT_IMPL(l, op, r, uint64_t, ctx)
+
+#define	KT_ASSERT3P(l, op, r, ctx)			\
+	KT_ASSERT_IMPL(l, op, r, uintptr_t, ctx)
+
+#define	KT_ASSERT(exp, ctx)				\
+	KT_ASSERT_IMPL(exp, ==, B_TRUE, boolean_t, ctx)
+
+#define	KT_ASSERT0(exp, ctx)				\
+	KT_ASSERT_IMPL(exp, ==, 0, uintmax_t, ctx)
+
+/*
+ * KTest ASSERT Goto
+ *
+ * If the expression fails, then stash the failure message in ctx and
+ * goto label.
+ */
+#define	KT_ASSERTG_IMPL(LEFT, OP, RIGHT, TYPE, ctx, label) do {		\
+	const TYPE __left = (TYPE)(LEFT);				\
+	const TYPE __right = (TYPE)(RIGHT);				\
+	const boolean_t __res = __left OP __right;			\
+	if (!__res) {							\
+		ktest_result_fail((ctx), __LINE__,			\
+		    "%s %s %s"						\
+		    " (0x%" PRIxMAX " %s 0x%" PRIxMAX ")",		\
+		    (#LEFT), (#OP), (#RIGHT),				\
+		    (uintmax_t)__left, (#OP), (uintmax_t)__right);	\
+		goto label;						\
+	}								\
+	_NOTE(CONSTCOND)  } while (0)
+
+#define	KT_ASSERT3SG(l, op, r, ctx, label)			\
+	KT_ASSERTG_IMPL(l, op, r, int64_t, ctx, label)
+
+#define	KT_ASSERT3UG(l, op, r, ctx, label)			\
+	KT_ASSERTG_IMPL(l, op, r, uint64_t, ctx, label)
+
+#define	KT_ASSERT3PG(l, op, r, ctx, label)			\
+	KT_ASSERTG_IMPL(l, op, r, uintptr_t, ctx, label)
+
+#define	KT_ASSERTG(exp, ctx, label)				\
+	KT_ASSERTG_IMPL(exp, ==, B_TRUE, boolean_t, ctx, label)
+
+#define	KT_ASSERT0G(x, ctx, label)				\
+	KT_ASSERTG_IMPL(x, ==, 0, uintmax_t, ctx, label)
+
+/*
+ * KTest ERROR Macros
+ *
+ * These are modeled after the KTest ASSERT macros, but are instead
+ * used to check for error conditions.
+ */
+#define	KT_EASSERT_IMPL(LEFT, OP, RIGHT, TYPE, ctx) do {		\
+	const TYPE __left = (TYPE)(LEFT);				\
+	const TYPE __right = (TYPE)(RIGHT);				\
+	const boolean_t __res = __left OP __right;			\
+	if (!__res) {							\
+		ktest_result_error((ctx), __LINE__,			\
+		    "%s %s %s"						\
+		    " (0x%" PRIxMAX " %s 0x%" PRIxMAX ")",		\
+		    (#LEFT), (#OP), (#RIGHT),				\
+		    (uintmax_t)__left, #OP, (uintmax_t)__right);	\
+		return;							\
+	}								\
+	_NOTE(CONSTCOND)  } while (0)
+
+#define	KT_EASSERT3S(l, op, r, ctx)			\
+	KT_EASSERT_IMPL(l, op, r, int64_t, ctx)
+
+#define	KT_EASSERT3U(l, op, r, ctx)			\
+	KT_EASSERT_IMPL(l, op, r, uint64_t, ctx)
+
+#define	KT_EASSERT3P(l, op, r, ctx)			\
+	KT_EASSERT_IMPL(l, op, r, uintptr_t, ctx)
+
+#define	KT_EASSERT(exp, ctx)				\
+	KT_EASSERT_IMPL(exp, ==, B_TRUE, boolean_t, ctx)
+
+#define	KT_EASSERT0(exp, ctx)				\
+	KT_EASSERT_IMPL(exp, ==, 0, uintmax_t, ctx)
+
+/*
+ * KTest ERROR Goto
+ *
+ * These are modeled after the KTest ASSERT Goto macros, but are
+ * instead used to check for error conditions.
+ */
+#define	KT_EASSERTG_IMPL(LEFT, OP, RIGHT, TYPE, ctx, label) do {	\
+	const TYPE __left = (TYPE)(LEFT);				\
+	const TYPE __right = (TYPE)(RIGHT);				\
+	const boolean_t __res = __left OP __right;			\
+	if (!__res) {							\
+		ktest_result_error((ctx), __LINE__,			\
+		    "%s %s %s"						\
+		    " (0x%" PRIxMAX " %s 0x%" PRIxMAX ")",		\
+		    (#LEFT), (#OP), (#RIGHT),				\
+		    (uintmax_t)__left, #OP, (uintmax_t)__right);	\
+		goto label;						\
+	}								\
+	_NOTE(CONSTCOND)  } while (0)
+
+#define	KT_EASSERT3SG(l, op, r, ctx, label)			\
+	KT_EASSERTG_IMPL(l, op, r, int64_t, ctx, label)
+
+#define	KT_EASSERT3UG(l, op, r, ctx, label)			\
+	KT_EASSERTG_IMPL(l, op, r, uint64_t, ctx, label)
+
+#define	KT_EASSERT3PG(l, op, r, ctx, label)			\
+	KT_EASSERTG_IMPL(l, op, r, uintptr_t, ctx, label)
+
+#define	KT_EASSERTG(exp, ctx, label)				\
+	KT_EASSERTG_IMPL(exp, ==, B_TRUE, boolean_t, ctx, label)
+
+#define	KT_EASSERT0G(x, ctx, label)				\
+	KT_EASSERTG_IMPL(x, ==, 0, uintmax_t, ctx, label)
+
+#endif	/* _KERNEL */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _SYS_KTEST_H */
diff --git a/usr/src/uts/common/sys/ktest_impl.h b/usr/src/uts/common/sys/ktest_impl.h
new file mode 100644
index 0000000000..23f14021df
--- /dev/null
+++ b/usr/src/uts/common/sys/ktest_impl.h
@@ -0,0 +1,73 @@
+/*
+ * This file and its contents are supplied under the terms of the
+ * Common Development and Distribution License ("CDDL"), version 1.0.
+ * You may only use this file in accordance with the terms of version
+ * 1.0 of the CDDL.
+ *
+ * A full copy of the text of the CDDL should have accompanied this
+ * source.  A copy of the CDDL is also available via the Internet at
+ * http://www.illumos.org/license/CDDL.
+ */
+
+/*
+ * Copyright 2023 Oxide Computer Company
+ */
+
+/*
+ * This file contains the private implementation details of the ktest
+ * facility -- which is limited strictly to the kernel. Neither
+ * userspace nor ktest modules should include this file or rely on any
+ * definitions herein. Rather, userspace programs and ktest modules
+ * should include sys/ktest.h for access to the appropriate APIs.
+ */
+#ifndef	_SYS_KTEST_IMPL_H
+#define	_SYS_KTEST_IMPL_H
+
+#include <sys/ktest.h>
+#include <sys/list.h>
+#include <sys/types.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifdef _KERNEL
+
+typedef struct ktest_module {
+	list_node_t	km_node;
+	char		km_name[KTEST_MAX_NAME_LEN];
+	uint64_t	km_num_suites;
+	uint64_t	km_num_tests;
+	list_t		km_suites;
+} ktest_module_t;
+
+typedef struct ktest_suite {
+	list_node_t	ks_node;
+	ktest_module_t	*ks_module;
+	char		ks_name[KTEST_MAX_NAME_LEN];
+	uint64_t	ks_num_tests;
+	list_t		ks_tests;
+} ktest_suite_t;
+
+typedef struct ktest_test {
+	list_node_t	kt_node;
+	ktest_suite_t	*kt_suite;
+	char		kt_name[KTEST_MAX_NAME_LEN];
+	ktest_fn_t	kt_fn;
+	boolean_t	kt_requires_input;
+} ktest_test_t;
+
+typedef struct ktest_ctx {
+	const ktest_test_t	*ktc_test;
+	ktest_result_t		*ktc_res;
+	uchar_t			*ktc_input;
+	uint64_t		ktc_input_len;
+} ktest_ctx_t;
+
+#endif	/* _KERNEL */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _SYS_KTEST_IMPL_H */
diff --git a/usr/src/uts/intel/Makefile.intel b/usr/src/uts/intel/Makefile.intel
index c355d9d8e0..0cbd84a329 100644
--- a/usr/src/uts/intel/Makefile.intel
+++ b/usr/src/uts/intel/Makefile.intel
@@ -763,3 +763,15 @@ DRV_KMODS	+= vmm_vtd
 # bhyve drv test interface
 #
 DRV_KMODS	+= vmm_drv_test
+
+#
+#	The kernel test driver.
+#
+DRV_KMODS	+= ktest
+
+#
+#	Test modules that can register with ktest.
+#
+MISC_KMODS	+= mac_test
+MISC_KMODS	+= ktest_selftest
+MISC_KMODS	+= stream_test
diff --git a/usr/src/uts/intel/ktest/Makefile b/usr/src/uts/intel/ktest/Makefile
new file mode 100644
index 0000000000..f7c010a6f6
--- /dev/null
+++ b/usr/src/uts/intel/ktest/Makefile
@@ -0,0 +1,42 @@
+#
+# This file and its contents are supplied under the terms of the
+# Common Development and Distribution License ("CDDL"), version 1.0.
+# You may only use this file in accordance with the terms of version
+# 1.0 of the CDDL.
+#
+# A full copy of the text of the CDDL should have accompanied this
+# source.  A copy of the CDDL is also available via the Internet at
+# http://www.illumos.org/license/CDDL.
+#
+
+#
+# Copyright 2023 Oxide Computer Company
+#
+
+UTSBASE = ../..
+
+MODULE		= ktest
+OBJECTS		= $(KTEST_OBJS:%=$(OBJS_DIR)/%)
+ROOTMODULE	= $(ROOT_DRV_DIR)/$(MODULE)
+CONF_SRCDIR	= $(UTSBASE)/common/io/ktest
+
+include $(UTSBASE)/intel/Makefile.intel
+
+ALL_TARGET	= $(BINARY) $(CONFMOD)
+INSTALL_TARGET	= $(BINARY) $(ROOTMODULE) $(ROOT_CONFFILE)
+
+# TODO mapfiles
+
+.KEEP_STATE:
+
+def:		$(DEF_DEPS)
+
+all:		$(ALL_DEPS)
+
+clean:		$(CLEAN_DEPS)
+
+clobber:	$(CLOBBER_DEPS)
+
+install:	$(INSTALL_DEPS)
+
+include $(UTSBASE)/intel/Makefile.targ
diff --git a/usr/src/uts/intel/ktest_selftest/Makefile b/usr/src/uts/intel/ktest_selftest/Makefile
new file mode 100644
index 0000000000..973acb2d16
--- /dev/null
+++ b/usr/src/uts/intel/ktest_selftest/Makefile
@@ -0,0 +1,41 @@
+#
+# This file and its contents are supplied under the terms of the
+# Common Development and Distribution License ("CDDL"), version 1.0.
+# You may only use this file in accordance with the terms of version
+# 1.0 of the CDDL.
+#
+# A full copy of the text of the CDDL should have accompanied this
+# source.  A copy of the CDDL is also available via the Internet at
+# http://www.illumos.org/license/CDDL.
+#
+
+#
+# Copyright 2023 Oxide Computer Company
+#
+
+UTSBASE = ../..
+
+MODULE		= ktest_selftest
+OBJECTS		= $(KTEST_SELFTEST_OBJS:%=$(OBJS_DIR)/%)
+ROOTMODULE	= $(USR_KTEST_DIR)/$(MODULE)
+
+include $(UTSBASE)/intel/Makefile.intel
+
+ALL_TARGET	= $(BINARY)
+INSTALL_TARGET	= $(BINARY) $(ROOTMODULE)
+
+LDFLAGS		+= -N drv/ktest
+
+.KEEP_STATE:
+
+def:		$(DEF_DEPS)
+
+all:		$(ALL_DEPS)
+
+clean:		$(CLEAN_DEPS)
+
+clobber:	$(CLOBBER_DEPS)
+
+install:	$(INSTALL_DEPS)
+
+include $(UTSBASE)/intel/Makefile.targ
diff --git a/usr/src/uts/intel/mac_test/Makefile b/usr/src/uts/intel/mac_test/Makefile
new file mode 100644
index 0000000000..d353006457
--- /dev/null
+++ b/usr/src/uts/intel/mac_test/Makefile
@@ -0,0 +1,42 @@
+#
+# This file and its contents are supplied under the terms of the
+# Common Development and Distribution License ("CDDL"), version 1.0.
+# You may only use this file in accordance with the terms of version
+# 1.0 of the CDDL.
+#
+# A full copy of the text of the CDDL should have accompanied this
+# source.  A copy of the CDDL is also available via the Internet at
+# http://www.illumos.org/license/CDDL.
+#
+
+#
+# Copyright 2023 Oxide Computer Company
+#
+
+UTSBASE	= ../..
+
+MODULE		= mac_test
+OBJECTS		= $(MAC_TEST_OBJS:%=$(OBJS_DIR)/%)
+ROOTMODULE	= $(USR_KTEST_DIR)/$(MODULE)
+
+include $(UTSBASE)/intel/Makefile.intel
+
+ALL_TARGET	= $(BINARY)
+INSTALL_TARGET	= $(BINARY) $(ROOTMODULE)
+
+LDFLAGS		+= -N misc/mac -N drv/ktest
+
+.KEEP_STATE:
+
+def:		$(DEF_DEPS)
+
+all:		$(ALL_DEPS)
+
+clean:		$(CLEAN_DEPS)
+
+clobber:	$(CLOBBER_DEPS)
+
+install:	$(INSTALL_DEPS)
+
+include $(UTSBASE)/intel/Makefile.targ
+
diff --git a/usr/src/uts/intel/stream_test/Makefile b/usr/src/uts/intel/stream_test/Makefile
new file mode 100644
index 0000000000..2d2ea24fdb
--- /dev/null
+++ b/usr/src/uts/intel/stream_test/Makefile
@@ -0,0 +1,41 @@
+#
+# This file and its contents are supplied under the terms of the
+# Common Development and Distribution License ("CDDL"), version 1.0.
+# You may only use this file in accordance with the terms of version
+# 1.0 of the CDDL.
+#
+# A full copy of the text of the CDDL should have accompanied this
+# source.  A copy of the CDDL is also available via the Internet at
+# http://www.illumos.org/license/CDDL.
+#
+
+#
+# Copyright 2023 Oxide Computer Company
+#
+
+UTSBASE	= ../..
+
+MODULE		= stream_test
+OBJECTS		= $(STREAM_TEST_OBJS:%=$(OBJS_DIR)/%)
+ROOTMODULE	= $(USR_KTEST_DIR)/$(MODULE)
+
+include $(UTSBASE)/intel/Makefile.intel
+
+ALL_TARGET	= $(BINARY)
+INSTALL_TARGET	= $(BINARY) $(ROOTMODULE)
+
+LDFLAGS		+= -N drv/ktest
+
+.KEEP_STATE:
+
+def:           $(DEF_DEPS)
+
+all:           $(ALL_DEPS)
+
+clean:         $(CLEAN_DEPS)
+
+clobber:       $(CLOBBER_DEPS)
+
+install:       $(INSTALL_DEPS)
+
+include $(UTSBASE)/intel/Makefile.targ
-- 
2.44.0

