Skip to content

VM platform: file sharing (DAX), USB passthrough, Rosetta x86, security + GitHub issue fixes#5

Merged
Augani merged 54 commits into
mainfrom
feat/vm-platform-tracks
Jul 5, 2026
Merged

VM platform: file sharing (DAX), USB passthrough, Rosetta x86, security + GitHub issue fixes#5
Augani merged 54 commits into
mainfrom
feat/vm-platform-tracks

Conversation

@Augani

@Augani Augani commented Jul 5, 2026

Copy link
Copy Markdown
Owner

What this does

Builds out Dory's VM platform (dory-hv guest stack) and fixes the issues reported after launch. It spans file sharing, USB passthrough, x86/Rosetta, a security fix, and the four open GitHub issues. 30 commits, all building green (engine 130 tests, app RuntimeSupport tests, Go agent tests).

Highlights

File sharing (virtio-fs)

  • Fixed a 6x gap vs OrbStack. Root cause was the guest page cache never being retained (bad attr_valid + mtime_nsec=0 under FUSE_AUTO_INVAL_DATA). After the fix, plain virtio-fs matches OrbStack on realistic workloads.
  • DAX end-to-end, mapping file pages into the guest via hv_vm_map: 1,074k IOPS on cache-bypassed 4k random read, ~4.8x past OrbStack. Opt-in via :dax.
  • Auto-wired sharing so docker run -v ~/project:/app resolves with no config ($HOME shared at its identical guest path).
  • Write correctness: SETATTR now honors FATTR_SIZE, so truncate/O_TRUNC no longer leave stale data. Verified in a real guest.
  • FUSE_DO_READDIRPLUS advertised so shared directories actually enumerate (ls was returning empty).

Security

  • :safe denylist on the $HOME auto-share hides credential stores and shell rc files (~/.ssh, ~/.aws, ~/.kube, Library, ...) from every container. This closes the risk that the whole home tree was exposed read-write. On-demand per-mount sharing is the tracked follow-up.

USB passthrough (usbip-over-vsock, Track 3.6)

  • Full data path: guest agent dials the host and runs the OP_REQ_IMPORT handshake, host UsbipBridge serves URBs against a claimed HostUsbDevice, and an engine control plane (dory usb attach/detach) claims the device, registers it, and triggers the guest dial. Loopback integration test proves the chain without hardware.
  • Validated the host claim on real hardware: the code runs correctly up to macOS's entitlement gate. Driver-owned devices (mass storage) need the restricted com.apple.vm.device-access entitlement + a notarized build; driverless devices (microcontrollers, serial) work with IOServiceAuthorize.

