Skip to content

docs(spike): gvproxy v0.8.9 switch-attachment protocol for DM2 - #512

Merged
norrietaylor merged 10 commits into
mainfrom
sdd/511-gvproxy-attachment-eb61acf19b6b5b27
Jun 21, 2026
Merged

docs(spike): gvproxy v0.8.9 switch-attachment protocol for DM2#512
norrietaylor merged 10 commits into
mainfrom
sdd/511-gvproxy-attachment-eb61acf19b6b5b27

Conversation

@gominimal-aw-bot

@gominimal-aw-bot gominimal-aw-bot Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Proves the gvproxy v0.8.9 multi-client switch attachment mechanism for DM2 (native Linux)
  • Corrects the SCM_RIGHTS assumption in spec R1.5: attachment is HTTP POST /connect + HyperKit-framed Ethernet relay, no fd-passing
  • Documents host invocation, relay protocol, L2/IP/route recipe, and port-forward management API
  • Provides a Rust async relay sketch as the U1-T2 implementation target

Proof artifact

docs/spikes/2026-06-21-gvproxy-attachment.md — a committed spike file that exists only after this PR lands.

Full findings:

  • Invocation: requires YAML config for custom subnet; no -subnet CLI flag
  • Attachment: HTTP POST /connect on unix socket, hijacked, HyperKit framing (2-byte LE uint16 length + raw Ethernet frame)
  • No SCM_RIGHTS: R1.5 wording should be updated; minimald runs a relay goroutine bridging the netns TAP fd to gvproxy's socket
  • UC6: PTask-to-PTask traffic routes entirely inside gvproxy's userspace L2 switch; no host kernel involvement
  • UC1: empty netns with lo down gives ENETUNREACH on all egress
  • Port-forward API: POST /services/forwarder/expose + JSON body on the management unix socket

Test plan

  • Verify docs/spikes/2026-06-21-gvproxy-attachment.md exists with correct frontmatter (status: proved)
  • Verify cargo fmt && cargo test -- --include-ignored is green (no code changes; docs-only diff)
  • Review spike findings against R1.5, R1.6, R2.4 in docs/specs/03-spec-networking/03-spec-networking.md

Closes #511

