From 22daf7720ceef0199bd203148ff00b85f4cf4095 Mon Sep 17 00:00:00 2001 From: Git Sagar Date: Wed, 9 Sep 2026 12:33:59 -0300 Subject: [PATCH] macOS: current Lilu/VirtualSMC/WhateverGreen/RestrictEvents, no isa-applesmc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OSX-KVM ESP ships Lilu 1.6.8 / VirtualSMC 1.3.3 / WhateverGreen 1.6.7, which disable themselves on macOS 26; Apple's SMC driver then runs on QEMU's isa-applesmc stub and the restart path panics (SMCWDT smcWriteKey kSMCBadCommand → nested panic after MACH Reboot). Overlay pinned current releases (upstream.json opencore.kexts) and drop isa-applesmc: with the stub present VirtualSMC steps aside ("multiple devices present"); alone it carries the OSK and reboots work (PE restart test: 10 s, clean). RestrictEvents with revpatch=memtab silences MacPro7,1's "Memory Modules Misconfigured". - makeOpenCore: kext overlay + Kernel.Add entries for overlaid kexts, --memory-mb (4 DIMMs), bootArgs default revpatch=memtab - qemu.nix: deviceArgsFor { appleSmc } (default false); cli: --applesmc for images built before this change - vm-driver: reboot-death detection (reset 60 s after a guest reboot request that never comes back), panics wait for XNU's own auto-reboot, debug dir works across nixbld users - generalize: QEMU USB keyboard declared ANSI (no Keyboard Setup Assistant) - README: architecture, reliability handling, debugging Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01XsESshRCoBoUVWV9qKURUF --- cli.nix | 5 ++++- lib/images/macos/README.md | 11 ++++++++++ lib/images/macos/helpers/makeImage.nix | 2 +- lib/images/macos/helpers/makeOpenCore.nix | 20 ++++++++++++++++-- lib/images/macos/helpers/oc-config.py | 13 ++++++++++++ lib/images/macos/helpers/qemu.nix | 14 +++++++++---- lib/images/macos/helpers/vm-driver.py | 25 ++++++++++++++++------- lib/images/macos/upstream.json | 22 ++++++++++++++++++++ 8 files changed, 97 insertions(+), 15 deletions(-) diff --git a/cli.nix b/cli.nix index e220dbb..82882d0 100644 --- a/cli.nix +++ b/cli.nix @@ -33,7 +33,8 @@ pkgs.writeShellScriptBin "vmix" '' echo " --to-remote-disk SSH:DEV Stream to remote disk via SSH and expand partitions" echo " e.g. root@10.10.10.100:/dev/sda" echo " --ahci Use AHCI storage for vmix run (for laptop images)" - echo " --macos macOS image for vmix run (OpenCore/AppleSMC flags, AHCI)" + echo " --macos macOS image for vmix run (OpenCore/VirtualSMC flags, AHCI)" + echo " --applesmc with --macos: add QEMU's isa-applesmc (images built before 2026-09-09)" echo " --vnc DISPLAY VNC instead of SDL for vmix run, e.g. :10 (port 5910) or 0.0.0.0:10" echo " --mac ADDR NIC MAC for vmix run (macOS: read from the image's ESP by default)" echo " -y, --yes Skip disk write confirmation" @@ -100,6 +101,7 @@ pkgs.writeShellScriptBin "vmix" '' --smp) RUN_SMP="$2"; shift 2 ;; --ahci) RUN_AHCI=true; shift ;; --macos) RUN_MACOS=true; shift ;; + --applesmc) RUN_APPLESMC=true; shift ;; --vnc) RUN_VNC="$2"; shift 2 ;; --mac) RUN_MAC="$2"; shift 2 ;; *) echo "Unknown option: $1"; exit 1 ;; @@ -138,6 +140,7 @@ pkgs.writeShellScriptBin "vmix" '' exec ${pkgs.qemu}/bin/qemu-system-x86_64 \ $VMIX_DISPLAY \ ${macosQemu.deviceArgs} ${macosQemu.vgaArgs} \ + $([[ "$RUN_APPLESMC" == true ]] && echo '-device isa-applesmc,osk="${macosQemu.osk}"') \ -accel kvm \ -machine type=q35 \ -cpu ${macosQemu.defaultCpu} \ diff --git a/lib/images/macos/README.md b/lib/images/macos/README.md index db8f90b..6525f20 100644 --- a/lib/images/macos/README.md +++ b/lib/images/macos/README.md @@ -98,3 +98,14 @@ AppleSMC with the OSK, XHCI keyboard/tablet, AHCI disks, VMware SVGA, virtio-net pinned to `PciRoot(0x0)/Pci(0x12,0x0)` so OpenCore marks it built-in (en0, required for Apple ID / iMessage). SMBIOS `MacPro7,1` with four DIMMs described (avoids the "Memory Modules Misconfigured" warning). + +OpenCore comes from OSX-KVM's proven ESP, with Lilu / VirtualSMC / +WhateverGreen replaced by current releases (`upstream.json` → `opencore.kexts`): +the versions OSX-KVM ships disable themselves on macOS 26, and without +VirtualSMC the guest's restart path panics on QEMU's SMC stub +(`SMCWDT smcWriteKey kSMCBadCommand`, nested panic after `MACH Reboot`). +For the same reason QEMU's `isa-applesmc` is not used any more: its presence +makes VirtualSMC step aside ("multiple devices present"); VirtualSMC carries +the OSK itself. Images built before this change still need the stub: +`vmix run --macos --applesmc`. RestrictEvents (`revpatch=memtab`) silences +MacPro7,1's "Memory Modules Misconfigured" at login. diff --git a/lib/images/macos/helpers/makeImage.nix b/lib/images/macos/helpers/makeImage.nix index 0d64426..70ff78c 100644 --- a/lib/images/macos/helpers/makeImage.nix +++ b/lib/images/macos/helpers/makeImage.nix @@ -20,7 +20,7 @@ cpu ? qemu.defaultCpu, model ? "MacPro7,1", # SMBIOS model; must be supported by the installed macOS seed ? name, # MAC address + SystemUUID are derived from this - bootArgs ? "keepsyms=1", + bootArgs ? "keepsyms=1 revpatch=memtab", vncDisplay ? null, # e.g. ":10" to watch the install on port 5910 timeout ? 4 * 3600, # seconds for the whole install extraOpenCoreConfig ? {}, # merged into config.plist diff --git a/lib/images/macos/helpers/makeOpenCore.nix b/lib/images/macos/helpers/makeOpenCore.nix index 5ae1475..59e777d 100644 --- a/lib/images/macos/helpers/makeOpenCore.nix +++ b/lib/images/macos/helpers/makeOpenCore.nix @@ -13,7 +13,7 @@ uuid, serial ? null, mlb ? null, - bootArgs ? "keepsyms=1", + bootArgs ? "keepsyms=1 revpatch=memtab", resolution ? "1024x768", showPicker ? true, pickerTimeout ? 2, @@ -22,6 +22,12 @@ }: let ocImage = pkgs.fetchurl { inherit (upstream.opencore.image) url sha256; name = "OSX-KVM-OpenCore.qcow2"; }; + # OSX-KVM's ESP ships Lilu 1.6.8 / VirtualSMC 1.3.3 / WhateverGreen 1.6.7, which + # disable themselves on macOS 26 ("unsupported operating system"); without + # VirtualSMC, macOS' restart path panics on QEMU's SMC stub. Overlay the + # current releases (upstream.opencore.kexts, pinned). + kextZips = lib.mapAttrsToList (n: k: { name = n; zip = pkgs.fetchurl { inherit (k) url hash; }; }) + (upstream.opencore.kexts or {}); in pkgs.runCommand "${name}-esp" { nativeBuildInputs = with pkgs; [ _7zz python3 mtools dosfstools gptfdisk jq macserial ]; @@ -45,13 +51,23 @@ pkgs.runCommand "${name}-esp" { mkdir -p $out/EFI/vmix cp -r esp/EFI/BOOT esp/EFI/OC $out/EFI/ chmod -R u+w $out/EFI + ${lib.concatMapStringsSep "\n" (k: '' + echo "=== vmix: updating ${k.name}.kext from ${k.zip.name} ===" + rm -rf kext-${k.name}; mkdir kext-${k.name} + ${pkgs.unzip}/bin/unzip -q -o ${k.zip} -d kext-${k.name} + K=$(find kext-${k.name} -type d -name "${k.name}.kext" | head -1) + [ -n "$K" ] || { echo "${k.name}.kext not found in ${k.zip.name}"; exit 1; } + rm -rf "$out/EFI/OC/Kexts/${k.name}.kext" + cp -r "$K" "$out/EFI/OC/Kexts/${k.name}.kext" + grep -A1 CFBundleVersion "$out/EFI/OC/Kexts/${k.name}.kext/Contents/Info.plist" | tail -1 + '') kextZips} # hide OpenCore's own launcher from its picker (otherwise it is the default entry and loops) echo -n Disabled > $out/EFI/BOOT/.contentVisibility python3 ${./oc-config.py} --base esp/EFI/OC/config.plist --esp esp --out $out/EFI/OC/config.plist \ --model "${model}" --serial "$SERIAL" --mlb "$MLB" --uuid "${uuid}" --mac "${mac}" \ --nic-path "${qemu.nicDevicePath}" --boot-args "${bootArgs}" --resolution "${resolution}" \ --show-picker "${lib.boolToString showPicker}" --timeout ${toString pickerTimeout} \ - --extra-json ${lib.escapeShellArg (builtins.toJSON extraConfig)} --memory-mb ${toString memSize} + --extra-json ${lib.escapeShellArg (builtins.toJSON extraConfig)} --memory-mb ${toString memSize} --add-kexts ${lib.concatStringsSep "," (map (k: k.name) kextZips)} ocvalidate $out/EFI/OC/config.plist || echo "vmix: ocvalidate reported issues (OpenCore version may differ from validator), continuing" jq -n --arg model "${model}" --arg serial "$SERIAL" --arg mlb "$MLB" --arg uuid "${uuid}" --arg mac "${mac}" \ diff --git a/lib/images/macos/helpers/oc-config.py b/lib/images/macos/helpers/oc-config.py index 307ce87..fc5b50d 100644 --- a/lib/images/macos/helpers/oc-config.py +++ b/lib/images/macos/helpers/oc-config.py @@ -44,6 +44,7 @@ def main(): p.add_argument('--timeout', type=int, default=2) p.add_argument('--extra-json', default='{}') p.add_argument('--memory-mb', type=int, default=8192, help='VM RAM, described as 4 DIMMs') + p.add_argument('--add-kexts', default='', help='comma-separated kext names (without .kext) that need a Kernel.Add entry') a = p.parse_args() with open(a.base, 'rb') as f: @@ -85,6 +86,18 @@ def main(): cfg['Misc']['Boot']['Timeout'] = a.timeout cfg['Misc']['Boot']['HideAuxiliary'] = True cfg['Misc']['Security']['ScanPolicy'] = 0 + # kexts overlaid into the ESP that the OSX-KVM config does not list yet + # (RestrictEvents: silences MacPro7,1's "Memory Modules Misconfigured", revpatch=memtab) + listed = {k['BundlePath'] for k in cfg['Kernel']['Add']} + for kext in [k + '.kext' for k in a.add_kexts.split(',') if k]: + if kext not in listed: + name = kext[:-5] + cfg['Kernel']['Add'].append({ + 'Arch': 'Any', 'BundlePath': kext, 'Comment': f'{name} (vmix)', 'Enabled': True, + 'ExecutablePath': f'Contents/MacOS/{name}', 'MaxKernel': '', 'MinKernel': '', + 'PlistPath': 'Contents/Info.plist'}) + print('Kernel.Add +', kext) + # MacPro7,1 firmware expects DIMMs in pairs (>= 4); with QEMU's single SMBIOS # module macOS shows "Memory Modules Misconfigured" at every login. Describe # the VM's RAM as four DDR4 modules instead. diff --git a/lib/images/macos/helpers/qemu.nix b/lib/images/macos/helpers/qemu.nix index 86a48d3..60ebb4d 100644 --- a/lib/images/macos/helpers/qemu.nix +++ b/lib/images/macos/helpers/qemu.nix @@ -17,14 +17,20 @@ rec { netArgs = { mac, netdev ? "user,id=net0", extra ? "" }: "-netdev ${netdev} -device virtio-net-pci,netdev=net0,mac=${mac},bus=pcie.0,addr=${nicAddr}${extra}"; - # Devices macOS needs (no accel, disks, display adapter or display server here) - deviceArgs = ''-device isa-applesmc,osk="${osk}" -smbios type=2 -device qemu-xhci,id=xhci -device usb-kbd,bus=xhci.0 -device usb-tablet,bus=xhci.0 -device usb-ehci,id=ehci -device ich9-intel-hda -device hda-duplex -device ich9-ahci,id=sata -global ICH9-LPC.disable_s3=1''; + # Devices macOS needs (no accel, disks, display adapter or display server here). + # No isa-applesmc: QEMU's stub only answers the OSK keys, and its presence makes + # VirtualSMC (which carries the OSK itself) step aside, leaving Apple's SMC + # driver on the stub — whose missing watchdog keys panic the restart path on + # macOS 26. VirtualSMC alone is the standard Hackintosh setup. + deviceArgsFor = { appleSmc ? false }: + ''${lib.optionalString appleSmc ''-device isa-applesmc,osk="${osk}" ''}-smbios type=2 -device qemu-xhci,id=xhci -device usb-kbd,bus=xhci.0 -device usb-tablet,bus=xhci.0 -device usb-ehci,id=ehci -device ich9-intel-hda -device hda-duplex -device ich9-ahci,id=sata -global ICH9-LPC.disable_s3=1''; + deviceArgs = deviceArgsFor { }; # macOS has no QXL/virtio-gpu driver; VMware SVGA gives a plain framebuffer vgaArgs = "-vga vmware"; - machineArgs = { cpu ? defaultCpu, smp ? 4, memSize ? 4096 }: - "-accel kvm -machine type=q35 -cpu ${cpu} -smp ${toString smp},sockets=1,cores=${toString smp},threads=1 -m ${toString memSize} ${deviceArgs} ${vgaArgs}"; + machineArgs = { cpu ? defaultCpu, smp ? 4, memSize ? 4096, appleSmc ? false }: + "-accel kvm -machine type=q35 -cpu ${cpu} -smp ${toString smp},sockets=1,cores=${toString smp},threads=1 -m ${toString memSize} ${deviceArgsFor { inherit appleSmc; }} ${vgaArgs}"; # SATA disk on a given port. Store files are read-only: callers create qcow2 overlays. sataDrive = { id, port, file, format ? "qcow2", extra ? "" }: diff --git a/lib/images/macos/helpers/vm-driver.py b/lib/images/macos/helpers/vm-driver.py index f27ba33..21d207f 100644 --- a/lib/images/macos/helpers/vm-driver.py +++ b/lib/images/macos/helpers/vm-driver.py @@ -25,6 +25,7 @@ import shutil import socket import subprocess import sys +import tempfile import time try: @@ -188,17 +189,25 @@ class Serial: def prepare_debug_dir(path): - os.makedirs(path, exist_ok=True) + """Create the debug dir; builds run as different nixbld users, so the parent + is made world-writable and a temp dir is used if the path is not writable.""" + parent = os.path.dirname(path) try: + if not os.path.isdir(parent): + os.makedirs(parent, exist_ok=True) + os.chmod(parent, 0o777) + os.makedirs(path, exist_ok=True) os.chmod(path, 0o777) except OSError: - pass + path = tempfile.mkdtemp(prefix=os.path.basename(path) + '-', dir='/tmp') + os.chmod(path, 0o777) for f in os.listdir(path): if f.endswith(('.png', '.ppm', '.log')) or f.startswith('.grab-') or f == 'qmp.sock': try: os.remove(os.path.join(path, f)) except OSError: pass + return path def launch(qemu_args, qmp_sock, log): @@ -270,9 +279,9 @@ def drive(args, proc, qmp, screen, serial, log): log(f'kernel panic #{panics}, giving up') proc.kill() return 3 - log(f'kernel panic #{panics}, system_reset') - qmp.system_reset() - screen.stable_since = time.time() + # XNU reboots by itself after a panic; only reset if no kernel comes back + log(f'kernel panic #{panics}, waiting for the guest to reboot') + serial.reboot_at = now continue # The guest asked for a reboot but no kernel came back: macOS' restart @@ -351,7 +360,9 @@ def drive(args, proc, qmp, screen, serial, log): log('QEMU exited after powerdown') return 0 time.sleep(1) - log('guest ignored powerdown, killing QEMU') + # macOS ignores the power button at the Setup Assistant; the + # volumes are journaled (APFS) and the PE mounts them cleanly next + log('guest ignores the ACPI power button here (Setup Assistant); stopping QEMU') proc.kill() return 0 continue @@ -395,7 +406,7 @@ def main(): p.error('QEMU command line required after --') debug_dir = args.debug_dir or f'/tmp/vmix-macos/{args.name}' - prepare_debug_dir(debug_dir) + debug_dir = prepare_debug_dir(debug_dir) log = Log(os.path.join(debug_dir, 'driver.log')) log(f'mode={args.mode} debug-dir={debug_dir} serial={args.serial_log}') diff --git a/lib/images/macos/upstream.json b/lib/images/macos/upstream.json index 7730b94..afb5f32 100644 --- a/lib/images/macos/upstream.json +++ b/lib/images/macos/upstream.json @@ -28,6 +28,28 @@ "fetchRecoveryScript": { "url": "https://raw.githubusercontent.com/kholia/OSX-KVM/4c378a4b5e0b219783683012bec680325eb40719/fetch-macOS-v2.py", "sha256": "39ac6d26bd265f5d32198062f515ad15ef93afb7a74e702be2b008090d5bd5f3" + }, + "kexts": { + "Lilu": { + "version": "1.7.2", + "url": "https://github.com/acidanthera/Lilu/releases/download/1.7.2/Lilu-1.7.2-RELEASE.zip", + "hash": "sha256-U5Z9fc+qsBAjoz3y6WmolSLxPWZUpqVqxHEbYtq/Org=" + }, + "VirtualSMC": { + "version": "1.3.7", + "url": "https://github.com/acidanthera/VirtualSMC/releases/download/1.3.7/VirtualSMC-1.3.7-RELEASE.zip", + "hash": "sha256-EvHTeZafkmMG+pLZTdvzOzKzEXZYncQgidhkomsxtwA=" + }, + "WhateverGreen": { + "version": "1.7.0", + "url": "https://github.com/acidanthera/WhateverGreen/releases/download/1.7.0/WhateverGreen-1.7.0-RELEASE.zip", + "hash": "sha256-bW/+gzStYPeEpmJ5TmeyVgt511fVBoQdyMqZlKs5l5s=" + }, + "RestrictEvents": { + "version": "1.1.6", + "url": "https://github.com/acidanthera/RestrictEvents/releases/download/1.1.6/RestrictEvents-1.1.6-RELEASE.zip", + "hash": "sha256-mBcN+uGV3dKLXZXj8EASWhPKeDvLm9HluMWI4hexTuY=" + } } } }