Skip to content

✨ feat: Linux x86_64 runtime + native AF_VSOCK control plane#52

Merged
yeazelm merged 2 commits intomainfrom
mt/x86_64-support
Apr 27, 2026
Merged

✨ feat: Linux x86_64 runtime + native AF_VSOCK control plane#52
yeazelm merged 2 commits intomainfrom
mt/x86_64-support

Conversation

@yeazelm
Copy link
Copy Markdown
Collaborator

@yeazelm yeazelm commented Apr 20, 2026

Summary

Brings mb on Linux x86_64 to parity with the Apple Virt experience on
macOS: native qemu-system-x86_64 with KVM, host↔guest control plane
over AF_VSOCK rather than SLIRP/TCP. Three latent gaps closed:

  • Runtime. pkg/vm/backend_linux.go and cmd/vmhost/platform_linux.go
    were hardcoded to qemu-system-aarch64 under a bare linux build tag,
    so mb up on an x86_64 Linux host launched the ARM emulator. Split by
    GOARCH — amd64 gets q35 and OVMF paths covering Fedora, Debian/Ubuntu,
    and NixOS.
  • Pull-side platform filter. pkg/mixtapes/pull.go:resolveFromIndex
    picked the first raw-format manifest with no arch check, so on a
    multi-arch index it would boot the wrong kernel. Extracted a pure
    selectIndexManifest with a 4-tier priority
    (platform+raw → platform-any → any raw → first) keyed off
    runtime.GOOS/GOARCH.
  • Native AF_VSOCK. New pkg/vsock/VsockTransport wiring
    github.com/mdlayher/vsock into the existing Transport interface.
    Go's stdlib net.FileConn rejects AF_VSOCK with "protocol not
    supported", so we defer to mdlayher's cgo-free (on Linux) wrapper that
    hooks AF_VSOCK into Go's runtime poller. A HostHasVsock() probe
    checks /dev/vhost-vsock, so requesting vsock in the platform config
    gracefully falls back to TCP hostfwd when unavailable. Per-VM CID
    allocator scans existing VM state to avoid kernel-level collisions.

Also folded in:

  • highmem=on moved from the generic machine-arg builder to per-platform
    MachineType strings — it's aarch64-virt-only and QEMU rejects it on
    x86 q35.
  • QEMU stderr now captured to <vm-dir>/qemu.log so launch failures
    leave a forensic trace (they previously went to /dev/null).

Testing

  • go test ./... passes.
  • Cross-builds clean on linux/amd64, linux/arm64, and
    darwin/arm64.
  • End-to-end on Fedora 43 x86_64 (kernel 6.19, QEMU 10.1.5):
    mb up completes in 7.2s, state.json: vsock_cid=3,
    -device vhost-vsock-pci,guest-cid=3 + SSH hostfwd only,
    mb ssh in → stereosd + agentd active, uname -m = x86_64,
    /run/stereos-ready populated, ss --vsock --listening
    inside the guest shows stereosd on port 1024.
  • Multi-VM CID allocation on the same Fedora host: with one
    pre-existing sandbox holding CID 3, two new sandboxes alpha and
    beta started sequentially got CIDs 4 and 5 respectively (the
    allocator correctly scanned and skipped the in-use CID). Both
    ran concurrently, each was independently reachable over its own
    SSH hostfwd port, and both guests reported stereosd + agentd
    active.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 20, 2026

Greptile Summary

This PR brings Linux x86_64 to full parity with the macOS Apple Virt experience by wiring up native qemu-system-x86_64 with KVM, a platform-aware OCI manifest selector, and a host↔guest AF_VSOCK control plane via mdlayher/vsock. Three latent gaps are closed: the GOARCH-blind linux build tag (which launched the ARM emulator on x86_64 hosts), the arch-unaware resolveFromIndex that could boot the wrong kernel on multi-arch indexes, and the missing native vsock transport (previously TCP-only on all Linux hosts).

Key changes:

  • Build-tag split: platform_linux.goplatform_linux_amd64.go + platform_linux_arm64.go; same split for pkg/vm/backend_linux_*.go. amd64 gets q35; arm64 retains virt
  • highmem=on scoped to aarch64: moved into the per-platform MachineType string; QEMU correctly rejects it on q35
  • selectIndexManifest: clean 4-tier priority (platform+raw → platform-any → any-raw → first) with per-manifest lazy raw-probe caching; well-tested
  • VsockTransport: races mdvsock.Dial against a timeout via a buffered goroutine+channel; guarded by HostHasVsock() which checks /dev/vhost-vsock char device and filters out WSL2 via /proc/version
  • allocateVsockCID: best-effort file-scan allocator returning the lowest unused CID ≥ 3; correctly documented as racy, with QEMU as authoritative collision detector
  • QEMU log capture: cmd.Stderr/cmd.Stdout now written to <vm-dir>/qemu.log for post-failure forensics