x86 / Rosetta (GitHub #3)

  • SQL Server 2022 proven to run under Rosetta on Apple Silicon (reached "ready for client connections" in ~4s, where qemu-user segfaults). Added an opt-in Settings toggle "Run x86 images with Rosetta" that switches the shared engine to the Virtualization.framework + Rosetta variant, publishing to the same socket so nothing else changes. Off by default (it trades away dory-hv's memory advantage).

GitHub issues fixed

Platform note

This release's own engine (dory-hv) is Apple silicon only; on Intel, Dory runs as a native front-end for an installed Docker engine. README + website updated.

Augani added 30 commits July 4, 2026 04:46
Own VMM on Hypervisor.framework: guest RAM is process-owned mmap (the
foundation for real memory reclaim), in-kernel GICv3 via hv_gic with
MMIO register forwarding, PL011 console, PSCI over SMC, arm64 Image
loader, and an FDT builder. Kernel reaches the expected VFS root
panic; block devices land next. Architecture doc included.
…n 0.4s

Split virtqueue engine with bounds-checked descriptor walks, the
virtio-mmio v2 register surface, a zero-copy block device, and a host
CSPRNG entropy device. The dind rootfs boots to a fully initialized
Docker Engine (API listen on /var/run/docker.sock) 405ms after kernel
entry, verified with an in-guest docker version round trip.
…eclaim

virtio-balloon negotiates VIRTIO_BALLOON_F_REPORTING; reported ranges
are hv_vm_unmap'd (stage-2 pins pages, madvise alone is a no-op) then
marked MADV_FREE_REUSABLE, and remapped lazily on guest fault with
REUSE for honest accounting. PL031 RTC gives the guest real wall time.
Measured: 2GiB VM fills to 900MB, guest frees it, host footprint falls
to 107MB in seconds; 800MB integrity checksum survives the cycle;
idle CPU 0.2%. Guest-side cache trimming via cgroup2 memory.reclaim +
compaction closes the page-cache ratchet (680MB -> ~330MB and falling).
…_ENGINE

virtio-net speaks the vfkit datagram protocol to a gvproxy sidecar:
guest DHCPs 192.168.127.2, full outbound NAT/DNS/TLS verified with a
real docker pull. gvproxy also serves ~/.dory/engine.sock as a unix
socket straight into guest dockerd. dory-hv engine mode owns the whole
lifecycle (one-time dind unpack to a PERSISTENT disk: images survive
restarts, fixing the VZ helper's re-clone regression), PL031 RTC for
correct guest wall time, SIGTERM reaps the sidecar. SharedVMProvisioner
prefers dory-hv when DORY_HV_ENGINE=1, falling back to the VZ helper
then the container CLI. Verified end to end: the Dory app provisioned
dory-hv and ran postgres:16 through its own hypervisor.
…adroom

Wires the in-process helper as the preferred engine with env-tunable
memory policy, counts dory-vmboot in readiness memory probes, and
covers the new host-support ladder in RuntimeSupportTests. Full suite
green.
Secondary vCPUs are created eagerly (so the kernel's redistributor walk
sees every GIC frame), park on a condition until PSCI CPU_ON releases
them at their entry point, and run one dedicated thread each per the
framework's create/run/destroy-on-one-thread rule. Per-vCPU MPIDR and
redistributor-frame mapping by the GIC-assigned base. stopAll cancels
running vCPUs exactly once via hv_vcpus_exit; run() joins every
secondary before returning so hv_vm_destroy never races a live thread.
Guest boots 4 CPUs (nproc=4).
Reclaim under SMP hit a real race: releaseRange unmapped a page before
marking it released, so a concurrent vCPU faulting in that window saw an
unmapped-but-unmarked page and crashed it as a genuine fault. The
bitmap flip and the stage-2 map/unmap now happen atomically under one
lock in both releaseRange and restorePage, and a stage-2 RAM fault
(which can only be a page we unmapped) always remaps instead of
crashing. Adds released/restored byte counters for the reclaim gauge.

Also serialize virtio-net RX drain against MMIO queue reconfig via the
transport register lock, and guard Virtqueue.push with size>0, closing
the reported divide-by-zero when a guest resets the RX ring mid-drain.
Verified: 5 rounds of 200MB memory churn under 4 vCPUs, no crash.
Split the single persistent disk into a throwaway APFS-cloned rootfs
(recreated each boot, so system state can't rot) plus a separate
journaled ext4 mounted at /var/lib/docker for docker state. An unclean
quit can no longer corrupt the engine: the rootfs is disposable and the
data disk journals (verified: EXT4-fs vdb recovery completes on
restart, images survive). SIGTERM/SIGINT now ask the guest to
sync+unmount+power off through a shutdown socket, with a 10s hard-kill
watchdog and gvproxy reaped. Guest trim profile v2: proportional
memory.reclaim toward a 128MB cache floor every 15s, vfs_cache_pressure
200, so the page cache stops ratcheting the footprint up.
- Data-disk mount failure now powers the guest off instead of letting
  dockerd write to the throwaway rootfs (was silent total data loss on
  the next boot's re-clone).
- Rootfs unpack and data-disk format are built at a .partial path,
  fsynced, then atomically renamed, so an interrupted first run can't
  leave a truncated file the fileExists guard treats as complete
  forever.
- PID 1 is now tini (docker-init, via exec so it keeps PID 1 while
  dockerd + loops run as its children): container shims orphan children
  onto PID 1; without a reaper they pile up as zombies until PID
  exhaustion. Verified 0 zombies after 50 --rm containers.
- SIGTERM shutdown touch retries until the gvproxy forward and guest
  listener exist, instead of firing one discarded connect that a
  boot-time signal would lose.

Verified end to end: images + volumes + volume data survive restart,
ext4 journal recovery clean, graceful shutdown ~2s, 4 vCPUs.
Live measurement of the signed app exposed two defects in the trim
loop, both confirmed against the running guest:

- compact_memory migrates pages and re-faults the ones free page
  reporting already handed back to the host. Measured 129 MiB restored
  per 45 s of pure idle: pure churn (wasted CPU + inflated footprint,
  transient 1.7 GiB after a heavy image pull). Removed.
- memory.reclaim on the ROOT cgroup is write-rejected, so the
  cache-drop half of the loop did nothing (silent, behind 2>/dev/null).

Now: free page reporting handles free pages automatically; the guest
gets vm.min_free_kbytes=256M so the kernel keeps cold cache moving back
to the free list, and a gentle pagecache drop fires only when cache
bloats past ~384 MiB, leaving active working sets intact.
…im guards

16 tests, no VM required (pure logic + GuestMemory mmap): FDT header +
string dedup, arm64 Image header parse + bad-magic reject, data-abort
syndrome decode, MMIO bus routing, PL011 transmit/flags, a full
virtqueue pop->write->push round trip, push-after-reset divide-by-zero
guard, out-of-bounds descriptor reject, and releaseRange/restorePage
bitmap invariants.
bundle-engine.sh now builds and signs dory-hv with
com.apple.security.hypervisor and injects a gvproxy binary (from
DORY_GVPROXY / podman / PATH) alongside the legacy dory-vm helper. The
HV-engine provisioner path falls back to the bundled compressed kernel
when no container toolchain is installed, so a self-contained install
boots the new engine with no external dependencies.
Slightly tighter pagecache cap trims the idle footprint without
disturbing active working sets. Documents that the residual floor is
guest cache + sub-16KiB fragmentation (the future lever is a custom
4KiB-granule reporting device), and that restore churn is now ~4-5% of
released bytes vs ~58% before the compaction fix.
The dory-hv engine runs on Hypervisor.framework's in-kernel GICv3
(macOS 15+) and ships its own kernel + userspace networking, so it needs
neither macOS 26 nor Apple's container toolchain. hostSupport now
returns supported on macOS 15+ Apple silicon whenever the dory-hv engine
is available (DORY_HV_ENGINE opt-in + bundled helper/gvproxy/kernel),
falling back to the legacy container/VZ evaluation otherwise. New
DoryHVSupport type + tests cover the broadened matrix; existing
container-path tests pin the fallback. All DoryTests green.
Dory now controls the whole stack for uniform performance: our own
Hypervisor.framework VMM (dory-hv) is the one and only shared-VM engine.
Removed the Apple `container` CLI provisioning path, the older
Virtualization.framework dory-vm helper path, the AppleContainerRuntime
backend, AppleContainerSupport (macOS 26 + toolchain gate), and the
guided container-toolchain install UX (EngineSetupView, needsContainer-
Toolchain, toolchainInstall*).

- SharedVMProvisioner is dory-hv only: hostSupport = DoryHVSupport
  (macOS 15+ Apple silicon), no toolchain required. Engine is default-on
  (opt out with DORY_HV_ENGINE=0).
- AppStore backend selection drops the Apple per-container fallback;
  Intel / older macOS still fall back to a Docker-compatible socket.
- Copy updated (macOS 15, 'Dory's engine', no 'Apple container engine').
- Tests updated; DoryTests + DoryHVTests green; app builds.

Known follow-up: published container ports (docker -p) need gvproxy
per-port expose wiring for the dory-hv path.
Closes the last dory-hv gap: docker run -p is now reachable from the
host. The engine polls the docker socket and keeps gvproxy's forwards
in sync with the live set of published ports (expose on publish,
unexpose on stop), using gvproxy's bare host:port TCP remote form. The
app no longer binds ports itself for the shared-VM path (that would
race gvproxy); it points *.dory.local domains at the gvproxy-exposed
localhost ports. Verified: nginx -p 18080:80 -> localhost:18080 HTTP
200 in ~9s, and released within 3s of docker rm.
Two defects caught by adversarial review of the sole-engine refactor:

- hostSupport() had stopped folding in hvEngineAvailable(), so a
  DORY_HV_ENGINE=0 opt-out and a missing/partial engine install both
  slipped through as 'supported' and then surfaced as a misleading
  'engine could not start'. hostSupport now reports capable-but-
  unavailable honestly (via the engineAvailable param, default
  hvEngineAvailable()), so the app falls back to a Docker-compatible
  engine instead. Covered by a new test.
- PortForwarder.unexpose dropped a port from its tracked set even when
  the gvproxy call failed, leaking a stale host forward with no retry.
  Removal is now gated on success, mirroring the expose path, so a
  transient failure is retried on the next tick.
A fresh Dory download now includes everything: the k8s features shell
out to a bundled kubectl and the optional docker context uses a bundled
docker CLI, so users install nothing beyond the app. New HostTools
resolves the bundled Contents/Helpers copy first, falling back to a
system install for dev builds. bundle-engine.sh fetches the
darwin/arm64 kubectl (stable) and the static docker CLI and signs them
into the bundle. Verified both download URLs resolve; DoryTests green.

With this, dory-hv (engine) + gvproxy (networking) + kernel + kubectl +
docker are all in the app; only the docker:dind image is fetched once
on first launch (the OrbStack model).
… pull)

Adds a --rootfs flag to the engine and a bundledRootfs config field. When
an offline build ships a decompressed engine rootfs (via the app's
prepareCompressedResource on dory-engine-rootfs.ext4.zst), the engine
installs it as the pristine rootfs on first launch instead of fetching
docker:dind over the network. Online builds omit the resource and fetch
once as before.
…P, binfmt; guest kernel+agent

Track 0 (vsock + guest agent), Track 1 (virtio-fs, FUSE server, DAX window +
coherence probe), Track 3 (usbip + IOUSBHost shim), Track 5 (DirectIP bridge,
machine port watcher). DAX flag-gated via --share :dax; dory-hv daxprobe proves
hv_vm_map file-backed coherence. Rosetta opt-in on shared vz engine.
…r bridges, DAX/Rosetta UX

Track 2 (recipe schema + RecipeStore + SSHConfigWriter + catalog), Track 3 UI
(UsbDevicesView + UsbAttachmentStore), Track 5 (TunRouter), Track 6 (CredentialBridge,
ExposeTunnel). Recipe tilde-mount expansion fix; USB replay idempotency; credential
fail-fast logging; DockerShim shared stdcopy frame; amd64 machines point users to
dory vm --rosetta.
…binary

Audit findings: SSHConfigWriter was built+tested but never called, so
~/.dory/ssh/config (for 'ssh <machine>' + VS Code Remote-SSH) was dead code —
now regenerated on every machine refresh. Also untrack + gitignore the stray
4.4MB guest/agent/agent Mach-O build artifact.
…fig to build+boot

Discovered by actually building the guest kernel and booting a real guest:
- dory.config lacked a serial console (PL011) + devtmpfs, so a from-source
  kernel booted blind; and defconfig pulled in GPU/DRM (nouveau) that fails to
  build. Disable GPU/sound/media/wlan/fb; add PL011 console + devtmpfs.
- Recorded fio 4k randread / dd write / git status: Dory virtio-fs works end to
  end; write+metadata competitive with OrbStack, random-read ~5.6x behind
  (motivates DAX + off-vCPU-thread FUSE I/O). DAX guest-mount not yet active
  (host hv_vm_map proven via daxprobe; guest dax_device handshake deferred).
…g off vCPU thread

Root cause of the ~6x file-sharing gap vs OrbStack: the guest page cache was
never retained, so every read (even a re-read) took a full FUSE round-trip.
Two attr-reply bugs combined under FUSE_AUTO_INVAL_DATA: LOOKUP returned
attr_valid=0 (revalidate every access), and every attr carried mtime_nsec=0, so
an unchanged host file looked modified on each revalidation and lost its cache.
Fix: 1s attr_valid/entry_valid window + real host atime/mtime/ctime nsecs
(HostFSAttributes gains *Nsec, encodeAttr emits them). Keep AUTO_INVAL_DATA —
with correct nsecs it invalidates only on genuine host changes.

Result (fio 4k randread, cache-resident): 18k -> 1,075k IOPS, vs OrbStack
1,011k (1.06x ahead). Raw cache-bypassed plane is 18.8k vs 223k = per-request
latency, the DAX/passthrough target (not the plain-path gate).

Also: move FUSE request handling off the vCPU thread into a bounded drainer
pool (a blocking pread in the MMIO exit froze the guest and collapsed deep
queues to depth 1); out-of-order completion; @unchecked Sendable on the
manually-synchronized virtqueue/transport primitives. And FUSE_MAX_PAGES so the
advertised 256-page request size is honored (guest was clamping to 128KiB).
FUSE READ used to alloc a read buffer, build a response array, then copy it back
into guest memory. Now preadv scatter-reads the payload straight into the guest's
device-writable descriptor segments and writes the fuse_out_header in place, with
an array-path fallback when the first segment can't hold the header. Raw single-
stream 4k randread 16.7k -> 20.1k IOPS (~20%); removes ~10us Swift per-request
overhead and the allocator pressure across concurrent workers. Test proves the
zero-copy output is byte-identical to the array path.
Augani added 24 commits July 4, 2026 23:38
…beats OrbStack)

DAX collapses the per-4k-read guest<->host round-trip: file pages are mapped
straight into the guest via hv_vm_map, so reads are direct memory access with
zero FUSE traffic. Three bugs blocked it, found by booting a real guest:

1. guest/kernel/dory.config: FUSE_DAX silently dropped because the FS_DAX -> DAX
   -> ZONE_DEVICE chain wasn't enabled (defconfig leaves ZONE_DEVICE off, so
   olddefconfig disabled FUSE_DAX even though set =y). Pull in ZONE_DEVICE +
   FS_DAX + deps. The guest now compiles DAX and queries the SHM window.
2. FuseSetupMappingIn was 48 bytes; the real fuse_setupmapping_in is 40 (5x u64).
   Every guest SETUPMAPPING was rejected as a short frame -> EINVAL.
3. virtio-fs sends fh=-1 for inode read-only mappings (resolve by node id), and
   Apple's hv_vm_map rejects a PROT_READ-only host region (HV_ERROR) -> map the
   host read-write while the guest stage-2 stays read-only. Read-only files
   degrade to plain FUSE reads.

fio 4k randread, cache-bypassed: plain 20.1k -> DAX 1,074k IOPS (OrbStack 223k).
Cached 1,186k; concurrent-16 4,147k. 110 engine tests pass.
…r leave stale data)

The setattr handler accepted mode no-ops but silently dropped FATTR_SIZE, so
truncate() and O_TRUNC opens (atomic_o_trunc is not negotiated, so the guest
relies on a separate SETATTR size=0) returned success without touching the host
file, leaving a stale tail after an overwrite. Add HostFS.truncate (by node id
and by open handle) and wire FATTR_SIZE through, returning the post-truncate
attrs. Affects both plain virtio-fs and DAX writes.

Verified end-to-end on a real dory-hv guest with -o dax=always: O_TRUNC (41->3
bytes), ftruncate shrink (16->5), ftruncate grow (3->100, NUL-padded),
extending write (0->4MiB), create+write, in-place overwrite, and write/read
coherency all persist correctly to the host. +2 unit tests.
…th no flags

File sharing was never wired into the app engine: SharedVMProvisioner passed no
--share, so `docker run -v ~/project:/app` had nothing to bind. Share the user's
home at its identical guest path (home=$HOME:rw:at=$HOME) so macOS paths resolve
transparently in containers, the OrbStack 'just works' default. Add a
guestMountPoint to the share config (:at=/guest/path) so a host dir can mount at
its real path instead of /mnt/dory/<tag>; also fold dax=always into the engine's
guest mount options.

Plain virtio-fs (no DAX) is the default: it matches OrbStack on realistic
cache-resident workloads and has none of DAX's window-thrashing or
read-only-file caveats, so it is safe across the whole home tree.

Verified in a real dory-hv guest: a host dir mounted at /Users/dorytest read a
host file and a guest write-back persisted to the host. +4 unit tests.
…se blocker)

USB passthrough is not wired end-to-end: components exist + are unit-tested
(discovery, IOUSBHost I/O, codec, dispatcher, guest vhci writer, UI, CLI) but
nothing instantiates the usbip server, there is no vsock data channel, and the
guest agent can't produce the connected fd vhci needs. Add a code-grounded,
adversarially-verified completion plan (3.6a-n): guest-dials/host-listens data
channel, VsockConnection EOF signal for cleanup, host UsbipBridge serve loop,
engine control plane, entitlement scope, readiness reconcile, loopback test,
and the COMPATIBILITY.md over-claim fix. Mark 3.1/3.2/3.4 done.
The server handles FUSE_READDIRPLUS but not plain FUSE_READDIR, and never
advertised DO_READDIRPLUS — so the guest sent READDIR, got ENOSYS, and every
`ls` of a shared directory returned empty (lookup/cat still worked). Force
readdirplus (without READDIRPLUS_AUTO, which would let the kernel fall back to
the unhandled plain readdir). Verified in a guest: a shared dir now lists.
The auto-share exposed all of $HOME read-write to every container, including
~/.ssh, ~/.aws, ~/.gnupg, ~/.kube, shell rc files, and ~/Library — a real key
exfiltration / rc-poisoning risk (exactly what launch feedback warned against).
HostFS gains hiddenNames: a hidden name fails lookup as not-found and is omitted
from listings, so no node id is ever minted for it (no read/open/write path).
The home share now passes :safe (VirtioFSShareConfiguration.sensitiveNames).

Interim defense-in-depth; the stronger guarantee is per-bind-mount on-demand
sharing (shim create-interception + engine authorization channel) — a tracked
follow-up. Verified in a guest: ~/.ssh/~/.aws/~/.zshrc invisible, ~/project
reads+writes+lists. +HostFS/parse/engine-args tests.
…e passthrough)

