From nobody Sun Sep 28 16:35:50 2025 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=fail(p=reject dis=none) header.from=rsg.ci.i.u-tokyo.ac.jp Return-Path: Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1756707291361469.4154355901459; Sun, 31 Aug 2025 23:14:51 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1usxmE-0001JI-Er; Mon, 01 Sep 2025 02:12:26 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1usxlc-0000mg-DL; Mon, 01 Sep 2025 02:11:49 -0400 Received: from www3579.sakura.ne.jp ([49.212.243.89]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1usxlY-0002aX-9O; Mon, 01 Sep 2025 02:11:48 -0400 Received: from h205.csg.ci.i.u-tokyo.ac.jp (h205.csg.ci.i.u-tokyo.ac.jp [133.11.54.205]) (authenticated bits=0) by www3579.sakura.ne.jp (8.16.1/8.16.1) with ESMTPSA id 5816ACAk076640 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NO); Mon, 1 Sep 2025 15:10:31 +0900 (JST) (envelope-from odaki@rsg.ci.i.u-tokyo.ac.jp) DKIM-Signature: a=rsa-sha256; bh=iGdH8z3BWY4aeVno3WKHTvunvaA5O+8C3rxcrDsvFq4=; c=relaxed/relaxed; d=rsg.ci.i.u-tokyo.ac.jp; h=From:Date:Subject:Message-Id:To; s=rs20250326; t=1756707031; v=1; b=hz2SAbXoQZZT8aXOmqDyWHr13ytqPcjT8zJ8FejEmllqZCELnflCBKUW7WS5HCdU nsdjd+76Z8RO/uqlytEOfDkQAjxJr0JdJtVQnLAdNdKOs/kDDHueyQeLnsVEyJjt 4Ga8xCzehcInGpDBIaJ098lS/Q1M0ORhVOD7Y7dF590xMKGWPh+KtfhgGskOpu8n i9njeWtMgKoLB8NXObF6IbMp/nYOjn3KxKJMj+pMcXu3RwNZDhGMLYwcQPkyPDrF uKshBgiIsc6m4paxGjH9lEBpYiVgKPBI0x0BUhd/RURcDcWpXX5jg8MElr6d/Sil Dz9WnTzl2h2x9bF9HZdkqQ== From: Akihiko Odaki Date: Mon, 01 Sep 2025 15:10:14 +0900 Subject: [PATCH 16/16] memory: Stop piggybacking on memory region owners MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20250901-mr-v1-16-dd7cb6b1480b@rsg.ci.i.u-tokyo.ac.jp> References: <20250901-mr-v1-0-dd7cb6b1480b@rsg.ci.i.u-tokyo.ac.jp> In-Reply-To: <20250901-mr-v1-0-dd7cb6b1480b@rsg.ci.i.u-tokyo.ac.jp> To: qemu-devel@nongnu.org Cc: Alex Williamson , =?utf-8?q?C=C3=A9dric_Le_Goater?= , Paolo Bonzini , =?utf-8?q?Daniel_P=2E_Berrang=C3=A9?= , Eduardo Habkost , Peter Xu , David Hildenbrand , =?utf-8?q?Philippe_Mathieu-Daud=C3=A9?= , Richard Henderson , Helge Deller , =?utf-8?q?Marc-Andr=C3=A9_Lureau?= , "Michael S. Tsirkin" , Gerd Hoffmann , John Snow , qemu-block@nongnu.org, Keith Busch , Klaus Jensen , Jesper Devantier , Marcel Apfelbaum , Nicholas Piggin , Harsh Prateek Bora , qemu-ppc@nongnu.org, John Levon , Thanos Makatos , Yanan Wang , BALATON Zoltan , Jiaxun Yang , Daniel Henrique Barboza , David Gibson , Harsh Prateek Bora , Alexey Kardashevskiy , =?utf-8?q?Alex_Benn=C3=A9e?= , Fabiano Rosas , Thomas Huth , Laurent Vivier , Peter Maydell , Akihiko Odaki X-Mailer: b4 0.14.2 Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=49.212.243.89; envelope-from=odaki@rsg.ci.i.u-tokyo.ac.jp; helo=www3579.sakura.ne.jp X-Spam_score_int: -16 X-Spam_score: -1.7 X-Spam_bar: - X-Spam_report: (-1.7 / 5.0 requ) BAYES_00=-1.9, DKIM_INVALID=0.1, DKIM_SIGNED=0.1, RCVD_IN_VALIDITY_RPBL_BLOCKED=0.001, RCVD_IN_VALIDITY_SAFE_BLOCKED=0.001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=no autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZM-MESSAGEID: 1756707293284116600 MemoryRegions used to "piggyback" on their owners instead of using their own reference counters due to the circular dependencies between them, which caused a few problems. Stop piggybacking, showing that the circular dependencies are actually broken. Problems with piggybacking =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D Piggybacking obscured the fact that a MemoryRegion used memory allocated to its owner, as it didn't take a reference to the owner. It also created another circular reference when an owner referred to its MemoryRegion via memory_region_ref(). This could happen in two scenarios: - A subregion and its container had the same owner. In this case, the container would use memory_region_ref() to take a reference to the subregion. - An owner called address_space_map() with a guest-controlled address that points back to its MemoryRegion. This scenario is similar to what MemReentrancyGuard handles. Avoiding such a circular reference required checking if the referrer is the owner, complicating the code further. Insight =3D=3D=3D=3D=3D=3D=3D This change challenges the underlying assumption that circulare dependencies exist and can be tolerated. The deletable MemoryRegions are of hotpluggable devices. A device and its MemoryRegion have the following dependencies: QOM tree -> Device Container -> MemoryRegion Device <-> MemoryRegion Techniques like piggybacking or a hypothetical garbage collector can finalize the device and the MemoryRegion once the QOM tree and container lose their dependencies. However, these methods are fundamentally insufficient because the MemoryRegion and the device have finalizers, and they do not respect the dependencies these objects may have during finalization. As long as the object model based on the device and the MemoryRegion is correct, one of them should lose its dependency on the first, establishing a valid finalization order. Understanding this allows using standard reference counting, which is immune to the problems of piggybacking. Analysis reveals that the device no longer depends on the MemoryRegion after unrealization. The device needs the MemoryRegion to add it to a container and expose it to an AddressSpace. Once unrealized, the device is hidden from the AddresSpaces and thus no longer needs the MemoryRegion. This suggests that the circular dependencies are broken and that the device should be finalized *after*: - It is unrealized. - The MemoryRegion is finalized. Solution =3D=3D=3D=3D=3D=3D=3D=3D Fortunately, this requirement can be coded with minimal complexity. Hotpluggable devices already explicitly call memory_region_del_subregion() to stop accepting new accesses to its MemoryRegions after unrealization. This change lets object_unparent() calls follow that, permitting the MemoryRegions to be finalized earlier than the device. This enforces a valid finalization order, allows MemoryRegions to rely on standard reference counting, and eliminates the entire class of memory leaks caused by piggybacking. Signed-off-by: Akihiko Odaki --- MAINTAINERS | 1 + docs/devel/memory.rst | 32 +++++++++++++++---------------- include/system/memory.h | 51 ++++++++++++++++++++++++---------------------= ---- hw/core/qdev.c | 25 +++++++++++++++++++----- stubs/memory.c | 9 +++++++++ system/memory.c | 11 +++-------- stubs/meson.build | 1 + 7 files changed, 75 insertions(+), 55 deletions(-) diff --git a/MAINTAINERS b/MAINTAINERS index 8147fff3523eaa45c4a0d2c21d40b4ade3f419ff..4665f0a4b7a513c5863f6d5227a= 0173c836505e6 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -3212,6 +3212,7 @@ F: include/system/memory.h F: include/system/ram_addr.h F: include/system/ramblock.h F: include/system/memory_mapping.h +F: stubs/memory.c F: system/dma-helpers.c F: system/ioport.c F: system/memory.c diff --git a/docs/devel/memory.rst b/docs/devel/memory.rst index 749f11d8a4ddc80f2d44b66fa41fb12c0fa54006..c7bb5602e9c20e19dd8dfe4a814= e958cc0518598 100644 --- a/docs/devel/memory.rst +++ b/docs/devel/memory.rst @@ -158,30 +158,31 @@ ioeventfd) can be changed during the region lifecycle= . They take effect as soon as the region is made visible. This can be immediately, later, or never. =20 -Destruction of a memory region happens automatically when the owner -object dies. +You should call object_unparent() if the memory region becomes +unnecessary for its owner. If its owner is a device, object_unparent() +will be called automatically after unrealization. =20 -You must not destroy a memory region as long as it may be in use by a -device or CPU. In order to do this, as a general rule do not create or -destroy memory regions dynamically during a device's lifetime. +You must not free a memory region as long as it may be in use by a +device or CPU. In order to do this, as a general rule do not allocate or +free memory regions dynamically during a device's lifetime. The dynamically allocated data structure that contains the memory region should be freed in the instance_finalize callback. =20 If you break this rule, the following situation can happen: =20 -- the memory region's owner had a reference taken via memory_region_ref +- the memory region had a reference taken via memory_region_ref (for example by address_space_map) =20 -- the region is unparented, and has no owner anymore +- the region is freed =20 -- when address_space_unmap is called, the reference to the memory region's - owner is leaked. +- when the mapped memory is used, the use of the memory region + results in use-after-free. =20 =20 -There is an exception to the above rule: it is okay to call -object_unparent at any time for an alias or a container region. It is -therefore also okay to create or destroy alias and container regions -dynamically during a device's lifetime. +There is an exception to the above rule: it is okay to free at any time +for an alias or a container region. It is therefore also okay to +allocate or free alias and container regions dynamically during a +device's lifetime. =20 This exceptional usage is valid because aliases and containers only help QEMU building the guest's memory map; they are never accessed directly. @@ -191,9 +192,8 @@ this exception is rarely necessary, and therefore it is= discouraged, but nevertheless it is used in a few places. =20 For regions that "have no owner" (NULL is passed at creation time), the -machine object is actually used as the owner. You must never call -object_unparent on regions that have no owner, unless they are aliases -or containers. +machine object is actually used as the owner. You must never free +regions that have no owner, unless they are aliases or containers. =20 =20 Overlapping regions and priority diff --git a/include/system/memory.h b/include/system/memory.h index e2cd6ed126144abaed6f3035e3ef091d747b4c34..1fc773ca49e26d605a4e152b5b4= 1426ad7666912 100644 --- a/include/system/memory.h +++ b/include/system/memory.h @@ -1307,7 +1307,7 @@ static inline bool memory_region_section_intersect_ra= nge(MemoryRegionSection *s, * memory_region_add_subregion() to add subregions. * * @mr: the #MemoryRegion to be initialized - * @owner: the object that tracks the region's reference count + * @owner: the object that provides the region's storage * @name: used for debugging; not visible to the user or ABI * @size: size of the region; any subregions beyond this size will be clip= ped */ @@ -1320,14 +1320,14 @@ void memory_region_init(MemoryRegion *mr, * memory_region_ref: Add 1 to a memory region's reference count * * Whenever memory regions are accessed outside the BQL, they need to be - * preserved against hot-unplug. MemoryRegions actually do not have their - * own reference count; they piggyback on a QOM object, their "owner". - * This function adds a reference to the owner. + * preserved against hot-unplug. This function adds a reference to the + * memory region. * - * All MemoryRegions must have an owner if they can disappear, even if the - * device they belong to operates exclusively under the BQL. This is beca= use - * the region could be returned at any time by memory_region_find, and this - * is usually under guest control. + * We do not ref/unref memory regions without an owner because it slows + * down DMA sensibly. All MemoryRegions must have an owner if they can + * disappear, even if the device they belong to operates exclusively + * under the BQL. This is because the region could be returned at any + * time by memory_region_find, and this is usually under guest control. * * @mr: the #MemoryRegion */ @@ -1337,9 +1337,8 @@ void memory_region_ref(MemoryRegion *mr); * memory_region_unref: Remove 1 to a memory region's reference count * * Whenever memory regions are accessed outside the BQL, they need to be - * preserved against hot-unplug. MemoryRegions actually do not have their - * own reference count; they piggyback on a QOM object, their "owner". - * This function removes a reference to the owner and possibly destroys it. + * preserved against hot-unplug. This function removes a reference to + * the memory and possibly destroys it. * * @mr: the #MemoryRegion */ @@ -1352,7 +1351,7 @@ void memory_region_unref(MemoryRegion *mr); * if @size is nonzero, subregions will be clipped to @size. * * @mr: the #MemoryRegion to be initialized. - * @owner: the object that tracks the region's reference count + * @owner: the object that provides the region's storage * @ops: a structure containing read and write callbacks to be used when * I/O is performed on the region. * @opaque: passed to the read and write callbacks of the @ops structure. @@ -1372,7 +1371,7 @@ void memory_region_init_io(MemoryRegion *mr, * directly. * * @mr: the #MemoryRegion to be initialized. - * @owner: the object that tracks the region's reference count + * @owner: the object that provides the region's storage * @name: Region name, becomes part of RAMBlock name used in migration str= eam * must be unique within any device * @size: size of the region. @@ -1395,7 +1394,7 @@ bool memory_region_init_ram_nomigrate(MemoryRegion *m= r, * modify memory directly. * * @mr: the #MemoryRegion to be initialized. - * @owner: the object that tracks the region's reference count + * @owner: the object that provides the region's storage * @name: Region name, becomes part of RAMBlock name used in migration str= eam * must be unique within any device * @size: size of the region. @@ -1425,7 +1424,7 @@ bool memory_region_init_ram_flags_nomigrate(MemoryReg= ion *mr, * canceled. * * @mr: the #MemoryRegion to be initialized. - * @owner: the object that tracks the region's reference count + * @owner: the object that provides the region's storage * @name: Region name, becomes part of RAMBlock name used in migration str= eam * must be unique within any device * @size: used size of the region. @@ -1454,7 +1453,7 @@ bool memory_region_init_resizeable_ram(MemoryRegion *= mr, * mmap-ed backend. * * @mr: the #MemoryRegion to be initialized. - * @owner: the object that tracks the region's reference count + * @owner: the object that provides the region's storage * @name: Region name, becomes part of RAMBlock name used in migration str= eam * must be unique within any device * @size: size of the region. @@ -1487,7 +1486,7 @@ bool memory_region_init_ram_from_file(MemoryRegion *m= r, * mmap-ed backend. * * @mr: the #MemoryRegion to be initialized. - * @owner: the object that tracks the region's reference count + * @owner: the object that provides the region's storage * @name: the name of the region. * @size: size of the region. * @ram_flags: RamBlock flags. Supported flags: RAM_SHARED, RAM_PMEM, @@ -1518,7 +1517,7 @@ bool memory_region_init_ram_from_fd(MemoryRegion *mr, * region will modify memory directly. * * @mr: the #MemoryRegion to be initialized. - * @owner: the object that tracks the region's reference count + * @owner: the object that provides the region's storage * @name: Region name, becomes part of RAMBlock name used in migration str= eam * must be unique within any device * @size: size of the region. @@ -1546,7 +1545,7 @@ void memory_region_init_ram_ptr(MemoryRegion *mr, * skip_dump flag. * * @mr: the #MemoryRegion to be initialized. - * @owner: the object that tracks the region's reference count + * @owner: the object that provides the region's storage * @name: the name of the region. * @size: size of the region. * @ptr: memory to be mapped; must contain at least @size bytes. @@ -1566,7 +1565,7 @@ void memory_region_init_ram_device_ptr(MemoryRegion *= mr, * part of another memory region. * * @mr: the #MemoryRegion to be initialized. - * @owner: the object that tracks the region's reference count + * @owner: the object that provides the region's storage * @name: used for debugging; not visible to the user or ABI * @orig: the region to be referenced; @mr will be equivalent to * @orig between @offset and @offset + @size - 1. @@ -1592,7 +1591,7 @@ void memory_region_init_alias(MemoryRegion *mr, * of the caller. * * @mr: the #MemoryRegion to be initialized. - * @owner: the object that tracks the region's reference count + * @owner: the object that provides the region's storage * @name: Region name, becomes part of RAMBlock name used in migration str= eam * must be unique within any device * @size: size of the region. @@ -1615,7 +1614,7 @@ bool memory_region_init_rom_nomigrate(MemoryRegion *m= r, * of the caller. * * @mr: the #MemoryRegion to be initialized. - * @owner: the object that tracks the region's reference count + * @owner: the object that provides the region's storage * @ops: callbacks for write access handling (must not be NULL). * @opaque: passed to the read and write callbacks of the @ops structure. * @name: Region name, becomes part of RAMBlock name used in migration str= eam @@ -1651,7 +1650,7 @@ bool memory_region_init_rom_device_nomigrate(MemoryRe= gion *mr, * @_iommu_mr: the #IOMMUMemoryRegion to be initialized * @instance_size: the IOMMUMemoryRegion subclass instance size * @mrtypename: the type name of the #IOMMUMemoryRegion - * @owner: the object that tracks the region's reference count + * @owner: the object that provides the region's storage * @name: used for debugging; not visible to the user or ABI * @size: size of the region. */ @@ -1667,7 +1666,7 @@ void memory_region_init_iommu(void *_iommu_mr, * region will modify memory directly. * * @mr: the #MemoryRegion to be initialized - * @owner: the object that tracks the region's reference count (must be + * @owner: the object that provides the region's storage (must be * TYPE_DEVICE or a subclass of TYPE_DEVICE, or NULL) * @name: name of the memory region * @size: size of the region in bytes @@ -1713,7 +1712,7 @@ bool memory_region_init_ram_guest_memfd(MemoryRegion = *mr, * If you pass a non-NULL non-device @owner then we will assert. * * @mr: the #MemoryRegion to be initialized. - * @owner: the object that tracks the region's reference count + * @owner: the object that provides the region's storage * @name: Region name, becomes part of RAMBlock name used in migration str= eam * must be unique within any device * @size: size of the region. @@ -1744,7 +1743,7 @@ bool memory_region_init_rom(MemoryRegion *mr, * If you pass a non-NULL non-device @owner then we will assert. * * @mr: the #MemoryRegion to be initialized. - * @owner: the object that tracks the region's reference count + * @owner: the object that provides the region's storage * @ops: callbacks for write access handling (must not be NULL). * @opaque: passed to the read and write callbacks of the @ops structure. * @name: Region name, becomes part of RAMBlock name used in migration str= eam diff --git a/hw/core/qdev.c b/hw/core/qdev.c index 8fdf6774f87ec8424348e8c9652dc4c99a2faeb5..3b1a01cdeec5b6823217fbc2d61= 1ec94ac1ccf51 100644 --- a/hw/core/qdev.c +++ b/hw/core/qdev.c @@ -451,17 +451,32 @@ static bool check_only_migratable(Object *obj, Error = **errp) return true; } =20 -static int del_memory_region(Object *child, void *opaque) +static int collect_memory_region(Object *child, void *opaque) { MemoryRegion *mr =3D (MemoryRegion *)object_dynamic_cast(child, TYPE_M= EMORY_REGION); =20 - if (mr && mr->container) { - memory_region_del_subregion(mr->container, mr); + if (mr) { + if (mr->container) { + memory_region_del_subregion(mr->container, mr); + } + + g_array_append_val(opaque, child); } =20 return 0; } =20 +static void del_memory_regions(DeviceState *dev) +{ + g_autoptr(GArray) array =3D g_array_new(FALSE, FALSE, sizeof(Object *)= ); + + object_child_foreach(OBJECT(dev), collect_memory_region, array); + + for (gsize i =3D 0; i < array->len; i++) { + object_unparent(g_array_index(array, Object *, i)); + } +} + static void device_set_realized(Object *obj, bool value, Error **errp) { DeviceState *dev =3D DEVICE(obj); @@ -593,7 +608,7 @@ static void device_set_realized(Object *obj, bool value= , Error **errp) if (dc->unrealize) { dc->unrealize(dev); } - object_child_foreach(OBJECT(dev), del_memory_region, NULL); + del_memory_regions(dev); dev->pending_deleted_event =3D true; DEVICE_LISTENER_CALL(unrealize, Reverse, dev); } @@ -618,7 +633,7 @@ post_realize_fail: } =20 fail: - object_child_foreach(OBJECT(dev), del_memory_region, NULL); + del_memory_regions(dev); =20 error_propagate(errp, local_err); if (unattached_parent) { diff --git a/stubs/memory.c b/stubs/memory.c new file mode 100644 index 0000000000000000000000000000000000000000..9c36531ae542d804dc19ed2a3c6= 57005881a2bca --- /dev/null +++ b/stubs/memory.c @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "qemu/osdep.h" +#include "system/memory.h" + +void memory_region_del_subregion(MemoryRegion *mr, + MemoryRegion *subregion) +{ +} diff --git a/system/memory.c b/system/memory.c index 56465479406f4a264bfe13e6a2bc7d9b6565410f..f81c634bcca4c4f32dea6796e5f= 98f6e2baaa94c 100644 --- a/system/memory.c +++ b/system/memory.c @@ -1222,7 +1222,7 @@ static void memory_region_do_init(MemoryRegion *mr, mr->size =3D int128_2_64(); } mr->name =3D g_strdup(name); - mr->owner =3D owner; + mr->owner =3D object_ref(owner); mr->dev =3D (DeviceState *) object_dynamic_cast(mr->owner, TYPE_DEVICE= ); mr->ram_block =3D NULL; =20 @@ -1816,6 +1816,7 @@ static void memory_region_finalize(Object *obj) memory_region_clear_coalescing(mr); g_free((char *)mr->name); g_free(mr->ioeventfds); + object_unref(mr->owner); } =20 Object *memory_region_owner(MemoryRegion *mr) @@ -1826,13 +1827,7 @@ Object *memory_region_owner(MemoryRegion *mr) =20 void memory_region_ref(MemoryRegion *mr) { - /* MMIO callbacks most likely will access data that belongs - * to the owner, hence the need to ref/unref the owner whenever - * the memory region is in use. - * - * The memory region is a child of its owner. As long as the - * owner doesn't call unparent itself on the memory region, - * ref-ing the owner will also keep the memory region alive. + /* * Memory regions without an owner are supposed to never go away; * we do not ref/unref them because it slows down DMA sensibly. */ diff --git a/stubs/meson.build b/stubs/meson.build index cef046e6854ddaa9f12714c317a541ea75b8d412..b4df4e60a1af89c9354d5b92449= ce5409095b9f1 100644 --- a/stubs/meson.build +++ b/stubs/meson.build @@ -95,5 +95,6 @@ if have_system or have_user =20 # Also included in have_system for tests/unit/test-qdev-global-props stub_ss.add(files('hotplug-stubs.c')) + stub_ss.add(files('memory.c')) stub_ss.add(files('sysbus.c')) endif --=20 2.51.0