From dfd6fe475fa1d53d1e4585dbaf7dd3cfb1c7ce00 Mon Sep 17 00:00:00 2001 From: James Stringer <38541878+jamesstringer90@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:12:32 -0400 Subject: [PATCH] Win-on-Mac: fix ivshmem AGENT/SSH region overlap and display readiness Post-review fixes and release prep for the win-on-mac feature. * asb_shm_layout.h: fix a region-sizing bug -- the AGENT (ch1) slot stride (0x20070) exceeded ASB_AGENT_REGION_SIZE (0x20000) by 112 bytes, so the agent host->guest ring overran into the adjacent SSH (ch7) region and corrupted its slot state under sustained traffic. Enlarge the AGENT region to 0x21000 (downstream offsets cascade) and add per-region compile-time guards (n_slots*stride <= region_size) so this class of overlap fails the build. Host/publisher-side only -- the guest reads geometry from the published directory. * headless.m: GET /display readiness now includes idd_ready for a Windows guest, matching the open precondition (asb_core_mac.m) and the Windows host (asb_vm_idd_ready). Before, the Mac readiness was running && agent_online only, so it could report ready before the VDD was up and a poll-then-open would spuriously 409 display_not_ready. macOS guests are unchanged. * iso-patch-mac.m (stage): wipe the plaintext password buffer on the --shadowhash-file read-failure path, matching the sibling --kcpassword-file path. * iso_patch_mac.m: point the signed Windows guest payload at the v0.1.4 release. * vm_lifecycle.py: raise the Windows build-stall budget to 3600 s for the signal-less first-boot window under concurrent multi-VM load. * tools/headless-api/README.md: document Windows guests on macOS hosts (platform matrix, host-fixed line, requirements heading, osType field). --- src/app_mac/headless.m | 11 +++++++---- src/backend_mac/iso_patch_mac.m | 9 ++++----- tools/headless-api/README.md | 8 ++++---- tools/headless-api/tests/vm_lifecycle.py | 2 +- tools/iso-patch-mac/iso-patch-mac.m | 2 +- tools/transport/asb_shm_layout.h | 16 +++++++++++++++- 6 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/app_mac/headless.m b/src/app_mac/headless.m index ed6601e..a77d007 100644 --- a/src/app_mac/headless.m +++ b/src/app_mac/headless.m @@ -33,6 +33,7 @@ #import #import /* NSApplication run loop (display windows in a GUI session) */ #import +#include /* strcasecmp */ #import "asb_core_mac.h" #import "vz_display.h" /* VzDisplayWindow.userClosed (displayOpen status) */ #import "host_info.h" @@ -941,15 +942,17 @@ static int handle_request(int fd, HttpReq *r) { return 0; } if (isGET) { - /* Pollable + passive: reads flags, opens no window, touches no - frame channel. macOS binds the in-process framebuffer directly, - so ready == running && agent_online -- nothing to probe. */ + /* Pollable + passive: reads flags, opens no window, touches no frame channel. + A macOS guest binds the in-process framebuffer (ready == running && agent_online); + a Windows guest streams VDD frames over ivshmem and is ready only once its IDD + driver is up (idd_ready) -- matching the open precondition (asb_mac_open_display). */ __block BOOL open = NO, ready = NO; on_main(^{ AsbVmMac *v = asb_mac_vm_find(name.UTF8String); if (!v) return; + BOOL is_win = (strcasecmp(v->os_type, "Windows") == 0); open = (v->display != nil && !v->display.userClosed); - ready = open || (v->running && v->agent_online); + ready = open || (v->running && v->agent_online && (!is_win || v->idd_ready)); }); send_json(fd, 200, "OK", json_str(@{ @"open": open ? @YES : @NO, diff --git a/src/backend_mac/iso_patch_mac.m b/src/backend_mac/iso_patch_mac.m index 07a7e88..22a225e 100644 --- a/src/backend_mac/iso_patch_mac.m +++ b/src/backend_mac/iso_patch_mac.m @@ -548,11 +548,10 @@ static BOOL write_prov_scripts(NSString *dir, NSString *sshMsiName) { * the root daemon + GUI. build-windows extracts the needed members into its own per-build temp. */ + (nullable NSString *)ensureSignedWinPayloadZipCached { /* Signed ARM64 release: EV-signed agent EXEs + MS-attestation-signed drivers (VDD/VAD/AppSandboxSHM/ - * devcon). The GitHub attachment id is embedded in the cached filename, so changing the URL below - * (even at the same app version) auto-invalidates the cache. The URL must stay publicly fetchable - * by an unauthenticated client. */ - NSString *name = @"AppSandbox-0.1.3-win-arm64-29486776.zip"; - NSString *urlStr = @"https://github.com/user-attachments/files/29486776/AppSandbox-0.1.3-win-arm64.zip"; + * devcon). The release version is pinned in the cached filename, so a newer release auto-invalidates + * the cache. The URL must stay publicly fetchable by an unauthenticated client. */ + NSString *name = @"AppSandbox-0.1.4-win-arm64.zip"; + NSString *urlStr = @"https://github.com/jamesstringer90/appsandbox/releases/download/v0.1.4/AppSandbox-0.1.4-win-arm64.zip"; NSString *cacheDir = [[VmDir vmsRootDirectory] URLByDeletingLastPathComponent].path; [[NSFileManager defaultManager] createDirectoryAtPath:cacheDir withIntermediateDirectories:YES attributes:nil error:nil]; diff --git a/tools/headless-api/README.md b/tools/headless-api/README.md index 5eb38b7..4d4266b 100644 --- a/tools/headless-api/README.md +++ b/tools/headless-api/README.md @@ -69,7 +69,7 @@ in create-validation (wrong `osType` → `400`); the host-feature row mirrors | | Windows host | macOS host | |---|:---:|:---:| -| **Windows guest** | ✅ | ❌ | +| **Windows guest** | ✅ | ✅ (Apple silicon) | | **Linux guest** (Ubuntu) | ✅ | ❌ | | **macOS guest** | ❌ | ✅ (Apple silicon) | | Snapshots & branches | ✅ | ❌ `501` | @@ -78,7 +78,7 @@ in create-validation (wrong `osType` → `400`); the host-feature row mirrors | GPU, NAT/network modes, SSH server, SSH-key auto-deploy, SSE events | ✅ | ✅ | Only `snapshots`/`templates` are advertised in the `capabilities` object; guest -OS is host-fixed (Windows host → Windows + Linux; macOS host → macOS only), so +OS is host-fixed (Windows host → Windows + Linux; macOS host → macOS + Windows), so a client picks its `osType` from `version()["hostOs"]`, not from `capabilities`. ## Requirements @@ -91,7 +91,7 @@ a client picks its `osType` from `version()["hostOs"]`, not from `capabilities`. | **Administrator** | HCS VM operations and the `http.sys` bind both require elevation. | | **`VirtualMachinePlatform` feature** | The HCS backend the core uses. Enable with `dism /online /Enable-Feature /FeatureName:VirtualMachinePlatform /All` then reboot. | -**macOS host** (runs macOS guests only — Virtualization.framework): +**macOS host** (runs macOS guests via Virtualization.framework, and Windows guests via QEMU+HVF): | Requirement | Why | |---|---| @@ -303,7 +303,7 @@ rather than forwarding bad input to the core. | Key | Notes | |---|---| | `name` | guest hostname; **set at create, not editable later**. Letters/digits/hyphens; not all-digits; no leading/trailing hyphen. Windows ≤15 chars (NetBIOS); Linux ≤63 and lowercase. Must be unique across VMs *and* templates. | -| `osType` | `"Linux"` or `"Windows"` | +| `osType` | `"Linux"` or `"Windows"` on a Windows host; `"macOS"` or `"Windows"` on a macOS host. | | `imagePath` | path to an install ISO — **required unless** `templateName` is given | | `templateName` | create from a Windows template instead of an ISO | | `ramMb` | ≥512, **2 MB-aligned** (even). The SDK rounds down to even for you. | diff --git a/tools/headless-api/tests/vm_lifecycle.py b/tools/headless-api/tests/vm_lifecycle.py index ef4663b..ca7a0b2 100644 --- a/tools/headless-api/tests/vm_lifecycle.py +++ b/tools/headless-api/tests/vm_lifecycle.py @@ -55,7 +55,7 @@ def run_vm_lifecycle(spec): # signal-less window. It can run 10+ minutes (longer under concurrent # multi-VM load), so give Windows a real budget; Linux/macOS stream live # progress and keep the tight 600 s stall. - BUILD_STALL = 1800 if os_type == "Windows" else 600 + BUILD_STALL = 3600 if os_type == "Windows" else 600 c = asb.connect() caps = c.version().get("capabilities", {}) diff --git a/tools/iso-patch-mac/iso-patch-mac.m b/tools/iso-patch-mac/iso-patch-mac.m index 65c096a..a9ca006 100644 --- a/tools/iso-patch-mac/iso-patch-mac.m +++ b/tools/iso-patch-mac/iso-patch-mac.m @@ -790,7 +790,7 @@ static int cmd_stage(int argc, char **argv) { NSData *shdData = nil, *kcData = nil; if (shadowhashFile) { shdData = [NSData dataWithContentsOfFile:shadowhashFile]; - if (!shdData) { emit_error(@"stage: cannot read --shadowhash-file"); return 3; } + if (!shdData) { memset(passBuf, 0, sizeof(passBuf)); emit_error(@"stage: cannot read --shadowhash-file"); return 3; } } else if (passLen > 0) { shdData = asb_macos_shadow_hash_data(passBuf, passLen); } diff --git a/tools/transport/asb_shm_layout.h b/tools/transport/asb_shm_layout.h index 4c513e8..7a98f9a 100644 --- a/tools/transport/asb_shm_layout.h +++ b/tools/transport/asb_shm_layout.h @@ -45,7 +45,7 @@ #define ASB_CLIP_RING_CAP 0x100000u /* 1 MiB per ring */ #define ASB_AGENT_REGION_OFF (ASB_CLIPR_REGION_OFF + ASB_CLIPR_REGION_SIZE) /* ch1 */ -#define ASB_AGENT_REGION_SIZE 0x20000ull /* 128 KiB */ +#define ASB_AGENT_REGION_SIZE 0x21000ull /* 132 KiB -- must be >= ASB_AGENT_SLOT_STRIDE so the slot fits the region */ #define ASB_AGENT_RING_CAP 0x10000u /* 64 KiB per ring */ #define ASB_SSH_REGION_OFF (ASB_AGENT_REGION_OFF + ASB_AGENT_REGION_SIZE) /* ch7 */ @@ -67,6 +67,20 @@ #define ASB_SSH_SLOT_STRIDE (ASB_SLOT_HDR + 2u * (ASB_RING_HDR + ASB_SSH_RING_CAP)) #define ASB_P9_SLOT_STRIDE (ASB_SLOT_HDR + 2u * (ASB_RING_HDR + ASB_P9_RING_CAP)) +/* Each region must hold all its slots: n_slots * slot_stride <= region_size. These compile-time + * checks fail the build (negative array dimension) if a region is too small to contain its slot(s), + * so a region/ring-cap mismatch cannot silently overlap the next region. File scope (no + * unused-typedef warning); compiled only on the host/publisher side -- the guest reads geometry + * from the published directory. */ +typedef char asb_fits_display[(ASB_DISPLAY_SLOT_STRIDE <= ASB_FRAME_REGION_SIZE) ? 1 : -1]; +typedef char asb_fits_input [(ASB_INPUT_SLOT_STRIDE <= ASB_INPUT_REGION_SIZE) ? 1 : -1]; +typedef char asb_fits_audio [(ASB_AUDIO_SLOT_STRIDE <= ASB_AUDIO_REGION_SIZE) ? 1 : -1]; +typedef char asb_fits_clipw [(ASB_CLIP_SLOT_STRIDE <= ASB_CLIPW_REGION_SIZE) ? 1 : -1]; +typedef char asb_fits_clipr [(ASB_CLIP_SLOT_STRIDE <= ASB_CLIPR_REGION_SIZE) ? 1 : -1]; +typedef char asb_fits_agent [(ASB_AGENT_SLOT_STRIDE <= ASB_AGENT_REGION_SIZE) ? 1 : -1]; +typedef char asb_fits_ssh [((uint64_t)ASB_SSH_N_SLOTS * ASB_SSH_SLOT_STRIDE <= ASB_SSH_REGION_SIZE) ? 1 : -1]; +typedef char asb_fits_p9 [((uint64_t)ASB_P9_N_SLOTS * ASB_P9_SLOT_STRIDE <= ASB_P9_REGION_SIZE) ? 1 : -1]; + #define ASB_SHM_N_REGIONS 8 /* Initialize one region's slots in place: zero each slot, set both rings' caps, mark FREE.