include/system/hvf_int.h | 4 +++- accel/hvf/hvf-all.c | 42 ++++++++++++++++++++++++++++++++- target/arm/hvf/hvf.c | 50 +++++++++++++++++++++++++++++++++++++++- target/i386/hvf/hvf.c | 10 +++++++- 4 files changed, 102 insertions(+), 4 deletions(-)
The IPA granule is the smallest page size hv_vm_map() support. For Venus, we
need to support 4KiB pages. macOS 26 introduces a public API for setting
the granule size. We can only use this when compiled with macOS 26 SDK and
run on macOS 26+. Otherwise, we fall back to an older, private, API which
achieves the same purpose.
Signed-off-by: Joelle van Dyne <j@getutm.app>
---
include/system/hvf_int.h | 4 +++-
accel/hvf/hvf-all.c | 42 ++++++++++++++++++++++++++++++++-
target/arm/hvf/hvf.c | 50 +++++++++++++++++++++++++++++++++++++++-
target/i386/hvf/hvf.c | 10 +++++++-
4 files changed, 102 insertions(+), 4 deletions(-)
diff --git a/include/system/hvf_int.h b/include/system/hvf_int.h
index 1d2616595cd..881e16e31b6 100644
--- a/include/system/hvf_int.h
+++ b/include/system/hvf_int.h
@@ -63,6 +63,7 @@ struct HVFState {
hvf_vcpu_caps *hvf_caps;
uint64_t vtimer_offset;
+ uint32_t ipa_granule_size;
QTAILQ_HEAD(, hvf_sw_breakpoint) hvf_sw_breakpoints;
};
extern HVFState *hvf_state;
@@ -82,7 +83,8 @@ void assert_hvf_ok_impl(hv_return_t ret, const char *file, unsigned int line,
#define assert_hvf_ok(EX) assert_hvf_ok_impl((EX), __FILE__, __LINE__, #EX)
const char *hvf_return_string(hv_return_t ret);
int hvf_arch_init(void);
-hv_return_t hvf_arch_vm_create(MachineState *ms, uint32_t pa_range);
+hv_return_t hvf_arch_vm_create(MachineState *ms, uint32_t pa_range,
+ uint32_t ipa_granule_size);
hvf_slot *hvf_find_overlap_slot(uint64_t, uint64_t);
void hvf_kick_vcpu_thread(CPUState *cpu);
diff --git a/accel/hvf/hvf-all.c b/accel/hvf/hvf-all.c
index a898359777c..7ebfc9bbe58 100644
--- a/accel/hvf/hvf-all.c
+++ b/accel/hvf/hvf-all.c
@@ -17,6 +17,8 @@
#include "system/hvf_int.h"
#include "hw/core/cpu.h"
#include "hw/boards.h"
+#include "qapi/error.h"
+#include "qapi/visitor.h"
#include "trace.h"
bool hvf_allowed;
@@ -152,6 +154,10 @@ static void hvf_set_phys_mem(MemoryRegionSection *section, bool add)
}
}
+ if (hvf_state->ipa_granule_size) {
+ page_size = hvf_state->ipa_granule_size;
+ }
+
if (!QEMU_IS_ALIGNED(int128_get64(section->size), page_size) ||
!QEMU_IS_ALIGNED(section->offset_within_address_space, page_size)) {
if (add) {
@@ -316,7 +322,7 @@ static int hvf_accel_init(AccelState *as, MachineState *ms)
}
}
- ret = hvf_arch_vm_create(ms, (uint32_t)pa_range);
+ ret = hvf_arch_vm_create(ms, (uint32_t)pa_range, s->ipa_granule_size);
if (ret == HV_DENIED) {
error_report("Could not access HVF. Is the executable signed"
" with com.apple.security.hypervisor entitlement?");
@@ -340,6 +346,34 @@ static int hvf_gdbstub_sstep_flags(AccelState *as)
return SSTEP_ENABLE | SSTEP_NOIRQ;
}
+static void hvf_get_ipa_granule_size(Object *obj, Visitor *v,
+ const char *name, void *opaque,
+ Error **errp)
+{
+ HVFState *s = HVF_STATE(obj);
+ uint32_t value = s->ipa_granule_size;
+
+ visit_type_uint32(v, name, &value, errp);
+}
+
+static void hvf_set_ipa_granule_size(Object *obj, Visitor *v,
+ const char *name, void *opaque,
+ Error **errp)
+{
+ HVFState *s = HVF_STATE(obj);
+ uint32_t value;
+
+ if (!visit_type_uint32(v, name, &value, errp)) {
+ return;
+ }
+ if (value & (value - 1)) {
+ error_setg(errp, "ipa-granule-size must be a power of two.");
+ return;
+ }
+
+ s->ipa_granule_size = value;
+}
+
static void hvf_accel_class_init(ObjectClass *oc, const void *data)
{
AccelClass *ac = ACCEL_CLASS(oc);
@@ -347,6 +381,12 @@ static void hvf_accel_class_init(ObjectClass *oc, const void *data)
ac->init_machine = hvf_accel_init;
ac->allowed = &hvf_allowed;
ac->gdbstub_supported_sstep_flags = hvf_gdbstub_sstep_flags;
+
+ object_class_property_add(oc, "ipa-granule-size", "uint32",
+ hvf_get_ipa_granule_size, hvf_set_ipa_granule_size,
+ NULL, NULL);
+ object_class_property_set_description(oc, "ipa-granule-size",
+ "Size of a single guest page");
}
static const TypeInfo hvf_accel_type = {
diff --git a/target/arm/hvf/hvf.c b/target/arm/hvf/hvf.c
index de1e8fb8a05..7c44325ca64 100644
--- a/target/arm/hvf/hvf.c
+++ b/target/arm/hvf/hvf.c
@@ -12,6 +12,9 @@
#include "qemu/osdep.h"
#include "qemu/error-report.h"
#include "qemu/log.h"
+#include <dlfcn.h>
+#include <AvailabilityMacros.h>
+#include <TargetConditionals.h>
#include "system/runstate.h"
#include "system/hvf.h"
@@ -880,7 +883,45 @@ void hvf_arch_vcpu_destroy(CPUState *cpu)
assert_hvf_ok(ret);
}
-hv_return_t hvf_arch_vm_create(MachineState *ms, uint32_t pa_range)
+static hv_return_t hvf_set_ipa_granule(hv_vm_config_t config,
+ uint32_t ipa_granule_size)
+{
+ static hv_return_t (*set_ipa_granule)(hv_vm_config_t, uint32_t);
+ uint64_t page_size = qemu_real_host_page_size();
+
+ /* macOS 26 introduces a public API for setting granule size */
+#if defined(MAC_OS_X_VERSION_MAX_ALLOWED) && defined(MAC_OS_VERSION_26_0) && \
+ MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_26_0
+ if (__builtin_available(macOS 26, *)) {
+ hv_ipa_granule_t granule = HV_IPA_GRANULE_16KB;
+
+ if (ipa_granule_size == 4096) {
+ granule = HV_IPA_GRANULE_4KB;
+ } else if (ipa_granule_size != 16384) {
+ error_report("Unsupported granule size: 0x%x", ipa_granule_size);
+ return HV_UNSUPPORTED;
+ }
+
+ return hv_vm_config_set_ipa_granule(config, granule);
+ }
+#endif
+
+ /* older macOS need to use a private API */
+ if (!set_ipa_granule) {
+ set_ipa_granule = dlsym(RTLD_NEXT, "_hv_vm_config_set_ipa_granule");
+ }
+ if (set_ipa_granule) {
+ return set_ipa_granule(config, ipa_granule_size);
+ } else if (ipa_granule_size != page_size) {
+ error_report("Failed to find _hv_vm_config_set_ipa_granule");
+ return HV_UNSUPPORTED;
+ }
+
+ return HV_SUCCESS;
+}
+
+hv_return_t hvf_arch_vm_create(MachineState *ms, uint32_t pa_range,
+ uint32_t ipa_granule_size)
{
hv_return_t ret;
hv_vm_config_t config = hv_vm_config_create();
@@ -891,6 +932,13 @@ hv_return_t hvf_arch_vm_create(MachineState *ms, uint32_t pa_range)
}
chosen_ipa_bit_size = pa_range;
+ if (ipa_granule_size) {
+ ret = hvf_set_ipa_granule(config, ipa_granule_size);
+ if (ret != HV_SUCCESS) {
+ goto cleanup;
+ }
+ }
+
ret = hv_vm_create(config);
cleanup:
diff --git a/target/i386/hvf/hvf.c b/target/i386/hvf/hvf.c
index 16febbac48f..395e13f467e 100644
--- a/target/i386/hvf/hvf.c
+++ b/target/i386/hvf/hvf.c
@@ -225,8 +225,16 @@ int hvf_arch_init(void)
return 0;
}
-hv_return_t hvf_arch_vm_create(MachineState *ms, uint32_t pa_range)
+hv_return_t hvf_arch_vm_create(MachineState *ms, uint32_t pa_range,
+ uint32_t ipa_granule_size)
{
+ uint64_t page_size = qemu_real_host_page_size();
+
+ if (ipa_granule_size != 0 && ipa_granule_size != page_size) {
+ error_report("Only supported IPA granule size: 0x%llx", page_size);
+ return HV_UNSUPPORTED;
+ }
+
return hv_vm_create(HV_VM_DEFAULT);
}
--
2.50.1 (Apple Git-155)
Hello,
> On 19. Dec 2025, at 19:37, Joelle van Dyne <j@getutm.app> wrote:
>
> The IPA granule is the smallest page size hv_vm_map() support. For Venus, we
> need to support 4KiB pages. macOS 26 introduces a public API for setting
> the granule size. We can only use this when compiled with macOS 26 SDK and
> run on macOS 26+. Otherwise, we fall back to an older, private, API which
> achieves the same purpose.
>
Let’s have an HVF_NO_PRIVATE_API define (or the opposite) to have a single toggle to disable all private API use at build time
> +
> + /* older macOS need to use a private API */
> + if (!set_ipa_granule) {
> + set_ipa_granule = dlsym(RTLD_NEXT, "_hv_vm_config_set_ipa_granule");
> + }
> + if (set_ipa_granule) {
> + return set_ipa_granule(config, ipa_granule_size);
> + } else if (ipa_granule_size != page_size) {
> + error_report("Failed to find _hv_vm_config_set_ipa_granule");
> + return HV_UNSUPPORTED;
> + }
> +
> + return HV_SUCCESS;
> +}
> +
> +hv_return_t hvf_arch_vm_create(MachineState *ms, uint32_t pa_range,
> + uint32_t ipa_granule_size)
> {
> hv_return_t ret;
> hv_vm_config_t config = hv_vm_config_create();
> @@ -891,6 +932,13 @@ hv_return_t hvf_arch_vm_create(MachineState *ms, uint32_t pa_range)
> }
> chosen_ipa_bit_size = pa_range;
>
The PA range is actually different depending on the IPA granule size.
An example from M4 Max:
% sysctl -a | grep ipa
kern.hv.ipa_size_16k: 4398046511104
kern.hv.ipa_size_4k: 1099511627776
> + if (ipa_granule_size) {
> + ret = hvf_set_ipa_granule(config, ipa_granule_size);
> + if (ret != HV_SUCCESS) {
> + goto cleanup;
> + }
> + }
> +
> ret = hv_vm_create(config);
>
> On 19. Dec 2025, at 19:46, Mohamed Mediouni <mohamed@unpredictable.fr> wrote:
>
> Hello,
>
>> On 19. Dec 2025, at 19:37, Joelle van Dyne <j@getutm.app> wrote:
>>
>> The IPA granule is the smallest page size hv_vm_map() support. For Venus, we
>> need to support 4KiB pages. macOS 26 introduces a public API for setting
>> the granule size. We can only use this when compiled with macOS 26 SDK and
>> run on macOS 26+. Otherwise, we fall back to an older, private, API which
>> achieves the same purpose.
>>
>
> Let’s have an HVF_NO_PRIVATE_API define (or the opposite) to have a single toggle to disable all private API use at build time
>
>> +
>> + /* older macOS need to use a private API */
>> + if (!set_ipa_granule) {
>> + set_ipa_granule = dlsym(RTLD_NEXT, "_hv_vm_config_set_ipa_granule");
>> + }
>> + if (set_ipa_granule) {
>> + return set_ipa_granule(config, ipa_granule_size);
>> + } else if (ipa_granule_size != page_size) {
>> + error_report("Failed to find _hv_vm_config_set_ipa_granule");
>> + return HV_UNSUPPORTED;
>> + }
>> +
>> + return HV_SUCCESS;
>> +}
>> +
>> +hv_return_t hvf_arch_vm_create(MachineState *ms, uint32_t pa_range,
>> + uint32_t ipa_granule_size)
>> {
>> hv_return_t ret;
>> hv_vm_config_t config = hv_vm_config_create();
>> @@ -891,6 +932,13 @@ hv_return_t hvf_arch_vm_create(MachineState *ms, uint32_t pa_range)
>> }
>> chosen_ipa_bit_size = pa_range;
>>
> The PA range is actually different depending on the IPA granule size.
>
> An example from M4 Max:
>
> % sysctl -a | grep ipa
> kern.hv.ipa_size_16k: 4398046511104
> kern.hv.ipa_size_4k: 1099511627776
>
Looks like the Apple APIs will always return the smallest supported IPA space size instead of the current one
so this is actually ok - but doesn’t provide access to the bigger IPA space the hardware supports unless explicitly queried via sysctl…
Something else to add as private API use I guess...
>> + if (ipa_granule_size) {
>> + ret = hvf_set_ipa_granule(config, ipa_granule_size);
>> + if (ret != HV_SUCCESS) {
>> + goto cleanup;
>> + }
>> + }
>> +
>> ret = hv_vm_create(config);
>>
>
>
© 2016 - 2026 Red Hat, Inc.