This patch implements the harness for hotplugging devices under the
QEMU driver. The fuzzer exercises both attachment and detachment of
various devices. It uses its own proto file xml_hotplug to fuzz only
one device type at a time.
Monitor setup is done such that QMP commands are processed
indefinitely and removal wait times are eliminated. A table of
dummy responses is referred to for generating QMP responses.
A semi-hardcoded domain XML is used to hotplug and unhotplug device XMLs
against it. It consists of various controllers to allow successful hotplugging.
Depending on the architecture selected, some controllers may be omitted or
modified.
LeakSanitizer slows down the fuzzer severely, so we disable it in
run_fuzz for now.
Signed-off-by: Rayhan Faizel <rayhan.faizel@gmail.com>
---
src/conf/domain_conf.h | 2 +
src/qemu/qemu_hotplug.c | 4 +
tests/fuzz/meson.build | 6 +
tests/fuzz/proto_header_common.h | 4 +
tests/fuzz/protos/meson.build | 1 +
tests/fuzz/protos/xml_hotplug.proto | 38 ++++
tests/fuzz/qemu_xml_hotplug_fuzz.cc | 340 ++++++++++++++++++++++++++++
tests/fuzz/run_fuzz.in | 5 +
tests/qemumonitortestutils.c | 48 ++++
tests/qemumonitortestutils.h | 6 +
10 files changed, 454 insertions(+)
create mode 100644 tests/fuzz/protos/xml_hotplug.proto
create mode 100644 tests/fuzz/qemu_xml_hotplug_fuzz.cc
diff --git a/src/conf/domain_conf.h b/src/conf/domain_conf.h
index 3e97dd6293..1327604fbd 100644
--- a/src/conf/domain_conf.h
+++ b/src/conf/domain_conf.h
@@ -3288,6 +3288,8 @@ struct _virDomainObj {
int taint;
size_t ndeprecations;
char **deprecations;
+
+ bool fuzz;
};
G_DEFINE_AUTOPTR_CLEANUP_FUNC(virDomainObj, virObjectUnref);
diff --git a/src/qemu/qemu_hotplug.c b/src/qemu/qemu_hotplug.c
index 75b97cf736..a4a5a32346 100644
--- a/src/qemu/qemu_hotplug.c
+++ b/src/qemu/qemu_hotplug.c
@@ -5455,6 +5455,10 @@ qemuDomainWaitForDeviceRemoval(virDomainObj *vm)
qemuDomainObjPrivate *priv = vm->privateData;
unsigned long long until;
+ /* Skip the wait entirely if fuzzing is active */
+ if (vm->fuzz)
+ return 1;
+
if (virTimeMillisNow(&until) < 0)
return 1;
until += qemuDomainGetUnplugTimeout(vm);
diff --git a/tests/fuzz/meson.build b/tests/fuzz/meson.build
index 12f9a719f2..9579f56749 100644
--- a/tests/fuzz/meson.build
+++ b/tests/fuzz/meson.build
@@ -37,6 +37,11 @@ if conf.has('WITH_QEMU')
'proto_to_xml.cc',
]
+ hotplug_src = [
+ 'qemu_xml_hotplug_fuzz.cc',
+ 'proto_to_xml.cc',
+ ]
+
qemu_libs = [
test_qemu_driver_lib,
test_utils_lib,
@@ -48,6 +53,7 @@ if conf.has('WITH_QEMU')
{ 'name': 'qemu_xml_domain_fuzz', 'src': [ fuzzer_src, xml_domain_proto_src ], 'libs': qemu_libs, 'macro': '-DXML_DOMAIN', 'deps': [ fuzz_autogen_xml_domain_dep ] },
{ 'name': 'qemu_xml_domain_fuzz_disk', 'src': [ fuzzer_src, xml_domain_disk_only_proto_src ], 'libs': qemu_libs, 'macro': '-DXML_DOMAIN_DISK_ONLY', 'deps': [ fuzz_autogen_xml_domain_dep ] },
{ 'name': 'qemu_xml_domain_fuzz_interface', 'src': [ fuzzer_src, xml_domain_interface_only_proto_src ], 'libs': qemu_libs, 'macro': '-DXML_DOMAIN_INTERFACE_ONLY', 'deps': [ fuzz_autogen_xml_domain_dep ] },
+ { 'name': 'qemu_xml_hotplug_fuzz', 'src': [ hotplug_src, xml_hotplug_proto_src ], 'libs': [ qemu_libs, test_utils_qemu_monitor_lib ], 'macro': '-DXML_HOTPLUG', 'deps': [ fuzz_autogen_xml_domain_dep ] },
]
endif
diff --git a/tests/fuzz/proto_header_common.h b/tests/fuzz/proto_header_common.h
index 5ee510896d..3f135c48e1 100644
--- a/tests/fuzz/proto_header_common.h
+++ b/tests/fuzz/proto_header_common.h
@@ -35,6 +35,10 @@
#include "xml_domain_interface_only.pb.h"
#endif
+#ifdef XML_HOTPLUG
+#include "xml_hotplug.pb.h"
+#endif
+
#define FUZZ_COMMON_INIT(...) \
if (virErrorInitialize() < 0) \
diff --git a/tests/fuzz/protos/meson.build b/tests/fuzz/protos/meson.build
index 42c3a7f6a9..0731ef1eca 100644
--- a/tests/fuzz/protos/meson.build
+++ b/tests/fuzz/protos/meson.build
@@ -3,6 +3,7 @@ protos = [
'xml_domain.proto',
'xml_domain_disk_only.proto',
'xml_domain_interface_only.proto',
+ 'xml_hotplug.proto',
]
autogen_proto_xml_domain_proto = custom_target('autogen_xml_domain.proto',
diff --git a/tests/fuzz/protos/xml_hotplug.proto b/tests/fuzz/protos/xml_hotplug.proto
new file mode 100644
index 0000000000..0490b2fdb6
--- /dev/null
+++ b/tests/fuzz/protos/xml_hotplug.proto
@@ -0,0 +1,38 @@
+syntax = "proto2";
+
+import "autogen_xml_domain.proto";
+
+package libvirt;
+
+message MainObj {
+ oneof new_device {
+ domainTag.devicesTag.soundTag T_sound = 1;
+ domainTag.devicesTag.filesystemTag T_filesystem = 2;
+ domainTag.devicesTag.inputTag T_input = 3;
+ domainTag.devicesTag.diskTag T_disk = 4;
+ domainTag.devicesTag.interfaceTag T_interface = 5;
+ domainTag.devicesTag.graphicsTag T_graphics = 6;
+ domainTag.devicesTag.serialTag T_serial = 7;
+ domainTag.devicesTag.parallelTag T_parallel = 8;
+ domainTag.devicesTag.channelTag T_channel = 9;
+ domainTag.devicesTag.consoleTag T_console = 10;
+ domainTag.devicesTag.controllerTag T_controller = 11;
+ domainTag.devicesTag.videoTag T_video = 12;
+ domainTag.devicesTag.rngTag T_rng = 13;
+ domainTag.devicesTag.watchdogTag T_watchdog = 14;
+ domainTag.devicesTag.memballoonTag T_memballoon = 15;
+ domainTag.devicesTag.smartcardTag T_smartcard = 16;
+ domainTag.devicesTag.redirdevTag T_redirdev = 17;
+ domainTag.devicesTag.audioTag T_audio = 18;
+ domainTag.devicesTag.cryptoTag T_crypto = 19;
+ domainTag.devicesTag.panicTag T_panic = 20;
+ domainTag.devicesTag.tpmTag T_tpm = 21;
+ domainTag.devicesTag.shmemTag T_shmem = 22;
+ domainTag.devicesTag.hostdevTag T_hostdev = 23;
+ domainTag.devicesTag.leaseTag T_lease = 24;
+ domainTag.devicesTag.redirfilterTag T_redirfilter = 25;
+ domainTag.devicesTag.iommuTag T_iommu = 26;
+ domainTag.devicesTag.vsockTag T_vsock = 27;
+ domainTag.devicesTag.nvramTag T_nvram = 28;
+ }
+}
diff --git a/tests/fuzz/qemu_xml_hotplug_fuzz.cc b/tests/fuzz/qemu_xml_hotplug_fuzz.cc
new file mode 100644
index 0000000000..6e391c1f51
--- /dev/null
+++ b/tests/fuzz/qemu_xml_hotplug_fuzz.cc
@@ -0,0 +1,340 @@
+/*
+ * qemu_xml_hotplug_fuzz.cc: QEMU hotplug fuzzing harness
+ *
+ * Copyright (C) 2024 Rayhan Faizel
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include "proto_header_common.h"
+
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include <libxml/xpath.h>
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdarg.h>
+
+extern "C" {
+#include "qemu/qemu_alias.h"
+#include "qemu/qemu_conf.h"
+#include "qemu/qemu_hotplug.h"
+#include "qemumonitortestutils.h"
+#include "testutils.h"
+#include "testutilsqemu.h"
+#include "testutilsqemuschema.h"
+#include "virhostdev.h"
+#include "virfile.h"
+}
+
+#include "port/protobuf.h"
+#include "proto_to_xml.h"
+#include "src/libfuzzer/libfuzzer_macro.h"
+
+#define QEMU_HOTPLUG_FUZZ_DOMAIN_ID 7
+
+uint64_t device_parse_pass = 0;
+uint64_t device_attach_pass = 0;
+uint64_t success_pass = 0;
+uint64_t detach_success_pass = 0;
+
+bool enable_xml_dump = false;
+
+typedef struct {
+ virQEMUCaps *caps;
+ GHashTable *schema;
+} qemuFuzzHotplugData;
+
+#define QMP_OK "{\"return\": {}}"
+#define QMP_EMPTY_ARRAY "{\"return\": []}"
+
+std::string getBaseDomainXML(std::string arch) {
+ std::string result = "<domain type='qemu'>\n"
+ " <name>MyGuest</name>\n"
+ " <uuid>4dea22b3-1d52-d8f3-2516-782e98ab3fa0</uuid>\n"
+ " <os>\n"
+ " <type arch='" + arch + "'>hvm</type>\n"
+ " </os>\n"
+ " <memory>4096</memory>\n"
+ " <devices>\n";
+
+ if (arch == "aarch64" || arch == "armv7l" ||
+ arch == "riscv64" ||
+ arch == "loongarch64") {
+ result += "<controller type='pci' model='pcie-root'/>\n";
+ } else {
+ result += "<controller type='pci' model='pci-root'/>\n";
+ }
+
+ /* s390x does not support USB */
+ if (arch != "s390x") {
+ result += "<controller type='usb'/>\n";
+ result += "<controller type='ccid'/>\n";
+ }
+
+ /* SATA is not supported on s390x and SPARC */
+ if (arch != "s390x" && arch != "sparc")
+ result += "<controller type='sata'/>\n";
+
+ result += "<controller type='virtio-serial'/>\n";
+ result += "<controller type='scsi'/>\n";
+
+ result += "<emulator>/usr/bin/qemu-system-" + arch + "</emulator>\n"
+ "</devices>\n"
+ "</domain>\n";
+
+ return result;
+}
+
+
+std::string getDeviceDeletedResponse(std::string dev) {
+ std::string result = "{" \
+ " \"timestamp\": {" \
+ " \"seconds\": 1374137171," \
+ " \"microseconds\": 2659" \
+ " }," \
+ " \"event\": \"DEVICE_DELETED\"," \
+ " \"data\": {" \
+ " \"device\": \"" + dev + "\"," \
+ " \"path\": \"/machine/peripheral/" + dev + "\"" \
+ " }" \
+ "}\r\n";
+
+ return result;
+}
+
+
+/* Table of QMP commands and dummy responses */
+std::unordered_map<std::string, std::string> qmp_cmd_table = {
+ {"device_add", QMP_OK},
+ {"object-add", QMP_OK},
+ {"object-del", QMP_OK},
+ {"netdev_add", QMP_OK},
+ {"netdev_del", QMP_OK},
+ {"chardev-add", QMP_OK},
+ {"chardev-remove", QMP_OK},
+ {"blockdev-add", QMP_OK},
+ {"blockdev-del", QMP_OK},
+ {"qom-list", QMP_EMPTY_ARRAY},
+ {"query-block", QMP_EMPTY_ARRAY},
+ {"query-fds", QMP_EMPTY_ARRAY},
+ {"set-action", QMP_OK},
+ {"set_link", QMP_OK},
+ {"query-fdsets", QMP_EMPTY_ARRAY},
+ {"add-fd", "{ \"return\": { \"fdset-id\": 1, \"fd\": 95 }}"},
+ {"block_set_io_throttle", QMP_OK},
+};
+
+
+char *qemuMonitorFuzzGetResponse(const char *command,
+ virJSONValue *cmdargs)
+{
+ if (STREQ(command, "device_del")) {
+ const char *id = virJSONValueObjectGetString(cmdargs, "id");
+ std::string result = getDeviceDeletedResponse(id);
+
+ return g_strdup(result.c_str());
+ }
+
+ if (qmp_cmd_table.find(command) != qmp_cmd_table.end()) {
+ return g_strdup(qmp_cmd_table[command].c_str());
+ }
+
+ /* If QMP command is unknown, assume QMP_OK and warn the user of the same. */
+ printf("[FUZZ WARN]: Unknown QMP command: %s\n", command);
+
+ return g_strdup(QMP_OK);
+}
+
+
+static void
+fuzzXMLHotplug(virQEMUDriver *driver,
+ qemuFuzzHotplugData *hotplugData,
+ const char *domain_xml_string,
+ const char *device_xml_string)
+{
+ virDomainObj *vm = NULL;
+ virDomainDef *def = NULL;
+ virDomainDeviceDef *dev = NULL;
+ virDomainDeviceDef *remove_dev = NULL;
+ qemuDomainObjPrivate *priv = NULL;
+ g_autoptr(qemuMonitorTest) test_mon = NULL;
+ bool attach_success = false;
+
+ if (!(def = virDomainDefParseString(domain_xml_string, driver->xmlopt, NULL,
+ VIR_DOMAIN_DEF_PARSE_INACTIVE))) {
+
+ printf("Failed to parse domain XML!\n");
+ exit(EXIT_FAILURE);
+ }
+
+ if (!(vm = virDomainObjNew(driver->xmlopt)))
+ goto cleanup;
+
+ vm->def = def;
+ priv = (qemuDomainObjPrivate *) vm->privateData;
+ priv->qemuCaps = hotplugData->caps;
+
+ if (qemuDomainAssignAddresses(vm->def, hotplugData->caps,
+ driver, vm, true) < 0) {
+ goto cleanup;
+ }
+
+ if (qemuAssignDeviceAliases(vm->def) < 0)
+ goto cleanup;
+
+ vm->def->id = QEMU_HOTPLUG_FUZZ_DOMAIN_ID;
+ vm->fuzz = true;
+
+ if (qemuDomainSetPrivatePaths(driver, vm) < 0)
+ goto cleanup;
+
+ device_parse_pass++;
+ if (!(dev = virDomainDeviceDefParse(device_xml_string, vm->def,
+ driver->xmlopt, NULL,
+ VIR_DOMAIN_DEF_PARSE_INACTIVE)))
+ goto cleanup;
+
+ /* Initialize test monitor
+ *
+ * Keep it after virDomainDeviceDefParse to avoid wasting time with monitor
+ * creation.
+ */
+
+ if (!(test_mon = qemuMonitorTestNew(driver->xmlopt, vm, NULL, hotplugData->schema)))
+ goto cleanup;
+
+ /* Enable fuzzing mode of the test monitor */
+ qemuMonitorTestFuzzSetup(test_mon, qemuMonitorFuzzGetResponse);
+
+ priv->mon = qemuMonitorTestGetMonitor(test_mon);
+
+ virObjectUnlock(priv->mon);
+
+ device_attach_pass++;
+
+ if (qemuDomainAttachDeviceLive(vm, dev, driver) == 0) {
+ success_pass++;
+ attach_success = true;
+ }
+
+ if (attach_success) {
+ /* The previous virDomainDeviceDefParse cleared out the data in dev, so
+ * we need to reparse it before doing the detachment.
+ */
+ remove_dev = virDomainDeviceDefParse(device_xml_string, vm->def,
+ driver->xmlopt, NULL,
+ 0);
+
+ if (remove_dev && qemuDomainDetachDeviceLive(vm, remove_dev, driver, false) == 0) {
+ detach_success_pass++;
+ }
+ }
+
+ virObjectLock(priv->mon);
+
+ cleanup:
+ if (vm) {
+ priv->qemuCaps = NULL;
+ priv->mon = NULL;
+ vm->def = NULL;
+
+ virDomainObjEndAPI(&vm);
+ }
+
+ virDomainDeviceDefFree(remove_dev);
+ virDomainDeviceDefFree(dev);
+ virDomainDefFree(def);
+}
+
+
+DEFINE_PROTO_FUZZER(const libvirt::MainObj &message)
+{
+ static GHashTable *capscache = virHashNew(virObjectUnref);
+ static GHashTable *capslatest = testQemuGetLatestCaps();
+ static GHashTable *qapiSchemaCache = virHashNew((GDestroyNotify) g_hash_table_unref);
+
+ static qemuFuzzHotplugData *hotplugData = g_new0(qemuFuzzHotplugData, 1);
+
+ static virQEMUDriver driver;
+ static bool initialized = false;
+
+ static const char *arch_env = g_getenv("LPM_FUZZ_ARCH");
+ static const char *dump_xml_env = g_getenv("LPM_XML_DUMP_INPUT");
+
+ static std::string arch = "";
+ static std::string domain_xml = "";
+
+ std::string device_xml = "";
+
+ /*
+ * One-time setup of QEMU driver. Re-running them in every
+ * iteration incurs a significant penalty to the speed of the fuzzer.
+ */
+ if (!initialized) {
+ FUZZ_COMMON_INIT();
+
+ if (qemuTestDriverInit(&driver) < 0)
+ exit(EXIT_FAILURE);
+
+ driver.lockManager = virLockManagerPluginNew("nop", "qemu",
+ driver.config->configBaseDir,
+ 0);
+
+ if (arch_env) {
+ arch = arch_env;
+ } else {
+ arch = "x86_64";
+ }
+
+ if (!(hotplugData->caps = testQemuGetRealCaps(arch.c_str(), "latest", "",
+ capslatest, capscache,
+ qapiSchemaCache, &hotplugData->schema))) {
+ printf("Failed to setup QEMU capabilities (invalid arch?)\n");
+ exit(EXIT_FAILURE);
+ }
+
+ if (qemuTestCapsCacheInsert(driver.qemuCapsCache, hotplugData->caps) < 0)
+ exit(EXIT_FAILURE);
+
+ if (!(driver.hostdevMgr = virHostdevManagerGetDefault()))
+ exit(EXIT_FAILURE);
+
+ virEventRegisterDefaultImpl();
+
+ domain_xml = getBaseDomainXML(arch);
+
+ /* Enable printing of XML to stdout (useful for debugging crashes) */
+ if (dump_xml_env && STREQ(dump_xml_env, "YES"))
+ enable_xml_dump = true;
+
+ initialized = true;
+ }
+
+ convertProtoToXML(message, device_xml);
+
+ if (enable_xml_dump)
+ printf("%s\n", device_xml.c_str());
+
+ fuzzXMLHotplug(&driver, hotplugData, domain_xml.c_str(), device_xml.c_str());
+
+ if (device_parse_pass % 1000 == 0)
+ printf("[FUZZ METRICS] Device parse: %lu, Device Attach: %lu, Attached: %lu, Detached: %lu\n",
+ device_parse_pass, device_attach_pass, success_pass, detach_success_pass);
+}
diff --git a/tests/fuzz/run_fuzz.in b/tests/fuzz/run_fuzz.in
index da3c7935b7..414b99b6cf 100644
--- a/tests/fuzz/run_fuzz.in
+++ b/tests/fuzz/run_fuzz.in
@@ -113,6 +113,11 @@ env["LPM_EXE_PATH"] = exe_path
process_args.extend(["-print_funcs=-1"])
+if args.fuzzer == "qemu_xml_hotplug_fuzz":
+ # LSAN slows down the hotplug fuzzer to a crawl,
+ # so we have to disable LeakSanitizer
+ env["ASAN_OPTIONS"] = "detect_leaks=0"
+
if args.libfuzzer_options:
process_args.extend([x for x in args.libfuzzer_options.split(' ') if x != ''])
diff --git a/tests/qemumonitortestutils.c b/tests/qemumonitortestutils.c
index 88a369188e..448710957e 100644
--- a/tests/qemumonitortestutils.c
+++ b/tests/qemumonitortestutils.c
@@ -83,8 +83,14 @@ struct _qemuMonitorTest {
virDomainObj *vm;
GHashTable *qapischema;
+
+ bool fuzz;
+ qemuMonitorFuzzResponseCallback fuzz_response_cb;
};
+static int
+qemuMonitorFuzzProcessCommandDefault(qemuMonitorTest *test,
+ const char *cmdstr);
static void
qemuMonitorTestItemFree(qemuMonitorTestItem *item)
@@ -227,6 +233,11 @@ qemuMonitorTestProcessCommand(qemuMonitorTest *test,
VIR_DEBUG("Processing string from monitor handler: '%s", cmdstr);
+ /* In fuzzing mode, process indefinite number of commands */
+ if (test->fuzz) {
+ return qemuMonitorFuzzProcessCommandDefault(test, cmdstr);
+ }
+
if (test->nitems == 0) {
qemuMonitorTestError("unexpected command: '%s'", cmdstr);
} else {
@@ -595,6 +606,34 @@ qemuMonitorTestAddItem(qemuMonitorTest *test,
}
+static int
+qemuMonitorFuzzProcessCommandDefault(qemuMonitorTest *test,
+ const char *cmdstr)
+{
+ g_autoptr(virJSONValue) val = NULL;
+ virJSONValue *cmdargs = NULL;
+
+ const char *cmdname = NULL;
+ g_autofree char *response = NULL;
+
+ if (!(val = virJSONValueFromString(cmdstr)))
+ return -1;
+
+ if (!(cmdname = virJSONValueObjectGetString(val, "execute"))) {
+ qemuMonitorTestError("Missing command name in %s", cmdstr);
+ return -1;
+ }
+
+ cmdargs = virJSONValueObjectGet(val, "arguments");
+
+ response = test->fuzz_response_cb(cmdname, cmdargs);
+
+ qemuMonitorTestAddResponse(test, response);
+
+ return 0;
+}
+
+
static int
qemuMonitorTestProcessCommandVerbatim(qemuMonitorTest *test,
qemuMonitorTestItem *item,
@@ -1021,6 +1060,15 @@ qemuMonitorTestSkipDeprecatedValidation(qemuMonitorTest *test,
}
+void
+qemuMonitorTestFuzzSetup(qemuMonitorTest *test,
+ qemuMonitorFuzzResponseCallback response_cb)
+{
+ test->fuzz = true;
+ test->fuzz_response_cb = response_cb;
+}
+
+
static int
qemuMonitorTestFullAddItem(qemuMonitorTest *test,
const char *filename,
diff --git a/tests/qemumonitortestutils.h b/tests/qemumonitortestutils.h
index 6d26526f60..105bd10486 100644
--- a/tests/qemumonitortestutils.h
+++ b/tests/qemumonitortestutils.h
@@ -29,6 +29,8 @@ typedef struct _qemuMonitorTestItem qemuMonitorTestItem;
typedef int (*qemuMonitorTestResponseCallback)(qemuMonitorTest *test,
qemuMonitorTestItem *item,
const char *message);
+typedef char *(*qemuMonitorFuzzResponseCallback)(const char *command,
+ virJSONValue *cmdargs);
void
qemuMonitorTestAddHandler(qemuMonitorTest *test,
@@ -61,6 +63,10 @@ void
qemuMonitorTestSkipDeprecatedValidation(qemuMonitorTest *test,
bool allowRemoved);
+void
+qemuMonitorTestFuzzSetup(qemuMonitorTest *test,
+ qemuMonitorFuzzResponseCallback response_cb);
+
int
qemuMonitorTestAddItem(qemuMonitorTest *test,
const char *command_name,
--
2.34.1
© 2016 - 2024 Red Hat, Inc.