From 5d9c0c1c4cf790d319f9ef4c8cb4825a2d739394 Mon Sep 17 00:00:00 2001 From: lizixin <2271170409@qq.com> Date: Mon, 1 Dec 2025 20:10:26 +0800 Subject: [PATCH] Fix: CVE-2025-12748 CVE-2025-13193 --- ...-before-parsing-the-whole-domain-XML.patch | 72 ++++ conf-Add-virDomainDefIDsParseString.patch | 85 +++++ conf-split-out-virDomainDefParseIDs.patch | 184 ++++++++++ libvirt.spec | 21 +- ...-before-parsing-the-whole-domain-XML.patch | 71 ++++ ...-before-parsing-the-whole-domain-XML.patch | 73 ++++ ...-before-parsing-the-whole-domain-XML.patch | 335 ++++++++++++++++++ ...t-umask-for-qemu-img-when-creating-e.patch | 36 ++ ...-before-parsing-the-whole-domain-XML.patch | 63 ++++ 9 files changed, 939 insertions(+), 1 deletion(-) create mode 100644 bhyve-Check-ACLs-before-parsing-the-whole-domain-XML.patch create mode 100644 conf-Add-virDomainDefIDsParseString.patch create mode 100644 conf-split-out-virDomainDefParseIDs.patch create mode 100644 libxl-Check-ACLs-before-parsing-the-whole-domain-XML.patch create mode 100644 lxc-Check-ACLs-before-parsing-the-whole-domain-XML.patch create mode 100644 qemu-Check-ACLs-before-parsing-the-whole-domain-XML.patch create mode 100644 qemu-snapshot-Set-umask-for-qemu-img-when-creating-e.patch create mode 100644 vz-Check-ACLs-before-parsing-the-whole-domain-XML.patch diff --git a/bhyve-Check-ACLs-before-parsing-the-whole-domain-XML.patch b/bhyve-Check-ACLs-before-parsing-the-whole-domain-XML.patch new file mode 100644 index 0000000..f5e45e6 --- /dev/null +++ b/bhyve-Check-ACLs-before-parsing-the-whole-domain-XML.patch @@ -0,0 +1,72 @@ +From cb69fea3fe82ea9b685d8f193fdb22ef719688ef Mon Sep 17 00:00:00 2001 +From: Martin Kletzander +Date: Thu, 6 Nov 2025 15:31:12 +0100 +Subject: [PATCH 3/8] bhyve: Check ACLs before parsing the whole domain XML +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Utilise the new virDomainDefIDsParseString() for that. + +Fixes: CVE-2025-12748 +Reported-by: Святослав Терешин +Signed-off-by: Martin Kletzander +Reviewed-by: Michal Privoznik +--- + src/bhyve/bhyve_driver.c | 24 ++++++++++++++++++------ + 1 file changed, 18 insertions(+), 6 deletions(-) + +diff --git a/src/bhyve/bhyve_driver.c b/src/bhyve/bhyve_driver.c +index b6204c7fb9..b0c78f8023 100644 +--- a/src/bhyve/bhyve_driver.c ++++ b/src/bhyve/bhyve_driver.c +@@ -520,6 +520,15 @@ bhyveDomainDefineXMLFlags(virConnectPtr conn, const char *xml, unsigned int flag + if (!caps) + return NULL; + ++ /* Avoid parsing the whole domain definition for ACL checks */ ++ if (!(def = virDomainDefIDsParseString(xml, parse_flags))) ++ return NULL; ++ ++ if (virDomainDefineXMLFlagsEnsureACL(conn, def) < 0) ++ return NULL; ++ ++ g_clear_pointer(&def, virDomainDefFree); ++ + if ((def = virDomainDefParseString(xml, privconn->xmlopt, + NULL, parse_flags)) == NULL) + goto cleanup; +@@ -527,9 +536,6 @@ bhyveDomainDefineXMLFlags(virConnectPtr conn, const char *xml, unsigned int flag + if (virXMLCheckIllegalChars("name", def->name, "\n") < 0) + goto cleanup; + +- if (virDomainDefineXMLFlagsEnsureACL(conn, def) < 0) +- goto cleanup; +- + if (bhyveDomainAssignAddresses(def, NULL) < 0) + goto cleanup; + +@@ -904,11 +910,17 @@ bhyveDomainCreateXML(virConnectPtr conn, + if (flags & VIR_DOMAIN_START_AUTODESTROY) + start_flags |= VIR_BHYVE_PROCESS_START_AUTODESTROY; + +- if ((def = virDomainDefParseString(xml, privconn->xmlopt, +- NULL, parse_flags)) == NULL) +- goto cleanup; ++ /* Avoid parsing the whole domain definition for ACL checks */ ++ if (!(def = virDomainDefIDsParseString(xml, parse_flags))) ++ return NULL; + + if (virDomainCreateXMLEnsureACL(conn, def) < 0) ++ return NULL; ++ ++ g_clear_pointer(&def, virDomainDefFree); ++ ++ if ((def = virDomainDefParseString(xml, privconn->xmlopt, ++ NULL, parse_flags)) == NULL) + goto cleanup; + + if (bhyveDomainAssignAddresses(def, NULL) < 0) +-- +2.50.1 + diff --git a/conf-Add-virDomainDefIDsParseString.patch b/conf-Add-virDomainDefIDsParseString.patch new file mode 100644 index 0000000..c406412 --- /dev/null +++ b/conf-Add-virDomainDefIDsParseString.patch @@ -0,0 +1,85 @@ +From c771c5922e546b463a3c4987ac48dc4786076b83 Mon Sep 17 00:00:00 2001 +From: Martin Kletzander +Date: Thu, 6 Nov 2025 14:33:31 +0100 +Subject: [PATCH 2/8] conf: Add virDomainDefIDsParseString + +This function performs only parsing with the underlying +virDomainDefParseIDs() function to get needed metadata for any ACL +checks, but nothing else to avoid extraneous allocations and any +parser-induced DoS over ACL-forbidden connections. + +Signed-off-by: Martin Kletzander +Reviewed-by: Michal Privoznik +--- + src/conf/domain_conf.c | 28 ++++++++++++++++++++++++++++ + src/conf/domain_conf.h | 2 ++ + src/libvirt_private.syms | 1 + + 3 files changed, 31 insertions(+) + +diff --git a/src/conf/domain_conf.c b/src/conf/domain_conf.c +index d85e044092..18aab8ca5d 100644 +--- a/src/conf/domain_conf.c ++++ b/src/conf/domain_conf.c +@@ -22480,6 +22480,34 @@ virDomainDefParse(const char *xmlStr, + return def; + } + ++virDomainDef * ++virDomainDefIDsParseString(const char *xmlStr, ++ unsigned int flags) ++{ ++ g_autoptr(virDomainDef) def = NULL; ++ g_autoptr(xmlDoc) xml = NULL; ++ g_autoptr(xmlXPathContext) ctxt = NULL; ++ bool uuid_generated = false; ++ int keepBlanksDefault = xmlKeepBlanksDefault(0); ++ ++ if (!(xml = virXMLParse(NULL, xmlStr, _("(domain_definition)")))) ++ goto cleanup; ++ ++ def = virDomainDefNew(); ++ if (!def) ++ goto cleanup; ++ ++ if (virDomainDefParseIDs(def, ctxt, flags, &uuid_generated) < 0) ++ goto cleanup; ++ ++ if (uuid_generated) ++ memset(def->uuid, 0, VIR_UUID_BUFLEN); ++ ++ cleanup: ++ xmlKeepBlanksDefault(keepBlanksDefault); ++ return g_steal_pointer(&def); ++} ++ + virDomainDefPtr + virDomainDefParseString(const char *xmlStr, + virDomainXMLOptionPtr xmlopt, +diff --git a/src/conf/domain_conf.h b/src/conf/domain_conf.h +index 43b42e433c..5b22838073 100644 +--- a/src/conf/domain_conf.h ++++ b/src/conf/domain_conf.h +@@ -3108,6 +3108,8 @@ virDomainDeviceDefPtr virDomainDeviceDefParse(const char *xmlStr, + virDomainDiskDefPtr virDomainDiskDefParse(const char *xmlStr, + virDomainXMLOptionPtr xmlopt, + unsigned int flags); ++virDomainDef * virDomainDefIDsParseString(const char *xmlStr, ++ unsigned int flags); + virDomainDefPtr virDomainDefParseString(const char *xmlStr, + virDomainXMLOptionPtr xmlopt, + void *parseOpaque, +diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms +index b9085e1c74..6f6b42cf6f 100644 +--- a/src/libvirt_private.syms ++++ b/src/libvirt_private.syms +@@ -323,6 +323,7 @@ virDomainDefHasOldStyleUEFI; + virDomainDefHasUSB; + virDomainDefHasVcpusOffline; + virDomainDefHasVFIOHostdev; ++virDomainDefIDsParseString; + virDomainDefLifecycleActionAllowed; + virDomainDefMaybeAddController; + virDomainDefMaybeAddInput; +-- +2.50.1 + diff --git a/conf-split-out-virDomainDefParseIDs.patch b/conf-split-out-virDomainDefParseIDs.patch new file mode 100644 index 0000000..61d1baf --- /dev/null +++ b/conf-split-out-virDomainDefParseIDs.patch @@ -0,0 +1,184 @@ +From 2b53b05c1e0fc65fb2fb4ffef2848e900ab9f6f9 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?J=C3=A1n=20Tomko?= +Date: Wed, 29 Jul 2020 00:18:27 +0200 +Subject: [PATCH 1/8] conf: split out virDomainDefParseIDs +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Signed-off-by: Ján Tomko +Reviewed-by: Michal Privoznik +--- + src/conf/domain_conf.c | 138 +++++++++++++++++++++++------------------ + 1 file changed, 78 insertions(+), 60 deletions(-) + +diff --git a/src/conf/domain_conf.c b/src/conf/domain_conf.c +index ed15f95519..d85e044092 100644 +--- a/src/conf/domain_conf.c ++++ b/src/conf/domain_conf.c +@@ -20407,6 +20407,82 @@ virDomainCachetuneDefParse(virDomainDefPtr def, + return ret; + } + ++static int ++virDomainDefParseIDs(virDomainDefPtr def, ++ xmlXPathContextPtr ctxt, ++ unsigned int flags, ++ bool *uuid_generated) ++{ ++ g_autofree xmlNodePtr *nodes = NULL; ++ g_autofree char *tmp = NULL; ++ long id = -1; ++ int n; ++ ++ if (!(flags & VIR_DOMAIN_DEF_PARSE_INACTIVE)) ++ if (virXPathLong("string(./@id)", ctxt, &id) < 0) ++ id = -1; ++ def->id = (int)id; ++ ++ /* Extract domain name */ ++ if (!(def->name = virXPathString("string(./name[1])", ctxt))) { ++ virReportError(VIR_ERR_NO_NAME, NULL); ++ goto error; ++ } ++ ++ /* Extract domain uuid. If both uuid and sysinfo/system/entry/uuid ++ * exist, they must match; and if only the latter exists, it can ++ * also serve as the uuid. */ ++ tmp = virXPathString("string(./uuid[1])", ctxt); ++ if (!tmp) { ++ if (virUUIDGenerate(def->uuid) < 0) { ++ virReportError(VIR_ERR_INTERNAL_ERROR, ++ "%s", _("Failed to generate UUID")); ++ goto error; ++ } ++ *uuid_generated = true; ++ } else { ++ if (virUUIDParse(tmp, def->uuid) < 0) { ++ virReportError(VIR_ERR_INTERNAL_ERROR, ++ "%s", _("malformed uuid element")); ++ goto error; ++ } ++ VIR_FREE(tmp); ++ } ++ ++ /* Extract domain genid - a genid can either be provided or generated */ ++ if ((n = virXPathNodeSet("./genid", ctxt, &nodes)) < 0) ++ goto error; ++ ++ if (n > 0) { ++ if (n != 1) { ++ virReportError(VIR_ERR_XML_ERROR, "%s", ++ _("element 'genid' can only appear once")); ++ goto error; ++ } ++ def->genidRequested = true; ++ if (!(tmp = virXPathString("string(./genid)", ctxt))) { ++ if (virUUIDGenerate(def->genid) < 0) { ++ virReportError(VIR_ERR_INTERNAL_ERROR, ++ "%s", _("Failed to generate genid")); ++ goto error; ++ } ++ def->genidGenerated = true; ++ } else { ++ if (virUUIDParse(tmp, def->genid) < 0) { ++ virReportError(VIR_ERR_INTERNAL_ERROR, ++ "%s", _("malformed genid element")); ++ goto error; ++ } ++ VIR_FREE(tmp); ++ } ++ } ++ VIR_FREE(nodes); ++ return 0; ++ ++ error: ++ return -1; ++} ++ + + static int + virDomainDefParseCaps(virDomainDefPtr def, +@@ -20668,7 +20744,6 @@ virDomainDefParseXML(xmlDocPtr xml, + xmlNodePtr node = NULL; + size_t i, j; + int n, gic_version; +- long id = -1; + virDomainDefPtr def; + bool uuid_generated = false; + bool usb_none = false; +@@ -20692,69 +20767,12 @@ virDomainDefParseXML(xmlDocPtr xml, + if (!(def = virDomainDefNew())) + return NULL; + +- if (!(flags & VIR_DOMAIN_DEF_PARSE_INACTIVE)) +- if (virXPathLong("string(./@id)", ctxt, &id) < 0) +- id = -1; +- def->id = (int)id; +- +- if (virDomainDefParseCaps(def, ctxt, xmlopt) < 0) ++ if (virDomainDefParseIDs(def, ctxt, flags, &uuid_generated) < 0) + goto error; + +- /* Extract domain name */ +- if (!(def->name = virXPathString("string(./name[1])", ctxt))) { +- virReportError(VIR_ERR_NO_NAME, NULL); +- goto error; +- } +- +- /* Extract domain uuid. If both uuid and sysinfo/system/entry/uuid +- * exist, they must match; and if only the latter exists, it can +- * also serve as the uuid. */ +- tmp = virXPathString("string(./uuid[1])", ctxt); +- if (!tmp) { +- if (virUUIDGenerate(def->uuid) < 0) { +- virReportError(VIR_ERR_INTERNAL_ERROR, +- "%s", _("Failed to generate UUID")); +- goto error; +- } +- uuid_generated = true; +- } else { +- if (virUUIDParse(tmp, def->uuid) < 0) { +- virReportError(VIR_ERR_INTERNAL_ERROR, +- "%s", _("malformed uuid element")); +- goto error; +- } +- VIR_FREE(tmp); +- } +- +- /* Extract domain genid - a genid can either be provided or generated */ +- if ((n = virXPathNodeSet("./genid", ctxt, &nodes)) < 0) ++ if (virDomainDefParseCaps(def, ctxt, xmlopt) < 0) + goto error; + +- if (n > 0) { +- if (n != 1) { +- virReportError(VIR_ERR_XML_ERROR, "%s", +- _("element 'genid' can only appear once")); +- goto error; +- } +- def->genidRequested = true; +- if (!(tmp = virXPathString("string(./genid)", ctxt))) { +- if (virUUIDGenerate(def->genid) < 0) { +- virReportError(VIR_ERR_INTERNAL_ERROR, +- "%s", _("Failed to generate genid")); +- goto error; +- } +- def->genidGenerated = true; +- } else { +- if (virUUIDParse(tmp, def->genid) < 0) { +- virReportError(VIR_ERR_INTERNAL_ERROR, +- "%s", _("malformed genid element")); +- goto error; +- } +- VIR_FREE(tmp); +- } +- } +- VIR_FREE(nodes); +- + /* Extract short description of domain (title) */ + def->title = virXPathString("string(./title[1])", ctxt); + if (def->title && strchr(def->title, '\n')) { +-- +2.50.1 + diff --git a/libvirt.spec b/libvirt.spec index aa0481e..92f871e 100644 --- a/libvirt.spec +++ b/libvirt.spec @@ -101,7 +101,7 @@ Summary: Library providing a simple virtualization API Name: libvirt Version: 6.2.0 -Release: 25 +Release: 26 License: LGPLv2+ URL: https://libvirt.org/ @@ -162,6 +162,14 @@ Patch0049: interface-fix-udev_device_get_sysattr_value-return-v.patch Patch0050: util-keep-track-of-full-GSource-object-not-source-ID.patch Patch0051: rpc-mark-source-returned-by-virEventGLibAddSocketWat.patch Patch0052: rpc-ensure-temporary-GSource-is-removed-from-client-.patch +Patch0053: conf-split-out-virDomainDefParseIDs.patch +Patch0054: conf-Add-virDomainDefIDsParseString.patch +Patch0055: bhyve-Check-ACLs-before-parsing-the-whole-domain-XML.patch +Patch0056: libxl-Check-ACLs-before-parsing-the-whole-domain-XML.patch +Patch0057: lxc-Check-ACLs-before-parsing-the-whole-domain-XML.patch +Patch0058: vz-Check-ACLs-before-parsing-the-whole-domain-XML.patch +Patch0059: qemu-Check-ACLs-before-parsing-the-whole-domain-XML.patch +Patch0060: qemu-snapshot-Set-umask-for-qemu-img-when-creating-e.patch Requires: libvirt-daemon = %{version}-%{release} Requires: libvirt-daemon-config-network = %{version}-%{release} @@ -1896,6 +1904,17 @@ exit 0 %changelog +* Mon Dec 1 2025 ZixinLi - 6.2.0-26 +- Fix CVE-2025-12748 CVE-2025-13193 +- conf: split out virDomainDefParseIDs (CVE-2025-12748) +- conf: Add virDomainDefIDsParseString (CVE-2025-12748) +- bhyve: Check ACLs before parsing the whole domain XML (CVE-2025-12748) +- libxl: Check ACLs before parsing the whole domain XML (CVE-2025-12748) +- lxc: Check ACLs before parsing the whole domain XML (CVE-2025-12748) +- vz: Check ACLs before parsing the whole domain XML (CVE-2025-12748) +- qemu: Check ACLs before parsing the whole domain XML (CVE-2025-12748) +- qemu: snapshot: Set umask for 'qemu-img' when creating external inactive snapshots (CVE-2025-13193) + * Fri May 24 2024 jiangjiacheng - util: keep track of full GSource object not source ID number - rpc: mark source returned by virEventGLibAddSocketWatch as unused diff --git a/libxl-Check-ACLs-before-parsing-the-whole-domain-XML.patch b/libxl-Check-ACLs-before-parsing-the-whole-domain-XML.patch new file mode 100644 index 0000000..c1f5039 --- /dev/null +++ b/libxl-Check-ACLs-before-parsing-the-whole-domain-XML.patch @@ -0,0 +1,71 @@ +From fbef17a351dacf1e7da7564b0f79d65ac8c11bd6 Mon Sep 17 00:00:00 2001 +From: Martin Kletzander +Date: Thu, 6 Nov 2025 15:43:57 +0100 +Subject: [PATCH 4/8] libxl: Check ACLs before parsing the whole domain XML +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Utilise the new virDomainDefIDsParseString() for that. + +Fixes: CVE-2025-12748 +Reported-by: Святослав Терешин +Signed-off-by: Martin Kletzander +Reviewed-by: Michal Privoznik +--- + src/libxl/libxl_driver.c | 20 +++++++++++++++----- + 1 file changed, 15 insertions(+), 5 deletions(-) + +diff --git a/src/libxl/libxl_driver.c b/src/libxl/libxl_driver.c +index f07700b71c..3493df63fb 100644 +--- a/src/libxl/libxl_driver.c ++++ b/src/libxl/libxl_driver.c +@@ -1050,13 +1050,18 @@ libxlDomainCreateXML(virConnectPtr conn, const char *xml, + if (flags & VIR_DOMAIN_START_VALIDATE) + parse_flags |= VIR_DOMAIN_DEF_PARSE_VALIDATE_SCHEMA; + +- if (!(def = virDomainDefParseString(xml, driver->xmlopt, +- NULL, parse_flags))) ++ if (!(def = virDomainDefIDsParseString(xml, parse_flags))) + goto cleanup; + + if (virDomainCreateXMLEnsureACL(conn, def) < 0) + goto cleanup; + ++ g_clear_pointer(&def, virDomainDefFree); ++ ++ if (!(def = virDomainDefParseString(xml, driver->xmlopt, ++ NULL, parse_flags))) ++ goto cleanup; ++ + if (!(vm = virDomainObjListAdd(driver->domains, def, + driver->xmlopt, + VIR_DOMAIN_OBJ_LIST_ADD_LIVE | +@@ -2858,6 +2863,14 @@ libxlDomainDefineXMLFlags(virConnectPtr conn, const char *xml, unsigned int flag + if (flags & VIR_DOMAIN_DEFINE_VALIDATE) + parse_flags |= VIR_DOMAIN_DEF_PARSE_VALIDATE_SCHEMA; + ++ if (!(def = virDomainDefIDsParseString(xml, parse_flags))) ++ goto cleanup; ++ ++ if (virDomainDefineXMLFlagsEnsureACL(conn, def) < 0) ++ goto cleanup; ++ ++ g_clear_pointer(&def, virDomainDefFree); ++ + if (!(def = virDomainDefParseString(xml, driver->xmlopt, + NULL, parse_flags))) + goto cleanup; +@@ -2865,9 +2878,6 @@ libxlDomainDefineXMLFlags(virConnectPtr conn, const char *xml, unsigned int flag + if (virXMLCheckIllegalChars("name", def->name, "\n") < 0) + goto cleanup; + +- if (virDomainDefineXMLFlagsEnsureACL(conn, def) < 0) +- goto cleanup; +- + if (!(vm = virDomainObjListAdd(driver->domains, def, + driver->xmlopt, + 0, +-- +2.50.1 + diff --git a/lxc-Check-ACLs-before-parsing-the-whole-domain-XML.patch b/lxc-Check-ACLs-before-parsing-the-whole-domain-XML.patch new file mode 100644 index 0000000..79cd1f3 --- /dev/null +++ b/lxc-Check-ACLs-before-parsing-the-whole-domain-XML.patch @@ -0,0 +1,73 @@ +From fc700f48ec3a375ee818a17caf5f5b8a2e82c072 Mon Sep 17 00:00:00 2001 +From: Martin Kletzander +Date: Thu, 6 Nov 2025 15:49:01 +0100 +Subject: [PATCH 5/8] lxc: Check ACLs before parsing the whole domain XML +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Utilise the new virDomainDefIDsParseString() for that. + +Fixes: CVE-2025-12748 +Reported-by: Святослав Терешин +Signed-off-by: Martin Kletzander +Reviewed-by: Michal Privoznik +--- + src/lxc/lxc_driver.c | 22 +++++++++++++++++----- + 1 file changed, 17 insertions(+), 5 deletions(-) + +diff --git a/src/lxc/lxc_driver.c b/src/lxc/lxc_driver.c +index 120d41f2b6..e2cf67258d 100644 +--- a/src/lxc/lxc_driver.c ++++ b/src/lxc/lxc_driver.c +@@ -416,6 +416,15 @@ lxcDomainDefineXMLFlags(virConnectPtr conn, const char *xml, unsigned int flags) + if (!(caps = virLXCDriverGetCapabilities(driver, false))) + goto cleanup; + ++ /* Avoid parsing the whole domain definition for ACL checks */ ++ if (!(def = virDomainDefIDsParseString(xml, parse_flags))) ++ goto cleanup; ++ ++ if (virDomainDefineXMLFlagsEnsureACL(conn, def) < 0) ++ goto cleanup; ++ ++ g_clear_pointer(&def, virDomainDefFree); ++ + if (!(def = virDomainDefParseString(xml, driver->xmlopt, + NULL, parse_flags))) + goto cleanup; +@@ -423,9 +432,6 @@ lxcDomainDefineXMLFlags(virConnectPtr conn, const char *xml, unsigned int flags) + if (virXMLCheckIllegalChars("name", def->name, "\n") < 0) + goto cleanup; + +- if (virDomainDefineXMLFlagsEnsureACL(conn, def) < 0) +- goto cleanup; +- + if (virSecurityManagerVerify(driver->securityManager, def) < 0) + goto cleanup; + +@@ -1101,13 +1107,19 @@ lxcDomainCreateXMLWithFiles(virConnectPtr conn, + if (!(caps = virLXCDriverGetCapabilities(driver, false))) + goto cleanup; + +- if (!(def = virDomainDefParseString(xml, driver->xmlopt, +- NULL, parse_flags))) ++ /* Avoid parsing the whole domain definition for ACL checks */ ++ if (!(def = virDomainDefIDsParseString(xml, parse_flags))) + goto cleanup; + + if (virDomainCreateXMLWithFilesEnsureACL(conn, def) < 0) + goto cleanup; + ++ g_clear_pointer(&def, virDomainDefFree); ++ ++ if (!(def = virDomainDefParseString(xml, driver->xmlopt, ++ NULL, parse_flags))) ++ goto cleanup; ++ + if (virSecurityManagerVerify(driver->securityManager, def) < 0) + goto cleanup; + +-- +2.50.1 + diff --git a/qemu-Check-ACLs-before-parsing-the-whole-domain-XML.patch b/qemu-Check-ACLs-before-parsing-the-whole-domain-XML.patch new file mode 100644 index 0000000..3d25478 --- /dev/null +++ b/qemu-Check-ACLs-before-parsing-the-whole-domain-XML.patch @@ -0,0 +1,335 @@ +From f28d4c3201f4fca7643fc4ef81ad25ef49a1fdb0 Mon Sep 17 00:00:00 2001 +From: Martin Kletzander +Date: Thu, 6 Nov 2025 14:33:41 +0100 +Subject: [PATCH] qemu: Check ACLs before parsing the whole domain XML +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Utilise the new virDomainDefIDsParseString() for that. + +This is one of the more complex ones since there is also a function that +reads relevant metadata from a save image XML. In order _not_ to extract +the parsing out of the function (and make the function basically trivial +and all callers more complex) add a callback to the function which will +be used to check the ACLs. + +Fixes: CVE-2025-12748 +Reported-by: Святослав Терешин +Signed-off-by: Martin Kletzander +Reviewed-by: Michal Privoznik +--- + src/qemu/qemu_driver.c | 105 ++++++++++++++++++++++---------------- + src/qemu/qemu_migration.c | 22 +++++++- + src/qemu/qemu_migration.h | 4 +- + 3 files changed, 84 insertions(+), 47 deletions(-) + +diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c +index c0e88bc..2f40867 100644 +--- a/src/qemu/qemu_driver.c ++++ b/src/qemu/qemu_driver.c +@@ -1756,13 +1756,19 @@ static virDomainPtr qemuDomainCreateXML(virConnectPtr conn, + + virNWFilterReadLockFilterUpdates(); + +- if (!(def = virDomainDefParseString(xml, driver->xmlopt, +- NULL, parse_flags))) ++ /* Avoid parsing the whole domain definition for ACL checks */ ++ if (!(def = virDomainDefIDsParseString(xml, parse_flags))) + goto cleanup; + + if (virDomainCreateXMLEnsureACL(conn, def) < 0) + goto cleanup; + ++ g_clear_pointer(&def, virDomainDefFree); ++ ++ if (!(def = virDomainDefParseString(xml, driver->xmlopt, ++ NULL, parse_flags))) ++ goto cleanup; ++ + if (!(vm = virDomainObjListAdd(driver->domains, def, + driver->xmlopt, + VIR_DOMAIN_OBJ_LIST_ADD_LIVE | +@@ -6695,6 +6701,8 @@ qemuDomainSaveImageUpdateDef(virQEMUDriverPtr driver, + * @wrapperFd: returns the file wrapper structure + * @open_write: open the file for writing (for updates) + * @unlink_corrupt: remove the image file if it is corrupted ++ * @conn:parameter for the @ensureACL callback ++ * @ensureACL: ACL callback to check against the definition or NULL + * + * Returns the opened fd of the save image file and fills the appropriate fields + * on success. On error returns -1 on most failures, -3 if corrupt image was +@@ -6709,7 +6717,9 @@ qemuDomainSaveImageOpen(virQEMUDriverPtr driver, + bool bypass_cache, + virFileWrapperFdPtr *wrapperFd, + bool open_write, +- bool unlink_corrupt) ++ bool unlink_corrupt, ++ virConnectPtr conn, ++ int (*ensureACL)(virConnectPtr, virDomainDef *)) + { + int fd = -1; + virQEMUSaveDataPtr data = NULL; +@@ -6822,6 +6832,16 @@ qemuDomainSaveImageOpen(virQEMUDriverPtr driver, + } + } + ++ if (ensureACL) { ++ /* Parse only the IDs for ACL checks */ ++ g_autoptr(virDomainDef) aclDef = virDomainDefIDsParseString(data->xml, ++ VIR_DOMAIN_DEF_PARSE_INACTIVE | ++ VIR_DOMAIN_DEF_PARSE_SKIP_VALIDATE); ++ ++ if (!aclDef || ensureACL(conn, aclDef) < 0) ++ return -1; ++ } ++ + /* Create a domain from this XML */ + if (!(def = virDomainDefParseString(data->xml, driver->xmlopt, qemuCaps, + VIR_DOMAIN_DEF_PARSE_INACTIVE | +@@ -7003,13 +7023,10 @@ qemuDomainRestoreFlags(virConnectPtr conn, + + fd = qemuDomainSaveImageOpen(driver, NULL, path, &def, &data, + (flags & VIR_DOMAIN_SAVE_BYPASS_CACHE) != 0, +- &wrapperFd, false, false); ++ &wrapperFd, false, false, conn, virDomainRestoreFlagsEnsureACL); + if (fd < 0) + goto cleanup; + +- if (virDomainRestoreFlagsEnsureACL(conn, def) < 0) +- goto cleanup; +- + if (virHookPresent(VIR_HOOK_DRIVER_QEMU)) { + int hookret; + +@@ -7098,14 +7115,12 @@ qemuDomainSaveImageGetXMLDesc(virConnectPtr conn, const char *path, + virCheckFlags(VIR_DOMAIN_SAVE_IMAGE_XML_SECURE, NULL); + + fd = qemuDomainSaveImageOpen(driver, NULL, path, &def, &data, +- false, NULL, false, false); ++ false, NULL, false, false, ++ conn, virDomainSaveImageGetXMLDescEnsureACL); + + if (fd < 0) + goto cleanup; + +- if (virDomainSaveImageGetXMLDescEnsureACL(conn, def) < 0) +- goto cleanup; +- + ret = qemuDomainDefFormatXML(driver, NULL, def, flags); + + cleanup: +@@ -7136,14 +7151,12 @@ qemuDomainSaveImageDefineXML(virConnectPtr conn, const char *path, + state = 0; + + fd = qemuDomainSaveImageOpen(driver, NULL, path, &def, &data, +- false, NULL, true, false); ++ false, NULL, true, false, ++ conn, virDomainSaveImageDefineXMLEnsureACL); + + if (fd < 0) + goto cleanup; + +- if (virDomainSaveImageDefineXMLEnsureACL(conn, def) < 0) +- goto cleanup; +- + if (STREQ(data->xml, dxml) && + (state < 0 || state == data->header.was_running)) { + /* no change to the XML */ +@@ -7220,7 +7233,8 @@ qemuDomainManagedSaveGetXMLDesc(virDomainPtr dom, unsigned int flags) + } + + if ((fd = qemuDomainSaveImageOpen(driver, priv->qemuCaps, path, &def, &data, +- false, NULL, false, false)) < 0) ++ false, NULL, false, false, ++ NULL, NULL)) < 0) + goto cleanup; + + ret = qemuDomainDefFormatXML(driver, priv->qemuCaps, def, flags); +@@ -7285,7 +7299,8 @@ qemuDomainObjRestore(virConnectPtr conn, + virFileWrapperFdPtr wrapperFd = NULL; + + fd = qemuDomainSaveImageOpen(driver, NULL, path, &def, &data, +- bypass_cache, &wrapperFd, false, true); ++ bypass_cache, &wrapperFd, false, true, ++ NULL, NULL); + if (fd < 0) { + if (fd == -3) + ret = 1; +@@ -7641,6 +7656,15 @@ qemuDomainDefineXMLFlags(virConnectPtr conn, + if (flags & VIR_DOMAIN_DEFINE_VALIDATE) + parse_flags |= VIR_DOMAIN_DEF_PARSE_VALIDATE_SCHEMA; + ++ /* Avoid parsing the whole domain definition for ACL checks */ ++ if (!(def = virDomainDefIDsParseString(xml, parse_flags))) ++ return NULL; ++ ++ if (virDomainDefineXMLFlagsEnsureACL(conn, def) < 0) ++ return NULL; ++ ++ g_clear_pointer(&def, virDomainDefFree); ++ + if (!(def = virDomainDefParseString(xml, driver->xmlopt, + NULL, parse_flags))) + goto cleanup; +@@ -7648,9 +7672,6 @@ qemuDomainDefineXMLFlags(virConnectPtr conn, + if (virXMLCheckIllegalChars("name", def->name, "\n") < 0) + goto cleanup; + +- if (virDomainDefineXMLFlagsEnsureACL(conn, def) < 0) +- goto cleanup; +- + if (!(vm = virDomainObjListAdd(driver->domains, def, + driver->xmlopt, + 0, &oldDef))) +@@ -12153,10 +12174,9 @@ qemuDomainMigratePrepareTunnel(virConnectPtr dconn, + goto cleanup; + } + +- if (!(def = qemuMigrationAnyPrepareDef(driver, NULL, dom_xml, dname, &origname))) +- goto cleanup; +- +- if (virDomainMigratePrepareTunnelEnsureACL(dconn, def) < 0) ++ if (!(def = qemuMigrationAnyPrepareDef(driver, NULL, dom_xml, dname, &origname, ++ dconn, ++ virDomainMigratePrepareTunnelEnsureACL))) + goto cleanup; + + ret = qemuMigrationDstPrepareTunnel(driver, dconn, +@@ -12213,11 +12233,10 @@ qemuDomainMigratePrepare2(virConnectPtr dconn, + goto cleanup; + } + +- if (!(def = qemuMigrationAnyPrepareDef(driver, NULL, dom_xml, dname, &origname))) +- goto cleanup; +- +- if (virDomainMigratePrepare2EnsureACL(dconn, def) < 0) +- goto cleanup; ++ if (!(def = qemuMigrationAnyPrepareDef(driver, NULL, dom_xml, dname, &origname, ++ dconn, ++ virDomainMigratePrepare2EnsureACL))) ++ goto cleanup; + + /* Do not use cookies in v2 protocol, since the cookie + * length was not sufficiently large, causing failures +@@ -12447,10 +12466,9 @@ qemuDomainMigratePrepare3(virConnectPtr dconn, + QEMU_MIGRATION_DESTINATION))) + goto cleanup; + +- if (!(def = qemuMigrationAnyPrepareDef(driver, NULL, dom_xml, dname, &origname))) +- goto cleanup; +- +- if (virDomainMigratePrepare3EnsureACL(dconn, def) < 0) ++ if (!(def = qemuMigrationAnyPrepareDef(driver, NULL, dom_xml, dname, &origname, ++ dconn, ++ virDomainMigratePrepare3EnsureACL))) + goto cleanup; + + ret = qemuMigrationDstPrepareDirect(driver, dconn, +@@ -12533,10 +12551,9 @@ qemuDomainMigratePrepare3Params(virConnectPtr dconn, + goto cleanup; + } + +- if (!(def = qemuMigrationAnyPrepareDef(driver, NULL, dom_xml, dname, &origname))) +- goto cleanup; +- +- if (virDomainMigratePrepare3ParamsEnsureACL(dconn, def) < 0) ++ if (!(def = qemuMigrationAnyPrepareDef(driver, NULL, dom_xml, dname, &origname, ++ dconn, ++ virDomainMigratePrepare3ParamsEnsureACL))) + goto cleanup; + + ret = qemuMigrationDstPrepareDirect(driver, dconn, +@@ -12585,10 +12602,9 @@ qemuDomainMigratePrepareTunnel3(virConnectPtr dconn, + QEMU_MIGRATION_DESTINATION))) + goto cleanup; + +- if (!(def = qemuMigrationAnyPrepareDef(driver, NULL, dom_xml, dname, &origname))) +- goto cleanup; +- +- if (virDomainMigratePrepareTunnel3EnsureACL(dconn, def) < 0) ++ if (!(def = qemuMigrationAnyPrepareDef(driver, NULL, dom_xml, dname, &origname, ++ dconn, ++ virDomainMigratePrepareTunnel3EnsureACL))) + goto cleanup; + + ret = qemuMigrationDstPrepareTunnel(driver, dconn, +@@ -12643,10 +12659,9 @@ qemuDomainMigratePrepareTunnel3Params(virConnectPtr dconn, + QEMU_MIGRATION_DESTINATION))) + goto cleanup; + +- if (!(def = qemuMigrationAnyPrepareDef(driver, NULL, dom_xml, dname, &origname))) +- goto cleanup; +- +- if (virDomainMigratePrepareTunnel3ParamsEnsureACL(dconn, def) < 0) ++ if (!(def = qemuMigrationAnyPrepareDef(driver, NULL, dom_xml, dname, &origname, ++ dconn, ++ virDomainMigratePrepareTunnel3ParamsEnsureACL))) + goto cleanup; + + ret = qemuMigrationDstPrepareTunnel(driver, dconn, +diff --git a/src/qemu/qemu_migration.c b/src/qemu/qemu_migration.c +index 65b47ec..a18a488 100644 +--- a/src/qemu/qemu_migration.c ++++ b/src/qemu/qemu_migration.c +@@ -2959,7 +2959,9 @@ qemuMigrationAnyPrepareDef(virQEMUDriverPtr driver, + virQEMUCapsPtr qemuCaps, + const char *dom_xml, + const char *dname, +- char **origname) ++ char **origname, ++ virConnectPtr sconn, ++ int (*ensureACL)(virConnectPtr, virDomainDef *)) + { + virDomainDefPtr def; + char *name = NULL; +@@ -2970,6 +2972,23 @@ qemuMigrationAnyPrepareDef(virQEMUDriverPtr driver, + return NULL; + } + ++ if (ensureACL) { ++ g_autoptr(virDomainDef) aclDef = NULL; ++ ++ /* Avoid parsing the whole domain definition for ACL checks */ ++ if (!(aclDef = virDomainDefIDsParseString(dom_xml, VIR_DOMAIN_DEF_PARSE_INACTIVE))) ++ return NULL; ++ ++ if (dname) { ++ VIR_FREE(aclDef->name); ++ aclDef->name = g_strdup(dname); ++ } ++ ++ if (ensureACL(sconn, aclDef) < 0) { ++ return NULL; ++ } ++ } ++ + if (!(def = virDomainDefParseString(dom_xml, driver->xmlopt, + qemuCaps, + VIR_DOMAIN_DEF_PARSE_INACTIVE | +@@ -3541,6 +3560,7 @@ qemuMigrationSrcRun(virQEMUDriverPtr driver, + if (!(persistDef = qemuMigrationAnyPrepareDef(driver, + priv->qemuCaps, + persist_xml, ++ NULL, NULL, + NULL, NULL))) + goto error; + } else { +diff --git a/src/qemu/qemu_migration.h b/src/qemu/qemu_migration.h +index b6f88d3..cdbe0fc 100644 +--- a/src/qemu/qemu_migration.h ++++ b/src/qemu/qemu_migration.h +@@ -119,7 +119,9 @@ qemuMigrationAnyPrepareDef(virQEMUDriverPtr driver, + virQEMUCapsPtr qemuCaps, + const char *dom_xml, + const char *dname, +- char **origname); ++ char **origname, ++ virConnectPtr sconn, ++ int (*ensureACL)(virConnectPtr, virDomainDef *)); + + int + qemuMigrationDstPrepareTunnel(virQEMUDriverPtr driver, +-- +2.50.1 + diff --git a/qemu-snapshot-Set-umask-for-qemu-img-when-creating-e.patch b/qemu-snapshot-Set-umask-for-qemu-img-when-creating-e.patch new file mode 100644 index 0000000..f6e4a1a --- /dev/null +++ b/qemu-snapshot-Set-umask-for-qemu-img-when-creating-e.patch @@ -0,0 +1,36 @@ +From ef2ae16622d9ba209a11c9f227d67179a1072b21 Mon Sep 17 00:00:00 2001 +From: Peter Krempa +Date: Wed, 12 Nov 2025 17:52:05 +0100 +Subject: [PATCH 8/8] qemu: snapshot: Set umask for 'qemu-img' when creating + external inactive snapshots + +External inactive snapshots are created by invoking 'qemu-img' which +creates the file. Currently qemu-img creates image with mode 644 based +on default umask as libvirt doesn't set any. + +Having a world-readable image is obviously wrong so set the umask to +077 to have the file readable only by the owner. + +Resolves: https://bugs.debian.org/1120119 +Signed-off-by: Peter Krempa +--- + src/qemu/qemu_driver.c | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c +index 88cc7fc5ea..7117c6a3fc 100644 +--- a/src/qemu/qemu_driver.c ++++ b/src/qemu/qemu_driver.c +@@ -14381,6 +14381,9 @@ qemuDomainSnapshotCreateInactiveExternal(virQEMUDriverPtr driver, + NULL))) + goto cleanup; + ++ /* ensure that new files are only readable by the user */ ++ virCommandSetUmask(cmd, 0077); ++ + /* adds cmd line arg: backing_fmt=format,backing_file=/path/to/backing/file */ + virBufferAsprintf(&buf, "backing_fmt=%s,backing_file=", + virStorageFileFormatTypeToString(defdisk->src->format)); +-- +2.50.1 + diff --git a/vz-Check-ACLs-before-parsing-the-whole-domain-XML.patch b/vz-Check-ACLs-before-parsing-the-whole-domain-XML.patch new file mode 100644 index 0000000..2242a28 --- /dev/null +++ b/vz-Check-ACLs-before-parsing-the-whole-domain-XML.patch @@ -0,0 +1,63 @@ +From b5f4dac639159e76b150ef38befb82a2cfd20f0f Mon Sep 17 00:00:00 2001 +From: Martin Kletzander +Date: Thu, 6 Nov 2025 16:03:26 +0100 +Subject: [PATCH 6/8] vz: Check ACLs before parsing the whole domain XML +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Utilise the new virDomainDefIDsParseString() for that. + +Fixes: CVE-2025-12748 +Reported-by: Святослав Терешин +Signed-off-by: Martin Kletzander +Reviewed-by: Michal Privoznik +--- + src/vz/vz_driver.c | 17 +++++++++++------ + 1 file changed, 11 insertions(+), 6 deletions(-) + +diff --git a/src/vz/vz_driver.c b/src/vz/vz_driver.c +index d882b91def..a835b2b1a3 100644 +--- a/src/vz/vz_driver.c ++++ b/src/vz/vz_driver.c +@@ -801,6 +801,15 @@ vzDomainDefineXMLFlags(virConnectPtr conn, const char *xml, unsigned int flags) + if (flags & VIR_DOMAIN_DEFINE_VALIDATE) + parse_flags |= VIR_DOMAIN_DEF_PARSE_VALIDATE_SCHEMA; + ++ /* Avoid parsing the whole domain definition for ACL checks */ ++ if (!(def = virDomainDefIDsParseString(xml, parse_flags))) ++ return NULL; ++ ++ if (virDomainDefineXMLFlagsEnsureACL(conn, def) < 0) ++ return NULL; ++ ++ g_clear_pointer(&def, virDomainDefFree); ++ + if ((def = virDomainDefParseString(xml, driver->xmlopt, + NULL, parse_flags)) == NULL) + goto cleanup; +@@ -808,9 +817,6 @@ vzDomainDefineXMLFlags(virConnectPtr conn, const char *xml, unsigned int flags) + if (virXMLCheckIllegalChars("name", def->name, "\n") < 0) + goto cleanup; + +- if (virDomainDefineXMLFlagsEnsureACL(conn, def) < 0) +- goto cleanup; +- + dom = virDomainObjListFindByUUID(driver->domains, def->uuid); + if (dom == NULL) { + virResetLastError(); +@@ -3000,9 +3006,8 @@ vzDomainMigratePrepare3Params(virConnectPtr conn, + | VZ_MIGRATION_COOKIE_DOMAIN_NAME) < 0) + goto cleanup; + +- if (!(def = virDomainDefParseString(dom_xml, driver->xmlopt, +- NULL, +- VIR_DOMAIN_DEF_PARSE_INACTIVE))) ++ /* Avoid parsing the whole domain definition for ACL checks */ ++ if (!(def = virDomainDefIDsParseString(dom_xml, VIR_DOMAIN_DEF_PARSE_INACTIVE))) + goto cleanup; + + if (dname) { +-- +2.50.1 + -- Gitee