dory vm --devices attaches a VZXHCIController with no host device, so the ✅
'USB passthrough delivered' row was inaccurate — 'USB controllers attached: 1'
confirms a controller, not a passthrough. Split audio (real, VZVirtioSoundDevice)
from USB device passthrough (🚧, real path is usbip Track 3.6). Fix the section
header's 'all four delivered' framing accordingly.
…ort from OrbStack')

Import auto-selected a single source via DockerEngineRuntime.detect(), which
returns the highest-priority responding docker socket. With Docker Desktop also
installed, /var/run/docker.sock outranked OrbStack and there was no way to choose
it — so 'import from OrbStack' silently used the wrong engine or showed nothing.

- Enumerate ALL present source engines, labeled by vendor (DockerSourceEngine +
  availableSources/engineLabel), and add a source picker to the Migrate panel.
- Preflight + import use the selected source (probed to confirm it's reachable).
- Surface MigrationSummary.failures in the UI instead of a bare success count.
- Defensively exclude ~/.dory/engine.sock too (not just dory.sock) so a
  migration can never pick Dory as its own source.

Note: the earlier hypothesis that the 'dory' context self-selected engine.sock
was wrong — the context points to dory.sock, already excluded (verified). The
real gap was the missing source choice. +6 discovery tests.
…,c,f,m)

