[PATCH v2 0/5] qemu: auto-select win-dmp for Windows guest crash dumps
Auto-triggered dumps (on_crash, watchdog) always use QEMU's elf default, which is not WinDbg-loadable for a Windows guest and cannot be reliably converted after the fact. This was proposed before as a per-domain XML knob (https://www.mail-archive.com/devel@lists.libvirt.org/msg09638.html). Daniel P. Berrange objected: QEMU's query-dump-guest-memory-capability reported win-dmp as available on any x86-64 guest regardless of whether it had actually published a Windows dump header, so trusting it was not safe. QEMU's win_dump_available() has since been fixed to check the guest's vmcoreinfo note (https://lore.kernel.org/qemu-devel/20260619101834.228432-1-den@openvz.org/, commit b4bdad7dce). Validated against real guests: a Windows Server 2022 guest crashed via Sysinternals NotMyFault produces a PAGEDU64 (win-dmp) dump; an AlmaLinux 9 guest crashed via sysrq produces an ELF one. A 1GiB quota across two Linux crashes pruned the older dump and kept the newer, over-quota one. syntax-check and virstringtest pass. Changes since v1: * detect the 'allowed-by-guest' feature QEMU 11.1 puts on the win-dmp member of DumpGuestMemoryFormat (thanks Daniel for idea) * skip the probe unless that capability is present, so QEMU that offers win-dmp to every x86 guest keeps the elf default (Daniel). * fix the sort position of both new symbols in libvirt_private.syms, which v1 got wrong; picked up Daniel's Reviewed-by. * update virtlogd.aug and test_virtlogd.aug.in for the quoted form (Daniel). * update libvirtd_qemu.aug and test_libvirtd_qemu.aug.in for the new setting, and VIR_INFO -> VIR_DEBUG (both Daniel); count and remove only the dumps libvirt itself wrote, and skip pruning after a failed dump. * renamed test helpers for virStrToBytes * NEWS.rst entries added. Denis V. Lunev (5): qemu: add capability for guest-aware win-dmp availability qemu: auto-select win-dmp for crash- and watchdog-triggered dumps util: add virStrToBytes() and virConfGetValueBytes() for scaled sizes logging: accept a unit suffix in virtlogd's max_size qemu: cap total size of auto-triggered dumps under auto_dump_path NEWS.rst | 19 ++ src/libvirt_private.syms | 2 + src/logging/log_daemon_config.c | 2 +- src/logging/log_daemon_config.h | 2 +- src/logging/test_virtlogd.aug.in | 2 +- src/logging/virtlogd.aug | 5 +- src/logging/virtlogd.conf | 7 +- src/qemu/libvirtd_qemu.aug | 4 + src/qemu/qemu.conf.in | 17 ++ src/qemu/qemu_capabilities.c | 4 + src/qemu/qemu_capabilities.h | 3 + src/qemu/qemu_conf.c | 2 + src/qemu/qemu_conf.h | 1 + src/qemu/qemu_driver.c | 163 +++++++++++++++++- src/qemu/test_libvirtd_qemu.aug.in | 1 + src/util/virconf.c | 47 +++++ src/util/virconf.h | 3 + src/util/virutil.c | 23 +++ src/util/virutil.h | 5 + .../caps_11.1.0_aarch64.replies | 5 +- .../caps_11.1.0_aarch64.xml | 1 + .../caps_11.1.0_x86_64.replies | 5 +- .../caps_11.1.0_x86_64.xml | 1 + tests/qemumonitorjsontest.c | 3 + tests/virstringtest.c | 76 ++++++++ 25 files changed, 394 insertions(+), 9 deletions(-) -- 2.53.0
query-dump-guest-memory-capability reported win-dmp on any x86 machine, whether or not the guest had published a Windows dump header. QEMU commit 1c0e259c5a35 tied it to the guest's vmcoreinfo note, but left the change invisible to introspection. QEMU 11.1 marks the win-dmp member of DumpGuestMemoryFormat with an 'allowed-by-guest' feature. Detect it, so a caller picking a dump format can tell whether a reported win-dmp says anything about the guest. Signed-off-by: Denis V. Lunev <den@openvz.org> --- src/qemu/qemu_capabilities.c | 4 ++++ src/qemu/qemu_capabilities.h | 3 +++ tests/qemucapabilitiesdata/caps_11.1.0_aarch64.replies | 5 ++++- tests/qemucapabilitiesdata/caps_11.1.0_aarch64.xml | 1 + tests/qemucapabilitiesdata/caps_11.1.0_x86_64.replies | 5 ++++- tests/qemucapabilitiesdata/caps_11.1.0_x86_64.xml | 1 + tests/qemumonitorjsontest.c | 3 +++ 7 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/qemu/qemu_capabilities.c b/src/qemu/qemu_capabilities.c index 6abb179951..e8ccc286b0 100644 --- a/src/qemu/qemu_capabilities.c +++ b/src/qemu/qemu_capabilities.c @@ -771,6 +771,9 @@ VIR_ENUM_IMPL(virQEMUCaps, "arm-smmuv3", /* QEMU_CAPS_DEVICE_ARM_SMMUV3 */ "arm-smmuv3.smmu_per_bus", /* QEMU_CAPS_ARM_SMMUV3_SMMU_PER_BUS */ "arm-smmuv3.accel", /* QEMU_CAPS_ARM_SMMUV3_ACCEL */ + + /* 500 */ + "win-dmp.guest-aware", /* QEMU_CAPS_WIN_DMP_GUEST_AWARE */ ); @@ -1669,6 +1672,7 @@ static struct virQEMUCapsStringFlags virQEMUCapsQMPSchemaQueries[] = { { "chardev-add/arg-type/backend/+socket/data/reconnect-ms", QEMU_CAPS_CHARDEV_RECONNECT_MILISECONDS }, { "chardev-add/arg-type/backend/+qemu-vdagent", QEMU_CAPS_CHARDEV_QEMU_VDAGENT }, { "device_add/$json-cli-hotplug", QEMU_CAPS_DEVICE_JSON }, + { "dump-guest-memory/arg-type/format/^win-dmp/$allowed-by-guest", QEMU_CAPS_WIN_DMP_GUEST_AWARE }, { "nbd-server-start/arg-type/tls-creds", QEMU_CAPS_NBD_TLS }, { "netdev_add/arg-type/+stream", QEMU_CAPS_NETDEV_STREAM }, { "netdev_add/arg-type/+stream/reconnect", QEMU_CAPS_NETDEV_STREAM_RECONNECT }, diff --git a/src/qemu/qemu_capabilities.h b/src/qemu/qemu_capabilities.h index 4563e15156..9adbf964ce 100644 --- a/src/qemu/qemu_capabilities.h +++ b/src/qemu/qemu_capabilities.h @@ -746,6 +746,9 @@ typedef enum { /* virQEMUCapsFlags grouping marker for syntax-check */ QEMU_CAPS_ARM_SMMUV3_SMMU_PER_BUS, /* arm-smmuv3.smmu_per_bus */ QEMU_CAPS_ARM_SMMUV3_ACCEL, /* arm-smmuv3.accel */ + /* 500 */ + QEMU_CAPS_WIN_DMP_GUEST_AWARE, /* 'win-dmp' is offered only to a guest that can use it */ + QEMU_CAPS_LAST /* this must always be the last item */ } virQEMUCapsFlags; diff --git a/tests/qemucapabilitiesdata/caps_11.1.0_aarch64.replies b/tests/qemucapabilitiesdata/caps_11.1.0_aarch64.replies index 921e667f27..45a26dce00 100644 --- a/tests/qemucapabilitiesdata/caps_11.1.0_aarch64.replies +++ b/tests/qemucapabilitiesdata/caps_11.1.0_aarch64.replies @@ -12624,7 +12624,10 @@ "name": "kdump-raw-snappy" }, { - "name": "win-dmp" + "name": "win-dmp", + "features": [ + "allowed-by-guest" + ] } ], "meta-type": "enum", diff --git a/tests/qemucapabilitiesdata/caps_11.1.0_aarch64.xml b/tests/qemucapabilitiesdata/caps_11.1.0_aarch64.xml index e53ad1d461..409757b5c5 100644 --- a/tests/qemucapabilitiesdata/caps_11.1.0_aarch64.xml +++ b/tests/qemucapabilitiesdata/caps_11.1.0_aarch64.xml @@ -191,6 +191,7 @@ <flag name='arm-smmuv3'/> <flag name='arm-smmuv3.smmu_per_bus'/> <flag name='arm-smmuv3.accel'/> + <flag name='win-dmp.guest-aware'/> <version>11000090</version> <microcodeVersion>61700287</microcodeVersion> <package>v11.1.0-rc0</package> diff --git a/tests/qemucapabilitiesdata/caps_11.1.0_x86_64.replies b/tests/qemucapabilitiesdata/caps_11.1.0_x86_64.replies index 06be0418ca..a747db1236 100644 --- a/tests/qemucapabilitiesdata/caps_11.1.0_x86_64.replies +++ b/tests/qemucapabilitiesdata/caps_11.1.0_x86_64.replies @@ -12769,7 +12769,10 @@ "name": "kdump-raw-snappy" }, { - "name": "win-dmp" + "name": "win-dmp", + "features": [ + "allowed-by-guest" + ] } ], "meta-type": "enum", diff --git a/tests/qemucapabilitiesdata/caps_11.1.0_x86_64.xml b/tests/qemucapabilitiesdata/caps_11.1.0_x86_64.xml index e32cedca2f..58dfaff779 100644 --- a/tests/qemucapabilitiesdata/caps_11.1.0_x86_64.xml +++ b/tests/qemucapabilitiesdata/caps_11.1.0_x86_64.xml @@ -222,6 +222,7 @@ <flag name='amd-iommu.xtsup'/> <flag name='blockdev-mirror.target-is-zero'/> <flag name='object-monitor-qmp'/> + <flag name='win-dmp.guest-aware'/> <version>11000090</version> <microcodeVersion>43100287</microcodeVersion> <package>v11.1.0-rc0</package> diff --git a/tests/qemumonitorjsontest.c b/tests/qemumonitorjsontest.c index f59b97c1c3..70f03afb81 100644 --- a/tests/qemumonitorjsontest.c +++ b/tests/qemumonitorjsontest.c @@ -3116,6 +3116,8 @@ mymain(void) DO_TEST_QAPI_QUERY("variant", "blockdev-add/arg-type/+file", 1, true); DO_TEST_QAPI_QUERY("variant property", "blockdev-add/arg-type/+file/filename", 1, true); DO_TEST_QAPI_QUERY("enum value", "query-status/ret-type/status/^debug", 1, false); + DO_TEST_QAPI_QUERY("enum value feature", "query-migrate-capabilities/ret-type/capability/^x-colo/$unstable", 1, false); + DO_TEST_QAPI_QUERY("enum value in command arguments", "dump-guest-memory/arg-type/format/^win-dmp", 1, false); DO_TEST_QAPI_QUERY("builtin type", "query-qmp-schema/ret-type/name/!string", 1, false); DO_TEST_QAPI_QUERY("alternate variant 1", "blockdev-add/arg-type/+qcow2/backing/!null", 1, false); DO_TEST_QAPI_QUERY("alternate variant 2", "blockdev-add/arg-type/+qcow2/backing/!string", 1, false); @@ -3125,6 +3127,7 @@ mymain(void) DO_TEST_QAPI_QUERY("nonexistent attr", "screendump/arg-type/nonexistent", 0, false); DO_TEST_QAPI_QUERY("nonexistent variant", "blockdev-add/arg-type/+nonexistent", 0, false); DO_TEST_QAPI_QUERY("nonexistent enum value", "query-status/ret-type/status/^nonexistentdebug", 0, false); + DO_TEST_QAPI_QUERY("nonexistent enum value feature", "query-migrate-capabilities/ret-type/capability/^x-colo/$nonexistentfeature", 0, false); DO_TEST_QAPI_QUERY("broken query for enum value", "query-status/ret-type/status/^debug/test", -1, false); DO_TEST_QAPI_QUERY("builtin type", "query-qmp-schema/ret-type/name/!number", 0, false); -- 2.53.0
Where QEMU_CAPS_WIN_DMP_GUEST_AWARE is present, QEMU reports win-dmp as available only for a guest that has published a vmcoreinfo dump header. In that case prefer it over the elf default: converting an elf dump into a Windows-debuggable format afterwards is possible but complicated and unreliable. Add qemuDomainGetAutoDumpFormat(), which probes win-dmp support right before an auto-triggered dump and falls back to elf otherwise. Use it from both doCoreDumpToAutoDumpPath() (on_crash) and processWatchdogEvent() (watchdog dump). virDomainCoreDump() and virDomainCoreDumpWithFormat() are left untouched, since RAW there is a documented part of their API contract, standing in for an explicit caller request rather than an internal default. The probe is best-effort: reset any error left by a failed monitor call or QMP command, so it cannot leak past a dump that otherwise succeeds. Without the capability the probe is skipped: older QEMU offers win-dmp on any x86 machine, where the resulting dump is useless. Signed-off-by: Denis V. Lunev <den@openvz.org> --- NEWS.rst | 7 +++++++ src/qemu/qemu_driver.c | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index ae4d8c18a5..3dadb6d4f3 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -19,6 +19,13 @@ v12.7.0 (unreleased) * **Improvements** + * qemu: Select the win-dmp format for Windows guest crash dumps + + Dumps triggered by ``<on_crash>`` and by watchdog handling used QEMU's elf + default, which WinDbg cannot load. Where QEMU reports the format as + available only to a guest that can use it, such dumps are now written in + win-dmp format instead. + * **Bug fixes** diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 44b41726fb..cfabaf0746 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -3532,6 +3532,34 @@ getAutoDumpPath(virQEMUDriver *driver, return g_strdup_printf("%s/%s-%s", cfg->autoDumpPath, domname, nowstr); } +/* Older QEMU offers win-dmp on any x86 machine, so trust the capability + * query only where QEMU says the answer depends on the guest. */ +static unsigned int +qemuDomainGetAutoDumpFormat(virDomainObj *vm) +{ + qemuDomainObjPrivate *priv = vm->privateData; + unsigned int dumpformat = VIR_DOMAIN_CORE_DUMP_FORMAT_RAW; + int rc; + + if (!virQEMUCapsGet(priv->qemuCaps, QEMU_CAPS_WIN_DMP_GUEST_AWARE)) + return dumpformat; + + if (qemuDomainObjEnterMonitorAsync(vm, VIR_ASYNC_JOB_DUMP) < 0) { + virResetLastError(); + return dumpformat; + } + + rc = qemuMonitorGetDumpGuestMemoryCapability(priv->mon, "win-dmp"); + if (rc < 0) + virResetLastError(); + else if (rc > 0) + dumpformat = VIR_DOMAIN_CORE_DUMP_FORMAT_WIN_DMP; + + qemuDomainObjExitMonitor(vm); + return dumpformat; +} + + static void processWatchdogEvent(virQEMUDriver *driver, virDomainObj *vm, @@ -3558,7 +3586,7 @@ processWatchdogEvent(virQEMUDriver *driver, flags |= cfg->autoDumpBypassCache ? VIR_DUMP_BYPASS_CACHE: 0; if ((ret = doCoreDump(driver, vm, dumpfile, flags, - VIR_DOMAIN_CORE_DUMP_FORMAT_RAW)) < 0) + qemuDomainGetAutoDumpFormat(vm))) < 0) virReportError(VIR_ERR_OPERATION_FAILED, "%s", _("Dump failed")); @@ -3578,6 +3606,7 @@ processWatchdogEvent(virQEMUDriver *driver, virDomainObjEndAsyncJob(vm); } + static int doCoreDumpToAutoDumpPath(virQEMUDriver *driver, virDomainObj *vm, @@ -3592,7 +3621,7 @@ doCoreDumpToAutoDumpPath(virQEMUDriver *driver, flags |= cfg->autoDumpBypassCache ? VIR_DUMP_BYPASS_CACHE: 0; if ((ret = doCoreDump(driver, vm, dumpfile, flags, - VIR_DOMAIN_CORE_DUMP_FORMAT_RAW)) < 0) + qemuDomainGetAutoDumpFormat(vm))) < 0) virReportError(VIR_ERR_OPERATION_FAILED, "%s", _("Dump failed")); return ret; -- 2.53.0
virScaleInteger() already turns a number plus a unit suffix into bytes, but every caller has to split a whole string like "10GiB" by hand via virStrToLong_ullp() first, the way virFileReadValueScaledInt() does for sysfs files. Add virStrToBytes(), which does that split once, and virConfGetValueBytes() on top of it, so a qemu.conf-style setting can accept a scaled size in one call. Named "Bytes" rather than "Size" to avoid reading as a variant of virConfGetValueSizeT()/SSizeT(), whose "T" is the C type they fill, not a unit. Add unit tests for virStrToBytes() in virstringtest.c. Signed-off-by: Denis V. Lunev <den@openvz.org> Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> --- src/libvirt_private.syms | 2 ++ src/util/virconf.c | 47 +++++++++++++++++++++++++ src/util/virconf.h | 3 ++ src/util/virutil.c | 23 ++++++++++++ src/util/virutil.h | 5 +++ tests/virstringtest.c | 76 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 156 insertions(+) diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms index c76e5cb08a..a1d42e93dd 100644 --- a/src/libvirt_private.syms +++ b/src/libvirt_private.syms @@ -2276,6 +2276,7 @@ virConfFree; virConfFreeValue; virConfGetValue; virConfGetValueBool; +virConfGetValueBytes; virConfGetValueInt; virConfGetValueLLong; virConfGetValueSizeT; @@ -3557,6 +3558,7 @@ virStringStripIPv6Brackets; virStringStripSuffix; virStringToUpper; virStringTrimOptionalNewline; +virStrToBytes; virStrToDouble; virStrToLong_i; virStrToLong_ll; diff --git a/src/util/virconf.c b/src/util/virconf.c index c820c94037..c4238e68d9 100644 --- a/src/util/virconf.c +++ b/src/util/virconf.c @@ -1116,6 +1116,53 @@ int virConfGetValueUInt(virConf *conf, } +/** + * virConfGetValueBytes: + * @conf: the config object + * @setting: the config entry name + * @value: pointer to hold the byte count + * + * Get the value of the config entry @setting, storing it in @value. + * The entry may be a plain integer, taken as a byte count, or a + * string holding a byte count followed by a unit suffix understood + * by virStrToBytes(). If the config entry is not present, then + * @value will be unmodified. + * + * Reports an error if the config entry is set but has an unexpected + * type, or if a string entry cannot be parsed as a size. + * + * Returns: 1 if the value was present, 0 if missing, -1 on error + */ +int virConfGetValueBytes(virConf *conf, + const char *setting, + unsigned long long *value) +{ + virConfValue *cval = virConfGetValue(conf, setting); + + VIR_DEBUG("Get value bytes %p %d", + cval, cval ? cval->type : VIR_CONF_NONE); + + if (!cval) + return 0; + + if (cval->type == VIR_CONF_ULLONG) { + *value = cval->l; + return 1; + } + + if (cval->type == VIR_CONF_STRING) { + if (virStrToBytes(cval->str, ULLONG_MAX, value) < 0) + return -1; + return 1; + } + + virReportError(VIR_ERR_INTERNAL_ERROR, + _("%1$s: expected an unsigned integer or a size string for '%2$s' parameter"), + conf->filename, setting); + return -1; +} + + /** * virConfGetValueSizeT: * @conf: the config object diff --git a/src/util/virconf.h b/src/util/virconf.h index e656a6a815..0932823ce3 100644 --- a/src/util/virconf.h +++ b/src/util/virconf.h @@ -100,6 +100,9 @@ int virConfGetValueInt(virConf *conf, int virConfGetValueUInt(virConf *conf, const char *setting, unsigned int *value); +int virConfGetValueBytes(virConf *conf, + const char *setting, + unsigned long long *value); int virConfGetValueSizeT(virConf *conf, const char *setting, size_t *value); diff --git a/src/util/virutil.c b/src/util/virutil.c index 3e107cdae6..2be9a460ac 100644 --- a/src/util/virutil.c +++ b/src/util/virutil.c @@ -235,6 +235,29 @@ virScaleInteger(unsigned long long *value, const char *suffix, } +/* Parse the whole of STR as a byte count into RESULT, rejecting the + * result if it exceeds LIMIT. STR is a plain decimal integer, or a + * decimal integer immediately followed by one of the unit suffixes + * recognized by virScaleInteger(); unlike virStrToLong_ullp(), no + * characters may be left over after that optional suffix. Return 0 on + * success, -1 with error message raised on failure. */ +int +virStrToBytes(const char *str, + unsigned long long limit, + unsigned long long *result) +{ + char *end; + + if (virStrToLong_ullp(str, &end, 10, result) < 0) { + virReportError(VIR_ERR_INVALID_ARG, + _("Unable to parse integer from size '%1$s'"), str); + return -1; + } + + return virScaleInteger(result, end, 1, limit); +} + + /** * Format @val as a base-10 decimal number, in the * buffer @buf of size @buflen. To allocate a suitable diff --git a/src/util/virutil.h b/src/util/virutil.h index 2accb5777d..5678c339ba 100644 --- a/src/util/virutil.h +++ b/src/util/virutil.h @@ -44,6 +44,11 @@ int virScaleInteger(unsigned long long *value, const char *suffix, unsigned long long scale, unsigned long long limit) ATTRIBUTE_NONNULL(1) G_GNUC_WARN_UNUSED_RESULT; +int virStrToBytes(const char *str, + unsigned long long limit, + unsigned long long *result) + ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(3) G_GNUC_WARN_UNUSED_RESULT; + char *virFormatIntDecimal(char *buf, size_t buflen, int val) ATTRIBUTE_NONNULL(1) G_GNUC_WARN_UNUSED_RESULT; diff --git a/tests/virstringtest.c b/tests/virstringtest.c index 0792155cc3..9c1448672c 100644 --- a/tests/virstringtest.c +++ b/tests/virstringtest.c @@ -18,10 +18,13 @@ #include <config.h> +#include <limits.h> + #include "testutils.h" #include "virlog.h" #include "virstring.h" +#include "virutil.h" #define VIR_FROM_THIS VIR_FROM_NONE @@ -378,6 +381,38 @@ testStringToLong(const void *opaque) } +struct stringToBytesData { + const char *str; + unsigned long long limit; + unsigned long long expect; + int expect_ret; +}; + +static int +testStringToBytes(const void *opaque) +{ + const struct stringToBytesData *data = opaque; + unsigned long long value; + int ret; + + ret = virStrToBytes(data->str, data->limit, &value); + + if (ret != data->expect_ret) { + fprintf(stderr, "Expected return '%d', got '%d' for '%s'\n", + data->expect_ret, ret, data->str); + return -1; + } + + if (ret == 0 && value != data->expect) { + fprintf(stderr, "Expected value '%llu', got '%llu' for '%s'\n", + data->expect, value, data->str); + return -1; + } + + return 0; +} + + struct stringToDoubleData { const char *str; const char *end_ptr; @@ -678,6 +713,47 @@ mymain(void) TEST_STRTOL("-18446744073709551616", NULL, 0, -1, 0U, -1, 0LL, -1, 0ULL, -1); +#define TEST_STRTOBYTES(str, limit, expect, expect_ret) \ + do { \ + struct stringToBytesData data = { \ + str, limit, expect, expect_ret, \ + }; \ + if (virTestRun("virStrToBytes '" str "'", \ + testStringToBytes, &data) < 0) \ + ret = -1; \ + } while (0) + + /* Plain byte counts, no suffix */ + TEST_STRTOBYTES("0", ULLONG_MAX, 0, 0); + TEST_STRTOBYTES("1073741824", ULLONG_MAX, 1073741824, 0); + + /* Binary suffixes, and their bare single-letter equivalents */ + TEST_STRTOBYTES("10K", ULLONG_MAX, 10240, 0); + TEST_STRTOBYTES("10KiB", ULLONG_MAX, 10240, 0); + TEST_STRTOBYTES("1M", ULLONG_MAX, 1048576, 0); + TEST_STRTOBYTES("1G", ULLONG_MAX, 1073741824, 0); + TEST_STRTOBYTES("1GiB", ULLONG_MAX, 1073741824, 0); + TEST_STRTOBYTES("1T", ULLONG_MAX, 1099511627776ULL, 0); + + /* Decimal (SI) suffixes */ + TEST_STRTOBYTES("10KB", ULLONG_MAX, 10000, 0); + TEST_STRTOBYTES("1GB", ULLONG_MAX, 1000000000, 0); + + /* Bytes, spelled out */ + TEST_STRTOBYTES("42b", ULLONG_MAX, 42, 0); + TEST_STRTOBYTES("42byte", ULLONG_MAX, 42, 0); + TEST_STRTOBYTES("42bytes", ULLONG_MAX, 42, 0); + + /* Unknown suffix */ + TEST_STRTOBYTES("10Q", ULLONG_MAX, 0, -1); + + /* Trailing garbage after a valid suffix */ + TEST_STRTOBYTES("10Gextra", ULLONG_MAX, 0, -1); + + /* Overflow */ + TEST_STRTOBYTES("18446744073709551615", 1000, 0, -1); + TEST_STRTOBYTES("100E", ULLONG_MAX, 0, -1); + #define TEST_STRTOD(str, end_ptr, res) \ do { \ struct stringToDoubleData data = { \ -- 2.53.0
Switch max_size from virConfGetValueSizeT() to virConfGetValueBytes(), so it accepts "2MiB" the same way domain XML memory sizes do, instead of only a raw byte count. virtlogd.aug accepts the quoted form too; its quoted branch requires a unit suffix, as one matching a bare number would overlap the integer branch. Signed-off-by: Denis V. Lunev <den@openvz.org> --- NEWS.rst | 5 +++++ src/logging/log_daemon_config.c | 2 +- src/logging/log_daemon_config.h | 2 +- src/logging/test_virtlogd.aug.in | 2 +- src/logging/virtlogd.aug | 5 ++++- src/logging/virtlogd.conf | 7 ++++++- 6 files changed, 18 insertions(+), 5 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index 3dadb6d4f3..800a69b943 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -26,6 +26,11 @@ v12.7.0 (unreleased) available only to a guest that can use it, such dumps are now written in win-dmp format instead. + * logging: ``max_size`` in virtlogd.conf accepts a unit suffix + + The setting now takes a size such as ``"2MiB"`` as well as a plain byte + count. + * **Bug fixes** diff --git a/src/logging/log_daemon_config.c b/src/logging/log_daemon_config.c index 60c424ad84..e0d6384818 100644 --- a/src/logging/log_daemon_config.c +++ b/src/logging/log_daemon_config.c @@ -91,7 +91,7 @@ virLogDaemonConfigLoadOptions(virLogDaemonConfig *data, return -1; if (virConfGetValueUInt(conf, "admin_max_clients", &data->admin_max_clients) < 0) return -1; - if (virConfGetValueSizeT(conf, "max_size", &data->max_size) < 0) + if (virConfGetValueBytes(conf, "max_size", &data->max_size) < 0) return -1; if (virConfGetValueSizeT(conf, "max_backups", &data->max_backups) < 0) return -1; diff --git a/src/logging/log_daemon_config.h b/src/logging/log_daemon_config.h index 5c10cc50d7..617b62258b 100644 --- a/src/logging/log_daemon_config.h +++ b/src/logging/log_daemon_config.h @@ -32,7 +32,7 @@ struct _virLogDaemonConfig { unsigned int admin_max_clients; size_t max_backups; - size_t max_size; + unsigned long long max_size; char *log_root; size_t max_age_days; diff --git a/src/logging/test_virtlogd.aug.in b/src/logging/test_virtlogd.aug.in index 8dfad39506..da1c8530d1 100644 --- a/src/logging/test_virtlogd.aug.in +++ b/src/logging/test_virtlogd.aug.in @@ -7,7 +7,7 @@ module Test_virtlogd = { "log_outputs" = "3:syslog:virtlogd" } { "max_clients" = "1024" } { "admin_max_clients" = "5" } - { "max_size" = "2097152" } + { "max_size" = "2MiB" } { "max_backups" = "3" } { "max_age_days" = "0" } { "log_root" = "/var/log/libvirt" } diff --git a/src/logging/virtlogd.aug b/src/logging/virtlogd.aug index bdf61dea6e..17b2aea452 100644 --- a/src/logging/virtlogd.aug +++ b/src/logging/virtlogd.aug @@ -22,6 +22,9 @@ module Virtlogd = let int_entry (kw:string) = [ key kw . value_sep . int_val ] let str_array_entry (kw:string) = [ key kw . value_sep . str_array_val ] + let bytes_val = del /\"/ "\"" . store /[0-9]+[a-zA-Z]+/ . del /\"/ "\"" + let bytes_entry (kw:string) = [ key kw . value_sep . bytes_val ] | [ key kw . value_sep . int_val ] + (* Config entry grouped by function - same order as example config *) let logging_entry = int_entry "log_level" @@ -29,7 +32,7 @@ module Virtlogd = | str_entry "log_outputs" | int_entry "max_clients" | int_entry "admin_max_clients" - | int_entry "max_size" + | bytes_entry "max_size" | int_entry "max_backups" | int_entry "max_age_days" | str_entry "log_root" diff --git a/src/logging/virtlogd.conf b/src/logging/virtlogd.conf index 5214e96121..51bc0590ed 100644 --- a/src/logging/virtlogd.conf +++ b/src/logging/virtlogd.conf @@ -87,6 +87,11 @@ # Maximum file size before rolling over. Defaults to 2 MB # +# The value is a plain byte count, or a byte count followed by a unit +# suffix: bytes/b, KB/k/KiB, MB/M/MiB, GB/G/GiB, TB/T/TiB, PB/P/PiB, or +# EB/E/EiB (decimal 'B' suffixes scale by 1000, binary 'iB' suffixes, +# and their bare single-letter equivalents, scale by 1024). +# # Setting max_size to zero will disable rollover entirely. # NOTE: disabling rollover exposes the host filesystem to # denial of service from a malicious guest. @@ -96,7 +101,7 @@ # the logrotate config is a no-op when virtlogd is running, # make sure that max_size here is smaller than size listed # in the logrotate config. -#max_size = 2097152 +#max_size = "2MiB" # Maximum number of backup files to keep. Defaults to 3, # not including the primary active file -- 2.53.0
Each on_crash or watchdog-triggered dump writes a full memory dump into auto_dump_path. A guest that keeps crashing and restarting (or crashing and getting destroyed, then respawned by the mgmt app) can fill the disk one dump at a time, with nothing to stop it. Add auto_dump_max_size (qemu.conf), parsed via virConfGetValueBytes() so it takes a plain byte count or a size with a unit suffix (e.g. "10GiB"). After a dump is written, the oldest dumps under auto_dump_path are removed until the total fits the configured quota. The dump that was just written is always kept by identity, not by sort position: mtime is only second-granularity, so two dumps written the same second would otherwise make the eviction order between them arbitrary and could delete the one just written instead of an older one. Defaults to 0, which keeps every dump forever, as before. Only files named the way getAutoDumpPath() names them count toward the quota, and nothing is pruned when the dump itself failed. libvirtd_qemu.aug gains a matching entry; its quoted branch requires a unit suffix, as one matching a bare number would overlap the integer branch. Signed-off-by: Denis V. Lunev <den@openvz.org> --- NEWS.rst | 7 ++ src/qemu/libvirtd_qemu.aug | 4 + src/qemu/qemu.conf.in | 17 ++++ src/qemu/qemu_conf.c | 2 + src/qemu/qemu_conf.h | 1 + src/qemu/qemu_driver.c | 130 +++++++++++++++++++++++++++++ src/qemu/test_libvirtd_qemu.aug.in | 1 + 7 files changed, 162 insertions(+) diff --git a/NEWS.rst b/NEWS.rst index 800a69b943..5bdf8ce2ee 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -17,6 +17,13 @@ v12.7.0 (unreleased) * **New features** + * qemu: Add ``auto_dump_max_size`` to cap the size of auto-triggered dumps + + A guest that keeps crashing fills ``auto_dump_path`` one dump at a time. + The new ``auto_dump_max_size`` setting in qemu.conf bounds the total size + those dumps may occupy, pruning the oldest after each new one. It takes a + byte count or a size such as ``"10GiB"``; 0, the default, keeps them all. + * **Improvements** * qemu: Select the win-dmp format for Windows guest crash dumps diff --git a/src/qemu/libvirtd_qemu.aug b/src/qemu/libvirtd_qemu.aug index 311992e441..dbcbbb04c0 100644 --- a/src/qemu/libvirtd_qemu.aug +++ b/src/qemu/libvirtd_qemu.aug @@ -25,6 +25,9 @@ module Libvirtd_qemu = let unlimited_val = del /\"/ "\"" . store /unlimited/ . del /\"/ "\"" let limits_entry (kw:string) = [ key kw . value_sep . unlimited_val ] | [ key kw . value_sep . int_val ] + let bytes_val = del /\"/ "\"" . store /[0-9]+[a-zA-Z]+/ . del /\"/ "\"" + let bytes_entry (kw:string) = [ key kw . value_sep . bytes_val ] | [ key kw . value_sep . int_val ] + (* Config entry grouped by function - same order as example config *) let default_tls_entry = str_entry "default_tls_x509_cert_dir" @@ -109,6 +112,7 @@ module Libvirtd_qemu = | str_entry "snapshot_image_format" | str_entry "auto_dump_path" | bool_entry "auto_dump_bypass_cache" + | bytes_entry "auto_dump_max_size" | bool_entry "auto_start_bypass_cache" | int_entry "auto_start_delay" | str_entry "auto_shutdown_try_save" diff --git a/src/qemu/qemu.conf.in b/src/qemu/qemu.conf.in index 97b0141cf6..de14db6cde 100644 --- a/src/qemu/qemu.conf.in +++ b/src/qemu/qemu.conf.in @@ -688,6 +688,23 @@ #auto_dump_bypass_cache = 0 +# Total size that auto-triggered dumps (from on_crash and watchdog +# handling) are allowed to occupy under auto_dump_path. After each new +# dump is written, the oldest dumps are removed until the total fits +# the quota again. The dump that was just written is never removed by +# this, even if it alone exceeds the quota. Only the dumps libvirt +# itself wrote are counted and removed. +# +# The value is a plain byte count, or a byte count followed by a unit +# suffix: bytes/b, KB/k/KiB, MB/M/MiB, GB/G/GiB, TB/T/TiB, PB/P/PiB, or +# EB/E/EiB (decimal 'B' suffixes scale by 1000, binary 'iB' suffixes, +# and their bare single-letter equivalents, scale by 1024). +# +# Defaults to 0, which disables the quota and keeps every dump forever. +# +#auto_dump_max_size = "10GiB" + + # When a domain is configured to be auto-started, enabling this flag # has the same effect as using the VIR_DOMAIN_START_BYPASS_CACHE flag # with the virDomainCreateWithFlags API. That is, the system will diff --git a/src/qemu/qemu_conf.c b/src/qemu/qemu_conf.c index e30b146634..6d67939b6e 100644 --- a/src/qemu/qemu_conf.c +++ b/src/qemu/qemu_conf.c @@ -748,6 +748,8 @@ virQEMUDriverConfigLoadSaveEntry(virQEMUDriverConfig *cfg, return -1; if (virConfGetValueBool(conf, "auto_dump_bypass_cache", &cfg->autoDumpBypassCache) < 0) return -1; + if (virConfGetValueBytes(conf, "auto_dump_max_size", &cfg->autoDumpMaxSize) < 0) + return -1; if (virConfGetValueBool(conf, "auto_start_bypass_cache", &cfg->autoStartBypassCache) < 0) return -1; if (virConfGetValueUInt(conf, "auto_start_delay", &cfg->autoStartDelayMS) < 0) diff --git a/src/qemu/qemu_conf.h b/src/qemu/qemu_conf.h index 1d29f35c5d..9faf6db206 100644 --- a/src/qemu/qemu_conf.h +++ b/src/qemu/qemu_conf.h @@ -226,6 +226,7 @@ struct _virQEMUDriverConfig { char *autoDumpPath; bool autoDumpBypassCache; + unsigned long long autoDumpMaxSize; bool autoStartBypassCache; unsigned int autoStartDelayMS; virDomainDriverAutoShutdownConfig autoShutdown; diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index cfabaf0746..41c454bb7d 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -3560,6 +3560,132 @@ qemuDomainGetAutoDumpFormat(virDomainObj *vm) } +typedef struct _qemuAutoDumpFile qemuAutoDumpFile; +struct _qemuAutoDumpFile { + char *path; + unsigned long long size; + long long mtime; +}; + +static void +qemuAutoDumpFileFree(void *opaque) +{ + qemuAutoDumpFile *file = opaque; + + g_free(file->path); + g_free(file); +} + + +static gint +qemuAutoDumpFileCompare(gconstpointer a, + gconstpointer b) +{ + qemuAutoDumpFile *fa = *(qemuAutoDumpFile **) a; + qemuAutoDumpFile *fb = *(qemuAutoDumpFile **) b; + + return fa->mtime < fb->mtime ? -1 : fa->mtime > fb->mtime; +} + + +static bool +qemuIsAutoDumpFileName(const char *name) +{ + /* "-YYYY-MM-DD-HH:MM:SS" as built by getAutoDumpPath(), 'd' any digit */ + static const char shape[] = "-dddd-dd-dd-dd:dd:dd"; + size_t shapelen = sizeof(shape) - 1; + size_t len = strlen(name); + const char *stamp; + size_t i; + + if (len <= shapelen) + return false; + + stamp = name + len - shapelen; + + for (i = 0; i < shapelen; i++) { + if (shape[i] == 'd') { + if (!g_ascii_isdigit(stamp[i])) + return false; + } else if (stamp[i] != shape[i]) { + return false; + } + } + + return true; +} + + +/* Removes the oldest dumps under autoDumpPath until the total size fits + * autoDumpMaxSize. KEEP (the dump just written) is never removed, even + * alone over quota: mtime alone can't protect it, since it is only + * second-granularity and ties with another dump written the same + * second would make the eviction order among them arbitrary. */ +static void +qemuPruneAutoDumpPath(virQEMUDriverConfig *cfg, + const char *keep) +{ + g_autoptr(DIR) dir = NULL; + struct dirent *entry; + g_autoptr(GPtrArray) files = NULL; + unsigned long long total = 0; + size_t i; + int rc; + + if (cfg->autoDumpMaxSize == 0) + return; + + if (virDirOpenQuiet(&dir, cfg->autoDumpPath) < 0) + return; + + files = g_ptr_array_new_with_free_func(qemuAutoDumpFileFree); + + while ((rc = virDirRead(dir, &entry, NULL)) > 0) { + g_autofree char *path = g_strdup_printf("%s/%s", cfg->autoDumpPath, + entry->d_name); + GStatBuf sb; + qemuAutoDumpFile *file; + + if (!qemuIsAutoDumpFileName(entry->d_name)) + continue; + + if (g_stat(path, &sb) < 0 || !S_ISREG(sb.st_mode)) + continue; + + total += sb.st_size; + + if (STREQ(path, keep)) + continue; + + file = g_new0(qemuAutoDumpFile, 1); + file->path = g_steal_pointer(&path); + file->size = sb.st_size; + file->mtime = sb.st_mtime; + + g_ptr_array_add(files, file); + } + + if (rc < 0) + return; + + g_ptr_array_sort(files, qemuAutoDumpFileCompare); + + for (i = 0; i < files->len && total > cfg->autoDumpMaxSize; i++) { + qemuAutoDumpFile *file = g_ptr_array_index(files, i); + + if (unlink(file->path) < 0 && errno != ENOENT) { + VIR_WARN("Failed to prune old dump %s: %s", + file->path, g_strerror(errno)); + continue; + } + + VIR_DEBUG("Pruned old dump %s to satisfy auto_dump_max_size quota", + file->path); + total -= file->size; + } +} + + static void processWatchdogEvent(virQEMUDriver *driver, virDomainObj *vm, @@ -3589,6 +3715,8 @@ processWatchdogEvent(virQEMUDriver *driver, qemuDomainGetAutoDumpFormat(vm))) < 0) virReportError(VIR_ERR_OPERATION_FAILED, "%s", _("Dump failed")); + else + qemuPruneAutoDumpPath(cfg, dumpfile); ret = qemuProcessStartCPUs(driver, vm, VIR_DOMAIN_RUNNING_UNPAUSED, @@ -3624,6 +3752,8 @@ doCoreDumpToAutoDumpPath(virQEMUDriver *driver, qemuDomainGetAutoDumpFormat(vm))) < 0) virReportError(VIR_ERR_OPERATION_FAILED, "%s", _("Dump failed")); + else + qemuPruneAutoDumpPath(cfg, dumpfile); return ret; } diff --git a/src/qemu/test_libvirtd_qemu.aug.in b/src/qemu/test_libvirtd_qemu.aug.in index c4cf9cf634..c97d8d7081 100644 --- a/src/qemu/test_libvirtd_qemu.aug.in +++ b/src/qemu/test_libvirtd_qemu.aug.in @@ -83,6 +83,7 @@ module Test_libvirtd_qemu = { "snapshot_image_format" = "raw" } { "auto_dump_path" = "/var/lib/libvirt/qemu/dump" } { "auto_dump_bypass_cache" = "0" } +{ "auto_dump_max_size" = "10GiB" } { "auto_start_bypass_cache" = "0" } { "auto_start_delay" = "0" } { "auto_shutdown_try_save" = "persistent" } -- 2.53.0
On 8/11/26 11:24, Denis V. Lunev wrote:
This email originated from an IP that might not be authorized by the domain it was sent from. Do not click links or open attachments unless it is an email you expected to receive. Auto-triggered dumps (on_crash, watchdog) always use QEMU's elf default, which is not WinDbg-loadable for a Windows guest and cannot be reliably converted after the fact.
This was proposed before as a per-domain XML knob (https://www.mail-archive.com/devel@lists.libvirt.org/msg09638.html). Daniel P. Berrange objected: QEMU's query-dump-guest-memory-capability reported win-dmp as available on any x86-64 guest regardless of whether it had actually published a Windows dump header, so trusting it was not safe. QEMU's win_dump_available() has since been fixed to check the guest's vmcoreinfo note (https://lore.kernel.org/qemu-devel/20260619101834.228432-1-den@openvz.org/, commit b4bdad7dce).
Validated against real guests: a Windows Server 2022 guest crashed via Sysinternals NotMyFault produces a PAGEDU64 (win-dmp) dump; an AlmaLinux 9 guest crashed via sysrq produces an ELF one. A 1GiB quota across two Linux crashes pruned the older dump and kept the newer, over-quota one. syntax-check and virstringtest pass.
Changes since v1: * detect the 'allowed-by-guest' feature QEMU 11.1 puts on the win-dmp member of DumpGuestMemoryFormat (thanks Daniel for idea) * skip the probe unless that capability is present, so QEMU that offers win-dmp to every x86 guest keeps the elf default (Daniel). * fix the sort position of both new symbols in libvirt_private.syms, which v1 got wrong; picked up Daniel's Reviewed-by. * update virtlogd.aug and test_virtlogd.aug.in for the quoted form (Daniel). * update libvirtd_qemu.aug and test_libvirtd_qemu.aug.in for the new setting, and VIR_INFO -> VIR_DEBUG (both Daniel); count and remove only the dumps libvirt itself wrote, and skip pruning after a failed dump. * renamed test helpers for virStrToBytes * NEWS.rst entries added.
Denis V. Lunev (5): qemu: add capability for guest-aware win-dmp availability qemu: auto-select win-dmp for crash- and watchdog-triggered dumps util: add virStrToBytes() and virConfGetValueBytes() for scaled sizes logging: accept a unit suffix in virtlogd's max_size qemu: cap total size of auto-triggered dumps under auto_dump_path
NEWS.rst | 19 ++ src/libvirt_private.syms | 2 + src/logging/log_daemon_config.c | 2 +- src/logging/log_daemon_config.h | 2 +- src/logging/test_virtlogd.aug.in | 2 +- src/logging/virtlogd.aug | 5 +- src/logging/virtlogd.conf | 7 +- src/qemu/libvirtd_qemu.aug | 4 + src/qemu/qemu.conf.in | 17 ++ src/qemu/qemu_capabilities.c | 4 + src/qemu/qemu_capabilities.h | 3 + src/qemu/qemu_conf.c | 2 + src/qemu/qemu_conf.h | 1 + src/qemu/qemu_driver.c | 163 +++++++++++++++++- src/qemu/test_libvirtd_qemu.aug.in | 1 + src/util/virconf.c | 47 +++++ src/util/virconf.h | 3 + src/util/virutil.c | 23 +++ src/util/virutil.h | 5 + .../caps_11.1.0_aarch64.replies | 5 +- .../caps_11.1.0_aarch64.xml | 1 + .../caps_11.1.0_x86_64.replies | 5 +- .../caps_11.1.0_x86_64.xml | 1 + tests/qemumonitorjsontest.c | 3 + tests/virstringtest.c | 76 ++++++++ 25 files changed, 394 insertions(+), 9 deletions(-)
ping
participants (2)
-
Denis V. Lunev -
Denis V. Lunev