The bhyveload(8) command does not have a native non-interactive mode.
It means that in case of errors, e.g. invalid boot media, it
just drops into a loader prompt and waits for user input. This behaviour
makes it tricky for users to understand what's going on.

To address that, run it with the timeout(1) tool which sends SIGTERM
after a certain timeout, and then optionally sends SIGKILL if the
command keeps hanging.

These timeout values could be configured in the bhyve.conf. Setting
timeout to 0 mean that bhyveload(8) will be executed directly, without
timeout(1).

Signed-off-by: Roman Bogorodskiy <bogorods...@gmail.com>
---
 src/bhyve/bhyve.conf                          |  9 +++++++
 src/bhyve/bhyve_command.c                     | 25 ++++++++++++++++---
 src/bhyve/bhyve_conf.c                        | 12 +++++++++
 src/bhyve/bhyve_utils.h                       |  3 +++
 src/bhyve/libvirtd_bhyve.aug                  |  4 ++-
 src/bhyve/test_libvirtd_bhyve.aug.in          |  2 ++
 .../bhyvexml2argv-bhyveload-timeout.args      | 10 ++++++++
 .../bhyvexml2argv-bhyveload-timeout.ldargs    |  7 ++++++
 .../bhyvexml2argv-bhyveload-timeout.xml       | 23 +++++++++++++++++
 tests/bhyvexml2argvtest.c                     |  6 +++++
 10 files changed, 96 insertions(+), 5 deletions(-)
 create mode 100644 tests/bhyvexml2argvdata/bhyvexml2argv-bhyveload-timeout.args
 create mode 100644 
tests/bhyvexml2argvdata/bhyvexml2argv-bhyveload-timeout.ldargs
 create mode 100644 tests/bhyvexml2argvdata/bhyvexml2argv-bhyveload-timeout.xml

diff --git a/src/bhyve/bhyve.conf b/src/bhyve/bhyve.conf
index 2a8baacff3..dc8d3d8fd8 100644
--- a/src/bhyve/bhyve.conf
+++ b/src/bhyve/bhyve.conf
@@ -5,3 +5,12 @@
 # Path to a directory with firmware files. By default it's pointing
 # to the directory that sysutils/bhyve-firmware installs files into.
 #firmware_dir = "/usr/local/share/uefi-firmware"
+
+# Set timeout for the bhyveload(8) command. This might be necessary
+# because in case of errors bhyveload(8) drops to an interactive
+# loader and hangs indefinitely. These timeout values are passed
+# to the timeout(1) command. Please refer to its manual page for more
+# details. When timeout is 0, bhyveload is executed directly.
+# Units are seconds.
+#bhyveload_timeout = 300
+#bhyveload_timeout_kill = 15
diff --git a/src/bhyve/bhyve_command.c b/src/bhyve/bhyve_command.c
index 5757a41e7e..ab6d6e92e4 100644
--- a/src/bhyve/bhyve_command.c
+++ b/src/bhyve/bhyve_command.c
@@ -921,11 +921,28 @@ virAppendBootloaderArgs(virCommand *cmd, virDomainDef 
*def)
 }
 
 static virCommand *
