fix(libkrun): patch vsock RX to fill the descriptor instead of fragmenting - #506
Conversation
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 10 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: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
Converting to draft — the patch this PR ports does not fix the bug. Runtime validation of the same patch on macOS (gominimal/minimal#884, now also draft) came back negative. Same dev stack, same minvmd binary, libkrun built from the same pinned commit with the same script, only the dylib differing:
The credit floor changes nothing. The likely reason the mechanism was misidentified: libkrun's packets are probably small because The build wiring in this PR (the Tracked in gominimal/minimal#869. |
…nting A bulk host->guest stream over a unix-backed vsock port dies mid-upload with ENOBUFS, surfacing as "Failed to upload project files: copying tar stream to channel: channel closed". recv_to_pkt() issued one recv() per RX descriptor and emitted whatever that call happened to find buffered, so the muxer outran the writer and fragmented the stream into sub-KiB packets. A Linux peer charges every queued skb SKB_TRUESIZE(0) (576 bytes on arm64) against buf_alloc regardless of payload, so those packets burn queue budget far faster than they deliver bytes; virtio_transport_inc_rx_pkt() then rejects at (queue_len + 1) * SKB_TRUESIZE(0) > buf_alloc and resets the connection while credit is still outstanding. Instrumented counters over 40,749 packets identified the constraint as socket occupancy, not the peer's window: 74.1% of reads drained the socket with descriptor space still free, mean 2,199 bytes available against mean available credit of 239,848 of a 262,144-byte window, and the zero-credit path was reached 10 times in 40,749 reads. Carry two patches, applied by name in a fixed order because 0002 is written against the tree 0001 produces: 0001 sets signal_queue on the credit-request path. push_packet() places the request in the used ring but does not raise the IRQ, so a proxy that must wait for credit disarms its own polling and never wakes the peer to send the update it is waiting on. An independent bug worth fixing; it does not fix the upload on its own (measured 0/6). 0002 loops until the RX descriptor is full or the socket returns EAGAIN. This is the fix. An earlier revision of this branch carried a minimum-credit floor instead. Runtime A/B refuted it -- 0/6 passes, identical to the stock control -- and the instrumentation above explains why: credit was never the constraint. That patch is dropped, not carried. The patch texts are byte-identical to the ones the macOS source build carries at vendor/libkrun/patches/ in gominimal/minimal. This change is the Linux half: Linux takes libkrun from this package's release tarball, which that repo cannot patch. Verified here that both patches apply cleanly, in sequence, to the exact pinned tarball (sha256 e8775fab..., whose tree is byte-identical to upstream commit 728df812) under both `git apply --check -p1` and the `patch -Np1` lines added to build.sh. Not built locally -- this machine has neither `mip` nor a `min` with `patched-build`/`check`, so CI is the gate. `patch` runs under the script's existing `set -e` and exits non-zero on a rejected hunk, so a stale patch aborts the build rather than silently publishing a libkrun that looks fixed and is not. Refs: gominimal/minimal#869 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4a4016f to
839db13
Compare
Carries libkrun vsock patches in
packages/libkrunso the Linux build gets the same fixes the macOS source build is getting in gominimal/minimal#884. That PR patches libkrun atvendor/libkrun/patches/, which only covers the macOS source build — Linux takes libkrun from this package's release tarball, so it needs its own change.Tracking issue: gominimal/minimal#869.
Important
This PR previously carried a different patch — a minimum-credit floor — which has been refuted and is now dropped. See Superseded approach below. If you reviewed the earlier revision, re-read from scratch; the mechanism is different.
Symptom
error: Failed to upload project files: copying tar stream to channel: channel closed— a bulk host→guest stream over a unix-backed vsock port dies mid-upload, taking the whole SSH session with it.Mechanism
recv_to_pkt()issued onerecv()per RX descriptor and emitted whatever that call happened to find buffered. Against a bulk sender that hands the stream over in small chunks, the muxer drains the socket faster than the writer fills it, so each descriptor carries a fraction of its capacity and the stream fragments into sub-KiB packets.Fragmentation is expensive for the receiver: a Linux peer charges every queued skb
SKB_TRUESIZE(0)(576 bytes on arm64) againstbuf_allocregardless of payload.virtio_transport_inc_rx_pkt()rejects once(queue_len + 1) * SKB_TRUESIZE(0) > buf_alloc— a 455-skb ceiling at a 256 KiB window — so sub-KiB packets burn queue budget far faster than they deliver bytes, and the peer resets withENOBUFSwhile credit is still outstanding.Instrumented counters over 40,749 packets identified the constraint as socket occupancy at the instant of the read, not the peer's window:
recv())Credit was essentially never the constraint.
What is carried
Two patches, applied by name in a fixed order —
0002is written against the tree0001produces, so the sequence is load-bearing.0001-vsock-signal-the-used-queue-when-requesting-credit.patch— the credit-request path setspush_credit_reqbut neversignal_queue, andpush_packet()alone does not raise the IRQ. A proxy that must wait for credit therefore disarms its own polling and never wakes the peer to send the update it is waiting on. Usually masked, because a batch normally pushes data packets first and those setsignal_queuethemselves; reachable when the first packet of a batch has to wait. Applies to bothunix.rsandtsi_stream.rs.Carried because it is a genuine independent bug, not because it is the fix — on its own it measured 0/6, identical to the stock control.
0002-vsock-fill-the-rx-descriptor-instead-of-one-recv-per-packet.patch— the actual fix. Loop until the descriptor is full or the socket returnsEAGAIN, instead of onerecv()per descriptor.Validation
Runtime A/B on macOS: same dev stack, same
minvmd, libkrun rebuilt from the same pinned commit with the same script, only the dylib differing.Two caveats on those numbers, both important:
Superseded approach
The earlier revision of this branch carried
0001-vsock-don-t-shrink-rx-packets-to-the-last-bytes-of-peer-credit.patch, which waited for a credit update once credit fell below a 4 KiB floor. It was refuted by the A/B above — 0/6, indistinguishable from the stock control. The instrumentation explains why it could not have worked: the zero-credit path was reached 10 times in 40,749 reads and mean available credit was 239,848 bytes, so a credit floor was addressing a constraint that was not binding. It is dropped, not carried.Wiring
Follows the existing local-patch convention (
packages/abseil-cpp,packages/jemalloc,packages/zeromq) rather than the fetched-tarball shapepackages/unzipuses:build.ncl: two{ file = "...patch" } | Localentries inbuild_deps,patchas a build dep, and a comment explaining what each patch does and when to drop them.build.sh: two explicitpatch -Np1 -i "<name>"lines, in order.Ordering is explicit, not glob-derived. The patches are named individually so the sequence cannot depend on how a shell sorts a wildcard.
Failing loudly.
build.shalready runs underset -ex. GNUpatchexits non-zero on a rejected hunk, so a stale patch aborts the build. A silently-skipped patch would publish a libkrun that looks fixed and is not.Verified vs assumed
Verified here
0001…signal-the-used-queue…→60044131235658786e699debfcc7469fdb4801d86644515741931e40041bd4a20002…fill-the-rx-descriptor…→9196245c5dc2c7319702372bb1877a1e07f3220023181c1522f2dc45b3b9aab2e8775fab2b460972a67ca6cd936296bb79cdb078d852d712a283cb290dd0b284, matching the pin inbuild.ncl, left unchanged.b2a84890000eeef60433712f99b95eab4141c845is byte-identical to the tree of upstream commit728df8125077d0db44265f6e997c72b81b65c015— the commit the patches were written against. Settled by hash, not assumed.git apply --check -p1exit 0 for0001on the pristine tree, then exit 0 for0002on the0001-patched tree.patch --dry-run -Np1likewise. No fuzz, no offset warnings.0002was re-checked from scratch rather than assumed to apply because the dropped patch touched the same function region.patch -Np1lines extracted from the committedbuild.sh(extract withstrip_prefix = "libkrun-1.19.4",Localfiles at tree root): both apply, exit 0.let mut total = 0; let ret = loop { … Err(Errno::EAGAIN) => …),update.signal_queue = trueon the credit-request path in bothunix.rsandtsi_stream.rs, zero occurrences of the droppedMIN_RX_PKT_PAYLOAD, and the originalif max_len == 0 { return RecvPkt::WaitForCredit; }guard intact.Errnois already imported inunix.rs(line 6, pre-existing), so0002'sErrno::EINTR/Errno::EAGAINarms resolve.build.sh755, patches 644).Not verified — assumed
minis the session CLI (0.5.0-rc2.dev.10), with nopatched-buildorcheck --packages;mipis not installed; nonickelbinary, sobuild.nclwas not evaluated locally. CI is the gate.SKB_TRUESIZE(0) == 576on arm64 and the 455-skb ceiling come from the original investigation.build_depsentries changes the spec hash, so this forces a libkrun rebuild. Intended.Status
Draft. Merging is the user's call — the fill-loop has not been validated on Linux, which is the platform this PR affects. Independent of gominimal/minimal#884; the two repos patch different build paths and can land in either order. Both files should be dropped once the fixes land in an upstream
containers/libkrunrelease we can pin.