Notable concerns (all non-blocking P2s):

  • qemu.log opened with O_TRUNC overwrites the previous failure log on retry — consider O_APPEND
  • Stopped VMs retain their VsockCID in state.json, consuming a new CID on each mb start (practically harmless given 2³¹ space)
  • cmd/vmhost/platform_linux_amd64.go duplicates pkg/vm/backend_linux_amd64.go config with a fragile "keep in lockstep" comment

Confidence Score: 4/5

Safe to merge; all identified issues are non-blocking P2 quality-of-life concerns with no correctness or data-loss risk

The core logic is sound and well-tested: the build-tag split is correct, the manifest selector is clean and has thorough table-driven tests, the vsock fallback to TCP is safe, and the CID allocator is correctly documented as best-effort. The three P2 comments (O_TRUNC log overwrite, stale CID retention, duplicated platform config) are improvement suggestions that don't affect correctness or safety of the primary user path. End-to-end testing on Fedora 43 x86_64 with multi-VM CID allocation is a strong signal.

pkg/vm/qemu.go (O_TRUNC on qemu.log), pkg/vm/util.go (CID recycling), cmd/vmhost/platform_linux_amd64.go + pkg/vm/backend_linux_amd64.go (duplicated config)

Important Files Changed

Filename Overview
pkg/vm/qemu.go Core boot logic extended with vsock CID allocation, effective control-plane mode resolution, and QEMU log capture; minor concern with O_TRUNC discarding previous crash logs on restart
pkg/vsock/transport_linux.go New VsockTransport.Dial races mdvsock.Dial against a timer; drain goroutine mitigates but can't guarantee cleanup if the kernel blocks indefinitely on a pathological CID
pkg/vm/util.go allocateVsockCID is well-structured with clear best-effort semantics, but stopped VMs retain their CIDs in state.json and are never recycled until VM destroy
pkg/mixtapes/pull.go 4-tier platform-aware manifest selection cleanly extracted into a testable selectIndexManifest with proper priority ordering and lazy raw-check caching
pkg/vm/backend_linux_amd64.go New file with correct linux/amd64 KVM+q35 config, vsock-pci, and io_uring; config duplicated from cmd/vmhost counterpart with a lockstep comment
cmd/vmhost/platform_linux_amd64.go New vmhost-side platform config for linux/amd64; duplicates the backend config struct literal with a fragile "keep in lockstep" comment instead of sharing code
pkg/vm/vsock_host_linux.go HostHasVsock() correctly gates on /dev/vhost-vsock char device presence and WSL2 marker in /proc/version; clean probe with graceful fallback
pkg/vm/backend_linux_arm64.go Correctly narrowed to arm64 build tag; adds highmem=on and vsock support to match amd64 parity
pkg/mixtapes/pull_test.go Good table-driven tests for all 4 priority tiers in selectIndexManifest, plus an empty-manifests error case
pkg/vm/util_test.go Tests cover hole-filling CID allocation, zero-CID entries being ignored, and missing vms directory graceful handling

Sequence Diagram

sequenceDiagram
    participant mb as mb (client)
    participant daemon as daemon
    participant vmhost as vmhost process
    participant qemu as QEMU child
    participant guest as stereosd (guest)

    mb->>daemon: mb up
    daemon->>daemon: PrepareQEMUDisk() copy image, EFI vars, state.json
    daemon->>vmhost: spawn vmhost process
    vmhost->>vmhost: HostHasVsock()? /dev/vhost-vsock + !WSL2
    alt vsock available
        vmhost->>vmhost: allocateVsockCID() scan state.json → lowest free CID >= 3
        vmhost->>qemu: qemu-system-x86_64 -machine q35,accel=kvm -device vhost-vsock-pci,guest-cid=N
    else fallback TCP
        vmhost->>vmhost: allocatePort() for stereosd
        vmhost->>qemu: qemu-system-x86_64 -machine q35,accel=kvm -netdev user,hostfwd=tcp::PORT-:1024
    end
    qemu->>guest: kernel boots via direct -kernel/-initrd
    guest->>guest: stereosd listens on vsock port 1024
    vmhost->>guest: VsockTransport.Dial(CID, 1024) mdvsock.Dial in goroutine + timeout race
    vmhost->>guest: WaitForReady / SetConfig / InjectSSHKey / Secrets / Mounts
    vmhost->>daemon: boot complete
    daemon->>mb: sandbox ready
Loading

Reviews (1): Last reviewed commit: "✨ feat: Linux x86_64 runtime + native AF..." | Re-trigger Greptile