The missing host-side glue: UsbipBridge pumps one guest usbip vsock connection
against one claimed device via UsbipServer — OP_REQ_IMPORT handshake, then the
CMD_SUBMIT/UNLINK loop with correct 48-byte-header + OUT-payload framing, on its
own queue so a blocking device submit never stalls the vsock queue. Ends and
releases the device on peer close.

Adds VsockPorts.usbip (1025) and a VsockConnection.isPeerClosed EOF signal so the
long-lived bridge can tell 'idle' from 'guest gone' (read returns 0 for both) —
without it a claimed device leaks on guest reboot. Makes InProcessConnection
thread-safe (bridge reads off the vsock queue).

Loopback integration test proves import + submit round-trip + unknown-busID
rejection + EOF cleanup, no hardware. 21 tests green.
…ke (Track 3.6d,e)

attachUSB previously required a caller-supplied socket_fd, which can't work — the
fd vhci needs must live in the guest's own fd table. Now when no fd is given it
dials VMADDR_CID_HOST on vsock_port, performs OP_REQ_IMPORT (drains the 312-byte
descriptor), and hands that guest-owned fd to vhci; the socket_fd path stays a
test seam. Closes the fd after the vhci write (kernel dups it) to avoid a leak.

connectVsock/usbipImport/closeFD live in the linux file with darwin stubs so the
dev-host build stays green; the 40-byte OP_REQ_IMPORT encoder is extracted and
unit-tested. Builds linux/arm64 + darwin; existing socket_fd seam tests green.
…Track 3.6f,g)

