A guest VM exits permanently the first time a TX write to its tap fails with EIO. Any transient host-side condition that leaves the tap down (or replaced) at that moment kills the guest.
Repro (v0.6.0, x86_64, single-node k3s):
- Create a sandbox from a template with a memory snapshot (the resume path transmits within ~300ms of boot; a cold-boot guest reproduces too, just needs guest traffic).
- On the node, set the sandbox tap down before the guest transmits:
ip link set z<sandbox-ip> down
- Guest transmits.
/data/log/CubeVmm/vmm.log:
net: tx: failed writing to tap: Input/output error (os error 5)
Error running worker: HandleEvent(Error processing TX queue: NetQueuePair(WriteTap(Os { code: 5, ... })))
VM exit event
The VM is gone. From the API side the sandbox shows running briefly, then envd stops answering and the session is lost.
Where: hypervisor/net_util/src/queue_pair.rs, TxVirtio::process_desc_chain. EAGAIN is retried, but every other errno from writev returns NetQueuePairError::WriteTap, which tears down the net worker thread and exits the VM.
Why we hit it in production: an external tap GC on the node deleted idle pooled taps (NO-CARRIER, not bound to any sandbox — indistinguishable from leaked ones from outside network-agent). Two things then compound:
network-agent GetTapFile hands out fds without checking the netdevice still exists or is up. TUNSETIFF on a deleted tap name does not fail — it silently creates a fresh admin-DOWN device with no TC filter/MTU/ARP, so the guest boots against a dead tap (network-agent/internal/service/tap_fd_provider.go, the openTapFdByName reopen path).
- The first guest TX gets EIO and the hypervisor kills the VM instead of dropping the frame.
Physical NICs drop frames when the link is down and the guest network stack retransmits; a transiently unready host tap should degrade to packet loss, not VM death.
Happy to send patches: (a) drop EIO'd TX frames (count + sampled log, keep EAGAIN retry and other errnos fatal), (b) verify the tap netdevice (exists / same ifindex / up, one netlink read per sandbox create) before every fd handoff in GetTapFile, (c) a --tap-init-num flag so the pool size (including 0) can be set without editing the packaged config.toml.
A guest VM exits permanently the first time a TX write to its tap fails with EIO. Any transient host-side condition that leaves the tap down (or replaced) at that moment kills the guest.
Repro (v0.6.0, x86_64, single-node k3s):
/data/log/CubeVmm/vmm.log:runningbriefly, then envd stops answering and the session is lost.Where:
hypervisor/net_util/src/queue_pair.rs,TxVirtio::process_desc_chain. EAGAIN is retried, but every other errno fromwritevreturnsNetQueuePairError::WriteTap, which tears down the net worker thread and exits the VM.Why we hit it in production: an external tap GC on the node deleted idle pooled taps (NO-CARRIER, not bound to any sandbox — indistinguishable from leaked ones from outside network-agent). Two things then compound:
network-agentGetTapFilehands out fds without checking the netdevice still exists or is up. TUNSETIFF on a deleted tap name does not fail — it silently creates a fresh admin-DOWN device with no TC filter/MTU/ARP, so the guest boots against a dead tap (network-agent/internal/service/tap_fd_provider.go, theopenTapFdByNamereopen path).Physical NICs drop frames when the link is down and the guest network stack retransmits; a transiently unready host tap should degrade to packet loss, not VM death.
Happy to send patches: (a) drop EIO'd TX frames (count + sampled log, keep EAGAIN retry and other errnos fatal), (b) verify the tap netdevice (exists / same ifindex / up, one netlink read per sandbox create) before every fd handoff in
GetTapFile, (c) a--tap-init-numflag so the pool size (including 0) can be set without editing the packaged config.toml.