From: Salil Mehta <salil.mehta@huawei.com>
Per Arm GIC Architecture Specification (IHI0069H_b, §11.1), the CPU interface
and its Processing Element (PE) share a power domain. If the PE is powered down
or administratively disabled, the CPU interface must be quiescent or off, and
any access is architecturally UNPREDICTABLE. Without explicit checks, QEMU may
issue GICC register operations for vCPUs that are offline, removed, or
otherwise unavailable—risking inconsistent state or undefined behavior in both
TCG and KVM accelerators.
To address this, introduce a per-vCPU gicc_accessible flag that reflects the
administrative enablement of the corresponding QOM vCPU in accordance with the
policy. This is permissible when the GICC (GIC CPU Interface) is online-capable,
meaning vCPUs can be brought online in the guest kernel after boot. The flag is
set during GIC realization and used to skip VGIC register reads/writes, SGI
generation, and CPU interface updates when the GICC is not accessible. This
prevents unsafe operations and ensures compliance when managing administratively
disabled but present vCPUs.
Co-developed-by: Keqian Zhu <zhukeqian1@huawei.com>
Signed-off-by: Keqian Zhu <zhukeqian1@huawei.com>
Signed-off-by: Salil Mehta <salil.mehta@huawei.com>
---
hw/core/qdev.c | 26 +++++++++++++++++
hw/intc/arm_gicv3_common.c | 23 +++++++++++++++
hw/intc/arm_gicv3_cpuif.c | 8 +++++
hw/intc/arm_gicv3_cpuif_common.c | 47 ++++++++++++++++++++++++++++++
hw/intc/arm_gicv3_kvm.c | 18 ++++++++++++
include/hw/intc/arm_gicv3_common.h | 24 +++++++++++++++
include/hw/qdev-core.h | 24 +++++++++++++++
7 files changed, 170 insertions(+)
diff --git a/hw/core/qdev.c b/hw/core/qdev.c
index 5816abae39..8e9a4da6b5 100644
--- a/hw/core/qdev.c
+++ b/hw/core/qdev.c
@@ -326,6 +326,32 @@ bool qdev_disable(DeviceState *dev, BusState *bus, Error **errp)
errp);
}
+int qdev_get_admin_power_state(DeviceState *dev)
+{
+ DeviceClass *dc;
+
+ if (!dev) {
+ return DEVICE_ADMIN_POWER_STATE_REMOVED;
+ }
+
+ dc = DEVICE_GET_CLASS(dev);
+ if (dc->admin_power_state_supported) {
+ return object_property_get_enum(OBJECT(dev), "admin_power_state",
+ "DeviceAdminPowerState", NULL);
+ }
+
+ return DEVICE_ADMIN_POWER_STATE_ENABLED;
+}
+
+bool qdev_check_enabled(DeviceState *dev)
+{
+ /*
+ * if device supports power state transitions, check if it is not in
+ * 'disabled' state.
+ */
+ return qdev_get_admin_power_state(dev) == DEVICE_ADMIN_POWER_STATE_ENABLED;
+}
+
bool qdev_machine_modified(void)
{
return qdev_hot_added || qdev_hot_removed;
diff --git a/hw/intc/arm_gicv3_common.c b/hw/intc/arm_gicv3_common.c
index f6a9f1c68b..f4428ad165 100644
--- a/hw/intc/arm_gicv3_common.c
+++ b/hw/intc/arm_gicv3_common.c
@@ -439,6 +439,29 @@ static void arm_gicv3_common_realize(DeviceState *dev, Error **errp)
CPUState *cpu = machine_get_possible_cpu(i);
uint64_t cpu_affid;
+ /*
+ * Ref: Arm Generic Interrupt Controller Architecture Specification
+ * (GIC Architecture version 3 and version 4), IHI0069H_b,
+ * Section 11.1: Power Management
+ * https://developer.arm.com/documentation/ihi0069
+ *
+ * According to this specification, the CPU interface and the
+ * Processing Element (PE) must reside in the same power domain.
+ * Therefore, when a CPU/PE is powered off, its corresponding CPU
+ * interface must also be in the off state or in a quiescent state—
+ * depending on the state of the associated Redistributor.
+ *
+ * The Redistributor may reside in a separate power domain and may
+ * remain powered even when the associated PE is turned off.
+ *
+ * Accessing the GIC CPU interface while the PE is powered down can
+ * lead to UNPREDICTABLE behavior.
+ *
+ * Accordingly, the QOM object `GICv3CPUState` should be marked as
+ * either accessible or inaccessible based on the power state of the
+ * associated `CPUState` vCPU.
+ */
+ s->cpu[i].gicc_accessible = qdev_check_enabled(DEVICE(cpu));
s->cpu[i].cpu = cpu;
s->cpu[i].gic = s;
/* Store GICv3CPUState in CPUARMState gicv3state pointer */
diff --git a/hw/intc/arm_gicv3_cpuif.c b/hw/intc/arm_gicv3_cpuif.c
index a7904237ac..6430b2c649 100644
--- a/hw/intc/arm_gicv3_cpuif.c
+++ b/hw/intc/arm_gicv3_cpuif.c
@@ -1052,6 +1052,10 @@ void gicv3_cpuif_update(GICv3CPUState *cs)
ARMCPU *cpu = ARM_CPU(cs->cpu);
CPUARMState *env = &cpu->env;
+ if (!gicv3_gicc_accessible(OBJECT(cs->gic), CPU(cpu)->cpu_index)) {
+ return;
+ }
+
g_assert(bql_locked());
trace_gicv3_cpuif_update(gicv3_redist_affid(cs), cs->hppi.irq,
@@ -2036,6 +2040,10 @@ static void icc_generate_sgi(CPUARMState *env, GICv3CPUState *cs,
for (i = 0; i < s->num_cpu; i++) {
GICv3CPUState *ocs = &s->cpu[i];
+ if (!gicv3_gicc_accessible(OBJECT(s), i)) {
+ continue;
+ }
+
if (irm) {
/* IRM == 1 : route to all CPUs except self */
if (cs == ocs) {
diff --git a/hw/intc/arm_gicv3_cpuif_common.c b/hw/intc/arm_gicv3_cpuif_common.c
index f9a9b2d8a3..8f9a5b6fa2 100644
--- a/hw/intc/arm_gicv3_cpuif_common.c
+++ b/hw/intc/arm_gicv3_cpuif_common.c
@@ -12,6 +12,9 @@
#include "qemu/osdep.h"
#include "gicv3_internal.h"
#include "cpu.h"
+#include "qemu/log.h"
+#include "monitor/monitor.h"
+#include "qapi/visitor.h"
void gicv3_set_gicv3state(CPUState *cpu, GICv3CPUState *s)
{
@@ -21,6 +24,41 @@ void gicv3_set_gicv3state(CPUState *cpu, GICv3CPUState *s)
env->gicv3state = (void *)s;
};
+static void
+gicv3_get_gicc_accessibility(Object *obj, Visitor *v, const char *name,
+ void *opaque, Error **errp)
+{
+ GICv3CPUState *cs = (GICv3CPUState *)opaque;
+ bool value = cs->gicc_accessible;
+
+ visit_type_bool(v, name, &value, errp);
+}
+
+static void
+gicv3_set_gicc_accessibility(Object *obj, Visitor *v, const char *name,
+ void *opaque, Error **errp)
+{
+ GICv3CPUState *gcs = opaque;
+ CPUState *cs = gcs->cpu;
+ bool value;
+
+ visit_type_bool(v, name, &value, errp);
+
+ /* Block external attempts to set */
+ if (monitor_cur_is_qmp()) {
+ error_setg(errp, "Property 'gicc-accessible' is read-only externally");
+ return;
+ }
+
+ if (gcs->gicc_accessible != value) {
+ gcs->gicc_accessible = value;
+
+ qemu_log_mask(LOG_UNIMP,
+ "GICC accessibility changed: vCPU %d = %s\n",
+ cs->cpu_index, value ? "accessible" : "inaccessible");
+ }
+}
+
void gicv3_init_cpuif(GICv3State *s)
{
ARMGICv3CommonClass *agcc = ARM_GICV3_COMMON_GET_CLASS(s);
@@ -28,6 +66,15 @@ void gicv3_init_cpuif(GICv3State *s)
/* define and register `system registers` with the vCPU */
for (i = 0; i < s->num_cpu; i++) {
+ g_autofree char *propname = g_strdup_printf("gicc-accessible[%d]", i);
+ object_property_add(OBJECT(s), propname, "bool",
+ gicv3_get_gicc_accessibility,
+ gicv3_set_gicc_accessibility,
+ NULL, &s->cpu[i]);
+
+ object_property_set_description(OBJECT(s), propname,
+ "Per-vCPU GICC interface accessibility (internal set only)");
+
agcc->init_cpu_reginfo(s->cpu[i].cpu);
}
}
diff --git a/hw/intc/arm_gicv3_kvm.c b/hw/intc/arm_gicv3_kvm.c
index 4ca889da45..e97578f59a 100644
--- a/hw/intc/arm_gicv3_kvm.c
+++ b/hw/intc/arm_gicv3_kvm.c
@@ -457,6 +457,16 @@ static void kvm_arm_gicv3_put(GICv3State *s)
GICv3CPUState *c = &s->cpu[ncpu];
int num_pri_bits;
+ /*
+ * We must ensure that we do not attempt to access or update KVM GICC
+ * registers if their corresponding QOM `GICv3CPUState` is marked as
+ * 'inaccessible', because their corresponding QOM vCPU objects
+ * are in administratively 'disabled' state.
+ */
+ if (!gicv3_gicc_accessible(OBJECT(s), ncpu)) {
+ continue;
+ }
+
kvm_gicc_access(s, ICC_SRE_EL1, ncpu, &c->icc_sre_el1, true);
kvm_gicc_access(s, ICC_CTLR_EL1, ncpu,
&c->icc_ctlr_el1[GICV3_NS], true);
@@ -615,6 +625,14 @@ static void kvm_arm_gicv3_get(GICv3State *s)
GICv3CPUState *c = &s->cpu[ncpu];
int num_pri_bits;
+ /*
+ * don't attempt to access KVM VGIC for the disabled vCPUs where
+ * GICv3CPUState is inaccessible.
+ */
+ if (!gicv3_gicc_accessible(OBJECT(s), ncpu)) {
+ continue;
+ }
+
kvm_gicc_access(s, ICC_SRE_EL1, ncpu, &c->icc_sre_el1, false);
kvm_gicc_access(s, ICC_CTLR_EL1, ncpu,
&c->icc_ctlr_el1[GICV3_NS], false);
diff --git a/include/hw/intc/arm_gicv3_common.h b/include/hw/intc/arm_gicv3_common.h
index 3720728227..bbf899184e 100644
--- a/include/hw/intc/arm_gicv3_common.h
+++ b/include/hw/intc/arm_gicv3_common.h
@@ -27,6 +27,7 @@
#include "hw/sysbus.h"
#include "hw/intc/arm_gic_common.h"
#include "qom/object.h"
+#include "qapi/error.h"
/*
* Maximum number of possible interrupts, determined by the GIC architecture.
@@ -164,6 +165,7 @@ struct GICv3CPUState {
uint64_t icc_apr[3][4];
uint64_t icc_igrpen[3];
uint64_t icc_ctlr_el3;
+ bool gicc_accessible;
/* Virtualization control interface */
uint64_t ich_apr[3][4]; /* ich_apr[GICV3_G1][x] never used */
@@ -329,4 +331,26 @@ void gicv3_init_irqs_and_mmio(GICv3State *s, qemu_irq_handler handler,
*/
const char *gicv3_class_name(void);
+/**
+ * gicv3_gicc_accessible:
+ * @obj: QOM object implementing the GICv3 device
+ * @cpu: Index of the vCPU whose GICC accessibility is being queried
+ *
+ * Returns: true if the GICC interface for vCPU @cpu is accessible.
+ * Uses QOM property lookup for "gicc-accessible[%d]".
+ */
+static inline bool gicv3_gicc_accessible(Object *obj, int cpu)
+{
+ g_autofree gchar *propname = g_strdup_printf("gicc-accessible[%d]", cpu);
+ Error *local_err = NULL;
+ bool value;
+
+ value = object_property_get_bool(obj, propname, &local_err);
+ if (local_err) {
+ error_report_err(local_err);
+ return false;
+ }
+
+ return value;
+}
#endif
diff --git a/include/hw/qdev-core.h b/include/hw/qdev-core.h
index 2c22b32a3f..b1d3fa4a25 100644
--- a/include/hw/qdev-core.h
+++ b/include/hw/qdev-core.h
@@ -589,6 +589,30 @@ bool qdev_realize_and_unref(DeviceState *dev, BusState *bus, Error **errp);
*/
bool qdev_disable(DeviceState *dev, BusState *bus, Error **errp);
+/**
+ * qdev_check_enabled - Check if a device is administratively enabled
+ * @dev: The device to check
+ *
+ * This function returns whether the device is currently in administrative
+ * ENABLED state. It does not reflect runtime operational power state, but
+ * rather the host policy on whether the guest may interact with the device.
+ *
+ * Returns true if the device is administratively enabled; false otherwise.
+ */
+bool qdev_check_enabled(DeviceState *dev);
+
+/**
+ * qdev_get_admin_power_state - Query administrative power state of a device
+ * @dev: The device whose state is being queried
+ *
+ * Returns the current administrative power state (ENABLED or DISABLED),
+ * as stored in the device's internal admin state field. This reflects
+ * host-level policy—not the operational runtime state seen by the guest.
+ *
+ * Returns an integer from the DeviceAdminPowerState enum.
+ */
+int qdev_get_admin_power_state(DeviceState *dev);
+
/**
* qdev_unrealize: Unrealize a device
* @dev: device to unrealize
--
2.34.1
Hi Salil,
On 10/1/25 11:01 AM, salil.mehta@opnsrc.net wrote:
> From: Salil Mehta <salil.mehta@huawei.com>
>
> Per Arm GIC Architecture Specification (IHI0069H_b, §11.1), the CPU interface
> and its Processing Element (PE) share a power domain. If the PE is powered down
> or administratively disabled, the CPU interface must be quiescent or off, and
> any access is architecturally UNPREDICTABLE. Without explicit checks, QEMU may
> issue GICC register operations for vCPUs that are offline, removed, or
> otherwise unavailable—risking inconsistent state or undefined behavior in both
> TCG and KVM accelerators.
>
> To address this, introduce a per-vCPU gicc_accessible flag that reflects the
> administrative enablement of the corresponding QOM vCPU in accordance with the
> policy. This is permissible when the GICC (GIC CPU Interface) is online-capable,
> meaning vCPUs can be brought online in the guest kernel after boot. The flag is
> set during GIC realization and used to skip VGIC register reads/writes, SGI
> generation, and CPU interface updates when the GICC is not accessible. This
> prevents unsafe operations and ensures compliance when managing administratively
> disabled but present vCPUs.
>
> Co-developed-by: Keqian Zhu <zhukeqian1@huawei.com>
> Signed-off-by: Keqian Zhu <zhukeqian1@huawei.com>
> Signed-off-by: Salil Mehta <salil.mehta@huawei.com>
> ---
> hw/core/qdev.c | 26 +++++++++++++++++
> hw/intc/arm_gicv3_common.c | 23 +++++++++++++++
> hw/intc/arm_gicv3_cpuif.c | 8 +++++
> hw/intc/arm_gicv3_cpuif_common.c | 47 ++++++++++++++++++++++++++++++
> hw/intc/arm_gicv3_kvm.c | 18 ++++++++++++
> include/hw/intc/arm_gicv3_common.h | 24 +++++++++++++++
> include/hw/qdev-core.h | 24 +++++++++++++++
> 7 files changed, 170 insertions(+)
>
> diff --git a/hw/core/qdev.c b/hw/core/qdev.c
> index 5816abae39..8e9a4da6b5 100644
> --- a/hw/core/qdev.c
> +++ b/hw/core/qdev.c
> @@ -326,6 +326,32 @@ bool qdev_disable(DeviceState *dev, BusState *bus, Error **errp)
> errp);
> }
>
> +int qdev_get_admin_power_state(DeviceState *dev)
> +{
> + DeviceClass *dc;
> +
> + if (!dev) {
> + return DEVICE_ADMIN_POWER_STATE_REMOVED;
> + }
> +
> + dc = DEVICE_GET_CLASS(dev);
> + if (dc->admin_power_state_supported) {
> + return object_property_get_enum(OBJECT(dev), "admin_power_state",
> + "DeviceAdminPowerState", NULL);
> + }
> +
> + return DEVICE_ADMIN_POWER_STATE_ENABLED;
> +}
> +
> +bool qdev_check_enabled(DeviceState *dev)
> +{
> + /*
> + * if device supports power state transitions, check if it is not in
> + * 'disabled' state.
> + */
> + return qdev_get_admin_power_state(dev) == DEVICE_ADMIN_POWER_STATE_ENABLED;
> +}
> +
> bool qdev_machine_modified(void)
> {
> return qdev_hot_added || qdev_hot_removed;
> diff --git a/hw/intc/arm_gicv3_common.c b/hw/intc/arm_gicv3_common.c
> index f6a9f1c68b..f4428ad165 100644
> --- a/hw/intc/arm_gicv3_common.c
> +++ b/hw/intc/arm_gicv3_common.c
> @@ -439,6 +439,29 @@ static void arm_gicv3_common_realize(DeviceState *dev, Error **errp)
> CPUState *cpu = machine_get_possible_cpu(i);
> uint64_t cpu_affid;
>
> + /*
> + * Ref: Arm Generic Interrupt Controller Architecture Specification
> + * (GIC Architecture version 3 and version 4), IHI0069H_b,
> + * Section 11.1: Power Management
> + * https://developer.arm.com/documentation/ihi0069
> + *
> + * According to this specification, the CPU interface and the
> + * Processing Element (PE) must reside in the same power domain.
> + * Therefore, when a CPU/PE is powered off, its corresponding CPU
> + * interface must also be in the off state or in a quiescent state—
> + * depending on the state of the associated Redistributor.
> + *
> + * The Redistributor may reside in a separate power domain and may
> + * remain powered even when the associated PE is turned off.
> + *
> + * Accessing the GIC CPU interface while the PE is powered down can
> + * lead to UNPREDICTABLE behavior.
> + *
> + * Accordingly, the QOM object `GICv3CPUState` should be marked as
> + * either accessible or inaccessible based on the power state of the
> + * associated `CPUState` vCPU.
> + */
> + s->cpu[i].gicc_accessible = qdev_check_enabled(DEVICE(cpu));
> s->cpu[i].cpu = cpu;
> s->cpu[i].gic = s;
> /* Store GICv3CPUState in CPUARMState gicv3state pointer */
> diff --git a/hw/intc/arm_gicv3_cpuif.c b/hw/intc/arm_gicv3_cpuif.c
> index a7904237ac..6430b2c649 100644
> --- a/hw/intc/arm_gicv3_cpuif.c
> +++ b/hw/intc/arm_gicv3_cpuif.c
> @@ -1052,6 +1052,10 @@ void gicv3_cpuif_update(GICv3CPUState *cs)
> ARMCPU *cpu = ARM_CPU(cs->cpu);
> CPUARMState *env = &cpu->env;
>
> + if (!gicv3_gicc_accessible(OBJECT(cs->gic), CPU(cpu)->cpu_index)) {
> + return;
> + }
> +
> g_assert(bql_locked());
>
> trace_gicv3_cpuif_update(gicv3_redist_affid(cs), cs->hppi.irq,
> @@ -2036,6 +2040,10 @@ static void icc_generate_sgi(CPUARMState *env, GICv3CPUState *cs,
> for (i = 0; i < s->num_cpu; i++) {
> GICv3CPUState *ocs = &s->cpu[i];
>
> + if (!gicv3_gicc_accessible(OBJECT(s), i)) {
> + continue;
> + }
> +
> if (irm) {
> /* IRM == 1 : route to all CPUs except self */
> if (cs == ocs) {
> diff --git a/hw/intc/arm_gicv3_cpuif_common.c b/hw/intc/arm_gicv3_cpuif_common.c
> index f9a9b2d8a3..8f9a5b6fa2 100644
> --- a/hw/intc/arm_gicv3_cpuif_common.c
> +++ b/hw/intc/arm_gicv3_cpuif_common.c
> @@ -12,6 +12,9 @@
> #include "qemu/osdep.h"
> #include "gicv3_internal.h"
> #include "cpu.h"
> +#include "qemu/log.h"
> +#include "monitor/monitor.h"
> +#include "qapi/visitor.h"
>
> void gicv3_set_gicv3state(CPUState *cpu, GICv3CPUState *s)
> {
> @@ -21,6 +24,41 @@ void gicv3_set_gicv3state(CPUState *cpu, GICv3CPUState *s)
> env->gicv3state = (void *)s;
> };
>
> +static void
> +gicv3_get_gicc_accessibility(Object *obj, Visitor *v, const char *name,
> + void *opaque, Error **errp)
> +{
> + GICv3CPUState *cs = (GICv3CPUState *)opaque;
> + bool value = cs->gicc_accessible;
> +
> + visit_type_bool(v, name, &value, errp);
> +}
> +
> +static void
> +gicv3_set_gicc_accessibility(Object *obj, Visitor *v, const char *name,
> + void *opaque, Error **errp)
> +{
> + GICv3CPUState *gcs = opaque;
> + CPUState *cs = gcs->cpu;
> + bool value;
> +
> + visit_type_bool(v, name, &value, errp);
> +
> + /* Block external attempts to set */
> + if (monitor_cur_is_qmp()) {
> + error_setg(errp, "Property 'gicc-accessible' is read-only externally");
> + return;
> + }
> +
> + if (gcs->gicc_accessible != value) {
> + gcs->gicc_accessible = value;
> +
> + qemu_log_mask(LOG_UNIMP,
> + "GICC accessibility changed: vCPU %d = %s\n",
> + cs->cpu_index, value ? "accessible" : "inaccessible");
> + }
> +}
> +
> void gicv3_init_cpuif(GICv3State *s)
> {
> ARMGICv3CommonClass *agcc = ARM_GICV3_COMMON_GET_CLASS(s);
> @@ -28,6 +66,15 @@ void gicv3_init_cpuif(GICv3State *s)
>
> /* define and register `system registers` with the vCPU */
> for (i = 0; i < s->num_cpu; i++) {
> + g_autofree char *propname = g_strdup_printf("gicc-accessible[%d]", i);
> + object_property_add(OBJECT(s), propname, "bool",
> + gicv3_get_gicc_accessibility,
> + gicv3_set_gicc_accessibility,
> + NULL, &s->cpu[i]);
> +
> + object_property_set_description(OBJECT(s), propname,
> + "Per-vCPU GICC interface accessibility (internal set only)");
> +
> agcc->init_cpu_reginfo(s->cpu[i].cpu);
> }
> }
> diff --git a/hw/intc/arm_gicv3_kvm.c b/hw/intc/arm_gicv3_kvm.c
> index 4ca889da45..e97578f59a 100644
> --- a/hw/intc/arm_gicv3_kvm.c
> +++ b/hw/intc/arm_gicv3_kvm.c
> @@ -457,6 +457,16 @@ static void kvm_arm_gicv3_put(GICv3State *s)
> GICv3CPUState *c = &s->cpu[ncpu];
> int num_pri_bits;
>
> + /*
> + * We must ensure that we do not attempt to access or update KVM GICC
> + * registers if their corresponding QOM `GICv3CPUState` is marked as
> + * 'inaccessible', because their corresponding QOM vCPU objects
> + * are in administratively 'disabled' state.
> + */
> + if (!gicv3_gicc_accessible(OBJECT(s), ncpu)) {
> + continue;
> + }
> +
> kvm_gicc_access(s, ICC_SRE_EL1, ncpu, &c->icc_sre_el1, true);
> kvm_gicc_access(s, ICC_CTLR_EL1, ncpu,
> &c->icc_ctlr_el1[GICV3_NS], true);
> @@ -615,6 +625,14 @@ static void kvm_arm_gicv3_get(GICv3State *s)
> GICv3CPUState *c = &s->cpu[ncpu];
> int num_pri_bits;
>
> + /*
> + * don't attempt to access KVM VGIC for the disabled vCPUs where
> + * GICv3CPUState is inaccessible.
> + */
> + if (!gicv3_gicc_accessible(OBJECT(s), ncpu)) {
> + continue;
> + }
> +
> kvm_gicc_access(s, ICC_SRE_EL1, ncpu, &c->icc_sre_el1, false);
> kvm_gicc_access(s, ICC_CTLR_EL1, ncpu,
> &c->icc_ctlr_el1[GICV3_NS], false);
Shall the GICC accessible state is also checked in arm_gicv3.c? If I'm
understanding correctly, the vCPU hotplug feature is also supported for
TCG mode when mttcg is enabled, as implemented in PATCH[3].
if ((tcg_enabled() && !qemu_tcg_mttcg_enabled()) || hvf_enabled() ||
qtest_enabled() || vms->gic_version == VIRT_GIC_VERSION_2) {
max_cpus = machine->smp.max_cpus = smp_cpus;
if (mc->has_online_capable_cpus) {
if (vms->gic_version == VIRT_GIC_VERSION_2) {
warn_report("GICv2 does not support online-capable CPUs");
}
mc->has_online_capable_cpus = false;
}
}
Thanks,
Gavin
> diff --git a/include/hw/intc/arm_gicv3_common.h b/include/hw/intc/arm_gicv3_common.h
> index 3720728227..bbf899184e 100644
> --- a/include/hw/intc/arm_gicv3_common.h
> +++ b/include/hw/intc/arm_gicv3_common.h
> @@ -27,6 +27,7 @@
> #include "hw/sysbus.h"
> #include "hw/intc/arm_gic_common.h"
> #include "qom/object.h"
> +#include "qapi/error.h"
>
> /*
> * Maximum number of possible interrupts, determined by the GIC architecture.
> @@ -164,6 +165,7 @@ struct GICv3CPUState {
> uint64_t icc_apr[3][4];
> uint64_t icc_igrpen[3];
> uint64_t icc_ctlr_el3;
> + bool gicc_accessible;
>
> /* Virtualization control interface */
> uint64_t ich_apr[3][4]; /* ich_apr[GICV3_G1][x] never used */
> @@ -329,4 +331,26 @@ void gicv3_init_irqs_and_mmio(GICv3State *s, qemu_irq_handler handler,
> */
> const char *gicv3_class_name(void);
>
> +/**
> + * gicv3_gicc_accessible:
> + * @obj: QOM object implementing the GICv3 device
> + * @cpu: Index of the vCPU whose GICC accessibility is being queried
> + *
> + * Returns: true if the GICC interface for vCPU @cpu is accessible.
> + * Uses QOM property lookup for "gicc-accessible[%d]".
> + */
> +static inline bool gicv3_gicc_accessible(Object *obj, int cpu)
> +{
> + g_autofree gchar *propname = g_strdup_printf("gicc-accessible[%d]", cpu);
> + Error *local_err = NULL;
> + bool value;
> +
> + value = object_property_get_bool(obj, propname, &local_err);
> + if (local_err) {
> + error_report_err(local_err);
> + return false;
> + }
> +
> + return value;
> +}
> #endif
> diff --git a/include/hw/qdev-core.h b/include/hw/qdev-core.h
> index 2c22b32a3f..b1d3fa4a25 100644
> --- a/include/hw/qdev-core.h
> +++ b/include/hw/qdev-core.h
> @@ -589,6 +589,30 @@ bool qdev_realize_and_unref(DeviceState *dev, BusState *bus, Error **errp);
> */
> bool qdev_disable(DeviceState *dev, BusState *bus, Error **errp);
>
> +/**
> + * qdev_check_enabled - Check if a device is administratively enabled
> + * @dev: The device to check
> + *
> + * This function returns whether the device is currently in administrative
> + * ENABLED state. It does not reflect runtime operational power state, but
> + * rather the host policy on whether the guest may interact with the device.
> + *
> + * Returns true if the device is administratively enabled; false otherwise.
> + */
> +bool qdev_check_enabled(DeviceState *dev);
> +
> +/**
> + * qdev_get_admin_power_state - Query administrative power state of a device
> + * @dev: The device whose state is being queried
> + *
> + * Returns the current administrative power state (ENABLED or DISABLED),
> + * as stored in the device's internal admin state field. This reflects
> + * host-level policy—not the operational runtime state seen by the guest.
> + *
> + * Returns an integer from the DeviceAdminPowerState enum.
> + */
> +int qdev_get_admin_power_state(DeviceState *dev);
> +
> /**
> * qdev_unrealize: Unrealize a device
> * @dev: device to unrealize
Hi Salil,
On 10/1/25 11:01 AM, salil.mehta@opnsrc.net wrote:
> From: Salil Mehta <salil.mehta@huawei.com>
>
> Per Arm GIC Architecture Specification (IHI0069H_b, §11.1), the CPU interface
> and its Processing Element (PE) share a power domain. If the PE is powered down
> or administratively disabled, the CPU interface must be quiescent or off, and
> any access is architecturally UNPREDICTABLE. Without explicit checks, QEMU may
> issue GICC register operations for vCPUs that are offline, removed, or
> otherwise unavailable—risking inconsistent state or undefined behavior in both
> TCG and KVM accelerators.
>
> To address this, introduce a per-vCPU gicc_accessible flag that reflects the
> administrative enablement of the corresponding QOM vCPU in accordance with the
> policy. This is permissible when the GICC (GIC CPU Interface) is online-capable,
> meaning vCPUs can be brought online in the guest kernel after boot. The flag is
> set during GIC realization and used to skip VGIC register reads/writes, SGI
> generation, and CPU interface updates when the GICC is not accessible. This
> prevents unsafe operations and ensures compliance when managing administratively
> disabled but present vCPUs.
>
> Co-developed-by: Keqian Zhu <zhukeqian1@huawei.com>
> Signed-off-by: Keqian Zhu <zhukeqian1@huawei.com>
> Signed-off-by: Salil Mehta <salil.mehta@huawei.com>
> ---
> hw/core/qdev.c | 26 +++++++++++++++++
> hw/intc/arm_gicv3_common.c | 23 +++++++++++++++
> hw/intc/arm_gicv3_cpuif.c | 8 +++++
> hw/intc/arm_gicv3_cpuif_common.c | 47 ++++++++++++++++++++++++++++++
> hw/intc/arm_gicv3_kvm.c | 18 ++++++++++++
> include/hw/intc/arm_gicv3_common.h | 24 +++++++++++++++
> include/hw/qdev-core.h | 24 +++++++++++++++
> 7 files changed, 170 insertions(+)
>
> diff --git a/hw/core/qdev.c b/hw/core/qdev.c
> index 5816abae39..8e9a4da6b5 100644
> --- a/hw/core/qdev.c
> +++ b/hw/core/qdev.c
> @@ -326,6 +326,32 @@ bool qdev_disable(DeviceState *dev, BusState *bus, Error **errp)
> errp);
> }
>
> +int qdev_get_admin_power_state(DeviceState *dev)
> +{
> + DeviceClass *dc;
> +
> + if (!dev) {
> + return DEVICE_ADMIN_POWER_STATE_REMOVED;
> + }
> +
> + dc = DEVICE_GET_CLASS(dev);
> + if (dc->admin_power_state_supported) {
> + return object_property_get_enum(OBJECT(dev), "admin_power_state",
> + "DeviceAdminPowerState", NULL);
> + }
> +
> + return DEVICE_ADMIN_POWER_STATE_ENABLED;
> +}
> +
> +bool qdev_check_enabled(DeviceState *dev)
> +{
> + /*
> + * if device supports power state transitions, check if it is not in
> + * 'disabled' state.
> + */
> + return qdev_get_admin_power_state(dev) == DEVICE_ADMIN_POWER_STATE_ENABLED;
> +}
> +
> bool qdev_machine_modified(void)
> {
> return qdev_hot_added || qdev_hot_removed;
> diff --git a/hw/intc/arm_gicv3_common.c b/hw/intc/arm_gicv3_common.c
> index f6a9f1c68b..f4428ad165 100644
> --- a/hw/intc/arm_gicv3_common.c
> +++ b/hw/intc/arm_gicv3_common.c
> @@ -439,6 +439,29 @@ static void arm_gicv3_common_realize(DeviceState *dev, Error **errp)
> CPUState *cpu = machine_get_possible_cpu(i);
> uint64_t cpu_affid;
>
> + /*
> + * Ref: Arm Generic Interrupt Controller Architecture Specification
> + * (GIC Architecture version 3 and version 4), IHI0069H_b,
> + * Section 11.1: Power Management
> + * https://developer.arm.com/documentation/ihi0069
> + *
> + * According to this specification, the CPU interface and the
> + * Processing Element (PE) must reside in the same power domain.
> + * Therefore, when a CPU/PE is powered off, its corresponding CPU
> + * interface must also be in the off state or in a quiescent state—
> + * depending on the state of the associated Redistributor.
> + *
> + * The Redistributor may reside in a separate power domain and may
> + * remain powered even when the associated PE is turned off.
> + *
> + * Accessing the GIC CPU interface while the PE is powered down can
> + * lead to UNPREDICTABLE behavior.
> + *
> + * Accordingly, the QOM object `GICv3CPUState` should be marked as
> + * either accessible or inaccessible based on the power state of the
> + * associated `CPUState` vCPU.
> + */
> + s->cpu[i].gicc_accessible = qdev_check_enabled(DEVICE(cpu));
> s->cpu[i].cpu = cpu;
> s->cpu[i].gic = s;
> /* Store GICv3CPUState in CPUARMState gicv3state pointer */
> diff --git a/hw/intc/arm_gicv3_cpuif.c b/hw/intc/arm_gicv3_cpuif.c
> index a7904237ac..6430b2c649 100644
> --- a/hw/intc/arm_gicv3_cpuif.c
> +++ b/hw/intc/arm_gicv3_cpuif.c
> @@ -1052,6 +1052,10 @@ void gicv3_cpuif_update(GICv3CPUState *cs)
> ARMCPU *cpu = ARM_CPU(cs->cpu);
> CPUARMState *env = &cpu->env;
>
> + if (!gicv3_gicc_accessible(OBJECT(cs->gic), CPU(cpu)->cpu_index)) {
> + return;
> + }
> +
> g_assert(bql_locked());
>
> trace_gicv3_cpuif_update(gicv3_redist_affid(cs), cs->hppi.irq,
> @@ -2036,6 +2040,10 @@ static void icc_generate_sgi(CPUARMState *env, GICv3CPUState *cs,
> for (i = 0; i < s->num_cpu; i++) {
> GICv3CPUState *ocs = &s->cpu[i];
>
> + if (!gicv3_gicc_accessible(OBJECT(s), i)) {
> + continue;
> + }
> +
> if (irm) {
> /* IRM == 1 : route to all CPUs except self */
> if (cs == ocs) {
> diff --git a/hw/intc/arm_gicv3_cpuif_common.c b/hw/intc/arm_gicv3_cpuif_common.c
> index f9a9b2d8a3..8f9a5b6fa2 100644
> --- a/hw/intc/arm_gicv3_cpuif_common.c
> +++ b/hw/intc/arm_gicv3_cpuif_common.c
> @@ -12,6 +12,9 @@
> #include "qemu/osdep.h"
> #include "gicv3_internal.h"
> #include "cpu.h"
> +#include "qemu/log.h"
> +#include "monitor/monitor.h"
> +#include "qapi/visitor.h"
>
> void gicv3_set_gicv3state(CPUState *cpu, GICv3CPUState *s)
> {
> @@ -21,6 +24,41 @@ void gicv3_set_gicv3state(CPUState *cpu, GICv3CPUState *s)
> env->gicv3state = (void *)s;
> };
>
> +static void
> +gicv3_get_gicc_accessibility(Object *obj, Visitor *v, const char *name,
> + void *opaque, Error **errp)
> +{
> + GICv3CPUState *cs = (GICv3CPUState *)opaque;
> + bool value = cs->gicc_accessible;
> +
> + visit_type_bool(v, name, &value, errp);
> +}
> +
> +static void
> +gicv3_set_gicc_accessibility(Object *obj, Visitor *v, const char *name,
> + void *opaque, Error **errp)
> +{
> + GICv3CPUState *gcs = opaque;
> + CPUState *cs = gcs->cpu;
> + bool value;
> +
> + visit_type_bool(v, name, &value, errp);
> +
> + /* Block external attempts to set */
> + if (monitor_cur_is_qmp()) {
> + error_setg(errp, "Property 'gicc-accessible' is read-only externally");
> + return;
> + }
> +
> + if (gcs->gicc_accessible != value) {
> + gcs->gicc_accessible = value;
> +
> + qemu_log_mask(LOG_UNIMP,
> + "GICC accessibility changed: vCPU %d = %s\n",
> + cs->cpu_index, value ? "accessible" : "inaccessible");
> + }
> +}
> +
The property can be modified from the external by 'qom-set'.
(qemu) qom-list /machine/unattached
device[2] (child<kvm-arm-gicv3>)
(qemu) qom-get /machine/unattached/device[2] gicc-accessible[0]
true
(qemu) qom-set /machine/unattached/device[2] gicc-accessible[0] false
(qemu) qom-get /machine/unattached/device[2] gicc-accessible[0]
false
Thanks,
Gavin
> void gicv3_init_cpuif(GICv3State *s)
> {
> ARMGICv3CommonClass *agcc = ARM_GICV3_COMMON_GET_CLASS(s);
> @@ -28,6 +66,15 @@ void gicv3_init_cpuif(GICv3State *s)
>
> /* define and register `system registers` with the vCPU */
> for (i = 0; i < s->num_cpu; i++) {
> + g_autofree char *propname = g_strdup_printf("gicc-accessible[%d]", i);
> + object_property_add(OBJECT(s), propname, "bool",
> + gicv3_get_gicc_accessibility,
> + gicv3_set_gicc_accessibility,
> + NULL, &s->cpu[i]);
> +
> + object_property_set_description(OBJECT(s), propname,
> + "Per-vCPU GICC interface accessibility (internal set only)");
> +
> agcc->init_cpu_reginfo(s->cpu[i].cpu);
> }
> }
> diff --git a/hw/intc/arm_gicv3_kvm.c b/hw/intc/arm_gicv3_kvm.c
> index 4ca889da45..e97578f59a 100644
> --- a/hw/intc/arm_gicv3_kvm.c
> +++ b/hw/intc/arm_gicv3_kvm.c
> @@ -457,6 +457,16 @@ static void kvm_arm_gicv3_put(GICv3State *s)
> GICv3CPUState *c = &s->cpu[ncpu];
> int num_pri_bits;
>
> + /*
> + * We must ensure that we do not attempt to access or update KVM GICC
> + * registers if their corresponding QOM `GICv3CPUState` is marked as
> + * 'inaccessible', because their corresponding QOM vCPU objects
> + * are in administratively 'disabled' state.
> + */
> + if (!gicv3_gicc_accessible(OBJECT(s), ncpu)) {
> + continue;
> + }
> +
> kvm_gicc_access(s, ICC_SRE_EL1, ncpu, &c->icc_sre_el1, true);
> kvm_gicc_access(s, ICC_CTLR_EL1, ncpu,
> &c->icc_ctlr_el1[GICV3_NS], true);
> @@ -615,6 +625,14 @@ static void kvm_arm_gicv3_get(GICv3State *s)
> GICv3CPUState *c = &s->cpu[ncpu];
> int num_pri_bits;
>
> + /*
> + * don't attempt to access KVM VGIC for the disabled vCPUs where
> + * GICv3CPUState is inaccessible.
> + */
> + if (!gicv3_gicc_accessible(OBJECT(s), ncpu)) {
> + continue;
> + }
> +
> kvm_gicc_access(s, ICC_SRE_EL1, ncpu, &c->icc_sre_el1, false);
> kvm_gicc_access(s, ICC_CTLR_EL1, ncpu,
> &c->icc_ctlr_el1[GICV3_NS], false);
> diff --git a/include/hw/intc/arm_gicv3_common.h b/include/hw/intc/arm_gicv3_common.h
> index 3720728227..bbf899184e 100644
> --- a/include/hw/intc/arm_gicv3_common.h
> +++ b/include/hw/intc/arm_gicv3_common.h
> @@ -27,6 +27,7 @@
> #include "hw/sysbus.h"
> #include "hw/intc/arm_gic_common.h"
> #include "qom/object.h"
> +#include "qapi/error.h"
>
> /*
> * Maximum number of possible interrupts, determined by the GIC architecture.
> @@ -164,6 +165,7 @@ struct GICv3CPUState {
> uint64_t icc_apr[3][4];
> uint64_t icc_igrpen[3];
> uint64_t icc_ctlr_el3;
> + bool gicc_accessible;
>
> /* Virtualization control interface */
> uint64_t ich_apr[3][4]; /* ich_apr[GICV3_G1][x] never used */
> @@ -329,4 +331,26 @@ void gicv3_init_irqs_and_mmio(GICv3State *s, qemu_irq_handler handler,
> */
> const char *gicv3_class_name(void);
>
> +/**
> + * gicv3_gicc_accessible:
> + * @obj: QOM object implementing the GICv3 device
> + * @cpu: Index of the vCPU whose GICC accessibility is being queried
> + *
> + * Returns: true if the GICC interface for vCPU @cpu is accessible.
> + * Uses QOM property lookup for "gicc-accessible[%d]".
> + */
> +static inline bool gicv3_gicc_accessible(Object *obj, int cpu)
> +{
> + g_autofree gchar *propname = g_strdup_printf("gicc-accessible[%d]", cpu);
> + Error *local_err = NULL;
> + bool value;
> +
> + value = object_property_get_bool(obj, propname, &local_err);
> + if (local_err) {
> + error_report_err(local_err);
> + return false;
> + }
> +
> + return value;
> +}
> #endif
> diff --git a/include/hw/qdev-core.h b/include/hw/qdev-core.h
> index 2c22b32a3f..b1d3fa4a25 100644
> --- a/include/hw/qdev-core.h
> +++ b/include/hw/qdev-core.h
> @@ -589,6 +589,30 @@ bool qdev_realize_and_unref(DeviceState *dev, BusState *bus, Error **errp);
> */
> bool qdev_disable(DeviceState *dev, BusState *bus, Error **errp);
>
> +/**
> + * qdev_check_enabled - Check if a device is administratively enabled
> + * @dev: The device to check
> + *
> + * This function returns whether the device is currently in administrative
> + * ENABLED state. It does not reflect runtime operational power state, but
> + * rather the host policy on whether the guest may interact with the device.
> + *
> + * Returns true if the device is administratively enabled; false otherwise.
> + */
> +bool qdev_check_enabled(DeviceState *dev);
> +
> +/**
> + * qdev_get_admin_power_state - Query administrative power state of a device
> + * @dev: The device whose state is being queried
> + *
> + * Returns the current administrative power state (ENABLED or DISABLED),
> + * as stored in the device's internal admin state field. This reflects
> + * host-level policy—not the operational runtime state seen by the guest.
> + *
> + * Returns an integer from the DeviceAdminPowerState enum.
> + */
> +int qdev_get_admin_power_state(DeviceState *dev);
> +
> /**
> * qdev_unrealize: Unrealize a device
> * @dev: device to unrealize
© 2016 - 2025 Red Hat, Inc.