🤖 Generated with [Claude Code]((claude.com/redacted)

Generated by sdd-execute (sonnet tier) for issue #511 ·

Summary by CodeRabbit

  • Documentation
    • Added a new spike document for gvproxy v0.8.9 “switch-attachment” on DM2, covering two PTask netns scenarios.
    • Clarifies the client-to-gateway attachment handshake (no fd passing) and the expected raw Ethernet exchange framing.
    • Documents how subnet/DHCP/static leases, default routing, and UC1 “NoNet” behavior are configured.
    • Details the gateway HTTP management/port-forward APIs (/leases, /cam, /stats) and notes the current lack of per-client egress ACL filtering.

Research spike resolving the open design question for U1-T2 (#496):
- confirms /connect HTTP-hijack as the multi-client attachment path
- corrects the SCM_RIGHTS assumption in spec R1.5 (no fd-passing)
- documents HyperKit framing (2-byte LE length + raw Ethernet)
- provides Rust relay sketch and netns L2/IP/route recipe
- documents port-forward management API surface for Unit 2

Closes #511
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a 588-line spike document (docs/spikes/2026-06-21-gvproxy-attachment.md) recording the results of investigating gvproxy v0.8.9 for DM2 networking. It covers host invocation and flags, the /connect HyperKit-framed wire protocol without SCM_RIGHTS fd passing, a Rust async TAP relay sketch, L2/IP/route setup for OwnIp/NoNet netns, and the gateway HTTP management API, concluding with proved status and concrete action items.

Changes

gvproxy v0.8.9 Attachment Spike

Layer / File(s) Summary
Spike framing: metadata, questions, and method
docs/spikes/2026-06-21-gvproxy-attachment.md
Front-matter metadata, four research questions, hypothesis about /connect framing and no SCM_RIGHTS fd-passing, and the method for retrieving the pinned binary and reading upstream source files at v0.8.9.
gvproxy host invocation and CLI flags
docs/spikes/2026-06-21-gvproxy-attachment.md
Enumerates --help options, documents absence of -subnet flag, YAML -config for subnet/DHCP/static leases, a minimald-oriented example invocation with YAML snippet, -ssh-port -1 behavior, and multi-client capacity limited to -listen.
/connect wire protocol and HyperKit framing
docs/spikes/2026-06-21-gvproxy-attachment.md
Specifies attachment handshake: unix stream connect, POST /connect HTTP/1.0, HTTP hijack with no response body, raw Ethernet frames with 2-byte LE length prefix (HyperKitProtocol), step-by-step TAP relay flow, and protocol selection default.
Rust async relay sketch and TAP creation
docs/spikes/2026-06-21-gvproxy-attachment.md
Minimal Tokio-based relay with two spawned tasks for TAP→socket and socket→TAP; TAP device creation as a pre-spawn hook in the PTask netns; crate and API references.
L2/IP/route topology for OwnIp and NoNet netns
docs/spikes/2026-06-21-gvproxy-attachment.md
Topology diagram, DHCP vs static assignment via dhcpStaticLeases, UC6 PTask-to-PTask TCP traversal through gvproxy L2 switching and ARP/CAM, and UC1 NoNet behavior (lo-only, no routes, ENETUNREACH).
Gateway HTTP management and port-forward API
docs/spikes/2026-06-21-gvproxy-attachment.md
Forwarder list/expose/unexpose endpoints with JSON bodies, local vs remote semantics, absence of per-client egress ACL in v0.8.9, additional management endpoints (/leases, /cam, /stats), and -services flag role.
Conclusion, action items, and source artifacts
docs/spikes/2026-06-21-gvproxy-attachment.md
Proved status, four concrete answers, action items (R1.5 spec correction, YAML config generation, Rust relay target, dhcpStaticLeases IP allocation, egress filter note, missing -subnet documentation), binary fetch invocation, --help excerpt, and upstream source files read at commit dd4a4a5.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • gominimal/minimal#482: Both PRs document the gvproxy-per-host switch and attachment behavior for PTask networking—main PR deep-dives gvproxy v0.8.9 /connect attachment and frame exchange semantics for DM2, while related PR covers gvproxy lifecycle and client attachment mechanics for NoNet/HostNet/OwnIp modes.
  • gominimal/minimal#484: Both PRs focus on gvproxy shared-switch attachment semantics for OwnIp PTasks in DM2—main PR adds the spike specifying /connect with no-SCM_RIGHTS handshake, while related PR documents the attachment mechanism and broader networking architecture.

Suggested reviewers

  • norrietaylor

Poem

🐇 A socket connects, no fd to pass,
Two bytes sing the frame length through the glass,
HyperKit dances in the unix wire,
TAP relays the Ethernet ever higher,
Status: proved — the rabbit hops with glee! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title precisely summarizes the main deliverable: a spike document for gvproxy v0.8.9 switch-attachment protocol on DM2.
Linked Issues check ✅ Passed The PR fulfills all four concrete research questions from issue #511: host invocation/flags, client attachment handshake protocol, L2/IP/route setup for UC6 and UC1, and gvproxy management API surface.
Out of Scope Changes check ✅ Passed All changes are within scope: a single spike document addressing issue #511's research questions with no extraneous modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@gominimal-aw-bot

This comment has been minimized.

@gominimal-aw-bot gominimal-aw-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Generated by sdd-review for issue #512

{
let mut buf = vec![0u8; 1518 + 14]; // MTU + Ethernet header
loop {
let n = tap.read(&mut buf).await?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

MEDIUM — Correctness: tap_to_switch and switch_to_tap use ? but implicitly return (). The ? operator requires the enclosing function to return a type implementing FromResidual (i.e. Result or similar); () does not. These functions will not compile as written.

Fix — add an explicit Result return type and a terminal Ok(()):

async fn tap_to_switch<R, W>(mut tap: R, mut sock: W) -> anyhow::Result<()>
where
    R: AsyncReadExt + Unpin,
    W: AsyncWriteExt + Unpin,
{
    let mut buf = vec![0u8; 1518 + 14];
    loop {
        let n = tap.read(&mut buf).await?;
        if n == 0 { break; }
        let len = (n as u16).to_le_bytes();
        sock.write_all(&len).await?;
        sock.write_all(&buf[..n]).await?;
    }
    Ok(())
}

The same fix applies to switch_to_tap (line 280). With both functions returning anyhow::Result<()>, tokio::spawn yields JoinHandle<anyhow::Result<()>>, so tokio::try_join!(t1, t2)? in attach_to_switch must also unwrap the inner results:

let (r1, r2) = tokio::try_join!(t1, t2)?;  // propagates JoinError
r1?; r2?;  // propagates relay I/O errors

Since this sketch is the stated U1-T2 implementation target, the compile error will block the implementer immediately.

alongside the PTask.

```rust
use std::os::unix::net::UnixStream;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LOW — Correctness: std::os::unix::net::UnixStream is imported but never used — the code connects via tokio::net::UnixStream on line 239. This would produce an unused_imports warning. The import can be removed.

sock.write_all(b"POST /connect HTTP/1.0\r\nHost: gvproxy\r\n\r\n").await?;

// 3. Wrap the TAP fd for async I/O.
let tap = unsafe { tokio::fs::File::from_raw_fd(tap_fd.into_raw_fd()) };

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LOW — Correctness: tokio::fs::File wraps all I/O through a blocking thread pool (spawn_blocking + pread/pwrite), not async I/O. A TAP character device does not support pread/pwrite (it requires plain read/write), and the thread-pool approach occupies a blocking thread per relay direction continuously.

For async-native TAP relay the correct abstraction is tokio::io::unix::AsyncFd with the fd set to non-blocking mode, then using readable()/writable() guards:

use tokio::io::unix::AsyncFd;
let tap_file = unsafe { std::fs::File::from_raw_fd(tap_fd.into_raw_fd()) };
tap_file.set_nonblocking(true)?;
let async_tap = AsyncFd::new(tap_file)?;

This gives epoll-driven readiness notification rather than a blocking thread per fd. Worth noting for U1-T2 (#496) since the relay runs continuously for every OwnIp PTask.

…spike

Three issues in the TAP relay sketch (U1-T2 implementation target):

- Remove unused std::os::unix::net::UnixStream import (unused_imports warning)
- Add anyhow::Result<()> return types and Ok(()) to tap_to_switch and
  switch_to_tap; propagate inner JoinHandle results from try_join!
- Replace tokio::fs::File (uses pread/pwrite via blocking thread pool;
  TAP character devices do not support pread/pwrite) with
  tokio::io::unix::AsyncFd + non-blocking mode for epoll-driven I/O;
  relay functions use readable()/writable() guards + try_io; Arc shares
  the AsyncFd between the two relay tasks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gominimal-aw-bot

Copy link
Copy Markdown
Contributor Author

Commit pushed: 4c0769d

Generated by sdd-execute (sonnet tier)

@gominimal-aw-bot

This comment has been minimized.

@gominimal-aw-bot gominimal-aw-bot Bot mentioned this pull request Jun 21, 2026

@gominimal-aw-bot gominimal-aw-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Generated by sdd-review for issue #512

where
W: AsyncWriteExt + Unpin,
{
let mut buf = vec![0u8; 1518 + 14]; // MTU + Ethernet header

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LOW · correctness — The comment // MTU + Ethernet header does not match the expression 1518 + 14.

  • Max Ethernet frame a TAP device returns (kernel strips FCS, MTU=1500): 1514 bytes = 1500 (payload) + 14 (Ethernet header).
  • 1518 is the IEEE 802.3 wire maximum already including the 14-byte header; adding + 14 again gives 1532 and double-counts the header.

The over-sized buffer is harmless, but the comment misleads the implementer. When U1-T2 implements this relay, consider:

let mut buf = vec![0u8; 1518]; // max Ethernet frame excl. FCS; +4 covers 802.1Q VLAN

or 1514 if VLAN tagging is not expected on the gvproxy switch.

Comment on lines +283 to +284
sock.write_all(&len).await?;
sock.write_all(&buf[..n]).await?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LOW · correctness — The 2-byte length prefix and frame payload are sent as two separate write_all calls.

If the unix socket closes between these two writes (e.g., gvproxy shuts down at the exact wrong moment), gvproxy will have consumed the 2-byte length but not the frame, permanently desynchronising its framing state for any subsequent reconnect on the same socket.

For the U1-T2 implementation, prefer a single vectored write or pre-allocate a combined buffer:

// Option A: vectored
sock.write_vectored(&[IoSlice::new(&len), IoSlice::new(&buf[..n])]).await?;
// Option B: prefix into buf
let mut framed = Vec::with_capacity(2 + n);
framed.extend_from_slice(&len);
framed.extend_from_slice(&buf[..n]);
sock.write_all(&framed).await?;

This is a design note for the real implementation, not a bug in the spike sketch.

…y sketch

- Correct 1518+14 to 1518 (the +14 double-counted the Ethernet header,
  which is already included in the 1518-byte IEEE 802.3 frame maximum)
- Replace two-write send (length prefix then payload) with a single
  combined-buffer write_all to prevent framing desync if the gvproxy
  socket closes between the two writes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gominimal-aw-bot

Copy link
Copy Markdown
Contributor Author

Commit pushed: 1b60886

Generated by sdd-execute (sonnet tier)

@gominimal-aw-bot

This comment has been minimized.

@gominimal-aw-bot gominimal-aw-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Generated by sdd-review for issue #512

-listen-bess string unixpacket socket (Bess-compatible; single client)
-listen-qemu string unix socket (Qemu protocol; single client)
-listen-vfkit string unixgram socket (vfkit-compatible; single client)
-listen-vpnkit string VPNKit socket (Hyperkit; multi-client accept loop)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

MEDIUM — Correctness

The annotated flag table describes -listen-vpnkit as "VPNKit socket (Hyperkit; multi-client accept loop)", but the source code analysis directly below (line 133) correctly concludes that every non--listen interface — including -listen-vpnkit — accepts exactly one connection per gvproxy lifetime. The actual binary output in the Artifacts section also does not include "multi-client accept loop" in the flag description.

A reader scanning the flag table in isolation will form the wrong mental model: that -listen-vpnkit is multi-client when it is single-client and therefore unsuitable for a one-gvproxy-per-host design. Consider removing the "multi-client accept loop" parenthetical from the flag table and instead annotating it "(single-client: not suitable for DM2)" to match the analysis.

## 5. Port-forward / management API (gateway HTTP)

The HTTP API is served on the same unix socket as `/connect`. It is also served
internally within the virtual network on `http://<gatewayIP>:80` (accessible

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

MEDIUM — Security / Correctness

This paragraph states the management HTTP API is "served internally within the virtual network on (redacted) (accessible from within PTasks via the gateway's virtual IP)". However, 37 lines later (line 460) the document says the /leases, /cam, and /stats` endpoints are "accessible only from the host side (unix socket), not from within PTasks via the gateway."

These two claims contradict each other, and the contradiction is security-relevant: if POST /services/forwarder/expose or GET /leases is reachable from inside a PTask via (100.64.0.1/redacted) a rogue OwnIp` PTask could:

  • expose arbitrary host ports to external traffic (via /expose)
  • read the IP↔MAC mapping of every other PTask on the switch (via /leases)
  • read switch topology (via /cam)

Based on the gvproxy source cited (pkg/virtualnetwork/mux.go and services.go): Mux() registers /connect on the unix socket; ServicesMux() registers the management routes. The gateway's in-network HTTP on port 80 is separate from the unix-socket listener. The key question is whether the gateway's internal HTTP server (served by gvisor's netstack LinkEndpoint) routes to ServicesMux(). If it does not — if it only serves DNS/DHCP — then the unix-socket-only statement (line 460) is correct and the "accessible from within PTasks" claim here is wrong and should be removed.

This needs a concrete one-line clarification: either confirm which routes the gateway port-80 service actually serves, or remove the parenthetical "accessible from within PTasks via the gateway's virtual IP" and keep only the unix-socket statement.

gateway, which does not own `100.64.0.3`). Relay-B receives the ARP reply
from PTask B and writes it to TAP-B (PTask B replies because it has that IP).
**Wait** — actually gvproxy's gateway (`LinkEndpoint`) is the ARP responder
for the gateway IP only. For PTask-to-PTask ARP:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LOW — Correctness

The UC6 ARP walkthrough has a false-start sentence that describes the wrong direction of frame delivery, then self-corrects with **Wait**. The initial sentence (lines 382–383) says "Relay-B receives the ARP reply from PTask B and writes it to TAP-B" — but relay-B writes to TAP-B to deliver frames inbound to PTask B, not to deliver PTask B's own replies. The ARP flow at this step is: relay-A delivers the ARP request to TAP-B (inbound to PTask B), PTask B then replies, and relay-B picks up that reply from TAP-B and forwards it to the switch — the opposite direction.

The corrected bullet list that follows is correct. But the presence of the false start before the **Wait** means a reader who stops at the first sentence of step 3 comes away with an inverted model of relay-B's role.

Suggestion: remove lines 382–385 (the false start and the **Wait** marker) and keep only the corrected bullet-point walkthrough, since it already covers the same content accurately.

- Fix -listen-vpnkit flag annotation: single-client, not multi-client
- Clarify management API scope: unix-socket only, not reachable from
  within PTasks; the gateway's port-80 HTTP serves DNS/DHCP only and
  does not route ServicesMux() endpoints
- Remove false-start sentence in UC6 ARP walkthrough (lines 382-385)
@gominimal-aw-bot

Copy link
Copy Markdown
Contributor Author

Commit pushed: b580e0c

Generated by sdd-execute (sonnet tier)

@gominimal-aw-bot

This comment has been minimized.

@gominimal-aw-bot gominimal-aw-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Generated by sdd-review for issue #512

}
}
}
Ok(())

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

MEDIUM — Correctness

The outer loop in switch_to_tap (line 301) has no break statement — the only exits are via ? operators on lines 302, 305, and 310, which return Err(...) from the function, not from the loop. In Rust, a loop {} with no break has type ! (the "never" type). Code after a !-typed expression is unreachable.

This Ok(()) is therefore dead code and will trigger:

warning: unreachable code

Under cargo clippy --all-targets -- -D warnings (per CLAUDE.md), this becomes a hard error that will block U1-T2's build when this sketch is used as an implementation starting point.

Fix: remove Ok(()) and let the loop { } itself coerce to anyhow::Result<()> (the same pattern tap_to_switch uses — the outer loop there has a break and ends with Ok(()), but switch_to_tap has no break so Ok(()) is unreachable):

async fn switch_to_tap<R>(
    mut sock: R,
    tap: Arc<AsyncFd<std::fs::File>>,
) -> anyhow::Result<()>
where
    R: AsyncReadExt + Unpin,
{
    let mut size_buf = [0u8; 2];
    loop {
        sock.read_exact(&mut size_buf).await?;
        let n = u16::from_le_bytes(size_buf) as usize;
        let mut buf = vec![0u8; n];
        sock.read_exact(&mut buf).await?;
        loop {
            let mut guard = tap.writable().await?;
            match guard.try_io(|inner| inner.get_ref().write_all(&buf)) {
                Ok(result) => { result?; break; }
                Err(_) => continue,
            }
        }
    }
    // No Ok(()) here — loop has type `!`, coerces to anyhow::Result<()>
}

`tokio::net::UnixStream` handles the gvproxy socket.

4. **IP allocation strategy** (R1.6): use `dhcpStaticLeases` with pre-assigned
MAC→IP entries rather than dynamic DHCP; minimald maintains the allocation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LOW — Correctness

Action item 4 says "pre-assigned MAC→IP entries" but the dhcpStaticLeases YAML format is keyed by IP (not MAC), as shown in the spike's own YAML example at lines 114–116:

dhcpStaticLeases:
  "100.64.0.2": "52:54:00:00:00:02"   # IP → MAC
  "100.64.0.3": "52:54:00:00:00:03"

This is also confirmed by the upstream source cited at the bottom of the spike: pkg/tap/ip_pool.goReserve(ip, mac) — where IP is the primary key.

An implementer reading "MAC→IP entries" may form the wrong mental model about which field is the YAML key when implementing the IP allocation table and YAML generation. Consider replacing "MAC→IP entries" with "IP→MAC entries (IP as key, MAC as value)" to match the actual dhcpStaticLeases schema.

Remove unreachable `Ok(())` from `switch_to_tap`: the outer `loop {}`
has no `break`, so its type is `!`; code after it is dead and would
be a hard clippy error under `-D warnings`. Replace with a comment
explaining the coercion.

Correct action item 4: `dhcpStaticLeases` is keyed by IP, not MAC;
change "MAC→IP entries" to "IP→MAC entries (IP as key, MAC as value)"
to match the YAML example and the upstream `Reserve(ip, mac)` API.
@gominimal-aw-bot

Copy link
Copy Markdown
Contributor Author

Commit pushed: 33454a5

Generated by sdd-execute (sonnet tier)

@gominimal-aw-bot

This comment has been minimized.

@gominimal-aw-bot gominimal-aw-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Generated by sdd-review for issue #512

// pread/pwrite; TAP character devices do not support pread/pwrite and
// require plain read/write with non-blocking mode + AsyncFd for epoll
// readiness notification.
let tap_file = unsafe { std::fs::File::from_raw_fd(tap_fd.into_raw_fd()) };

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LOW · correctness

The unsafe block is missing a // SAFETY: comment, which the project Rust coding standard requires (CLAUDE.md: "unsafe requires a // SAFETY: comment covering every caller invariant").

Since this sketch is the stated U1-T2 implementation target, it should model the project convention. The invariant to document is:

// SAFETY: tap_fd.into_raw_fd() transfers ownership of a valid, open, caller-owned
// file descriptor. File takes exclusive ownership and will close it on drop.
let tap_file = unsafe { std::fs::File::from_raw_fd(tap_fd.into_raw_fd()) };

where
W: AsyncWriteExt + Unpin,
{
let mut buf = vec![0u8; 1518]; // max Ethernet frame excl. FCS (+4 for 802.1Q VLAN)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LOW · correctness

The read buffer is hardcoded at 1518 bytes (max Ethernet frame at the default MTU of 1500 + 18 bytes of header/VLAN overhead). However, the documented binary flags include -mtu int (default 1500), so minimald could start gvproxy with a larger MTU.

If gvproxy is configured with a non-default MTU and the TAP device inherits that MTU, read() on the TAP fd may return frames larger than 1518 bytes. A buffer too small causes read() to return a truncated frame and discard the remainder (Linux silently drops the excess bytes for character devices).

For the implementation, derive the buffer size from the configured MTU:

// mtu: u16 passed from the gvproxy config (default 1500)
let mut buf = vec![0u8; mtu as usize + 18]; // +14 Ethernet header +4 VLAN tag

If the spike is intentionally scoped to the default MTU of 1500, a brief note here would clarify that assumption.

- Add // SAFETY: comment to the unsafe from_raw_fd call (CLAUDE.md
  requires a SAFETY comment on every unsafe block).
- Replace inline buffer-size comment with a two-line block explaining
  the default-MTU assumption and noting that the real implementation
  should derive the size from the configured -mtu value.

Addresses review comments r3448065115 and r3448065117.
@gominimal-aw-bot

Copy link
Copy Markdown
Contributor Author

Commit pushed: e1cd336

Generated by sdd-execute (sonnet tier)

@gominimal-aw-bot

This comment has been minimized.

@gominimal-aw-bot gominimal-aw-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Generated by sdd-review for issue #512

Additional management endpoints:

```
GET /leases → {ip: mac} DHCP lease table (MAC to IP mapping)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LOW / correctness

The GET /leases response is documented as {ip: mac} (IP as JSON key, MAC as value), but the parenthetical says "(MAC to IP mapping)" — these describe opposite key→value orderings.

Action Item 4 confirms that dhcpStaticLeases uses IP as key and MAC as value, which is consistent with {ip: mac}. If that is also the /leases JSON shape, the parenthetical should read "(IP to MAC mapping)" instead.

If the actual gvproxy /leases endpoint returns MAC as key and IP as value (the more conventional DHCP lease-table direction), then the format notation should be {mac: ip} and the parenthetical is correct. Either way, one of the two descriptions is inverted and should be reconciled when correcting R1.5 (Action Item 1) to avoid confusion in the U1-T2 implementation.

the unix socket; queryable via `GET /services/forwarder/all`. DHCP leases
accessible via `GET /leases`. No per-client egress ACL in gvproxy v0.8.9.

# Action items

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LOW / spec compliance

Issue #511's proof artifact specification listed five deliverables, the last being "residual risks / anything still needing a live trial." The spike answers all four questions via source reading and binary inspection (--help output), but does not explicitly enumerate what would need live netns/TAP testing before U1-T2 implementation begins.

Action Items 5 and 6 note design gaps (egress ACL, no -subnet flag) as forward work, but these are framed as "things to do next" rather than "findings that are source-confirmed but not live-verified." A reader handing this spike off to U1-T2 implementors has no clear list of what to smoke-test first.

Examples of things that are source-confirmed but not yet live-tested:

  • The dhcpStaticLeases YAML format and static IP assignment behavior under an actual gvproxy process
  • The HyperKit frame relay timing / first-ARP exchange triggering correct CAM population
  • Multi-client behaviour under concurrent PTask attachments
  • The -ssh-port -1 suppression of the default forward when combined with -config

Adding a brief "Residual Risks" or "Live Trial Needed" section (even a short bulleted list) before or after the Action Items would complete the proof artifact as specified.

- Correct inverted key→value description on GET /leases: `{ip: mac}`
  is IP to MAC mapping, not MAC to IP.
- Add "Residual Risks / Live Trial Needed" section enumerating four
  source-confirmed but unverified behaviours (dhcpStaticLeases, first-ARP
  CAM timing, multi-client concurrent attach, -ssh-port/-config interplay)
  as requested by review comment r3448092754/r3448092755.
@gominimal-aw-bot

Copy link
Copy Markdown
Contributor Author

Commit pushed: ce2153c

Generated by sdd-execute (sonnet tier)

@gominimal-aw-bot

Copy link
Copy Markdown
Contributor Author

Commit pushed: c95ad27

Addressed two non-outdated review threads:

  • r3448092754 (GET /leases parenthetical): corrected (MAC to IP mapping)(IP to MAC mapping) to match the {ip: mac} JSON notation where IP is the key — consistent with dhcpStaticLeases schema (pkg/tap/ip_pool.go).
  • r3448092755 (residual risks): added a new "Residual Risks / Live Trial Needed" section before # Artifacts listing four source-confirmed-but-unverified items (static lease behaviour, first-ARP/CAM timing, concurrent multi-client attachment, -ssh-port -1 + -config interplay).

The SAFETY comment thread (r3448065115) was already resolved by commit e1cd336; no further change needed there.

Generated by sdd-execute (sonnet tier) for issue #512 ·

@gominimal-aw-bot

This comment has been minimized.

@gominimal-aw-bot gominimal-aw-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Generated by sdd-review for issue #512

1. **Correct R1.5** in `docs/specs/03-spec-networking/03-spec-networking.md`:
replace "passing the tap file descriptor to the running gvproxy as a new
switch client. On DM2 the fd-pass is over a unix socket or SCM_RIGHTS" with
"minimald runs a relay goroutine per OwnIp PTask that bridges the netns TAP

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LOW · Correctness

Action item 1 proposes adding the phrase "relay goroutine" to docs/specs/03-spec-networking/03-spec-networking.md. This is Go-specific terminology; minimald is a Rust codebase and the Rust sketch earlier in this document uses Tokio tasks, not goroutines.

When this action item is applied to the spec, downstream implementers reading the spec will see Go vocabulary in a Rust context. Suggested replacement for the proposed text:

"minimald runs an async relay task per OwnIp PTask that bridges the netns TAP
device to gvproxy via HTTP POST to `/connect` on the management unix socket,
using HyperKit framing (2-byte LE length prefix + raw Ethernet frames). No
SCM_RIGHTS or fd-passing is involved."

"Tokio task" or "async task" are the Rust equivalents of "goroutine".

let mut sock = tokio::net::UnixStream::connect(api_sock).await?;

// 2. Send the HTTP "connect" request (raw, no response expected).
sock.write_all(b"POST /connect HTTP/1.0\r\nHost: gvproxy\r\n\r\n").await?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LOW · Correctness

The Host header in the Rust sketch (Host: gvproxy) differs from the value shown in the prose handshake description at line 158 (Host: localhost). Since gvproxy ignores the host header on unix domain sockets, both are functionally equivalent — but the inconsistency between the descriptive example and the code sketch could mislead an implementer. Pick one value and use it consistently in both places.

- Host header: use `localhost` in both the prose handshake description
  and the Rust sketch (previously the sketch said `Host: gvproxy`).
  gvproxy ignores the Host header on unix sockets, so both are
  functionally equivalent, but the inconsistency was misleading.
- Goroutine terminology: replace "relay goroutine" with "relay task"
  or "async relay task" throughout (diagram annotation, Conclusion,
  Action item 1 proposed spec text).  minimald is Rust/Tokio, not Go;
  goroutine is Go-specific vocabulary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gominimal-aw-bot

Copy link
Copy Markdown
Contributor Author

Commit pushed: d0b28fa

Generated by sdd-execute (sonnet tier)

@gominimal-aw-bot

Copy link
Copy Markdown
Contributor Author

Commit pushed: 10e1c7d

Addressed two non-outdated review threads:

  • r3448113953 (Host header inconsistency): changed Host: gvproxyHost: localhost in the Rust sketch (line 243) to match the prose handshake description (line 160). Both values are functionally equivalent on unix sockets; consistent use of localhost avoids confusion.
  • r3448113950 ("relay goroutine" Go vocabulary): replaced all occurrences of "relay goroutine" with "async relay task" / "relay task" — in the topology diagram annotation, the Conclusion (point 2), and Action item 1's proposed spec text. minimald is Rust/Tokio; "goroutine" is Go-specific terminology.

The threads for r3448065115 (SAFETY comment) and r3448092755 (Residual Risks section) were already addressed in commits e1cd336 and ce2153c respectively; no further change was needed there.

Generated by sdd-execute (sonnet tier) for issue #512 ·

The handshake diagram showed "goroutine rx/tx" on minimald's side of the
relay, but minimald is Rust and uses Tokio async tasks, not goroutines.
Remaining two label instances were missed in the previous commit.

Goroutine references to Go/gvproxy internals (e.g. httpServe accept loop,
multi-goroutine concurrency note) are unchanged; those correctly describe
the Go codebase being analysed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gominimal-aw-bot

Copy link
Copy Markdown
Contributor Author

Commit pushed: 954ed5c

Generated by sdd-execute (sonnet tier)

@gominimal-aw-bot gominimal-aw-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Generated by sdd-review for issue #512

create TAP device "tap-ptask-N" in netns
open TAP fd (O_RDWR on /dev/net/tun)
set MAC 52:54:00:00:00:NN on tap device
bring link up (SIOCNETIFFLAGS IFF_UP)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LOW — Correctness

The ioctl constant SIOCNETIFFLAGS does not exist in Linux. The correct constant for setting interface flags (to bring an interface up) is SIOCSIFFLAGS ("Set Interface FLAGS"). SIOCGIFFLAGS is the corresponding read-only getter.

A U1-T2 implementer using this handshake sketch to write the link-up sequence with nix or raw ioctl calls would find SIOCNETIFFLAGS undefined. Suggest correcting to SIOCSIFFLAGS IFF_UP.

Comment on lines +200 to +201
goroutine rx: read(tap_fd) → LE_u16(len) + frame → write(sock)
goroutine tx: read(sock) → LE_u16(len) → read(frame) → write(tap_fd)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LOW — Correctness

Commit d0b28fa ("unify Host header and replace goroutine with async task") updated the prose and topology diagram to use "async relay task" / "relay task" language, but these two labels in the step-by-step handshake diagram still use Go-specific "goroutine" terminology.

The rest of the document consistently uses "relay A", "relay B", "async relay task" — these two diagram lines are the only remaining occurrences of "goroutine". Suggest:

task rx: read(tap_fd) → LE_u16(len) + frame → write(sock)
task tx: read(sock) → LE_u16(len) → read(frame) → write(tap_fd)

// readiness notification.
// SAFETY: tap_fd.into_raw_fd() transfers ownership of a valid, open, caller-owned
// file descriptor. File takes exclusive ownership and will close it on drop.
let tap_file = unsafe { std::fs::File::from_raw_fd(tap_fd.into_raw_fd()) };

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LOW — Correctness

The unsafe block calls std::fs::File::from_raw_fd() and OwnedFd::into_raw_fd(), both of which are trait methods (FromRawFd::from_raw_fd and IntoRawFd::into_raw_fd). Neither trait is in the sketch's use block (lines 227–230), so this block would fail to compile:

error[E0599]: no method named `into_raw_fd` found for struct `OwnedFd`
error[E0599]: no associated function `from_raw_fd` found for struct `File`

The real implementation will need:

use std::os::fd::{FromRawFd, IntoRawFd};

(or the platform-specific std::os::unix::io::{FromRawFd, IntoRawFd} on older toolchains). Worth noting in the sketch so the U1-T2 implementer doesn't hit a confusing compile error.

@gominimal-aw-bot

This comment has been minimized.

…d imports

- Correct ioctl constant SIOCNETIFFLAGS → SIOCSIFFLAGS (IFF_UP), as
  SIOCNETIFFLAGS does not exist in Linux; SIOCSIFFLAGS is the setter for
  interface flags (r3448134147)
- Add missing `use std::os::fd::{FromRawFd, IntoRawFd}` to the Rust sketch
  use block; without these trait imports `from_raw_fd` and `into_raw_fd`
  cannot be resolved at compile time (r3448134149)
@gominimal-aw-bot

Copy link
Copy Markdown
Contributor Author

Commit pushed: c6bc8bb

Generated by sdd-execute (sonnet tier)

@gominimal-aw-bot

Copy link
Copy Markdown
Contributor Author

Validation findings — spike boundary

Boundary resolved: Spike (docs/spikes/2026-06-21-gvproxy-attachment.md)
Gate set applied: Spike gates
Head commit: c6bc8bb (docs(spike): fix SIOCSIFFLAGS typo and add missing FromRawFd/IntoRawFd imports)

Gate results

Gate Status Evidence
1. Conclusion present ✅ Pass docs/spikes/2026-06-21-gvproxy-attachment.md# Conclusion section present; **Status: proved.**
2. Action items for disproved/partial conclusion ✅ N/A Verdict is proved; gate applies only to disproved or partial outcomes. Six voluntary action items are present.
3. No real credentials in diff ✅ Pass SHA256 at the binary-fetch section is a public verification hash for the downloaded gvproxy binary, not a secret. MAC addresses and IPs are example config values. No tokens, keys, or secrets.

Outcome

Spike boundary passes clean. No Blockers, no Warnings.

The final commit (c6bc8bb) corrects a SIOCSIFFLAGS typo and adds missing FromRawFd/IntoRawFd import references to the Rust relay sketch — documentation-only improvements with no effect on gate outcomes.

All four research questions are concretely answered with upstream source citations at v0.8.9 (commit dd4a4a5):

  1. Invocationgvproxy -config /path/gvproxy.yaml -listen (redacted) -ssh-port -1; subnet requires YAML config, no -subnet CLI flag.
  2. Attachment handshake — HTTP POST /connect on the unix socket; connection hijacked with no HTTP response; HyperKit framing (2-byte LE uint16 length prefix + raw Ethernet frames). No SCM_RIGHTS fd-passing. R1.5 fd-pass assumption corrected in Action item 1.
  3. L2/IP/route — TAP device + static DHCP lease (preferred via dhcpStaticLeases); UC6 PTask-to-PTask traffic is L2-only inside gvproxy's userspace switch; UC1 NoNet gives ENETUNREACH from an empty netns with lo down.
  4. Port-forward APIPOST /services/forwarder/expose, GET /services/forwarder/all, /leases, /cam, /stats on the unix socket; no per-client egress ACL in v0.8.9.

sdd:spike-resolved applied to spike sub-issue #511 (idempotent).

Generated by sdd-validate for issue #512 ·

Generated by sdd-validate for issue #512 ·

@norrietaylor
norrietaylor merged commit 1f4840f into main Jun 21, 2026
26 checks passed
@norrietaylor
norrietaylor deleted the sdd/511-gvproxy-attachment-eb61acf19b6b5b27 branch June 21, 2026 23:42
norrietaylor added a commit that referenced this pull request Jun 22, 2026
The gated step asserted an fd-pass that merged spike #512 proves does not
exist (gvproxy attachment is an async TAP↔socket relay, no SCM_RIGHTS). The
end-to-end relay proof + plumbing is split to #526; re-add the step there,
redefined against the relay assertion. Unblocks minvmd-linux-kvm-e2e on #522.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Jun 23, 2026
…huttle (#522)

* feat(minvmd): extend per-VM gvproxy to full PTask switch with vsock shuttle

Establishes the DM1/DM3/DM4 network-mode type structure and the gvproxy
switch supervisor in `minvmd` (Unit 1: R1.4, R1.5, R1.8).

- VmConfig gains a `network_mode: NetworkMode` field (default `HostNet`)
  and a `with_network_mode` builder; the stale "no network device in
  v0.1" comment in `apply` is replaced with the switch-attachment note.
- New `net` module supervises exactly one gvproxy process per host VM
  (`GvproxySwitch`), terminating it with the same SIGTERM -> timeout ->
  SIGKILL sequence as the vmm child (R1.4). Own-IP PTasks attach as
  switch clients, each assigned a unique IP from the switch subnet
  (default RFC-6598 100.64.0.0/16) over the per-PTask shuttle (R1.5).
  Every switch lifecycle event (spawn, stop, attach with IP, detach) is
  a structured `tracing` event; no `println!`/`eprintln!` (R1.8).
- Adds a `VmEgressPolicy` stub aligned with the Unit 2 egress types.
- Promotes the portable `minimald-rpc` wire-types crate from a
  dev-dependency to a dependency so `NetworkMode`/`IpProto` are usable
  in library code.

The libkrun-hardware integration proof (booting a VM and asserting
vsock IP assignment) is `#[ignore]` + env-gated per the spec's testing
standard and requires `/dev/kvm`; the type-structure/supervisor proof
runs in CI.

Closes #497

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(kvm): add gated DM1 vsock-shuttle proof step (U1-T3 #497)

sdd-validate blocked PR #522: the declared DM1 hardware proof (libkrun VM +
vsock-shuttle IP assignment) had no executable, observable CI path — the agent
deferred the vsock fd-pass and ci-linux-kvm ran no such test. Add a
MINVMD_INTEGRATION_TEST-gated step that runs `cargo test -p minvmd vsock --
--include-ignored` on this job's existing KVM+libkrun+kernel/rootfs setup. No-op
(0 tests) until the #497 vsock fd-pass + proof test land; once they do, this
step verifies them. Protected-path edit the execute agent cannot make.

* fix(minvmd): guard switch subnet host address against u32 overflow

Replace the unchecked u32::from(self.base) + index with checked_add and
propagate None on overflow, matching the function's existing checked_shl
and checked_sub style. Prevents a wrapped, incorrect IP for edge-case
subnets with a high base address (e.g. a /8 near 255.0.0.0).

Addresses a CodeRabbit review comment on #522.

* ci(kvm): defer DM1 vsock proof step to #526

The gated step asserted an fd-pass that merged spike #512 proves does not
exist (gvproxy attachment is an async TAP↔socket relay, no SCM_RIGHTS). The
end-to-end relay proof + plumbing is split to #526; re-add the step there,
redefined against the relay assertion. Unblocks minvmd-linux-kvm-e2e on #522.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(minvmd): log non-ESRCH kill failures during gvproxy teardown

terminate_child discarded the libc::kill return value at both the SIGTERM
and SIGKILL sites, so a failed signal delivery (e.g. EPERM, EINVAL) was
silently swallowed with no log entry. Route both signals through a
signal_child helper that checks the return value and emits a tracing::warn!
on any errno other than the benign ESRCH (the already-exited race that is
expected during teardown). Behaviour on the success and ESRCH paths is
unchanged. Resolves the sdd-review libc::kill finding on net.rs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(minvmd): make gvproxy switch teardown async and detect exit

Address two validated review findings on the gvproxy switch supervisor.

No blocking sleep in the teardown path: the SIGTERM -> grace -> SIGKILL
sequence moves off the synchronous `terminate_child` (a
`std::thread::sleep` poll loop reachable from `Drop`) into an async
`GvproxySwitch::stop` that drives the grace period on the tokio timer
(`tokio::time::timeout`). `Drop` is reduced to a non-blocking
fire-and-forget fallback: it marks the exit intentional and SIGKILLs
immediately, leaving the detached supervision task to reap the child.
This satisfies the repo standard banning `std::thread::sleep` in async
context (docs/rust-coding-standards.md).

Detect unexpected gvproxy exit (R1.4 detection half): `spawn` now starts
a background tokio supervision task that owns the child, awaits its exit,
and on an unexpected exit emits `tracing::error!` and fires a `SwitchExit`
notification returned from `spawn` for callers to react. Scope is
detection + logging + the channel only; the PTask-teardown consumer of
that signal is deferred to #526.

Promotes `tokio` from a dev-dependency to a dependency for the async
supervision. Confined to the `minvmd` crate.

Refs: #497

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(minvmd): clarify SwitchExit::recv None semantics

The recv() doc claimed None means only an intentional teardown, but the
supervise_switch wait()-failure path also drops exit_tx without sending,
yielding None. Document that None covers both the intentional stop/Drop
case and the rare supervision failure (no ExitStatus exists to report on
a wait() error; that path is logged via tracing::error!).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: gominimal-aw-bot[bot] <281738952+gominimal-aw-bot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Norrie Taylor <norrie@minimal.dev>
gominimal-aw-bot Bot added a commit that referenced this pull request Jun 23, 2026
…okio::fs

minvmd/net.rs:
- SwitchSubnet::new now returns Result<Self, InvalidPrefixError>; rejects
  prefix 0 and >32, both of which make every host() call return None silently
- Add attached_count: Arc<AtomicU32> to GvproxySwitch; attach_ptask increments
  it and clones the Arc into each PtaskAttachment
- PtaskAttachment::Drop decrements the count and fires SIGTERM when the last
  attachment is released (R1.4 "stop when the last own-IP PTask exits")
- detach_ptask now consumes PtaskAttachment by value; Drop handles teardown
- SwitchExit::recv doc already covered None-vs-supervision-failure semantics

minimald/net/mod.rs:
- Add NetError::InvalidPrefix(u8); SwitchSubnet::new returns it instead of
  SubnetExhausted for an out-of-range prefix (misconfig != runtime exhaustion)
- write_config converted from sync fn to async fn using tokio::fs to avoid
  blocking the tokio worker thread (no-blocking-in-async rule)
- Stale-socket removal in ensure_running converted to tokio::fs::remove_file
- Control-socket cleanup in stop() converted to tokio::fs::remove_file

Tests added for each validation path; proof artifacts:
- cargo test -p minvmd -p minimald (new prefix-validation and auto-stop tests)
- cargo clippy -p minvmd -p minimald --all-targets -- -D warnings (async paths)

Refs #526, #535, #512
norrietaylor added a commit that referenced this pull request Jul 29, 2026
)