-virBhyveProcessBuildBhyveloadCmd(virDomainDef *def, virDomainDiskDef *disk)
+virBhyveProcessBuildBhyveloadCmd(virDomainDef *def,
+                                 struct _bhyveConn *driver,
+                                 virDomainDiskDef *disk)
 {
     virCommand *cmd;
-
-    cmd = virCommandNew("bhyveload");
+    g_autoptr(virBhyveDriverConfig) cfg = virBhyveDriverGetConfig(driver);
+
+    if (cfg->bhyveloadTimeout > 0) {
+        /* TODO: update bhyve_process.c to interpret timeout(1) exit
+         * codes 124-127 to produce more meaningful error messages */
+        cmd = virCommandNew("timeout");
+        virCommandAddArg(cmd, "--foreground");
+        virCommandAddArg(cmd, "--verbose");
+        if (cfg->bhyveloadTimeoutKill > 0) {
+            virCommandAddArg(cmd, "-k");
+            virCommandAddArgFormat(cmd, "%ds", cfg->bhyveloadTimeoutKill);
+        }
+        virCommandAddArgFormat(cmd, "%ds", cfg->bhyveloadTimeout);
+        virCommandAddArg(cmd, "bhyveload");
+    } else {
+        cmd = virCommandNew("bhyveload");
+    }
 
     if (def->os.bootloaderArgs == NULL) {
         VIR_DEBUG("bhyveload with default arguments");
@@ -1212,7 +1229,7 @@ virBhyveProcessBuildLoadCmd(struct _bhyveConn *driver, 
virDomainDef *def,
         if (disk == NULL)
             return NULL;
 
-        return virBhyveProcessBuildBhyveloadCmd(def, disk);
+        return virBhyveProcessBuildBhyveloadCmd(def, driver, disk);
     } else if (strstr(def->os.bootloader, "grub-bhyve") != NULL) {
         return virBhyveProcessBuildGrubbhyveCmd(def, driver, devmap_file,
                                                 devicesmap_out);
diff --git a/src/bhyve/bhyve_conf.c b/src/bhyve/bhyve_conf.c
index f18b24f91d..182e00ee1d 100644
--- a/src/bhyve/bhyve_conf.c
+++ b/src/bhyve/bhyve_conf.c
@@ -2,6 +2,7 @@
  * bhyve_conf.c: bhyve config file
  *
  * Copyright (C) 2017 Roman Bogorodskiy
+ * Copyright (C) 2025 The FreeBSD Foundation
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -60,6 +61,9 @@ virBhyveDriverConfigNew(void)
     cfg->libDir = g_strdup_printf("%s/lib/libvirt/bhyve", LOCALSTATEDIR);
     cfg->nvramDir = g_strdup_printf("%s/nvram", cfg->libDir);
 
+    cfg->bhyveloadTimeout = 300;
+    cfg->bhyveloadTimeoutKill = 15;
+
     return cfg;
 }
 
@@ -81,6 +85,14 @@ virBhyveLoadDriverConfig(struct _virBhyveDriverConfig *cfg,
                               &cfg->firmwareDir) < 0)
         return -1;
 
+    if (virConfGetValueInt(conf, "bhyveload_timeout",
+                           &cfg->bhyveloadTimeout) < 0)
+        return -1;
+
+    if (virConfGetValueInt(conf, "bhyveload_timeout_kill",
+                           &cfg->bhyveloadTimeoutKill) < 0)
+        return -1;
+
     return 0;
 }
 
diff --git a/src/bhyve/bhyve_utils.h b/src/bhyve/bhyve_utils.h
index 9c9ea0a01a..8ed1fa5509 100644
--- a/src/bhyve/bhyve_utils.h
+++ b/src/bhyve/bhyve_utils.h
@@ -41,6 +41,9 @@ struct _virBhyveDriverConfig {
     char *firmwareDir;
     char *libDir;
     char *nvramDir;
+
+    int bhyveloadTimeout;
+    int bhyveloadTimeoutKill;
 };
 
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(virBhyveDriverConfig, virObjectUnref);
diff --git a/src/bhyve/libvirtd_bhyve.aug b/src/bhyve/libvirtd_bhyve.aug
index b6bee261a6..0fd74d4bb3 100644
--- a/src/bhyve/libvirtd_bhyve.aug
+++ b/src/bhyve/libvirtd_bhyve.aug
@@ -23,9 +23,11 @@ module Libvirtd_bhyve =
    let str_array_entry (kw:string) = [ key kw . value_sep . str_array_val ]
 
    let log_entry = str_entry "firmware_dir"
+   let bhyveload_timeout = int_entry "bhyveload_timeout"
+   let bhyveload_timeout_kill = int_entry "bhyveload_timeout_kill"
 
    (* Each entry in the config is one of the following three ... *)
-   let entry = log_entry
+   let entry = log_entry | bhyveload_timeout | bhyveload_timeout_kill
    let comment = [ label "#comment" . del /#[ \t]*/ "# " .  store /([^ 
\t\n][^\n]*)?/ . del /\n/ "\n" ]
    let empty = [ label "#empty" . eol ]
 
diff --git a/src/bhyve/test_libvirtd_bhyve.aug.in 
b/src/bhyve/test_libvirtd_bhyve.aug.in
index ec932b4b11..391648e71f 100644
--- a/src/bhyve/test_libvirtd_bhyve.aug.in
+++ b/src/bhyve/test_libvirtd_bhyve.aug.in
@@ -3,3 +3,5 @@ module Test_libvirtd_bhyve =
 
   test Libvirtd_bhyve.lns get conf =
 { "firmware_dir" = "/usr/local/share/uefi-firmware" }
