macOS + Metal dans une VM Proxmox sur un Mac Mini 2018

Le pitch

J’avais une app iOS à livrer (read@home, en Flutter), et pour ça j’ai acheté un Mac Mini 2018. Sauf qu’une fois la livraison faite, je me retrouve avec du matos plutôt perf qui dort 80 % du temps. Du coup je voulais le rentabiliser en le collant dans mon cluster Proxmox — tout en gardant macOS sous la main avec un GPU accéléré (Xcode, Simulateur iOS, un navigateur correct — pas du rendu software poussif).

Le nerf de la guerre : le Simulateur iOS et l’UI macOS veulent Metal. Sans accélération GPU, l’expérience est misérable. D’où le passthrough de l’iGPU intégrée (Intel UHD 630) vers la VM.

Spoiler : ça marche, macOS tourne en Metal 3, et les crashs qui m’ont bouffé trois jours ne venaient pas du GPU.

Le matos :

  • Mac Mini 2018, Intel i7-8700B (6 cœurs / 12 threads), iGPU UHD 630 (8086:3e9b), puce T2.
  • Cible VM : macOS Sequoia 15.7.5, machine q35, OVMF, OpenCore.

Partie 1 — Proxmox sur un Mac Mini T2

Le Mac Mini 2018 a deux trucs qui pourrissent la vie sous Linux : la puce T2 (secure boot + contrôleur du SSD + SMC + thermals) et plusieurs périphériques pas nativement supportés.

1. Désactiver le Secure Boot du T2. Depuis macOS, via l’Utilitaire de sécurité au démarrage (recovery ⌘R) : passer la sécurité de démarrage sur « Aucune » et autoriser le boot depuis un média externe. Sans ça, impossible de booter un OS non signé Apple.

2. Installer Proxmox direct sur le NVMe interne. Bonne surprise : le SSD du Mac Mini est piloté via apple-nvme, mainline dans les kernels récents (celui de Proxmox 8/9 le voit). Donc pas besoin d’ISO custom ni de SSD externe, l’installeur Proxmox standard détecte le disque interne. Version utilisée : Proxmox VE 9.2.2 (pve-manager/9.2.2), pas de nomodeset ni autre param de boot à passer au premier démarrage.

3. Le kernel T2 pour les ventilos et le thermal. Sur le kernel stock, la gestion ventilateurs / thermals du Mac Mini est absente → la machine chauffe et les ventilos réagissent pas. Solution : le kernel patché T2 d’AdityaGarg8 (pve-edge-kernel-t2), qui apporte le support SMC / thermal. Version installée : Linux 7.0.12-1-pve-t2 (build 2026-06-09T21:07Z).

# dpkg -i proxmox-kernel-7.0.12-1-pve-t2_*_amd64.deb
# proxmox-boot-tool kernel pin 7.0.12-1-pve-t2
# reboot

Réseau : le Wi-Fi / Bluetooth du T2 marchent pas proprement sous Linux. Câble Ethernet, l’hôte reste connecté par le port filaire, et on en parle plus.

4. Vérifier l’IOMMU (indispensable pour le passthrough plus tard) :

dmesg | grep -e DMAR -e IOMMU   # doit montrer l'init IOMMU / VT-d

Sur Proxmox 9 l’IOMMU est déjà active out of the box (rien à ajouter dans GRUB), la commande sort bien l’init VT-d directement après l’install.

Partie 2 — La VM macOS

Pour créer la VM (OpenCore + récup de macOS), j’ai utilisé OSX-PROXMOX (luchina-gabriel/OSX-PROXMOX), qui automatise l’EFI OpenCore, la récup de l’installeur et la config QEMU.

J’ai cloné et édité le script plutôt que de le lancer tel quel, parce que sa version « clé en main » réécrit des trucs côté hôte que je voulais pas toucher (ligne de commande GRUB, dépôts apt, blacklist GPU, reboot auto…). Mes deux modifs clés :

  • neutraliser la partie qui reconfigure l’hôte (garder juste la copie de l’EFI + les options modprobe KVM pour macOS type ignore_msrs) ;
  • changer le SMBIOS par défaut de iMacPro1,1 vers Macmini8,1 — le vrai modèle, cohérent avec le hardware.