UsbipManager holds the set of claimed host devices by busID and registers the
usbip listener on VsockPorts.usbip in EngineMode: an accepted guest dial is served
by a UsbipBridge backed by whatever devices are registered, and the guest's
OP_REQ_IMPORT busID picks which one. The listener is a no-op until the control
plane claims a device, so it is always safe to register. Bridge refactored to a
multi-device UsbipServer (busID read from the wire) with a single-device
convenience init. 124 engine tests green.
…ansfer smoke test

Claims a real host device via HostUsbDeviceFactory.open and drives one
GET_DESCRIPTOR through the exact usbip submit path the guest uses — a host-side
hardware smoke with no guest/VM. Validated on real hardware (SanDisk mass storage,
busID 1-2): the claim code path executes correctly and hits macOS's security gate
exactly as documented — .capture needs the restricted com.apple.vm.device-access
(ad-hoc binaries carrying it are SIGKILLed at launch, exit 137), and
.userAuthorized IOServiceAuthorize returns notPermitted (0xe00002c6) for a
driver-owned device from a headless CLI. Confirms v1 scope: driverless devices via
a properly-authorized app; capture-class needs the notarized entitlement.
…er+notify (Track 3.6h,i,j)

Completes the USB last mile. UsbControlHandler (engine-side): on attach it claims
the host device (HostUsbDeviceFactory.open), registers it with the UsbipManager,
allocates a vhci port, and tells the guest agent to dial+vhci-attach — rolling the
claim back if the guest notify fails so the device returns to macOS. detach
notifies the guest, unregisters, frees the port. UsbControlServer serves this over
a unix socket (~/.dory/hv/usb-control.sock, newline-JSON), wired in EngineMode with
real deps (device open + AgentChannel usb.attach/detach). dory-hv usb attach/detach
are the client; scripts/dory routes through them (mode defaults userAuthorized;
capture needs the notarized entitlement).