Comment thread pkg/vm/qemu.go Outdated
Comment thread pkg/vm/util.go
Comment on lines +33 to +61
func allocateVsockCID(baseDir string) (uint32, error) {
used := map[uint32]bool{}

entries, err := os.ReadDir(VMsDir(baseDir))
if err != nil && !os.IsNotExist(err) {
return 0, fmt.Errorf("reading VMs directory: %w", err)
}
for _, e := range entries {
if !e.IsDir() {
continue
}
state, err := loadState(filepath.Join(VMsDir(baseDir), e.Name()))
if err != nil {
continue
}
if state.VsockCID > 0 {
used[state.VsockCID] = true
}
}

// Pick the lowest free CID. We start at 3 (first non-reserved) and cap
// at 2^31 defensively — in practice we run out of RAM long before CIDs.
for cid := uint32(3); cid < 1<<31; cid++ {
if !used[cid] {
return cid, nil
}
}
return 0, fmt.Errorf("no vsock CID available (all in use under %s)", baseDir)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 CIDs from stopped VMs are never recycled

allocateVsockCID marks a CID as used if it appears in any VM's state.json, regardless of whether that VM is currently running. When a VM is stopped via mb down, its state.json retains the VsockCID field. Each subsequent mb start on that VM consumes a new CID (the old one is still present in the state file and is thus skipped), and the stale CID is never freed until the VM is destroyed.

While the 2³¹ CID space makes exhaustion practically impossible, this also means mb start on a stopped VM always changes the guest's CID rather than reusing the previously assigned one. A simpler fix is to clear VsockCID in the state file when the VM stops:

// In Down/ForceDown: update state to clear the CID once the process exits.
stateFile.VsockCID = 0
_ = saveState(inst.Dir, stateFile)

Comment thread pkg/vsock/transport_linux.go
Comment thread cmd/vmhost/platform_linux_amd64.go Outdated
@yeazelm yeazelm requested a review from jpmcb April 21, 2026 15:30
Copy link
Copy Markdown
Contributor

@jpmcb jpmcb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit: :shipit: :shipit:

yeazelm added 2 commits April 25, 2026 15:11
* Splits `pkg/vm/backend_linux.go` and `cmd/vmhost/platform_linux.go`
  by GOARCH so amd64 gets `qemu-system-x86_64`, q35, and OVMF firmware
  paths covering Fedora, Debian/Ubuntu, and NixOS. Was hardcoded to
  aarch64 under a bare `linux` build tag.
* `pkg/mixtapes/pull.go:resolveFromIndex` now filters by
  `runtime.GOOS`/`GOARCH` with a 4-tier priority (platform+raw →
  platform-any → any raw → first). Previously picked the first raw
  manifest, which on a multi-arch index would boot the wrong kernel.
* Adds a native AF_VSOCK transport for Linux via
  `github.com/mdlayher/vsock` — Go's stdlib `net.FileConn` rejects
  AF_VSOCK with "protocol not supported". Host capability probe
  checks `/dev/vhost-vsock` and falls back to TCP on WSL2 or hosts
  without `vhost_vsock`. Per-VM CID allocator scans existing state
  files to avoid collisions.
* Moves `highmem=on` into per-platform `MachineType` strings; it's
  aarch64-virt-only and QEMU rejects it on x86 q35.
* Captures QEMU stderr to `<vm-dir>/qemu.log` so launch failures
  leave a forensic trace.

Signed-off-by: Matt Yeazel <matt@papercompute.com>
The `bucketuploader`/`checksumer`/`ghrelease`/`golangcilint` daggerverse
deps pinned in `dagger.json` now require dagger engine v0.20.6, so CI
installing v0.20.1 failed with "module requires dagger v0.20.6, but
you have v0.20.1". Bumps `dagger.json` `engineVersion` and the
`DAGGER_VERSION` env var in all five workflows (ci, pr, cut-release,
nightly, release) so the installed engine matches what the resolved
module graph wants.

Signed-off-by: Matt Yeazel <matt@papercompute.com>
@yeazelm yeazelm force-pushed the mt/x86_64-support branch from c9a7546 to dc8daa2 Compare April 25, 2026 22:26
@yeazelm yeazelm merged commit 3160f75 into main Apr 27, 2026
10 checks passed
@yeazelm yeazelm deleted the mt/x86_64-support branch April 27, 2026 02:55
@continue
Copy link
Copy Markdown

continue Bot commented Apr 27, 2026

No docs update needed. This PR adds Linux x86_64 runtime support with native AF_VSOCK control plane to masterblaster (mb), but the tapes.dev documentation site covers only the tapes telemetry product (LLM proxy, Merkle DAG storage, search, MCP). There is no documentation about stereos, masterblaster, VMs, or sandbox runtime in tapes.dev.

PR #52 was merged: ✨ feat: Linux x86_64 runtime + native AF_VSOCK control plane
PR URL: #52
Merged by: yeazelm

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.

2 participants