Config VM de base obtenue (qm config <VMID>) :

bios: ovmf
machine: q35
vga: none              # pas d'affichage virtuel : l'iGPU s'en chargera
cores: 4               # voir Partie 4 — surtout PAS 8
sockets: 1
memory: 8192
smbios1: ...           # SMBIOS Macmini8,1, board-id Mac-7BA5B2DFE22DDD8C

OpenCore embarque Lilu + WhateverGreen + VirtualSMC. À ce stade, macOS boote et s’accède en SSH + Screen Sharing (pas d’écran physique).

Partie 3 — Le passthrough iGPU (le vrai boss)

C’est ici que ça se corse. Objectif : l’UHD 630 accessible dans macOS avec Metal.

Legacy vs UPT : le choix qui conditionne tout

Deux modes de passthrough Intel IGD :

  • Legacy (legacy-igd=1, x-igd-gms…) : le plus complet… mais exige i440fx. Or macOS veut q35. → Exclu.
  • UPT (Universal Passthrough) : sur Coffee Lake, il marche uniquement en q35. → C’est notre voie.

Le piège qui m’a coûté une heure : pcie.0 vs pci.0

En UPT, l’iGPU doit se retrouver à l’adresse guest 00:02.0 (la position réelle d’une IGD), sinon QEMU lui configure pas l’OpRegion (la table VBT dont le driver a besoin) et le framebuffer part en vrille.

Ma première tentative la plaçait derrière un pcie-root-port (via pcie=1) → adresse guest bidon, pas d’OpRegion. Puis j’ai forcé bus=pci.0… et elle a atterri à 06:02.0, derrière deux bridges legacy. Parce que sur q35, pci.0 = le bridge PCI legacy, pas la racine. La racine PCIe, c’est pcie.0.

La config qui marche :

qm stop <VMID>

# hostpci SANS pcie=1 ni x-vga=1 : on place l'iGPU manuellement via les args
qm set <VMID> --hostpci0 0000:00:02.0,rombar=0

# args macOS existants + placement 00:02.0 + OpRegion
#   ⚠️  bus=pcie.0  (surtout PAS pci.0)
qm set <VMID> --args '<... args macOS existants: applesmc/OSK, -cpu host …+invtsc, globals ...> -set device.hostpci0.bus=pcie.0 -set device.hostpci0.addr=2.0 -set device.hostpci0.x-igd-opregion=on'

qm start <VMID>

Pas de ROM / GOP en UPT → écran noir pendant le boot, c’est normal : la sortie iGPU s’active qu’une fois les drivers macOS chargés ; Screen Sharing se reconnecte ensuite.

Vérifier que c’est bon

Avec gfxutil (release 1.84b), dans macOS :

./gfxutil | grep -i 8086
# ✅ attendu : 8086:3e9b ... = PciRoot(0x0)/Pci(0x2,0x0)
# ❌ mauvais : ...Pci(0x1E,0x0)/Pci(0x1,0x0)/Pci(0x2,0x0)  (derrière des bridges → pci.0)

Puis :

system_profiler SPDisplaysDataType

Le Graal :

Intel UHD Graphics 630
  Bus: Built-In
  VRAM (Dynamic, Max): 1536 MB
  Device ID: 0x3e92          # fake auto de WhateverGreen, normal
  Metal Support: Metal 3

Avant le fix, ce même champ affichait Intel HD Graphics CFL CRB (framebuffer générique, VRAM non définie). 1536 MB + Built-In + Metal 3 = iGPU correctement câblée — exactement ce que reporte un vrai Mac Mini 2018.

Partie 4 — Le twist : les crashs ne venaient PAS du GPU

Avec l’iGPU enfin en Metal 3, j’ai stressé la VM (navigateur + WebGL lourd)… et kernel panic, toute la VM par terre. J’ai passé un temps fou à incriminer le framebuffer, la stolen memory, l’ig-platform-id.

Le déclic est venu d’un panic sur un process non-GPU (Spotlight), avec la même signature qu’un crash « GPU ». Le symbole qui traîne dans toutes les backtraces : _lck_spinlock_timeout…. C’était pas le GPU. C’était le panic de timeout de lock, un classique des VM macOS sur hôte chargé (bien documenté par Nick Sherlock) :