Handler logic + codec fully unit-tested (claim/register/notify, distinct-port
allocation, duplicate reject, rollback-on-guest-failure, detach, round-trip
codec). Client verified against a down engine. 130 engine tests green. Only the
real device-claim + guest dial remain hardware-gated.
… via proxy, native Intel later

Make the platform split explicit and correct the stale 'macOS 26 (Tahoe)' floor
(the shared VM needs macOS 15). Apple silicon = full self-contained experience;
Intel = native front-end for a separately-installed Docker engine, with a native
Intel engine (Virtualization.framework) planned for a later update.
The DNS/HTTP/HTTPS proxy ports were hardcoded constants (15353/8080/8443) and the
*.dory.local domain feature started unconditionally — 8080 collides with common
dev servers, and MDM-managed DNS can't be pointed at Dory, making domains unusable
rather than optional. Promote the ports to UserDefaults-backed settings, add a
domainsEnabled toggle (gates startLocalNetworking), and applyNetworkingSettings()
does a clean teardown+restart so listeners rebind. New Network settings panel:
domains on/off + three port fields. Also corrected the settings copy that
overclaimed certs are 'trusted system-wide' (the system-trust install is not
wired). Published ports stay on localhost regardless. Builds.
…imit (#3)

#4: k3s runs its own embedded containerd, separate from the shared dockerd store,
so 'built images immediately usable in Pods, no registry push' was false (→
ImagePullBackOff). Correct both claim sites (KubernetesProvisioner doc + the
Kubernetes empty-state) to say built images must be pushed to a registry or
imported into k3s. Real auto image-sync is a tracked follow-up.

#3: document that heavy amd64 images (SQL Server, Oracle, AVX-heavy) can segfault
under qemu-user on the default engine — a qemu-user limit, not a Dory bug — and
point to the Rosetta fast path (dory vm --arch amd64 --rosetta). Rosetta can't run
on dory-hv (raw Hypervisor.framework), so this is a documented limitation.
The single-container VZ path hardcoded a 1 GB ceiling, which silently blocks heavy
amd64 images (SQL Server refuses to start under 2 GB) regardless of Rosetta.
PROVEN on real hardware: with DORY_VM_MEM_MB=4096, SQL Server 2022 (linux/amd64)
starts under Rosetta and reaches 'ready for client connections' on port 1433 in
~4s — where qemu-user segfaults (signal 11). Rosetta emulates AVX2 + the atomics
SQL Server needs; qemu-user does not.
…ver)