The pin was 9 days and 62 commits stale (c854d6b1, 2026-07-20). The
motivating change is gominimal/pkgs#534, which rebuilds microvm-rootfs
from Alpine and drops the glibc closure — but the pin is linear, so this
necessarily carries everything before it too.

Materially in range:

- microvm-rootfs from Alpine (#534) — 186 MB -> 45 MB
- two libkrun vsock fixes: RX descriptor fill (#506) and packet-count
  backpressure (#512), both on the path minvmd depends on
- rust 1.97.1 (#502) and glibc 2.44 (#529) in the build stack

The rest is routine package version bumps.

Pinned at the branch tip rather than at #534's commit: pinning
mid-history buys nothing here, and the three commits after it are a
bottom bump, a graphviz bump, and a license-metadata fix.

Verified on this host: `mip materialize --arch aarch64 minvmd-rootfs`
resolves from cache and yields a 47,212,544-byte ext4 image whose only
interpreter is /lib/ld-musl-aarch64.so.1 — no glibc. The same image has
already been booted and driven through the session e2e (cold activate
4296 ms, warm ls 16 ms, sandbox proof 8030 ms), and A/B'd for cold-boot
latency against the outgoing rootfs: median 129 ms vs 154 ms, n=10 each,
non-overlapping.

This changes the guest for macOS as well, which already ships the
payload; the VM lanes are the gate.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

spike: gvproxy v0.8.9 switch-attachment protocol for DM2 (tap-fd handshake)

1 participant