macOS en VM, quand il flushe le TLB, signale tous les cœurs et attend une réponse dans un délai. Si l’hôte est trop chargé pour scheduler les vCPU à temps → deadline ratée → panic, sans aucune vraie faute matérielle. D’où des crashs « all over the map » sur n’importe quel process qui meurt au mauvais moment.

Le fix

macOS gonfle déjà ses timeouts ×2⁶ = 64 quand il se sait virtualisé (paramètre vti). Sur hôte chargé, on pousse le curseur. Dans le config.plist OpenCore, section NVRAM > Add > 7C436110-AB2A-4BBB-A880-FE41995C9F82 > boot-args :

keepsyms=1 debug=0x100 tlbto_us=0 vti=9

(vti=9 → ×2⁹ = 512 de marge ; tlbto_us=0 → désactive le timeout TLB.)

Vérif après boot :

log show --predicate "processID == 0" --start $(date "+%Y-%m-%d") --debug | grep "Timeouts adjusted"
# attendu : Timeouts adjusted for virtualization (<<9)

Et surtout, s’attaquer à la vraie cause (la contention) : j’avais donné 8 vCPU à la VM sur un CPU 6 cœurs. Avec le passthrough + le reste du homelab, ça se marchait dessus.

qm set <VMID> --cores 4         # 4 vCPU, pas 8
qm set <VMID> --cpuunits 10000  # priorité scheduler pour la VM

Depuis : stable sous charge, Metal actif. Les deux problèmes (framebuffer et stabilité) étaient distincts — le premier bien réel, le second n’ayant jamais eu de rapport avec le GPU.

Ce qui marche… et ce qui ne marchera pas

✅ Marche : iGPU en Metal 3, Xcode, Simulateur iOS, navigateur accéléré, et tout le pipeline de dev iOS (build, signature, upload App Store Connect via clé API, CI/CD) — sans jamais avoir besoin de se connecter à un compte Apple dans la VM.

❌ Ne marche pas (et c’est un mur voulu par Apple) : se connecter à l’App Store / iCloud depuis la VM. Sous Sequoia, macOS détecte la virtualisation (AppleVirtualPlatform) et exige une attestation matérielle (DeviceCheck BAA) ancrée dans le T2, que la VM peut pas produire. Résultat : Verification Failed, cascade -10000 → -8008 → -7018 dans les logs akd. Aucun bidouillage SMBIOS n’y change rien — l’identité injectée était parfaitement cohérente, c’est pas le sujet.

À noter aussi : ne surtout pas downgrader en Sonoma pour récupérer l’App Store. Sonoma plafonne à Xcode 16 / SDK iOS 18, or depuis le 28 avril 2026 l’App Store Connect exige Xcode 26 + SDK iOS 26. Tu perdrais la capacité de livrer ton app. Sequoia 15.7.5 fait justement tourner Xcode 26 (minimum requis : macOS 15.6). Reste dessus.

Les leçons, en une liste

  1. q35 impose UPT pour l’IGD Intel — le mode legacy (i440fx) est incompatible macOS.
  2. bus=pcie.0, pas pci.0 — sinon l’iGPU finit derrière des bridges, sans OpRegion.
  3. x-igd-opregion=on + adresse 00:02.0 = la combinaison qui donne une VRAM correcte et Metal stable.
  4. Un kernel panic sous charge GPU ≠ un bug GPU. Signature _lck_spinlock_timeout + crashs sur des process random = timeout de lock VM / hôte → tlbto_us=0 vti=9 et moins de vCPU.
  5. N’oversubscribe pas les vCPU : 8 vCPU sur 6 cœurs, c’est la contention assurée.
  6. App Store / iCloud = mur d’attestation sous Sequoia en VM. Le dev iOS, lui, en a pas besoin (clés API + certifs importés).

Setup : Mac Mini 2018 (i7-8700B, UHD 630, T2) · Proxmox · macOS Sequoia 15.7.5 · OpenCore + WhateverGreen · passthrough iGPU UPT.


← Tous les articles Accueil
djalim.fr
--:--