fix(minvmd): carry libkrun vsock patches, fixing bulk host->guest uploads - #884
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThe macOS libkrun build now applies carried patches in sorted order. Two VSOCK patches improve credit-request signaling and RX descriptor filling, with explicit failure handling for rejected patches. Changeslibkrun VSOCK patch integration
Estimated code review effort: 3 (Moderate) | ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
7258ee5 to
5aa0de5
Compare
…oads Adds the missing patch step to the macOS libkrun source build and carries two vsock fixes against the pinned commit. Together they take the 12,784,144 byte project upload from 0/6 to 12/12 on the dev stack. Build wiring: every *.patch in vendor/libkrun/patches/ is applied with `git apply` after the pinned commit is fetched and its SHA verified. A patch that no longer applies aborts the build; a silently-skipped patch would stage a dylib that looks patched and is not. Drop a patch file once the fix lands in a new pin. 0001 -- signal the used queue when requesting a credit update. A proxy that runs out of credit emits VSOCK_OP_CREDIT_REQUEST and disarms polling on its host fd, but push_packet() only places the request in the used ring; the IRQ comes solely from process_proxy_update() when signal_queue is set, and signal_queue is taken from recv_pkt()'s have_used. When the first packet of a batch is the one that must wait, the peer is never woken, so it never sends the credit update, and nothing on the host side wakes the connection either. Reachable upstream today, just rare. tsi_stream.rs has the identical gap; both are fixed. 0002 -- fill the RX descriptor instead of emitting one recv() per packet. This is the fix for the upload failure. recv_to_pkt() issued a single recv() per descriptor and shipped whatever happened to be buffered, so against a bulk sender the muxer outran the writer and fragmented the stream. Measured over a failing 12.8 MB transfer: 40,749 packets, mean payload 1895 B, 35.6% of reads under 1 KiB, FIONREAD before each read averaging 2199 B while peer credit averaged 239,848 of a 262,144-byte window, and the zero-credit path reached 10 times in 40,749 reads. Socket occupancy was the binding constraint on 74% of reads; the peer's window essentially never was. Fragmentation is what kills the transfer, because a Linux receiver charges every queued packet SKB_TRUESIZE(0) against buf_alloc regardless of payload. An earlier credit-floor patch, written on the theory that the peer's window was the constraint, was measured at 0/6 and is not carried -- the instrumentation above is what refuted it. macOS source build only. Linux takes libkrun from the gominimal/pkgs packages/libkrun release tarball, which this repo cannot patch; that needs a separate pkgs change.
5aa0de5 to
5dc8cf0
Compare
|
@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 48 minutes. |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
Takes the 12,784,144-byte project upload from 0/6 to 12/12. The credit-floor
patch this PR originally carried was refuted by measurement and has been
dropped; what replaces it is a different fix, aimed at the constraint that
actually binds.
Counts, per build
Every batch ran on the same dev stack (
just up), the same minvmd binary, andlibkrun built from the same pinned commit with the same script — only the dylib
differed, swapped under a stopped VM and confirmed by
lsofon the live VMMprocess before and after each batch.
MINVMD_KRUN_LOG=debugMINVMD_KRUN_LOG=debugThe two
debugrows are a confound, not a result: enabling libkrun's loggerperturbs timing enough to mask the bug on both builds. It is recorded because
it nearly produced a false positive, and because it disqualifies per-packet
logging as a diagnostic here.
Note the third row: the IRQ fix alone does not fix the upload. The fill-loop
does.
What the measurement showed
Per-packet logging dissolves the bug, so the discriminator was counters only —
no locks, allocation, or formatting on the data path, one summary per connection
at teardown, with
FIONREADsampled immediately before eachrecv()soavailable bytes are directly comparable with
max_len.Instrument validated before being believed: it preserved the failure at 6/6.
Aggregate over the six failing 12.8 MB uploads — 40,749 packets:
Representative connection:
The two histograms track each other almost exactly up to 2^11 (401/400,
171/169, 902/901, 830/826, 1492/1492): we read precisely what the socket
happened to hold. That is the signature of socket-buffer-limited. They diverge
only at the top, where
buf.len()(~4 KiB) caps a read that could have taken4–8 KiB — the descriptor, still not credit.
Credit had ~109× more headroom than the socket had data, and the zero-credit
path fired 10 times in 40,749 reads. The peer's window was never the binding
constraint, which is exactly why raising a credit floor did nothing.
One honest caveat on the counter names:
credit_limitedis computed againstmax_len = min(buf.len(), peer_credit)and so conflates the two. Givencredit_mean≈ 240 KB against a ~4 KiBbuf.len(), that bucket isdescriptor-full, not credit-limited. Read it that way.
The fix (0002)
recv_to_pkt()issued onerecv()per RX descriptor and shipped whatever thatcall happened to find. Against a bulk sender delivering the stream in chunks,
the muxer outruns the writer and fragments it. Each fragment then costs the peer
a whole skb of queue accounting — a Linux receiver charges every queued packet
SKB_TRUESIZE(0)(576 B on arm64) againstbuf_allocregardless of payload —so sub-KiB packets burn queue budget far faster than they deliver bytes.
The patch loops until the descriptor is full or the socket returns
EAGAIN.On the level-triggered trap: polling registers
EV_ADDwithoutEV_CLEAR(
src/utils/src/macos/epoll.rs), so anything that declines to read availabledata spins. A fill-loop is safe precisely because it never leaves readable data
behind on purpose: every exit either drained the socket or filled a descriptor,
so the next notification corresponds to genuinely new data or to a descriptor we
can immediately make progress on. A threshold-and-defer design would have spun —
which is what the dropped credit-floor patch did, and why it needed 0001 to stop
hanging.
EOF and errors encountered after some bytes are read deliver the data first and
re-report on the next call.
The IRQ fix (0001)
Independent of the upload bug and worth upstreaming on its own. A proxy that
runs out of credit sets
WaitingCreditUpdate, emitsVSOCK_OP_CREDIT_REQUEST,and disarms polling on its host fd. But
push_packet()only places the requestin the used ring — the IRQ comes solely from
process_proxy_update()whenProxyUpdate::signal_queueis set, and that is taken fromrecv_pkt()'shave_used. When the first packet of a batch is the one that has to wait,have_usedis false: the peer is never woken, never sends the credit update,and polling is already disarmed on our side.
Reachable on unpatched upstream, just rare — a batch normally pushes data
packets first, and those set
signal_queuethemselves. It was not rare underthe credit-floor patch, which turned it into a multi-minute hang; that hang is
how it was found.
tsi_stream.rshas the identical construct and the identicalgap. Both fixed.
Build wiring
scripts/build-libkrun-macos.shhad no patch step. It now applies every*.patchinvendor/libkrun/patches/, sorted, withgit apply, after thepinned commit is fetched and its SHA verified. Verified both directions: the
real patches apply cleanly and build; a deliberately broken patch aborts with
exit 1 and
::error::9999-bogus.patch does not apply to libkrun 728df812….A silently-skipped patch is impossible.
Instrumentation: not in this PR, deliberately
The counter patch is a debugging aid, not shippable code. It writes to a file
path from an env var, bypasses
log!/debug!on purpose (the macros are inertunless the embedder installs a logger, and installing one masks the bug), and
adds a
FIONREADsyscall per read. My recommendation is that it does notbelong in the repo — it has no value except while this specific question is
open, and a checked-in debug patch in
vendor/libkrun/patches/would be appliedto every macOS build by the very mechanism this PR adds. It is preserved in the
investigation record; ask if you want it carried somewhere.
Trustworthiness of the A/B
flags=0x2(adhoc),TeamIdentifier=not set) withno hardened runtime, so library validation does not apply — that is what makes
swapping a locally built dylib possible at all.
LIBKRUN_PREFIXpointed at a private scratch dir./opt/homebrewwas nevertouched and never appeared in minvmd's rpath, and no
libkrun*sat next to thebinary, so neither a Homebrew copy nor
@loader_pathcould mask a swap.lsofs the dylib actually mappedinto the
__krun-vmmprocess, before and after; mapped size tracked every swap.rather than reusing the shipped dylib, so arms differ only by the patches.
Server: minimald 0.5.0-rc1vs0.5.0-rc2.dev.10.g4bfdcb09.test happily talked to it. Caught by checking the live binary; every batch now
verifies before and after.
Verified vs assumed
Verified
UnixAcceptorProxy→UnixProxy::new_reverseinunix.rs, notmuxer.rs'sUnixProxy::newpath.push_packet()does not raise the IRQ; onlyprocess_proxy_updatedoes. Theresulting stall was observed and the fix removed it.
stock and 6/6 on stock+IRQ-fix+instrumentation.
728df812…and build; a stale patch aborts the build.e2e8ebca…unchanged, installed stacklive,
Server: minimald 0.5.0-rc2.dev.10.g4bfdcb09, noMINVMD_*overrides,all validation sessions destroyed.
Assumed / not verified
SKB_TRUESIZE(0) == 576on arm64 and the 455-skb ceiling — carried from theearlier investigation, not re-derived. The fix does not depend on the exact
number, only on fragmentation being costly to the receiver.
fill-loop to show packet sizes rising. The mechanism is inferred from the
before-numbers plus the fix working, not directly observed post-fix.
per byte, but no benchmark was run.
minimald 0.5.0-rc1,.scratchkernel/rootfs/initramfs). Not run against the installed guest, which cannot
load an unsigned dylib.
be more convincing than two batches of six.
tsi_dgram.rsshares the one-recv-per-descriptor shape; untouched, untested.UnixProxy.tsi_stream.rshas the samepattern and likely the same fragmentation, but it does not serve this port and
was left alone to keep the change reviewable.
Note
Fix bulk host-to-guest vsock uploads by patching libkrun credit signaling and read aggregation
UnixProxyrecv path to loop reads until the RX descriptor is full or the socket blocks, reducing packet fragmentation on bulk transfers.Macroscope summarized 5dc8cf0.
Summary by CodeRabbit
Bug Fixes
Build