Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

macOS: a VM's phys_footprint counts guest pages written by the VMM twice

Aperta
#947 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 3 giorni

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
35/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
c, macos, rust
Ambito
performance

Direzione di ricerca

Start with the supplied hvf-double.c reproducer and the reported measurements for hv_vm_map() on macOS; compare process phys_footprint with system-wide vm_stat as the guest touches mapped pages. The report says the double charge appears to be macOS accounting behavior and notes no known libkrun workaround. Done looks like confirming whether a mapping avoids the double count, or recording Apple's response; the reporter says they will add that response here.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

While measuring free page reporting (#931, #941) through Colima, I found that krunkit's physical footprint, the number Activity Monitor shows, overstates what a VM costs the host. It isn't a libkrun bug, but it affects how anyone measures libkrun's memory use on macOS, so I'm writing it down here.

What happens: when the VMM writes a page of guest RAM through its own mapping and the guest then uses that page, macOS charges the page to the process twice. The page exists only once.

Reproducer without libkrun (hvf-double.c below, ~80 lines): it maps anonymous memory into a VM with hv_vm_map() and runs a vCPU with a load or store loop over it.

512 MiB of guest RAM phys_footprint
the VMM writes every page +512 MB
the guest writes every page +512 MB
the VMM writes every page, then the guest reads it +1025 MB

It's the same with MAP_SHARED. With 2 GiB, phys_footprint grows by 4098 MB while system-wide used memory (vm_stat) grows by 1948 MB, so only the accounting is doubled.

With libkrun (krunkit through Colima, #941 with --free-page-reporting replace, 4 vCPU, 2 GiB; guest memory in use counted from /proc/kpageflags):

guest memory in use: +602 MiB krunkit phys_footprint
written by the guest (tmpfs) +616 MB
read from the disk through virtio-blk +1180 MB

Both go away when the guest frees the pages.

What it means in practice:

  • Device I/O inflates the footprint: disk reads, received packets, virtio-fs reads, and the kernel and initramfs loaded at boot. On a freshly booted guest here, the footprint is ~210 MB + ~17 MB per vCPU above what the guest can account for, which probably is mostly this.
  • Comparing footprints between configurations that do different amounts of I/O is misleading. System-wide vm_stat (or Memory Pressure) shows the real cost.
  • The free page reporting numbers in #931/#941 aren't affected: those tests write guest memory from inside the guest.

I'm reporting it to Apple through Feedback Assistant and will add the number here. I don't see a way for libkrun to avoid it, but if someone knows of a mapping that isn't counted twice, I'd be happy to test it. I've also opened a short note for krunkit's docs: libkrun/krunkit#123.

Tested on macOS 26.5.2 (25F84), Apple M5.

hvf-double.c
// Is guest RAM that the VMM writes through its own mapping charged twice once
// the guest touches it? No libkrun: map N MiB of anonymous memory into a VM,
// then per mode:
//   vmm-then-guest   the VMM writes every page, then a vCPU reads every page
//   guest-only       a vCPU writes every page
//   vmm-only         the VMM writes every page, the guest never runs
// and print phys_footprint. A 1x charge is N MiB; 2x means the VMM's pages and
// the guest's are not the same pages any more.
//   hvf-double <mode> <mem_mib> [private|shared]
//
// Build: clang -O2 -o hvf-double hvf-double.c -framework Hypervisor
//        codesign --entitlements ent.plist -s - hvf-double
// (ent.plist grants com.apple.security.hypervisor)
#include <Hypervisor/Hypervisor.h>
#include <libproc.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

#define CODE_IPA 0x10000000ULL   // one 16 KiB page of guest code
#define RAM_IPA  0x40000000ULL

static long fp(void) {
  struct rusage_info_v4 ri;
  proc_pid_rusage(getpid(), RUSAGE_INFO_V4, (rusage_info_t *)&ri);
  return (long)(ri.ri_phys_footprint >> 20);
}
#define CHECK(x) do { hv_return_t r = (x); if (r != HV_SUCCESS) { fprintf(stderr, "%s: %d\n", #x, r); exit(1); } } while (0)

// Guest code, MMU off: x0 = start, x1 = end, x2 = 1 to store, 0 to load.
// loop: if (x2) str x3,[x0] else ldr x3,[x0]; x0 += 4096; if x0 < x1 loop; hvc #0
static const uint32_t code[] = {
  0xb4000062, // cbz  x2, +12      -> load
  0xf9000003, // str  x3, [x0]
  0x14000002, // b    +8
  0xf9400003, // ldr  x3, [x0]
  0x91400400, // add  x0, x0, #4096
  0xeb01001f, // cmp  x0, x1
  0x54ffff43, // b.lo -24          -> loop
  0xd4000002, // hvc  #0
};

static void run_guest(uint64_t start, uint64_t end, int store) {
  hv_vcpu_t v; hv_vcpu_exit_t *e;
  CHECK(hv_vcpu_create(&v, &e, NULL));
  CHECK(hv_vcpu_set_reg(v, HV_REG_PC, CODE_IPA));
  CHECK(hv_vcpu_set_reg(v, HV_REG_CPSR, 0x3c5));       // EL1h, interrupts masked
  CHECK(hv_vcpu_set_reg(v, HV_REG_X0, start));
  CHECK(hv_vcpu_set_reg(v, HV_REG_X1, end));
  CHECK(hv_vcpu_set_reg(v, HV_REG_X2, store));
  CHECK(hv_vcpu_set_reg(v, HV_REG_X3, 0x5a5a5a5a5a5a5a5aULL));
  for (;;) {
    CHECK(hv_vcpu_run(v));
    if (e->reason == HV_EXIT_REASON_EXCEPTION && (e->exception.syndrome >> 26) == 0x16) break;  // hvc
    if (e->reason != HV_EXIT_REASON_EXCEPTION) continue;
    fprintf(stderr, "guest exception, syndrome %llx va %llx\n", e->exception.syndrome, e->exception.virtual_address);
    exit(1);
  }
  hv_vcpu_destroy(v);
}

int main(int argc, char **argv) {
  const char *mode = argv[1]; size_t mem = (size_t)atol(argv[2]) << 20;
  int shared = argc > 3 && !strcmp(argv[3], "shared");
  CHECK(hv_vm_create(NULL));
  void *cpage = mmap(NULL, 16384, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
  memcpy(cpage, code, sizeof code);
  CHECK(hv_vm_map(cpage, CODE_IPA, 16384, HV_MEMORY_READ | HV_MEMORY_EXEC));
  uint8_t *ram = mmap(NULL, mem, PROT_READ | PROT_WRITE,
                      (shared ? MAP_SHARED : MAP_PRIVATE) | MAP_ANON | MAP_NORESERVE, -1, 0);
  CHECK(hv_vm_map(ram, RAM_IPA, mem, HV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXEC));
  long base = fp();
  if (!strcmp(mode, "guest-only")) {
    run_guest(RAM_IPA, RAM_IPA + mem, 1);
  } else {
    for (size_t o = 0; o < mem; o += 4096) ram[o] = 0xa5;
    if (!strcmp(mode, "vmm-then-guest")) run_guest(RAM_IPA, RAM_IPA + mem, 0);
  }
  printf("%-15s %-7s %4zu MiB: +%ld MB\n", mode, shared ? "shared" : "private", mem >> 20, fp() - base);
  fflush(stdout);
  // Stay alive so that the system-wide numbers can be read with the memory held.
  if (getenv("HOLD")) pause();
  return 0;
}
Lingua principale
Rust
Stelle
2.8k
Fork
279
Merge medio
5g 12h
PR unite (30g)
27

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di libkrun/libkrun

Tutte le issue di libkrun/libkrun

Issue simili

Altre issue su Rust

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.