git: e809c7d810ce - main - bhyve: add command to list config options

From: Roman Bogorodskiy <novel_at_FreeBSD.org>
Date: Sat, 24 Oct 2026 11:44:49 UTC
The branch main has been updated by novel:

URL: https://cgit.FreeBSD.org/src/commit/?id=e809c7d810ced2307741a7c0c135a8b1bef1851b

commit e809c7d810ced2307741a7c0c135a8b1bef1851b
Author:     Roman Bogorodskiy <novel@FreeBSD.org>
AuthorDate: 2026-07-22 16:08:09 +0000
Commit:     Roman Bogorodskiy <novel@FreeBSD.org>
CommitDate: 2026-10-24 11:01:53 +0000

    bhyve: add command to list config options
    
    For a software built on top of bhyve(8) and/or managing bhyve(8),
    it is very convenient to know what specific features the given binary supports.
    Currently, bhyve(8) provides some of this information, and it could be
    retrieved by parsing `bhyve -h`, `bhyve -s help`, `bhyve -l help`.
    
    Sometimes it is not enough, for example, the recently added
    "oemstrings" feature cannot be fully probed using these commands.
    Over time, some workarounds were suggested. Specifically:
    
     - Mapping features to __FreeBSD_version. I see at least two issues with
       that:
    
         * __FreeBSD_version does not directly map to bhyve(8) features,
           so this is simply not accurate.
         * bhyve(8) might be updated separately from the system, which
           is not so uncommon, especially for development purposes.
           Then such a probing will be inaccurate.
    
     - Manual pages parsing. I see more disadvantages with that:
    
         * Manual pages are meant to be read by a human. Extracting
           information from it by a machine is not the most convenient thing.
           Especially understanding side notes like: "Note, feature 'foo'
           is arm64-only!".
         * Manual pages might be inaccurate, they might miss options.
         * bhyve(8) could be used in an environment where manual pages
           are not installed.
    
    This commit attempts to close this gap by introducing the "bhyve -o
    help" command which prints all the supported configuration variables.
    In my opinion, it is, in combination with the other commands mentioned
    above, sufficient to retrieve most of the capabilities of the given
    bhyve(8) binary.
    
    The current implementation defines a simple configuration schema for
    common and machdep code and prints out a list of supported configuration
    variables for the current architecture.
    
    The next step as I see it would be extending set_config*() functions
    to validate the user provided values against the schema, so users get
    notified early on incorrect or misspelled configuration variables.
    It would also be nice to allow users to only validate their
    configuration, think of 'nginx configtest / -t'.
    
    Reviewed by:    chuck (previous revision), bnovkov
    MFC after:      1 month
    MFC to:         stable/15
    Sponsored by:   The FreeBSD Foundation
    Differential Revision:  https://reviews.freebsd.org/D58419
---
 usr.sbin/bhyve/Makefile                   |   1 +
 usr.sbin/bhyve/aarch64/bhyverun_machdep.c |  22 ++++
 usr.sbin/bhyve/amd64/bhyverun_machdep.c   |  95 ++++++++++++++
 usr.sbin/bhyve/bhyve.8                    |   6 +-
 usr.sbin/bhyve/block_if.c                 |  17 +++
 usr.sbin/bhyve/block_if.h                 |   3 +
 usr.sbin/bhyve/config_schema.c            | 202 ++++++++++++++++++++++++++++++
 usr.sbin/bhyve/config_schema.h            |  40 ++++++
 usr.sbin/bhyve/pci_nvme.c                 |  20 +++
 usr.sbin/bhyve/riscv/bhyverun_machdep.c   |  18 +++
 10 files changed, 423 insertions(+), 1 deletion(-)

diff --git a/usr.sbin/bhyve/Makefile b/usr.sbin/bhyve/Makefile
index eb7cfe7b602e..0fb0e1028398 100644
--- a/usr.sbin/bhyve/Makefile
+++ b/usr.sbin/bhyve/Makefile
@@ -25,6 +25,7 @@ SRCS=	\
 	block_if.c		\
 	bootrom.c		\
 	config.c		\