Adds a manual Settings toggle that switches the shared engine from dory-hv to the
Virtualization.framework engine with Rosetta, so heavy amd64 images run reliably —
SQL Server 2022 (linux/amd64) was PROVEN to reach 'ready for client connections'
in ~4s under Rosetta on real hardware, where qemu-user segfaults.

- Config.rosettaX86 (persisted, UserDefaults) routes provision() to a new
  provisionWithRosettaEngine: launches dory-vm --shared-engine + DORY_ENGINE_ROSETTA=1
  + 3 GB (SQL Server needs >2 GB), publishing to the same engine.sock so the shim
  and docker context are unchanged. Bundled dory-vm helper + dory-vm-kernel/initfs.
- AppStore.setRosettaX86 restarts the engine on toggle; Engine settings gets the
  toggle (honest memory-tradeoff copy). Off = dory-hv + the memory advantage.

Mechanism proven end-to-end manually; the same VZ engine also unlocks the future
Intel self-contained track. Docker Desktop's model. App builds, 28 tests green.
…rename

Extend the :safe hiddenNames gate beyond lookup/readdir: requireVisible() now
guards every entry-creating or entry-removing op, so a container can't create,
delete, or rename a hidden name (e.g. drop a new ~/.ssh/authorized_keys or clobber
a hidden path) before the host is touched. Plus small USB test refinements.
Engine 131 tests pass.
…nner host death)

The gate's retry only fired when too few tests ran, so a PARTIAL host death (one
xctest worker crashes, its tests report 'failed' at 0.000s while 300+ others pass)
tripped the unexpected-failures check and exited 1 without retrying, turning an
infra flake into a red check. Now retry on any non-clean attempt and fail only if
failures persist across both. Real failures still fail (they reproduce); host
deaths get their intended second chance. Full DoryTests suite passes locally
(525 tests, 0 failures).
@Augani Augani merged commit 20b8a7e into main Jul 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant