I noticed an issue only on MacOS where a VM seemed to be taking up ~2x the max allocated RAM. I had Astra evaluate and this is what it found. I had it create a repro as well (attached)
Summary
On Apple Silicon, guest RAM allocated with ordinary anonymous mmap and then
accessed through both host and HVF guest mappings is charged approximately twice
in TASK_VM_INFO.phys_footprint. Using hv_vm_allocate for the same memory
removes the extra footprint charge.
The standalone C repro below allocates 256 MiB and executes a seven-instruction ARM64 guest. It needs only the Apple SDK and Hypervisor.framework; no guest OS, virtual disk, filesystem image, VMM library, or network is involved.
Connection to libkrun
Source inspection found this allocation path in both:
Both use GuestMemoryMmap::from_ranges for this path. The corresponding
vm-memory 0.17.1
and 0.18.0
Unix allocators use MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE.
The macOS memory initialization
passes these host addresses to the HVF mapping wrapper.
The repro isolates that allocation/mapping sequence. The upstream revisions above were inspected, not built or booted as part of this standalone test.
Environment
- Apple M5 Max, arm64; 128 GiB host RAM; 16 KiB host pages.
- macOS 26.6.2 (25G83).
- Apple clang 21.0.0 (clang-2100.3.34.2), macOS SDK 27.0.
- Built with a macOS 12.1 deployment target and ad-hoc Hypervisor entitlement.
- Only this macOS/hardware combination was executed for this repro.
Reproduce
Save the C source and entitlement below as hvf-memory.c and
hypervisor.entitlements, then run on Apple Silicon with macOS 12.1 or later:
xcrun clang -std=c11 -Wall -Wextra -Werror -O2 -mmacosx-version-min=12.1 \
-framework Hypervisor hvf-memory.c -o hvf-memory
codesign --force --sign - --entitlements hypervisor.entitlements hvf-memory
./hvf-memory mmap
./hvf-memory hvEach run allocates 256 MiB plus one code page, has a 60-second watchdog, verifies host/guest coherence twice, and explicitly destroys/unmaps/deallocates its resources before exit. No administrator privileges are required.
hvf-memory.c — complete standalone source
// SPDX-License-Identifier: Apache-2.0
// Standalone Apple Silicon HVF accounting repro; no guest OS or VMM dependency.
#include <Hypervisor/Hypervisor.h>
#include <Hypervisor/hv_vm_allocate.h>
#include <inttypes.h>
#include <mach/mach.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#if !defined(__APPLE__) || !defined(__aarch64__)
#error "This repro requires macOS on Apple Silicon"
#endif
#define MIB (1024ULL * 1024ULL)
#define RAM_SIZE (256ULL * MIB)
static uint64_t baseline;
static void check(hv_return_t result, const char *operation) {
if (result != HV_SUCCESS) {
fprintf(stderr, "%s failed: 0x%x\n", operation, (unsigned)result);
exit(1);
}
}
static uint64_t sample(const char *phase) {
task_vm_info_data_t info = {0};
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO,
(task_info_t)&info, &count);
if (result != KERN_SUCCESS) {
fprintf(stderr, "task_info: %s\n", mach_error_string(result));
exit(1);
}
if (phase != NULL) {
printf("%-18s %14" PRIu64 " %12.2f %12.2f\n", phase,
(uint64_t)info.phys_footprint,
((double)info.phys_footprint - (double)baseline) / MIB,
(double)info.resident_size / MIB);
fflush(stdout);
}
return info.phys_footprint;
}
static void run_guest(hv_vcpu_t cpu, hv_vcpu_exit_t *exit_info,
hv_ipa_t code_gpa, hv_ipa_t data_gpa, size_t page_size) {
check(hv_vcpu_set_reg(cpu, HV_REG_PC, code_gpa), "set PC");
check(hv_vcpu_set_reg(cpu, HV_REG_X0, data_gpa), "set X0");
check(hv_vcpu_set_reg(cpu, HV_REG_X1, page_size), "set X1");
check(hv_vcpu_set_reg(cpu, HV_REG_X2, RAM_SIZE / page_size), "set X2");
check(hv_vcpu_run(cpu), "hv_vcpu_run");
// HVC #0 from AArch64 has exception class 0x16.
if (exit_info->reason != HV_EXIT_REASON_EXCEPTION ||
(exit_info->exception.syndrome >> 26) != 0x16) {
fprintf(stderr, "Unexpected guest exit: reason=%u syndrome=0x%" PRIx64 "\n",
(unsigned)exit_info->reason,
(uint64_t)exit_info->exception.syndrome);
exit(1);
}
}
int main(int argc, char **argv) {
if (argc != 2 || (strcmp(argv[1], "mmap") && strcmp(argv[1], "hv"))) {
fprintf(stderr, "Usage: %s mmap|hv\n", argv[0]);
return 2;
}
alarm(60); // Bound the process lifetime even if guest execution misbehaves.
const bool use_hv = strcmp(argv[1], "hv") == 0;
const size_t page = (size_t)getpagesize();
const size_t size = (size_t)RAM_SIZE + page; // One extra page holds guest code.
const hv_ipa_t gpa = 0x40000000;
void *allocation = NULL;
printf("\nallocator=%s data_mib=256 code_bytes=%zu\n", argv[1], page);
printf("%-18s %14s %12s %12s\n", "phase", "footprint_B", "delta_MiB", "resident_MiB");
baseline = sample(NULL);
sample("baseline");
if (use_hv) {
check(hv_vm_allocate(&allocation, size, HV_ALLOCATE_DEFAULT), "hv_vm_allocate");
} else {
allocation = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0);
if (allocation == MAP_FAILED) {
perror("mmap");
return 1;
}
}
const uint32_t code[] = {
0xf9400003, // ldr x3, [x0]
0x91000463, // add x3, x3, #1
0xf9000003, // str x3, [x0]
0x8b010000, // add x0, x0, x1
0xf1000442, // subs x2, x2, #1
0x54ffff61, // b.ne back to ldr
0xd4000002, // hvc #0
};
memcpy(allocation, code, sizeof(code));
uint8_t *data = (uint8_t *)allocation + page;
for (size_t offset = 0; offset < RAM_SIZE; offset += page)
*(volatile uint64_t *)(data + offset) = 41;
sample("host-touched");
check(hv_vm_create(NULL), "hv_vm_create");
check(hv_vm_map(allocation, gpa, size,
HV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXEC), "hv_vm_map");
sample("mapped-only");
hv_vcpu_t cpu;
hv_vcpu_exit_t *exit_info;
check(hv_vcpu_create(&cpu, &exit_info, NULL), "hv_vcpu_create");
check(hv_vcpu_set_reg(cpu, HV_REG_CPSR, 0x3c5), "set CPSR");
run_guest(cpu, exit_info, gpa, gpa + page, page);
sample("guest-touched");
// Confirm coherence, then repeat access to the same pages in both directions.
for (size_t offset = 0; offset < RAM_SIZE; offset += page) {
if (*(volatile uint64_t *)(data + offset) != 42) {
fprintf(stderr, "Guest/host mismatch at byte %zu\n", offset);
return 1;
}
*(volatile uint64_t *)(data + offset) = 43;
}
run_guest(cpu, exit_info, gpa, gpa + page, page);
for (size_t offset = 0; offset < RAM_SIZE; offset += page) {
if (*(volatile uint64_t *)(data + offset) != 44) {
fprintf(stderr, "Second guest/host mismatch at byte %zu\n", offset);
return 1;
}
}
sample("guest-retouched");
check(hv_vcpu_destroy(cpu), "hv_vcpu_destroy");
check(hv_vm_unmap(gpa, size), "hv_vm_unmap");
sample("guest-unmapped");
check(hv_vm_destroy(), "hv_vm_destroy");
if (use_hv) {
check(hv_vm_deallocate(allocation, size), "hv_vm_deallocate");
} else if (munmap(allocation, size) != 0) {
perror("munmap");
return 1;
}
sample("released");
puts("Guest/host coherence and cleanup: PASS");
return 0;
}hypervisor.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.hypervisor</key>
<true/>
</dict>
</plist>Observed result
Footprint increase over the pre-allocation baseline, in MiB:
| Phase | Anonymous mmap | hv_vm_allocate |
|---|---|---|
| Host touches every page | 256.17 | 256.17 |
| Guest mapping registered, vCPU not run | 256.27 | 256.27 |
| vCPU touches every page | 512.58 | 256.56 |
| Same pages accessed a second time | 512.58 | 256.56 |
| Guest mapping removed; host allocation retained | 256.38 | 256.38 |
| Host allocation released | 0.19 | 0.19 |
Both runs report Guest/host coherence and cleanup: PASS.
The total phys_footprint after guest access was 539,034,488 bytes with mmap
and 270,582,648 bytes with the HVF allocator. Small overheads vary between runs.
The additional charge appears when the vCPU actually accesses the pages;
calling hv_vm_map alone is insufficient. Removing the guest mapping reverses
that additional charge while the host allocation remains valid. Repeated access
does not add a third charge.
The raw resident_size counter was approximately 518 MiB in both modes.
That counter can count aliases and should not be used alone to assess this fix
or infer a second physical copy of guest RAM. This report concerns inflated
process footprint, not evidence that the guest can address more RAM than configured.
Expected behavior / possible fix
Guest RAM should contribute approximately its resident allocation size to process footprint, plus normal VMM overhead, after both host and guest access.
Apple's SDK header for
hv_vm_allocate
states: “This API enables accurate memory accounting of the allocations it creates”.
Changing the repro's allocator/deallocator pair to
hv_vm_allocate/hv_vm_deallocate is sufficient to remove the duplicate charge.
Please consider using that allocation path for anonymous macOS guest RAM. Integration needs to retain ownership until all guest-memory users and HVF mappings are finished, use the matching deallocator, and preserve accounting through free-page reporting/remapping. File-backed DAX/device windows need their existing separate treatment. This repro establishes the allocator behavior; it is not a proposed wholesale replacement of libkrun's memory-management code.
Activity
0No activity yet.