+	config_schema.c		\
 	console.c		\
 	crc16.c			\
 	ctl_nvme_all.c		\
diff --git a/usr.sbin/bhyve/aarch64/bhyverun_machdep.c b/usr.sbin/bhyve/aarch64/bhyverun_machdep.c
index f38c719a71a7..72194dd0e269 100644
--- a/usr.sbin/bhyve/aarch64/bhyverun_machdep.c
+++ b/usr.sbin/bhyve/aarch64/bhyverun_machdep.c
@@ -46,6 +46,7 @@
 #include "bhyve_machdep.h"
 #include "bhyverun.h"
 #include "config.h"
+#include "config_schema.h"
 #include "debug.h"
 #include "fdt.h"
 #include "mem.h"
@@ -55,6 +56,23 @@
 #include "rtc_pl031.h"
 #include "uart_emul.h"
 
+static const struct config_option config_options_machdep[] = {
+	{ "gdb.address", CONFIG_OPTION_STRING },
+	{ "gdb.port", CONFIG_OPTION_INTEGER },
+	{ "gdb.wait", CONFIG_OPTION_BOOL },
+	{ "rtc.use_localtime", CONFIG_OPTION_BOOL },
+	{ "console", CONFIG_OPTION_STRING },
+	{ "fdt.bootargs", CONFIG_OPTION_STRING },
+};
+
+void
+config_schema_merge_options_machdep(struct config_schema *schema)
+{
+
+	config_schema_merge_options(schema, NULL, config_options_machdep,
+	    nitems(config_options_machdep));
+}
+
 /* Start of mem + 1M */
 #define	FDT_BASE	0x100000
 #define	FDT_SIZE	(64 * 1024)
@@ -161,6 +179,10 @@ bhyve_optparse(int argc, char **argv)
 			set_config_bool("monitor.no_reboot", true);
 			break;
 		case 'o':