+{ "bhyveload_timeout" = "300" }
+{ "bhyveload_timeout_kill" = "15" }
diff --git a/tests/bhyvexml2argvdata/bhyvexml2argv-bhyveload-timeout.args 
b/tests/bhyvexml2argvdata/bhyvexml2argv-bhyveload-timeout.args
new file mode 100644
index 0000000000..153a1d5035
--- /dev/null
+++ b/tests/bhyvexml2argvdata/bhyvexml2argv-bhyveload-timeout.args
@@ -0,0 +1,10 @@
+bhyve \
+-c 1 \
+-m 214 \
+-u \
+-H \
+-P \
+-s 0:0,hostbridge \
+-s 2:0,ahci-hd,/tmp/freebsd.img \
+-s 3:0,virtio-net,faketapdev,mac=52:54:00:b9:94:02 \
+bhyve
diff --git a/tests/bhyvexml2argvdata/bhyvexml2argv-bhyveload-timeout.ldargs 
b/tests/bhyvexml2argvdata/bhyvexml2argv-bhyveload-timeout.ldargs
new file mode 100644
index 0000000000..264ae48441
--- /dev/null
+++ b/tests/bhyvexml2argvdata/bhyvexml2argv-bhyveload-timeout.ldargs
@@ -0,0 +1,7 @@
+timeout \
+--foreground \
+--verbose \
+-k 20s 300s bhyveload \
+-m 214 \
+-d /tmp/freebsd.img \
+bhyve
diff --git a/tests/bhyvexml2argvdata/bhyvexml2argv-bhyveload-timeout.xml 
b/tests/bhyvexml2argvdata/bhyvexml2argv-bhyveload-timeout.xml
new file mode 100644
index 0000000000..0b8066733d
--- /dev/null
+++ b/tests/bhyvexml2argvdata/bhyvexml2argv-bhyveload-timeout.xml
@@ -0,0 +1,23 @@
+<domain type='bhyve'>
+  <name>bhyve</name>
+  <uuid>df3be7e7-a104-11e3-aeb0-50e5492bd3dc</uuid>
+  <memory>219136</memory>
+  <vcpu>1</vcpu>
+  <os>
+    <type>hvm</type>
+  </os>
+  <devices>
+    <disk type='file'>
+      <driver name='file' type='raw'/>
+      <source file='/tmp/freebsd.img'/>
+      <target dev='hda' bus='sata'/>
+      <address type='drive' controller='0' bus='0' target='2' unit='0'/>
+    </disk>
+    <interface type='bridge'>
+      <mac address='52:54:00:b9:94:02'/>
+      <model type='virtio'/>
+      <source bridge="virbr0"/>
+      <address type='pci' domain='0x0000' bus='0x00' slot='0x03' 
function='0x0'/>
+    </interface>
+  </devices>
+</domain>
diff --git a/tests/bhyvexml2argvtest.c b/tests/bhyvexml2argvtest.c
index 2838b20c29..cc6b17233d 100644
--- a/tests/bhyvexml2argvtest.c
+++ b/tests/bhyvexml2argvtest.c
@@ -165,6 +165,8 @@ mymain(void)
 
     driver.config->firmwareDir = fakefirmwaredir;
     driver.config->nvramDir = fakenvramdir;
+    driver.config->bhyveloadTimeout = 0;
+    driver.config->bhyveloadTimeoutKill = 0;
 
 # define DO_TEST_FULL(name, flags) \
     do { \
@@ -305,6 +307,10 @@ mymain(void)
     driver.bhyvecaps &= ~BHYVE_CAP_VNC_PASSWORD;
     DO_TEST_FAILURE("vnc-password");
 
+    driver.config->bhyveloadTimeout = 300;
+    driver.config->bhyveloadTimeoutKill = 20;
+    DO_TEST("bhyveload-timeout");
+
     virObjectUnref(driver.caps);
     virObjectUnref(driver.xmlopt);
     virPortAllocatorRangeFree(driver.remotePorts);
-- 
2.49.0

Reply via email to