[PATCH 0/4] 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). Patch 1 queries it directly instead of adding an XML knob. Patch 4 adds a quota (auto_dump_max_size): a guest that keeps crashing otherwise fills auto_dump_path one dump at a time. Oldest dumps are pruned after each new one; the dump just written is never removed, even alone over quota. Patches 2-3 give that setting and virtlogd's max_size a "10GiB"-style syntax instead of a raw byte count, reusing virScaleInteger()'s existing unit table. 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. Denis V. Lunev (4): 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 src/libvirt_private.syms | 2 + src/logging/log_daemon_config.c | 2 +- src/logging/log_daemon_config.h | 2 +- src/logging/virtlogd.conf | 7 +- src/qemu/qemu.conf.in | 16 ++++ src/qemu/qemu_conf.c | 2 + src/qemu/qemu_conf.h | 1 + src/qemu/qemu_driver.c | 127 +++++++++++++++++++++++++++++++- src/util/virconf.c | 47 ++++++++++++ src/util/virconf.h | 3 + src/util/virutil.c | 23 ++++++ src/util/virutil.h | 5 ++ tests/virstringtest.c | 76 +++++++++++++++++++ 13 files changed, 308 insertions(+), 5 deletions(-) -- 2.53.0
QEMU reports win-dmp as available only when the guest is Windows and 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. Signed-off-by: Denis V. Lunev <den@openvz.org> --- src/qemu/qemu_driver.c | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 8ec9e1f9c4..b59a714a81 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -3532,6 +3532,31 @@ getAutoDumpPath(virQEMUDriver *driver, return g_strdup_printf("%s/%s-%s", cfg->autoDumpPath, domname, nowstr); } +/* win-dmp is reported only for a Windows guest that has published a + * vmcoreinfo dump header, so prefer it whenever QEMU confirms it. */ +static unsigned int +qemuDomainGetAutoDumpFormat(virDomainObj *vm) +{ + qemuDomainObjPrivate *priv = vm->privateData; + unsigned int dumpformat = VIR_DOMAIN_CORE_DUMP_FORMAT_RAW; + int rc; + + 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 +3583,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 +3603,7 @@ processWatchdogEvent(virQEMUDriver *driver, virDomainObjEndAsyncJob(vm); } + static int doCoreDumpToAutoDumpPath(virQEMUDriver *driver, virDomainObj *vm, @@ -3592,7 +3618,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> --- 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..758861b266 100644 --- a/src/libvirt_private.syms +++ b/src/libvirt_private.syms @@ -2278,6 +2278,7 @@ virConfGetValue; virConfGetValueBool; virConfGetValueInt; virConfGetValueLLong; +virConfGetValueBytes; virConfGetValueSizeT; virConfGetValueSSizeT; virConfGetValueString; @@ -3566,6 +3567,7 @@ virStrToLong_ul; virStrToLong_ull; virStrToLong_ullp; virStrToLong_ulp; +virStrToBytes; virTrimSpaces; diff --git a/src/util/virconf.c b/src/util/virconf.c index c820c94037..20a54905b0 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 size %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..e98e3c6c59 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..0f34ae3047 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..1b943d75e6 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..64a594eb6f 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 stringToScaledIntegerData { + const char *str; + unsigned long long limit; + unsigned long long expect; + int expect_ret; +}; + +static int +testStringToScaledInteger(const void *opaque) +{ + const struct stringToScaledIntegerData *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_SCALED_SIZE(str, limit, expect, expect_ret) \ + do { \ + struct stringToScaledIntegerData data = { \ + str, limit, expect, expect_ret, \ + }; \ + if (virTestRun("virStrToBytes '" str "'", \ + testStringToScaledInteger, &data) < 0) \ + ret = -1; \ + } while (0) + + /* Plain byte counts, no suffix */ + TEST_SCALED_SIZE("0", ULLONG_MAX, 0, 0); + TEST_SCALED_SIZE("1073741824", ULLONG_MAX, 1073741824, 0); + + /* Binary suffixes, and their bare single-letter equivalents */ + TEST_SCALED_SIZE("10K", ULLONG_MAX, 10240, 0); + TEST_SCALED_SIZE("10KiB", ULLONG_MAX, 10240, 0); + TEST_SCALED_SIZE("1M", ULLONG_MAX, 1048576, 0); + TEST_SCALED_SIZE("1G", ULLONG_MAX, 1073741824, 0); + TEST_SCALED_SIZE("1GiB", ULLONG_MAX, 1073741824, 0); + TEST_SCALED_SIZE("1T", ULLONG_MAX, 1099511627776ULL, 0); + + /* Decimal (SI) suffixes */ + TEST_SCALED_SIZE("10KB", ULLONG_MAX, 10000, 0); + TEST_SCALED_SIZE("1GB", ULLONG_MAX, 1000000000, 0); + + /* Bytes, spelled out */ + TEST_SCALED_SIZE("42b", ULLONG_MAX, 42, 0); + TEST_SCALED_SIZE("42byte", ULLONG_MAX, 42, 0); + TEST_SCALED_SIZE("42bytes", ULLONG_MAX, 42, 0); + + /* Unknown suffix */ + TEST_SCALED_SIZE("10Q", ULLONG_MAX, 0, -1); + + /* Trailing garbage after a valid suffix */ + TEST_SCALED_SIZE("10Gextra", ULLONG_MAX, 0, -1); + + /* Overflow */ + TEST_SCALED_SIZE("18446744073709551615", 1000, 0, -1); + TEST_SCALED_SIZE("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. Signed-off-by: Denis V. Lunev <den@openvz.org> --- src/logging/log_daemon_config.c | 2 +- src/logging/log_daemon_config.h | 2 +- src/logging/virtlogd.conf | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) 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/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 each dump attempt, the oldest files 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. Signed-off-by: Denis V. Lunev <den@openvz.org> --- src/qemu/qemu.conf.in | 16 +++++++ src/qemu/qemu_conf.c | 2 + src/qemu/qemu_conf.h | 1 + src/qemu/qemu_driver.c | 97 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+) diff --git a/src/qemu/qemu.conf.in b/src/qemu/qemu.conf.in index 97b0141cf6..3828af0fab 100644 --- a/src/qemu/qemu.conf.in +++ b/src/qemu/qemu.conf.in @@ -688,6 +688,22 @@ #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. +# +# 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 b59a714a81..3f021bb7c5 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -3557,6 +3557,101 @@ 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; +} + + +/* Removes the oldest files 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 (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_INFO("Pruned old dump %s to satisfy auto_dump_max_size quota", + file->path); + total -= file->size; + } +} + + static void processWatchdogEvent(virQEMUDriver *driver, virDomainObj *vm, @@ -3586,6 +3681,7 @@ processWatchdogEvent(virQEMUDriver *driver, qemuDomainGetAutoDumpFormat(vm))) < 0) virReportError(VIR_ERR_OPERATION_FAILED, "%s", _("Dump failed")); + qemuPruneAutoDumpPath(cfg, dumpfile); ret = qemuProcessStartCPUs(driver, vm, VIR_DOMAIN_RUNNING_UNPAUSED, @@ -3621,6 +3717,7 @@ doCoreDumpToAutoDumpPath(virQEMUDriver *driver, qemuDomainGetAutoDumpFormat(vm))) < 0) virReportError(VIR_ERR_OPERATION_FAILED, "%s", _("Dump failed")); + qemuPruneAutoDumpPath(cfg, dumpfile); return ret; } -- 2.53.0
On Tue, Jul 07, 2026 at 09:48:22AM +0200, Denis V. Lunev via Devel wrote:
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 each dump attempt, the oldest files 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.
This is a good idea, however, I wonder if we should also have a global cross-VM limit, as with cloud you might equally see a loop where a VM fails to spawn and automation spins up a new VM with different identity each time. Perhaps also "auto_dump_min_free_space" where we statfs the dump location to figure out current free space, subtract the VM RAM size (as a rough approximation for worst case dump size), and then reject the dump if the result would be under the limit. If you want to do that, it could be a separate followup patch so no need to repost this series just for that.
Signed-off-by: Denis V. Lunev <den@openvz.org> --- src/qemu/qemu.conf.in | 16 +++++++ src/qemu/qemu_conf.c | 2 + src/qemu/qemu_conf.h | 1 + src/qemu/qemu_driver.c | 97 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+)
diff --git a/src/qemu/qemu.conf.in b/src/qemu/qemu.conf.in index 97b0141cf6..3828af0fab 100644 --- a/src/qemu/qemu.conf.in +++ b/src/qemu/qemu.conf.in @@ -688,6 +688,22 @@ #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. +# +# 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 b59a714a81..3f021bb7c5 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -3557,6 +3557,101 @@ 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; +} + + +/* Removes the oldest files 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 (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_INFO("Pruned old dump %s to satisfy auto_dump_max_size quota", + file->path); + total -= file->size; + } +} + + static void processWatchdogEvent(virQEMUDriver *driver, virDomainObj *vm, @@ -3586,6 +3681,7 @@ processWatchdogEvent(virQEMUDriver *driver, qemuDomainGetAutoDumpFormat(vm))) < 0) virReportError(VIR_ERR_OPERATION_FAILED, "%s", _("Dump failed")); + qemuPruneAutoDumpPath(cfg, dumpfile);
ret = qemuProcessStartCPUs(driver, vm, VIR_DOMAIN_RUNNING_UNPAUSED, @@ -3621,6 +3717,7 @@ doCoreDumpToAutoDumpPath(virQEMUDriver *driver, qemuDomainGetAutoDumpFormat(vm))) < 0) virReportError(VIR_ERR_OPERATION_FAILED, "%s", _("Dump failed")); + qemuPruneAutoDumpPath(cfg, dumpfile); return ret; }
-- 2.53.0
With regards, Daniel -- |: https://berrange.com ~~ https://hachyderm.io/@berrange :| |: https://libvirt.org ~~ https://entangle-photo.org :| |: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :|
On 7/7/26 11:23, Daniel P. Berrangé wrote:
On Tue, Jul 07, 2026 at 09:48:22AM +0200, Denis V. Lunev via Devel wrote:
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 each dump attempt, the oldest files 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. This is a good idea, however, I wonder if we should also have a global cross-VM limit, as with cloud you might equally see a loop where a VM fails to spawn and automation spins up a new VM with different identity each time.
Perhaps also "auto_dump_min_free_space" where we statfs the dump location to figure out current free space, subtract the VM RAM size (as a rough approximation for worst case dump size), and then reject the dump if the result would be under the limit.
If you want to do that, it could be a separate followup patch so no need to repost this series just for that.
I have tried to implement node-wide limit, not VM specific one. We have global directory (node-wide) and we enumerate all files there, effectively all dumps. Thus this should be summary limit. Den
participants (3)
-
Daniel P. Berrangé -
Denis V. Lunev -
Denis V. Lunev