+			if (strcmp(optarg, "help") == 0) {
+				print_config_options();
+				exit(0);
+			}
 			if (!bhyve_parse_config_option(optarg)) {
 				errx(EX_USAGE,
 				    "invalid configuration option '%s'",
diff --git a/usr.sbin/bhyve/amd64/bhyverun_machdep.c b/usr.sbin/bhyve/amd64/bhyverun_machdep.c
index 3d498879cd58..94fda0cd212a 100644
--- a/usr.sbin/bhyve/amd64/bhyverun_machdep.c
+++ b/usr.sbin/bhyve/amd64/bhyverun_machdep.c
@@ -26,10 +26,13 @@
  * SUCH DAMAGE.
  */
 
+#include <sys/param.h>
+
 #include <assert.h>
 #include <err.h>
 #include <stdbool.h>
 #include <stdlib.h>
+#include <string.h>
 #include <sysexits.h>
 
 #include <vmmapi.h>
@@ -42,6 +45,7 @@
 #endif
 #include "bootrom.h"
 #include "config.h"
+#include "config_schema.h"
 #include "debug.h"
 #include "e820.h"
 #include "fwctl.h"
@@ -57,6 +61,93 @@
 #include "smbiostbl.h"
 #include "xmsr.h"
 
+static const struct config_option config_options_machdep[] = {
+	{ "bootvars", CONFIG_OPTION_STRING },
+	{ "acpi_tables", CONFIG_OPTION_BOOL },
+	{ "acpi_tables_in_memory", CONFIG_OPTION_BOOL },
+
+	{ "gdb.address", CONFIG_OPTION_STRING },
+	{ "gdb.port", CONFIG_OPTION_INTEGER },
+	{ "gdb.wait", CONFIG_OPTION_BOOL },
+	{ "keyboard.layout", CONFIG_OPTION_STRING },
+	{ "rtc.use_localtime", CONFIG_OPTION_BOOL },
+
+	{ "bios.vendor", CONFIG_OPTION_STRING },
+	{ "bios.version", CONFIG_OPTION_STRING },
+	{ "bios.release_date", CONFIG_OPTION_STRING },
+	{ "system.family_name", CONFIG_OPTION_STRING },
+	{ "system.manufacturer", CONFIG_OPTION_STRING },
+	{ "system.product_name", CONFIG_OPTION_STRING },
+	{ "system.serial_number", CONFIG_OPTION_STRING },
+	{ "system.sku", CONFIG_OPTION_STRING },
+	{ "system.version", CONFIG_OPTION_STRING },
+	{ "board.manufacturer", CONFIG_OPTION_STRING },
+	{ "board.product_name", CONFIG_OPTION_STRING },
+	{ "board.version", CONFIG_OPTION_STRING },
+	{ "board.serial_number", CONFIG_OPTION_STRING },
+	{ "board.asset_tag", CONFIG_OPTION_STRING },
+	{ "board.location", CONFIG_OPTION_STRING },
+	{ "chassis.manufacturer", CONFIG_OPTION_STRING },
+	{ "chassis.version", CONFIG_OPTION_STRING },
+	{ "chassis.serial_number", CONFIG_OPTION_STRING },
+	{ "chassis.asset_tag", CONFIG_OPTION_STRING },
+	{ "chassis.sku", CONFIG_OPTION_STRING },
+	{ "oemstring.N", CONFIG_OPTION_STRING },
+
+	{ "x86.mptable", CONFIG_OPTION_BOOL },
+	{ "x86.x2apic", CONFIG_OPTION_BOOL },
+	{ "x86.strictio", CONFIG_OPTION_BOOL },
+	{ "x86.strictmsr", CONFIG_OPTION_BOOL },
+	{ "x86.verbosemsr", CONFIG_OPTION_BOOL },
+	{ "x86.vmexit_on_hlt", CONFIG_OPTION_BOOL },
+	{ "x86.vmexit_on_pause", CONFIG_OPTION_BOOL },
+
+	{ "pci.B.S.F.wait", CONFIG_OPTION_BOOL },
+	{ "pci.B.S.F.rfb", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.vga", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.w", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.h", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.password", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.tcp", CONFIG_OPTION_STRING },
+
+	{ "lpc.com1.path", CONFIG_OPTION_STRING },
+	{ "lpc.com1.tcp", CONFIG_OPTION_STRING },
+	{ "lpc.com2.path", CONFIG_OPTION_STRING },
+	{ "lpc.com2.tcp", CONFIG_OPTION_STRING },
+	{ "lpc.com3.path", CONFIG_OPTION_STRING },
+	{ "lpc.com3.tcp", CONFIG_OPTION_STRING },
+	{ "lpc.com4.path", CONFIG_OPTION_STRING },
+	{ "lpc.com4.tcp", CONFIG_OPTION_STRING },
+	{ "lpc.fwcfg", CONFIG_OPTION_STRING },
+	{ "lpc.pc-testdev", CONFIG_OPTION_BOOL },
+	{ "lpc.pcireg.*", CONFIG_OPTION_INTEGER },
+	{ "lpc.pcireg.vendor", CONFIG_OPTION_INTEGER },
+	{ "lpc.pcireg.device", CONFIG_OPTION_INTEGER },
+	{ "lpc.pcireg.revid", CONFIG_OPTION_INTEGER },
+	{ "lpc.pcireg.subvendor", CONFIG_OPTION_INTEGER },
+	{ "lpc.pcireg.subdevice", CONFIG_OPTION_INTEGER },
+
+	{ "tpm.path", CONFIG_OPTION_STRING },
+	{ "tpm.type", CONFIG_OPTION_STRING },
+	{ "tpm.version", CONFIG_OPTION_STRING },
+	{ "tpm.intf", CONFIG_OPTION_STRING },
+	{ "tpm.ppi", CONFIG_OPTION_STRING },
+
+	{ "pci.B.S.F.bus", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.slot", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.func", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.pptdev", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.rom", CONFIG_OPTION_STRING },
+};
+
+void
+config_schema_merge_options_machdep(struct config_schema *schema)
+{
+
+	config_schema_merge_options(schema, NULL, config_options_machdep,
+	    nitems(config_options_machdep));
+}
+
 void
 bhyve_init_config(void)
 {
@@ -219,6 +310,10 @@ bhyve_optparse(int argc, char **argv)
 			set_config_bool("monitor.no_reboot", true);
 			break;
 		case 'o':
+			if (strcmp(optarg, "help") == 0) {
+				print_config_options();
+				exit(0);
+			}
 			if (!bhyve_parse_config_option(optarg)) {
 				errx(EX_USAGE,
 				    "invalid configuration option '%s'",
diff --git a/usr.sbin/bhyve/bhyve.8 b/usr.sbin/bhyve/bhyve.8
index 14d91e0711d1..c5b6f538f0ae 100644
--- a/usr.sbin/bhyve/bhyve.8
+++ b/usr.sbin/bhyve/bhyve.8
@@ -26,7 +26,7 @@
 .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 .\" SUCH DAMAGE.
 .\"
-.Dd October 3, 2026
+.Dd October 24, 2026
 .Dt BHYVE 8
 .Os
 .Sh NAME
@@ -92,6 +92,8 @@
 .Nm
 .Fl l Cm help
 .Nm
+.Fl o Cm help
+.Nm
 .Fl s Cm help
 .Sh DESCRIPTION
 .Nm
@@ -352,6 +354,8 @@ This option has no effect unless the
 configuration option is enabled or
 .Fl M
 was specified.
+.It Fl o Cm help
+Print a list of supported configuration variables.
 .It Fl o Ar var Ns Cm = Ns Ar value
 Set the configuration variable
 .Ar var
diff --git a/usr.sbin/bhyve/block_if.c b/usr.sbin/bhyve/block_if.c
index 0a4f76ae197d..311809f0c0e5 100644
--- a/usr.sbin/bhyve/block_if.c
+++ b/usr.sbin/bhyve/block_if.c
@@ -57,6 +57,7 @@
 
 #include "bhyverun.h"
 #include "config.h"
+#include "config_schema.h"
 #include "debug.h"
 #include "mevent.h"
 #include "pci_emul.h"
@@ -67,6 +68,22 @@
 #define BLOCKIF_NUMTHR	8
 #define BLOCKIF_MAXREQ	(BLOCKIF_RING_MAX + BLOCKIF_NUMTHR)
 
+static const struct config_option blockif_config_options[] = {
+	{ "path", CONFIG_OPTION_STRING },
+	{ "nocache", CONFIG_OPTION_BOOL },
+	{ "nodelete", CONFIG_OPTION_BOOL },
+	{ "sync", CONFIG_OPTION_BOOL },
+	{ "direct", CONFIG_OPTION_BOOL },
+	{ "ro", CONFIG_OPTION_BOOL },
+	{ "sectorsize", CONFIG_OPTION_STRING },
+	{ "bootindex", CONFIG_OPTION_INTEGER },
+};
+
+const struct config_option_set blockif_config_option_set = {
+	.options = blockif_config_options,
+	.count = nitems(blockif_config_options),
+};
+
 enum blockop {
 	BOP_READ,
 	BOP_WRITE,
diff --git a/usr.sbin/bhyve/block_if.h b/usr.sbin/bhyve/block_if.h
index 9a1a65a1bfe9..afe56570035b 100644
--- a/usr.sbin/bhyve/block_if.h
+++ b/usr.sbin/bhyve/block_if.h
@@ -40,8 +40,11 @@
 #include <sys/uio.h>
 #include <sys/unistd.h>
 
+struct config_option_set;
 struct vm_snapshot_meta;
 
+extern const struct config_option_set blockif_config_option_set;
+
 
 /*
  * BLOCKIF_IOV_MAX is the maximum number of scatter/gather entries in
diff --git a/usr.sbin/bhyve/config_schema.c b/usr.sbin/bhyve/config_schema.c
new file mode 100644
index 000000000000..d73803b6cb77
--- /dev/null
+++ b/usr.sbin/bhyve/config_schema.c
@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2026 The FreeBSD Foundation
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/cdefs.h>
+#include <sys/param.h>
+#include <sys/tree.h>
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+
+#include "config_schema.h"
+
+SET_DECLARE(config_option_set, struct config_option_set);
+
+struct config_schema_option {
+	RB_ENTRY(config_schema_option) entry;
+	char *name;
+	enum config_option_type type;
+};
+
+RB_HEAD(config_schema, config_schema_option);
+
+static int
+config_schema_option_compare(struct config_schema_option *a,
+    struct config_schema_option *b)
+{
+
+	return (strcmp(a->name, b->name));
+}
+
+RB_GENERATE_STATIC(config_schema, config_schema_option, entry,
+    config_schema_option_compare);
+
+static const struct config_option config_options[] = {
+	{ "name", CONFIG_OPTION_STRING },
+	{ "cpus", CONFIG_OPTION_INTEGER },
+	{ "cores", CONFIG_OPTION_INTEGER },
+	{ "threads", CONFIG_OPTION_INTEGER },
+	{ "sockets", CONFIG_OPTION_INTEGER },
+	{ "memory.guest_in_core", CONFIG_OPTION_BOOL },
+	{ "memory.size", CONFIG_OPTION_STRING },
+	{ "memory.wired", CONFIG_OPTION_BOOL },
+	{ "bootrom", CONFIG_OPTION_STRING },
+	{ "destroy_on_poweroff", CONFIG_OPTION_BOOL },
+	{ "pci.enable_bars", CONFIG_OPTION_BOOL },
+	{ "uuid", CONFIG_OPTION_STRING },
+	{ "virtio.msix", CONFIG_OPTION_BOOL },
+	{ "config.dump", CONFIG_OPTION_BOOL },
+	{ "rundir", CONFIG_OPTION_STRING },
+	{ "monitor.enabled", CONFIG_OPTION_BOOL },
+	{ "monitor.no_reboot", CONFIG_OPTION_BOOL },
+
+	{ "vcpu.N.cpuset", CONFIG_OPTION_STRING },
+	{ "domains.N.size", CONFIG_OPTION_STRING },
+	{ "domains.N.domain_policy", CONFIG_OPTION_STRING },
+	{ "domains.N.cpus", CONFIG_OPTION_STRING },
+
+	{ "pci.B.S.F.device", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.vendor", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.devid", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.pcireg.*", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.pcireg.vendor", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.pcireg.device", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.port.N.type", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.port.N.path", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.port.N.nocache", CONFIG_OPTION_BOOL },
+	{ "pci.B.S.F.port.N.nodelete", CONFIG_OPTION_BOOL },
+	{ "pci.B.S.F.port.N.sync", CONFIG_OPTION_BOOL },
+	{ "pci.B.S.F.port.N.direct", CONFIG_OPTION_BOOL },
+	{ "pci.B.S.F.port.N.ro", CONFIG_OPTION_BOOL },
+	{ "pci.B.S.F.port.N.sectorsize", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.port.N.nmrr", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.port.N.ser", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.port.N.rev", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.port.N.model", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.port.N.bootindex", CONFIG_OPTION_INTEGER },
+
+	{ "pci.B.S.F.backend", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.type", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.peerhook", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.socket", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.hook", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.hostfwd", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.mac", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.mtu", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.play", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.rec", CONFIG_OPTION_STRING },
+
+	{ "pci.B.S.F.serial", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.sharename", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.port.N.name", CONFIG_OPTION_STRING },
+
+	{ "pci.B.S.F.ctl_ringsz", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.evt_ringsz", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.req_ringsz", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.num_queues", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.seg_max", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.thr_per_q", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.target.N", CONFIG_OPTION_STRING },
+	{ "pci.B.S.F.iid", CONFIG_OPTION_INTEGER },
+	{ "pci.B.S.F.slot.N.device", CONFIG_OPTION_STRING },
+};
+
+static const char *
+config_option_type_name(enum config_option_type type)
+{
+
+	switch (type) {
+	case CONFIG_OPTION_BOOL:
+		return ("bool");
+	case CONFIG_OPTION_INTEGER:
+		return ("integer");
+	case CONFIG_OPTION_STRING:
+		return ("string");
+	}
+	errx(EX_SOFTWARE, "invalid configuration option type");
+}
+
+void
+config_schema_merge_options(struct config_schema *schema, const char *prefix,
+    const struct config_option *options, size_t count)
+{
+	struct config_schema_option *existing, *option;
+	char *name;
+	size_t i;
+
+	if (prefix == NULL)
+		prefix = "";
+	for (i = 0; i < count; i++) {
+		if (asprintf(&name, "%s%s", prefix, options[i].name) == -1)
+			err(EX_OSERR, "asprintf");
+		option = malloc(sizeof(*option));
+		if (option == NULL)
+			err(EX_OSERR, "malloc");
+		option->name = name;
+		option->type = options[i].type;
+
+		existing = RB_INSERT(config_schema, schema, option);
+		if (existing == NULL)
+			continue;
+		if (existing->type != option->type) {
+			errx(EX_SOFTWARE,
+			    "conflicting types for configuration option "
+			    "'%s': %s and %s", option->name,
+			    config_option_type_name(existing->type),
+			    config_option_type_name(option->type));
+		}
+		free(option->name);
+		free(option);
+	}
+}
+
+static void
+config_schema_merge_option_set(struct config_schema *schema,
+    const struct config_option_set *set, const char *inherited_prefix)
+{
+	const char *prefix;
+
+	prefix = set->prefix != NULL ? set->prefix : inherited_prefix;
+	if (set->base != NULL)
+		config_schema_merge_option_set(schema, set->base, prefix);
+	config_schema_merge_options(schema, prefix, set->options, set->count);
+}
+
+static void
+config_schema_destroy(struct config_schema *schema)
+{
+	struct config_schema_option *option, *tmp;
+
+	RB_FOREACH_SAFE(option, config_schema, schema, tmp) {
+		RB_REMOVE(config_schema, schema, option);
+		free(option->name);
+		free(option);
+	}
+}
+
+void
+print_config_options(void)
+{
+	struct config_schema_option *option;
+	struct config_option_set **setp;
+	struct config_schema schema;
+
+	RB_INIT(&schema);
+	config_schema_merge_options(&schema, NULL, config_options,
+	    nitems(config_options));
+	SET_FOREACH(setp, config_option_set) {
+		config_schema_merge_option_set(&schema, *setp, NULL);
+	}
+	config_schema_merge_options_machdep(&schema);
+
+	RB_FOREACH(option, config_schema, &schema) {
+		printf("%s\n", option->name);
+	}
+	config_schema_destroy(&schema);
+}
diff --git a/usr.sbin/bhyve/config_schema.h b/usr.sbin/bhyve/config_schema.h
new file mode 100644
index 000000000000..2a6890ac6675
--- /dev/null
+++ b/usr.sbin/bhyve/config_schema.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2026 The FreeBSD Foundation
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#ifndef __CONFIG_SCHEMA_H__
+#define __CONFIG_SCHEMA_H__
+
+#include <sys/linker_set.h>
+#include <sys/types.h>
+
+enum config_option_type {
+	CONFIG_OPTION_BOOL,
+	CONFIG_OPTION_INTEGER,
+	CONFIG_OPTION_STRING,
+};
+
+struct config_option {
+	const char *name;
+	enum config_option_type type;
+};
+
+struct config_option_set {
+	const char *prefix;
+	const struct config_option_set *base;
+	const struct config_option *options;
+	size_t count;
+};
+
+struct config_schema;
+
+#define	CONFIG_OPTION_SET(x)	DATA_SET(config_option_set, x)
+
+void	config_schema_merge_options(struct config_schema *, const char *,
+	    const struct config_option *, size_t);
+void	config_schema_merge_options_machdep(struct config_schema *);
+void	print_config_options(void);
+
+#endif /* !__CONFIG_SCHEMA_H__ */
diff --git a/usr.sbin/bhyve/pci_nvme.c b/usr.sbin/bhyve/pci_nvme.c
index 7e6768de8550..50ad8ea21f88 100644
--- a/usr.sbin/bhyve/pci_nvme.c
+++ b/usr.sbin/bhyve/pci_nvme.c
@@ -55,6 +55,7 @@
 
 #include <sys/cdefs.h>
 #include <sys/errno.h>
+#include <sys/param.h>
 #include <sys/types.h>
 #include <sys/crc16.h>
 #include <net/ieee_oui.h>
@@ -79,9 +80,28 @@
 #include "bhyverun.h"
 #include "block_if.h"
 #include "config.h"
+#include "config_schema.h"
 #include "debug.h"
 #include "pci_emul.h"
 
+static const struct config_option pci_nvme_config_options[] = {
+	{ "maxq", CONFIG_OPTION_INTEGER },
+	{ "qsz", CONFIG_OPTION_INTEGER },
+	{ "ioslots", CONFIG_OPTION_INTEGER },
+	{ "sectsz", CONFIG_OPTION_INTEGER },
+	{ "ser", CONFIG_OPTION_STRING },
+	{ "eui64", CONFIG_OPTION_INTEGER },
+	{ "dsm", CONFIG_OPTION_STRING },
+	{ "ram", CONFIG_OPTION_INTEGER },
+};
+
+static const struct config_option_set pci_nvme_config_option_set = {
+	.prefix = "pci.B.S.F.",
+	.base = &blockif_config_option_set,
+	.options = pci_nvme_config_options,
+	.count = nitems(pci_nvme_config_options),
+};
+CONFIG_OPTION_SET(pci_nvme_config_option_set);
 
 static int nvme_debug = 0;
 #define	DPRINTF(fmt, args...) if (nvme_debug) PRINTLN(fmt, ##args)
diff --git a/usr.sbin/bhyve/riscv/bhyverun_machdep.c b/usr.sbin/bhyve/riscv/bhyverun_machdep.c
index 50e68c958ab3..41adbbd51fc1 100644
--- a/usr.sbin/bhyve/riscv/bhyverun_machdep.c
+++ b/usr.sbin/bhyve/riscv/bhyverun_machdep.c
@@ -49,6 +49,7 @@
 
 #include "bhyverun.h"
 #include "config.h"
+#include "config_schema.h"
 #include "debug.h"
 #include "fdt.h"
 #include "ipc.h"
@@ -58,6 +59,19 @@
 #include "uart_emul.h"
 #include "riscv.h"
 
+static const struct config_option config_options_machdep[] = {
+	{ "console", CONFIG_OPTION_STRING },
+	{ "fdt.bootargs", CONFIG_OPTION_STRING },
+};
+
+void
+config_schema_merge_options_machdep(struct config_schema *schema)
+{
+
+	config_schema_merge_options(schema, NULL, config_options_machdep,
+	    nitems(config_options_machdep));
+}
+
 #define	FDT_SIZE	(64 * 1024)
 #define	FDT_DTB_ALIGN	8
 
@@ -152,6 +166,10 @@ bhyve_optparse(int argc, char **argv)
 			set_config_bool("monitor.no_reboot", true);
 			break;
 		case 'o':
+			if (strcmp(optarg, "help") == 0) {
+				print_config_options();
+				exit(0);
+			}
 			if (!bhyve_parse_config_option(optarg)) {
 				errx(EX_USAGE,
 				    "invalid configuration option '%s'",