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).
This commit is contained in:
James Stringer committed 2026-06-30 18:12:32 -04:00
1 parent 96fbc9ae60
commit dfd6fe475f
6 files changed
+32 -16

No files matched your search

+7 -4
View File
@@ -33,6 +33,7 @@
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h> /* NSApplication run loop (display windows in a GUI session) */
#import <Virtualization/Virtualization.h>
#include <strings.h> /* 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,
+4 -5
View File
@@ -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];
+4 -4
View File
@@ -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. |
+1 -1
View File
@@ -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", {})
+1 -1
View File
@@ -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);
}
+15 -1
View File
@@ -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.