fix(minimald): raise the guest vsock receive window to 8 MiB - #885
Conversation
Workspace uploads over the guest vsock die with ENOBUFS, surfacing on the client as "copying tar stream to channel: channel closed". `virtio_transport_inc_rx_pkt` (net/vmw_vsock/virtio_transport_common.c, guest kernel 6.12.94) refuses an incoming packet on either of two conditions: the peer overran its credit (`buf_used + len > buf_alloc`) or the queued socket-buffer overhead did (`(queue_len + 1) * SKB_TRUESIZE(0) > buf_alloc`). Rejection resets the connection and sets `sk_err` to ENOBUFS. An instrumented guest kernel attributed 8 of 8 rejections to the overhead condition, never the credit one, with 5-15 KB of window still unused: libkrun clamps each read to the credit outstanding, so under receiver backpressure its packets shrink to ~540 bytes, below the 576-byte `SKB_TRUESIZE(0)` on arm64, and the 455-skb overhead ceiling of a 256 KiB window arrives first. Set the window on the vsock listener, which `__vsock_create` copies onto every accepted socket. `SO_VM_SOCKETS_BUFFER_MAX_SIZE` must be raised before `SO_VM_SOCKETS_BUFFER_SIZE` — `vsock_update_buffer_size` clamps the size to `buffer_max_size`, whose default is the same 256 KiB, so setting the size alone returns success having changed nothing. The effective value is read back because a clamped request also returns success. A failed sockopt is logged and the daemon carries on: the default window is the old behaviour, not a boot blocker. 8 MiB is empirical headroom, not immunity. It does not change the ratio between the two ceilings, it keeps a normal upload out of the credit-starved tail where packets degenerate; a larger upload or a slower reader can still reach it. The real fix belongs in libkrun and is in flight separately. Measured on a 12,784,144-byte fixture: 6 of 6 uploads failed on a stock 256 KiB window, 0 of 6 with 8 MiB in effect. Refs: #869
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 47 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai full review |
✅ Action performedFull review finished. You're currently rate limited under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. Your next review will be available in 47 minutes. |
|
@macroscope review |
|
Manual reviews triggered for commit All prior checks · these links stay valid even if you push more commits. |
|
Just FYI for future @mentions, I'm Code review has been triggered and is in progress. Results will be posted as check runs on this PR when they complete. |
ApprovabilityVerdict: Would Approve Targeted buffer size fix with extensive documentation and graceful error handling. The author owns the modified file, and the change is a low-risk configuration adjustment to prevent ENOBUFS errors during large uploads. Macroscope would have approved this PR. Enable approvability here. |
Reverts commit b0f07a2 (PR #916), which reverted 4823653 (PR #885). #885 was reverted before the libkrun fix had been tested in production. That test has now run and the fix is not sufficient on its own: the shipped 6d239bc build carries the fill loop and still fails, because the loop exits on EAGAIN with a partly filled descriptor whenever the writer is slower than libkrun's reader. The window is the one measure with an unambiguous result behind it -- 0 of 6 failures at 8 MiB against 6 of 6 on the same machine at the 256 KiB default. It raises the skb-depth ceiling from 455 queued packets to 14,563, which is the bound every observed reset has hit. This is headroom, not immunity: it does not change the ratio between the two ceilings, it keeps a normal upload out of the fragmented tail where the reset happens. It belongs alongside a libkrun fix rather than instead of one. Note `SO_VM_SOCKETS_BUFFER_MAX_SIZE` must be raised before `SO_VM_SOCKETS_BUFFER_SIZE`: vsock_update_buffer_size() clamps the latter to the former and both default to 256 KiB, so the obvious ordering silently changes nothing. Refs: #869
Fixes gominimal/minimal#869: workspace uploads over the guest vsock die with
ENOBUFS, and the client reportscopying tar stream to channel: channel closed.Mechanism
The guest kernel (6.12.94,
net/vmw_vsock/virtio_transport_common.c) rejects an incoming packet invirtio_transport_inc_rx_pkt()on either of two conditions:buf_used + len > buf_alloc; the peer sent more than its advertised window.(skb_queue_len + 1) * SKB_TRUESIZE(0) > buf_alloc; the bookkeeping cost of the queued socket buffers alone exceeds the window.Either rejection resets the connection and sets
sk_err = ENOBUFS.An instrumented guest kernel attributed 8 of 8 observed rejections to the overhead condition, never the credit one, each with 5–15 KB of window still unused. The arithmetic explains why.
SKB_TRUESIZE(0)is 576 bytes on arm64 andbuf_allocdefaults toVSOCK_DEFAULT_BUFFER_SIZE= 256 KiB, so the overhead ceiling sits at 455 queued skbs regardless of how much data those skbs carry. libkrun clamps each read to the credit still outstanding, so under receiver backpressure its packets shrink toward ~540 bytes — less than the 576-byte per-skb overhead they cost to queue. Past that crossover every additional packet consumes more window as overhead than it delivers as payload, and the overhead ceiling arrives before the credit ceiling ever can.The change
Raise the receive window on the guest's vsock listener to 8 MiB.
__vsock_create(af_vsock.c) copiesbuffer_sizefrom the listening socket onto every socket it accepts, so one call at bind time covers every session.Two details are load-bearing, and getting either wrong fails silently:
SO_VM_SOCKETS_BUFFER_MAX_SIZEmust be set beforeSO_VM_SOCKETS_BUFFER_SIZE.vsock_update_buffer_size()clamps the requested size tobuffer_max_size, whose default (VSOCK_DEFAULT_BUFFER_MAX_SIZE) is also 256 KiB. Set the size alone andsetsockoptreturns 0 having changed nothing. This produced a convincing false negative during investigation.setsockoptreturning 0 does not mean the value took effect. A clamped request succeeds. The effective window is read back withgetsockoptand the result logged.A failed sockopt logs a warning and the daemon carries on booting: a daemon with the default window is the pre-existing bug, not a reason to refuse to start.
Evidence
Fixture: a project whose single file compresses to exactly 12,784,144 bytes. Baseline failure rate on a stock guest is ~89% (8 of 9).
The third run is the useful one. It ran the patched binary, so the only variable that differed from the passing run was the window the kernel actually installed — which isolates the window as the cause rather than any other difference between the two builds.
This is headroom, not a fix
8 MiB does not change the ratio between the two ceilings; the overhead condition can still be reached. What it does is keep a normal upload out of the credit-starved tail where libkrun's packets degenerate below the per-skb overhead. A larger upload, a slower reader, or more aggressive backpressure could still get there.
The real fix belongs in libkrun, which should not shrink a packet below the overhead it costs to queue — a separate PR is in flight for that. This is the consumer-side mitigation, and it should stay useful regardless, since it also buys back ordinary buffering headroom.
Testing
cargo test -p minimalddoes not build on macOS (procfs), which is pre-existing, so verification ran undercrossagainst the Linux target:src/main.rsunit tests: 3 passed, 0 failednetns_root_integration: 3 ignored (require netns + gvproxy; run in theci-linux-nativenetns job)cross clippy -p minimald --target aarch64-unknown-linux-musl --all-targets -- -D warningsis clean, as iscargo fmt --check.The runtime evidence above was collected before this PR and is not reproduced by CI — the guest window is only observable from inside a booted microVM.
Note
Raise guest vsock receive window to 8 MiB in
minimaldSets
SO_VM_SOCKETS_BUFFER_MAX_SIZEandSO_VM_SOCKETS_BUFFER_SIZEon the vsock listener socket immediately after binding, then reads back the effective size viagetsockopt. The result is logged at debug level if the effective size meets the request, or warn level if the OS grants less or the call fails. This is Linux-only and runs before the daemon emits READY or begins accepting connections.Macroscope summarized ac23be0.