mirror of
https://github.com/qemu/qemu.git
synced 2026-07-08 17:46:17 +00:00
Listeners now hold a reference to the RamDiscardManager, ensuring it stays alive while listeners are registered. The RDM is eagerly freed when the last source and listener are removed, and also unreffed during MemoryRegion finalization as a safety net. This completes the TODO left in the previous commit and prevents both use-after-free and memory leaks of the RamDiscardManager. Reviewed-by: Peter Xu <peterx@redhat.com> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com> Link: https://lore.kernel.org/r/20260604-rdm5-v5-9-5768e6a0943d@redhat.com Signed-off-by: Peter Xu <peterx@redhat.com>
613 lines
18 KiB
C
613 lines
18 KiB
C
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
/*
|
|
* RAM Discard Manager
|
|
*
|
|
* Copyright Red Hat, Inc. 2026
|
|
*/
|
|
|
|
#include "qemu/osdep.h"
|
|
#include "qemu/error-report.h"
|
|
#include "qemu/queue.h"
|
|
#include "system/memory.h"
|
|
|
|
static uint64_t ram_discard_source_get_min_granularity(const RamDiscardSource *rds,
|
|
const MemoryRegion *mr)
|
|
{
|
|
RamDiscardSourceClass *rdsc = RAM_DISCARD_SOURCE_GET_CLASS(rds);
|
|
|
|
g_assert(rdsc->get_min_granularity);
|
|
return rdsc->get_min_granularity(rds, mr);
|
|
}
|
|
|
|
static bool ram_discard_source_is_populated(const RamDiscardSource *rds,
|
|
const MemoryRegionSection *section)
|
|
{
|
|
RamDiscardSourceClass *rdsc = RAM_DISCARD_SOURCE_GET_CLASS(rds);
|
|
|
|
g_assert(rdsc->is_populated);
|
|
return rdsc->is_populated(rds, section);
|
|
}
|
|
|
|
/*
|
|
* Iterate a single source's populated or discarded regions and call
|
|
* replay_fn for each contiguous run.
|
|
*/
|
|
static int replay_source_by_state(const RamDiscardSource *source,
|
|
const MemoryRegion *mr,
|
|
const MemoryRegionSection *section,
|
|
bool replay_populated,
|
|
ReplayRamDiscardState replay_fn,
|
|
void *opaque)
|
|
{
|
|
uint64_t granularity, offset, size, end, pos, run_start = 0;
|
|
bool in_run = false;
|
|
int ret = 0;
|
|
|
|
granularity = ram_discard_source_get_min_granularity(source, mr);
|
|
offset = section->offset_within_region;
|
|
size = int128_get64(section->size);
|
|
end = offset + size;
|
|
|
|
/* Align iteration to granularity boundaries */
|
|
pos = QEMU_ALIGN_DOWN(offset, granularity);
|
|
|
|
for (; pos < end; pos += granularity) {
|
|
MemoryRegionSection chunk = {
|
|
.mr = section->mr,
|
|
.offset_within_region = pos,
|
|
.size = int128_make64(granularity),
|
|
};
|
|
bool populated = ram_discard_source_is_populated(source, &chunk);
|
|
|
|
if (populated == replay_populated) {
|
|
if (!in_run) {
|
|
run_start = pos;
|
|
in_run = true;
|
|
}
|
|
} else if (in_run) {
|
|
MemoryRegionSection tmp = *section;
|
|
|
|
if (memory_region_section_intersect_range(&tmp, run_start,
|
|
pos - run_start)) {
|
|
ret = replay_fn(&tmp, opaque);
|
|
if (ret) {
|
|
return ret;
|
|
}
|
|
}
|
|
in_run = false;
|
|
}
|
|
}
|
|
|
|
if (in_run) {
|
|
MemoryRegionSection tmp = *section;
|
|
|
|
if (memory_region_section_intersect_range(&tmp, run_start,
|
|
pos - run_start)) {
|
|
ret = replay_fn(&tmp, opaque);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
RamDiscardManager *ram_discard_manager_new(MemoryRegion *mr)
|
|
{
|
|
RamDiscardManager *rdm;
|
|
|
|
rdm = RAM_DISCARD_MANAGER(object_new(TYPE_RAM_DISCARD_MANAGER));
|
|
rdm->mr = mr;
|
|
return rdm;
|
|
}
|
|
|
|
static void ram_discard_manager_update_granularity(RamDiscardManager *rdm)
|
|
{
|
|
RamDiscardSourceEntry *entry;
|
|
uint64_t granularity = 0;
|
|
|
|
QLIST_FOREACH(entry, &rdm->source_list, next) {
|
|
uint64_t src_granularity;
|
|
|
|
src_granularity =
|
|
ram_discard_source_get_min_granularity(entry->rds, rdm->mr);
|
|
g_assert(src_granularity != 0);
|
|
if (granularity == 0) {
|
|
granularity = src_granularity;
|
|
} else {
|
|
granularity = MIN(granularity, src_granularity);
|
|
}
|
|
}
|
|
rdm->min_granularity = granularity;
|
|
}
|
|
|
|
static RamDiscardSourceEntry *
|
|
ram_discard_manager_find_source(RamDiscardManager *rdm, RamDiscardSource *rds)
|
|
{
|
|
RamDiscardSourceEntry *entry;
|
|
|
|
QLIST_FOREACH(entry, &rdm->source_list, next) {
|
|
if (entry->rds == rds) {
|
|
return entry;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int rdl_populate_cb(const MemoryRegionSection *section, void *opaque)
|
|
{
|
|
RamDiscardListener *rdl = opaque;
|
|
MemoryRegionSection tmp = *rdl->section;
|
|
|
|
g_assert(section->mr == rdl->section->mr);
|
|
|
|
if (!memory_region_section_intersect_range(&tmp,
|
|
section->offset_within_region,
|
|
int128_get64(section->size))) {
|
|
return 0;
|
|
}
|
|
|
|
return rdl->notify_populate(rdl, &tmp);
|
|
}
|
|
|
|
static int rdl_discard_cb(const MemoryRegionSection *section, void *opaque)
|
|
{
|
|
RamDiscardListener *rdl = opaque;
|
|
MemoryRegionSection tmp = *rdl->section;
|
|
|
|
g_assert(section->mr == rdl->section->mr);
|
|
|
|
if (!memory_region_section_intersect_range(&tmp,
|
|
section->offset_within_region,
|
|
int128_get64(section->size))) {
|
|
return 0;
|
|
}
|
|
|
|
rdl->notify_discard(rdl, &tmp);
|
|
return 0;
|
|
}
|
|
|
|
static bool rdm_is_all_populated_skip(const RamDiscardManager *rdm,
|
|
const MemoryRegionSection *section,
|
|
const RamDiscardSource *skip_source)
|
|
{
|
|
RamDiscardSourceEntry *entry;
|
|
|
|
QLIST_FOREACH(entry, &rdm->source_list, next) {
|
|
if (skip_source && entry->rds == skip_source) {
|
|
continue;
|
|
}
|
|
if (!ram_discard_source_is_populated(entry->rds, section)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
typedef struct SourceNotifyCtx {
|
|
RamDiscardManager *rdm;
|
|
RamDiscardListener *rdl;
|
|
RamDiscardSource *source; /* added or removed */
|
|
} SourceNotifyCtx;
|
|
|
|
/*
|
|
* Unified helper to replay regions based on populated state.
|
|
* If replay_populated is true: replay regions where ALL sources are populated.
|
|
* If replay_populated is false: replay regions where ANY source is discarded.
|
|
*/
|
|
static int replay_by_populated_state(const RamDiscardManager *rdm,
|
|
const MemoryRegionSection *section,
|
|
const RamDiscardSource *skip_source,
|
|
bool replay_populated,
|
|
ReplayRamDiscardState replay_fn,
|
|
void *user_opaque)
|
|
{
|
|
uint64_t granularity = rdm->min_granularity;
|
|
uint64_t offset, end_offset;
|
|
uint64_t run_start = 0;
|
|
bool in_run = false;
|
|
int ret = 0;
|
|
|
|
if (QLIST_EMPTY(&rdm->source_list)) {
|
|
if (replay_populated) {
|
|
return replay_fn(section, user_opaque);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
g_assert(granularity != 0);
|
|
|
|
offset = section->offset_within_region;
|
|
end_offset = offset + int128_get64(section->size);
|
|
|
|
while (offset < end_offset) {
|
|
MemoryRegionSection subsection = {
|
|
.mr = section->mr,
|
|
.offset_within_region = offset,
|
|
.size = int128_make64(MIN(granularity, end_offset - offset)),
|
|
};
|
|
bool all_populated;
|
|
bool included;
|
|
|
|
all_populated = rdm_is_all_populated_skip(rdm, &subsection,
|
|
skip_source);
|
|
included = replay_populated ? all_populated : !all_populated;
|
|
|
|
if (included) {
|
|
if (!in_run) {
|
|
run_start = offset;
|
|
in_run = true;
|
|
}
|
|
} else {
|
|
if (in_run) {
|
|
MemoryRegionSection run_section = {
|
|
.mr = section->mr,
|
|
.offset_within_region = run_start,
|
|
.size = int128_make64(offset - run_start),
|
|
};
|
|
ret = replay_fn(&run_section, user_opaque);
|
|
if (ret) {
|
|
return ret;
|
|
}
|
|
in_run = false;
|
|
}
|
|
}
|
|
if (granularity > end_offset - offset) {
|
|
break;
|
|
}
|
|
offset += granularity;
|
|
}
|
|
|
|
if (in_run) {
|
|
MemoryRegionSection run_section = {
|
|
.mr = section->mr,
|
|
.offset_within_region = run_start,
|
|
.size = int128_make64(end_offset - run_start),
|
|
};
|
|
ret = replay_fn(&run_section, user_opaque);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int add_source_check_discard_cb(const MemoryRegionSection *section,
|
|
void *opaque)
|
|
{
|
|
SourceNotifyCtx *ctx = opaque;
|
|
|
|
return replay_by_populated_state(ctx->rdm, section, ctx->source, true,
|
|
rdl_discard_cb, ctx->rdl);
|
|
}
|
|
|
|
static int del_source_check_populate_cb(const MemoryRegionSection *section,
|
|
void *opaque)
|
|
{
|
|
SourceNotifyCtx *ctx = opaque;
|
|
|
|
return replay_by_populated_state(ctx->rdm, section, ctx->source, true,
|
|
rdl_populate_cb, ctx->rdl);
|
|
}
|
|
|
|
int ram_discard_manager_add_source(RamDiscardManager *rdm,
|
|
RamDiscardSource *source)
|
|
{
|
|
RamDiscardSourceEntry *entry;
|
|
RamDiscardListener *rdl, *rdl2;
|
|
int ret = 0;
|
|
|
|
if (ram_discard_manager_find_source(rdm, source)) {
|
|
return -EBUSY;
|
|
}
|
|
|
|
/*
|
|
* If there are existing listeners, notify them about regions that
|
|
* become discarded due to adding this source. Only notify for regions
|
|
* that were previously populated (all other sources agreed).
|
|
*/
|
|
QLIST_FOREACH(rdl, &rdm->rdl_list, next) {
|
|
SourceNotifyCtx ctx = {
|
|
.rdm = rdm,
|
|
.rdl = rdl,
|
|
/* no need to set source */
|
|
};
|
|
ret = replay_source_by_state(source, rdm->mr, rdl->section,
|
|
false,
|
|
add_source_check_discard_cb, &ctx);
|
|
if (ret) {
|
|
break;
|
|
}
|
|
}
|
|
if (ret) {
|
|
QLIST_FOREACH(rdl2, &rdm->rdl_list, next) {
|
|
SourceNotifyCtx ctx = {
|
|
.rdm = rdm,
|
|
.rdl = rdl2,
|
|
};
|
|
replay_source_by_state(source, rdm->mr, rdl2->section,
|
|
false,
|
|
del_source_check_populate_cb,
|
|
&ctx);
|
|
if (rdl == rdl2) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
entry = g_new0(RamDiscardSourceEntry, 1);
|
|
entry->rds = source;
|
|
QLIST_INSERT_HEAD(&rdm->source_list, entry, next);
|
|
|
|
ram_discard_manager_update_granularity(rdm);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int ram_discard_manager_del_source(RamDiscardManager *rdm,
|
|
RamDiscardSource *source)
|
|
{
|
|
RamDiscardSourceEntry *entry;
|
|
RamDiscardListener *rdl, *rdl2;
|
|
int ret = 0;
|
|
|
|
entry = ram_discard_manager_find_source(rdm, source);
|
|
if (!entry) {
|
|
return -ENOENT;
|
|
}
|
|
|
|
/*
|
|
* If there are existing listeners, check if any regions become
|
|
* populated due to removing this source.
|
|
*/
|
|
QLIST_FOREACH(rdl, &rdm->rdl_list, next) {
|
|
SourceNotifyCtx ctx = {
|
|
.rdm = rdm,
|
|
.rdl = rdl,
|
|
.source = source,
|
|
};
|
|
/*
|
|
* From the previously discarded regions, check if any
|
|
* regions become populated.
|
|
*/
|
|
ret = replay_source_by_state(source, rdm->mr, rdl->section,
|
|
false,
|
|
del_source_check_populate_cb,
|
|
&ctx);
|
|
if (ret) {
|
|
break;
|
|
}
|
|
}
|
|
if (ret) {
|
|
QLIST_FOREACH(rdl2, &rdm->rdl_list, next) {
|
|
SourceNotifyCtx ctx = {
|
|
.rdm = rdm,
|
|
.rdl = rdl2,
|
|
.source = source,
|
|
};
|
|
replay_source_by_state(source, rdm->mr, rdl2->section,
|
|
false,
|
|
add_source_check_discard_cb,
|
|
&ctx);
|
|
if (rdl == rdl2) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
QLIST_REMOVE(entry, next);
|
|
g_free(entry);
|
|
ram_discard_manager_update_granularity(rdm);
|
|
return ret;
|
|
}
|
|
|
|
uint64_t ram_discard_manager_get_min_granularity(const RamDiscardManager *rdm,
|
|
const MemoryRegion *mr)
|
|
{
|
|
g_assert(mr == rdm->mr);
|
|
return rdm->min_granularity;
|
|
}
|
|
|
|
/*
|
|
* Aggregated query: returns true only if ALL sources report populated (AND).
|
|
*/
|
|
bool ram_discard_manager_is_populated(const RamDiscardManager *rdm,
|
|
const MemoryRegionSection *section)
|
|
{
|
|
RamDiscardSourceEntry *entry;
|
|
|
|
QLIST_FOREACH(entry, &rdm->source_list, next) {
|
|
if (!ram_discard_source_is_populated(entry->rds, section)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int ram_discard_manager_replay_populated(const RamDiscardManager *rdm,
|
|
const MemoryRegionSection *section,
|
|
ReplayRamDiscardState replay_fn,
|
|
void *opaque)
|
|
{
|
|
return replay_by_populated_state(rdm, section, NULL, true,
|
|
replay_fn, opaque);
|
|
}
|
|
|
|
int ram_discard_manager_replay_discarded(const RamDiscardManager *rdm,
|
|
const MemoryRegionSection *section,
|
|
ReplayRamDiscardState replay_fn,
|
|
void *opaque)
|
|
{
|
|
return replay_by_populated_state(rdm, section, NULL, false,
|
|
replay_fn, opaque);
|
|
}
|
|
|
|
static void ram_discard_manager_initfn(Object *obj)
|
|
{
|
|
RamDiscardManager *rdm = RAM_DISCARD_MANAGER(obj);
|
|
|
|
QLIST_INIT(&rdm->source_list);
|
|
QLIST_INIT(&rdm->rdl_list);
|
|
rdm->min_granularity = 0;
|
|
}
|
|
|
|
static void ram_discard_manager_finalize(Object *obj)
|
|
{
|
|
RamDiscardManager *rdm = RAM_DISCARD_MANAGER(obj);
|
|
|
|
g_assert(QLIST_EMPTY(&rdm->rdl_list));
|
|
g_assert(QLIST_EMPTY(&rdm->source_list));
|
|
}
|
|
|
|
int ram_discard_manager_notify_populate(RamDiscardManager *rdm,
|
|
RamDiscardSource *source,
|
|
uint64_t offset, uint64_t size)
|
|
{
|
|
RamDiscardListener *rdl, *rdl2;
|
|
MemoryRegionSection section = {
|
|
.mr = rdm->mr,
|
|
.offset_within_region = offset,
|
|
.size = int128_make64(size),
|
|
};
|
|
int ret = 0;
|
|
|
|
g_assert(ram_discard_manager_find_source(rdm, source));
|
|
|
|
/*
|
|
* Only notify about regions that are populated in ALL sources.
|
|
* Skip the calling source: it has implicitly declared itself populated
|
|
* for this range but may not have updated its bitmap yet.
|
|
*/
|
|
QLIST_FOREACH(rdl, &rdm->rdl_list, next) {
|
|
ret = replay_by_populated_state(rdm, §ion, source, true,
|
|
rdl_populate_cb, rdl);
|
|
if (ret) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ret) {
|
|
/*
|
|
* Rollback: notify discard for listeners we already notified,
|
|
* including the failing listener which may have been partially
|
|
* notified. Listeners must handle discard notifications for
|
|
* regions they didn't receive populate notifications for.
|
|
*/
|
|
QLIST_FOREACH(rdl2, &rdm->rdl_list, next) {
|
|
replay_by_populated_state(rdm, §ion, source, true,
|
|
rdl_discard_cb, rdl2);
|
|
if (rdl2 == rdl) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void ram_discard_manager_notify_discard(RamDiscardManager *rdm,
|
|
RamDiscardSource *source,
|
|
uint64_t offset, uint64_t size)
|
|
{
|
|
RamDiscardListener *rdl;
|
|
MemoryRegionSection section = {
|
|
.mr = rdm->mr,
|
|
.offset_within_region = offset,
|
|
.size = int128_make64(size),
|
|
};
|
|
|
|
g_assert(ram_discard_manager_find_source(rdm, source));
|
|
|
|
/*
|
|
* Only notify about ranges that were aggregately populated before this
|
|
* source's discard. Since the source has already updated its state,
|
|
* we use replay_by_populated_state with this source skipped - it will
|
|
* replay only the ranges where all OTHER sources are populated.
|
|
*/
|
|
QLIST_FOREACH(rdl, &rdm->rdl_list, next) {
|
|
replay_by_populated_state(rdm, §ion, source, true,
|
|
rdl_discard_cb, rdl);
|
|
}
|
|
}
|
|
|
|
void ram_discard_manager_notify_discard_all(RamDiscardManager *rdm,
|
|
RamDiscardSource *source)
|
|
{
|
|
RamDiscardListener *rdl;
|
|
|
|
g_assert(ram_discard_manager_find_source(rdm, source));
|
|
|
|
QLIST_FOREACH(rdl, &rdm->rdl_list, next) {
|
|
rdl->notify_discard(rdl, rdl->section);
|
|
}
|
|
}
|
|
|
|
void ram_discard_manager_register_listener(RamDiscardManager *rdm,
|
|
RamDiscardListener *rdl,
|
|
MemoryRegionSection *section)
|
|
{
|
|
int ret;
|
|
|
|
g_assert(section->mr == rdm->mr);
|
|
|
|
object_ref(rdm);
|
|
rdl->section = memory_region_section_new_copy(section);
|
|
QLIST_INSERT_HEAD(&rdm->rdl_list, rdl, next);
|
|
|
|
ret = ram_discard_manager_replay_populated(rdm, rdl->section,
|
|
rdl_populate_cb, rdl);
|
|
if (ret) {
|
|
error_report("%s: Replaying populated ranges failed: %s", __func__,
|
|
strerror(-ret));
|
|
}
|
|
}
|
|
|
|
void ram_discard_manager_unregister_listener(RamDiscardManager *rdm,
|
|
RamDiscardListener *rdl)
|
|
{
|
|
g_assert(rdl->section);
|
|
g_assert(rdl->section->mr == rdm->mr);
|
|
|
|
rdl->notify_discard(rdl, rdl->section);
|
|
memory_region_section_free_copy(rdl->section);
|
|
rdl->section = NULL;
|
|
QLIST_REMOVE(rdl, next);
|
|
object_unref(rdm);
|
|
}
|
|
|
|
int ram_discard_manager_replay_populated_to_listeners(RamDiscardManager *rdm)
|
|
{
|
|
RamDiscardListener *rdl;
|
|
int ret = 0;
|
|
|
|
QLIST_FOREACH(rdl, &rdm->rdl_list, next) {
|
|
ret = ram_discard_manager_replay_populated(rdm, rdl->section,
|
|
rdl_populate_cb, rdl);
|
|
if (ret) {
|
|
break;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static const TypeInfo ram_discard_manager_info = {
|
|
.parent = TYPE_OBJECT,
|
|
.name = TYPE_RAM_DISCARD_MANAGER,
|
|
.instance_size = sizeof(RamDiscardManager),
|
|
.instance_init = ram_discard_manager_initfn,
|
|
.instance_finalize = ram_discard_manager_finalize,
|
|
};
|
|
|
|
static const TypeInfo ram_discard_source_info = {
|
|
.parent = TYPE_INTERFACE,
|
|
.name = TYPE_RAM_DISCARD_SOURCE,
|
|
.class_size = sizeof(RamDiscardSourceClass),
|
|
};
|
|
|
|
static void ram_discard_manager_register_types(void)
|
|
{
|
|
type_register_static(&ram_discard_manager_info);
|
|
type_register_static(&ram_discard_source_info);
|
|
}
|
|
|
|
type_init(ram_discard_manager_register_types)
|