Skip to content

fix(minimald): raise the guest vsock receive window to 8 MiB - #885

Merged
norrietaylor merged 1 commit into
mainfrom
fix/vsock-rx-window-869
Jul 22, 2026
Merged

fix(minimald): raise the guest vsock receive window to 8 MiB#885
norrietaylor merged 1 commit into
mainfrom
fix/vsock-rx-window-869

Conversation

@norrietaylor

@norrietaylor norrietaylor commented Jul 22, 2026

Copy link
Copy Markdown
Member

Fixes gominimal/minimal#869: workspace uploads over the guest vsock die with ENOBUFS, and the client reports copying tar stream to channel: channel closed.

Mechanism

The guest kernel (6.12.94, net/vmw_vsock/virtio_transport_common.c) rejects an incoming packet in virtio_transport_inc_rx_pkt() on either of two conditions:

  1. Creditbuf_used + len > buf_alloc; the peer sent more than its advertised window.
  2. Overhead(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 and buf_alloc defaults to VSOCK_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) copies buffer_size from 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_SIZE must be set before SO_VM_SOCKETS_BUFFER_SIZE. vsock_update_buffer_size() clamps the requested size to buffer_max_size, whose default (VSOCK_DEFAULT_BUFFER_MAX_SIZE) is also 256 KiB. Set the size alone and setsockopt returns 0 having changed nothing. This produced a convincing false negative during investigation.
  • setsockopt returning 0 does not mean the value took effect. A clamped request succeeds. The effective window is read back with getsockopt and 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).

Run Effective window Result
Control, stock guest 256 KiB 6/6 failed
Patched, same machine and fixture 8 MiB 0/6 failed
Accidental negative control 256 KiB (request silently clamped — MAX not raised first) 6/6 failed

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 minimald does not build on macOS (procfs), which is pre-existing, so verification ran under cross against the Linux target:

CROSS_CONTAINER_OPTS="--env HOME=/tmp" cross test -p minimald --target aarch64-unknown-linux-musl
  • lib: 145 passed, 0 failed
  • src/main.rs unit tests: 3 passed, 0 failed
  • netns_root_integration: 3 ignored (require netns + gvproxy; run in the ci-linux-native netns job)

cross clippy -p minimald --target aarch64-unknown-linux-musl --all-targets -- -D warnings is clean, as is cargo 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 minimald

Sets SO_VM_SOCKETS_BUFFER_MAX_SIZE and SO_VM_SOCKETS_BUFFER_SIZE on the vsock listener socket immediately after binding, then reads back the effective size via getsockopt. 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.

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
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b3a864dc-9e27-4c5b-bf7a-77b08de111d7

📥 Commits

Reviewing files that changed from the base of the PR and between f03ced1 and ac23be0.

📒 Files selected for processing (1)
  • crates/minimald/src/main.rs

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

@norrietaylor

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@norrietaylor

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
✅ Action performed

Full 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.

@norrietaylor

Copy link
Copy Markdown
Member Author

@macroscope review

@macroscopeapp

macroscopeapp Bot commented Jul 22, 2026

Copy link
Copy Markdown

Manual reviews triggered for commit ac23be0:

All prior checks · these links stay valid even if you push more commits.

@macroscopeapp

macroscopeapp Bot commented Jul 22, 2026

Copy link
Copy Markdown

Just FYI for future @mentions, I'm Macroscope-App, not Macroscope.

Code review has been triggered and is in progress. Results will be posted as check runs on this PR when they complete.

@macroscopeapp

macroscopeapp Bot commented Jul 22, 2026

Copy link
Copy Markdown

Approvability

Verdict: 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.

@norrietaylor
norrietaylor merged commit 4823653 into main Jul 22, 2026
30 checks passed
@norrietaylor
norrietaylor deleted the fix/vsock-rx-window-869 branch July 22, 2026 17:13
norrietaylor added a commit that referenced this pull request Jul 22, 2026
Reverts commit 4823653 (PR #885).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
twitchyliquid64 pushed a commit that referenced this pull request Jul 22, 2026
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
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.

minimald: vsock upload killed by guest skb-overhead reset (ENOBUFS) — libkrun emits sub-KB packets under backpressure

2 participants