Repository navigation
Conversation
…OS under QEMU An empty commit that opens the draft pull request for the design and its roast, which live in the pull request's body. Nothing is built: the build round drops this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
|
Design review, round 1, head Net lines ( Evidence: The brief's questions
BLOCKER
NOTE
DESIGN NEEDS CHANGES: let the opening REQUEST's answer land (owner |
|
The orchestrator's rulings on round 1 (#894 (comment)), following spice-vdagent and QEMU per #885's rule:
|
|
Design round 2, head New measurements, all on ToyOSOrg/evidence The owner's later direction (ToyOS does not follow Linux). The design now rests on three definitions only: virtio 1.2, spice-protocol's BLOCKER
NOTE
|
Design round 2. Nothing is built: the branch is one empty commit, and the build begins only once this design is accepted in a comment here. The owner gave his go for a shared clipboard between the development host and ToyOS under QEMU: copy on the host and paste in ToyOS, and the reverse. Round 1 was reviewed in #894 (comment) and ruled on in #894 (comment); the mapping from each finding to this body is a comment below.
What the published definitions say
The design follows three published definitions and nothing else: the virtio 1.2 specification (OASIS, cs01), spice-protocol's
spice/vd_agent.hat499cc832, and QEMU v11.1.1 (build/ci/qemu-version), which makes both the device and the host end of the channel. No other operating system's guest agent or driver is followed.QEMU's host side is the
qemu-vdagentchardev,clipboard=on(QAPIChardevQemuVDAgent, compiled only with spice-protocol:qapi/char.json:460-474,ui/meson.build:68).ui/vdagent.c:313-321).CLIPBOARD_BY_DEMAND,CLIPBOARD_SELECTIONandCLIPBOARD_GRAB_SERIALtogether (:192-196). A capability changes the wire only where the guest announced it too (:303-311).:167-176), and drops any message to the guest past its 1 MiB buffer withbuffer full, dropping message(:162-165).CLIPBOARDits owner sends unrequested (:585-606,ui/clipboard.c:232-257). Once the data is stored, a host request returns at once (ui/clipboard.c:196-209). If it owns nothing the guest asked about, it answers a guestREQUESTwith no data (ui/vdagent.c:556-583).The grab serial. spice-protocol gives a
GRABaserialunderCLIPBOARD_GRAB_SERIAL(vd_agent.h:301-310) and does not say what it means. QEMU's side is the definition the agent talks to:last_serialwhen the guest announces its capabilities (ui/vdagent.c:724-729).last_serial, which then goes up by one (:342-351).last_serialis discarded, and any other one is kept and setslast_serial(:516-530). ACLIPBOARDsent after a discarded grab is dropped too (:602-605).:455-465). The guest seesPORT_OPEN(0), and QEMU reopens only once the guest has closed its side (:874-895,hw/char/virtio-console.c:111-125,hw/char/virtio-serial-bus.c:264-290). While the host end is closed, the guest's transmits are discarded (:479-495).The host's UI peers wait with no bound for text the guest owns and has not sent:
ui/cocoa.m:1793-1821);ui/gtk-clipboard.c:51-57).Both waits end as soon as the data is stored.
GTK's clipboard is off by default, and why. Commit
29e0bfffab87disabled it at build time because it "may cause guest hangs": QEMU issue #1150, guest soft lockups. Its reads of the host's clipboard then waited on GTK inside QEMU's main loop (gtk_clipboard_wait_for_text,gtk_clipboard_wait_is_text_available). In 11.1,9a007326e83fmade those reads asynchronous. Then335e32cbd0a2turned the build option into-display gtk,clipboard=on, still off by default (qapi/ui.json:1322-1323). All three commits are in v11.1.1. The one wait left in 11.1.1 is the loop above, and the push after each guest grab (§4) closes it: QEMU holds the text before any paste can ask for it.virtio-serial (virtio 1.2 §5.3):
VIRTIO_CONSOLE_F_MULTIPORT(§5.3.2, §5.3.3).max_ports > 1(hw/char/virtio-serial-bus.c:541-543).max_nr_portsnames and sendsDEVICE_READY. The device then adds ports, and the driver answers each withPORT_READY(§5.3.5, §5.3.6.2.2). The device then names the port (PORT_NAME) and opens it (PORT_OPEN, both directions).DRIVER_OK(§3.1.1).The vdagent stream (
vd_agent.h:44-61,:83-105,:271-326,:399-402):VDIChunkHeader {port, size}frames aVDAgentMessage {protocol = 1, type, opaque, size}, andVD_AGENT_MAX_DATA_SIZEis 2048.GRAB [serial][types],REQUEST [type],CLIPBOARD [type, data]andRELEASE [].GRABtells the remote to "empty clipboard and wait for one of the types passed", and aCLIPBOARD"set[s] clipboard data (both directions)".The design
1. The QEMU launch (
build/qemu.rs,tests/common/qemu.rs)Every virtio profile adds three arguments, created just before the console's function:
cargo runalso passes-display cocoaon macOS and-display gtk,clipboard=onon other hosts, one choice per host asaudio_backend()makes. The harness keeps-display none.1af4:1043would drive it and lose its console. Nothing else moves: the harness's slots (tests/common/iommu.rs:195-199,:292) are all created earlier.max_ports = 2come from one function in thetoyos_buildlibrary. It reads the codec crate's declaration (§3), which the agent reads too, and both launchers call it.-chardev helpforqemu-vdagentbeforecargo runor the harness launches anything.cargo runon a host that is not macOS also asks its-display helpforgtk. A missing one is refused by name: "built without spice-protocol" or "no GTK display; Debian ships it asqemu-system-gui". Nothing falls back.EXIT=0. Each run is round 1's command line with onlyinfo pciandquit, thencargo run's Virtio profile on x86_64 and on aarch64 as designed here, with no firmware and the drives onnull-co. The agent's function is at00:06.0before the console's at00:07.0on q35, and at00:07.0before00:08.0onvirt.:69-109,:111-186,:188-254).debian@sha256:a2aa4626…, its snapshot's QEMU1:11.1.1+ds-1): log. It listsqemu-vdagentfor both architectures (:50,:78). The same three runs exit 0 (:143,:220,:288). The deps step's packages pull inqemu-system-gui(:29).chardev-qemu-vdagentand GTK's clipboard peer (log, read as bytes, never run).2. The kernel: two functions with one ID
Two
1af4:1043functions break two things today:kernel/src/drivers/virtio_console.rs:199);Ambiguous, and the kernel's own function counts (kernel/src/pcidev/mod.rs:715-747).Each rule is a pure decision in
toyos_pci(kernel/pci/), besideslot, with host tests that fail without it.VIRTIO_CONSOLE_F_MULTIPORT. That is the only shape its single-port driver drives: a multiport device names its console ports byCONSOLE_PORTmessages on queues this driver has none of (virtio 1.2 §5.3.6.2).ACKNOWLEDGE,DRIVER, read. That happens before bus mastering and before an address space, becauseenable_bus_masteris what records a function as kernel-driven (kernel/src/drivers/pci.rs:253-257,kernel/src/pcidev/mod.rs:480-486).toyos_pci::console(features: &[u64]) -> Option<usize>.vendor:deviceresolves among the functions the kernel does not drive. One undriven match is the claim's, two areAmbiguous, and none among driven ones isKernelDriven. The existingKernelDrivencontrol,usbd_drives_the_spare, still holds: that machine has one1b36:000d.toyos_pci::claimed(driven: impl IntoIterator<Item = bool>) -> Result<usize, Unclaimable>.kernel/src/drivers/virtio.rsbeside the three paths the ruling widened the fence to. Reading a feature word without bus mastering needsVirtioPciConfig::parse, which is private there (:84,:194).VirtioDevice::initenables bus mastering before it reads any feature (:721-731).The alternative is the kernel's console driver going multiport and passing port 1 to a process. That is a kernel addition userland can own.
3. The codec crate,
toyos-vdagent(lib/vdagent)An input boundary of its own (Fit), pure,
no_std, host-tested. Every word in it comes from the host:com.redhat.spice.0,MAX_PORTS = 2and the agent's port id 1. The launch (§1) and the agent read these.{id, event, value}plusPORT_NAME's bytes, bounded and UTF-8 checked;max_nr_portsis refused by name;DEVICE_READY,PORT_READYandPORT_OPEN.A chunk over
VD_AGENT_MAX_DATA_SIZE(2048) leaves the stream unframed. The program then ends by name.A chunk for a port other than
VDP_CLIENT_PORTis skipped by name.A message needs protocol 1 and at most
4 + MAX_CLIPBOARD_BYTES(2 MiB + 4) bytes. A larger one is counted off without being buffered and refused by name, and the next message still decodes.The buffer grows with the bytes that arrive, never to a size a header declares.
Each type's size follows its layout in
vd_agent.h:REQUEST: exactly 4;RELEASE: 0;ANNOUNCE_CAPABILITIES: at least 8;GRAB: a serial plus a multiple of 4, at most 16 types;CLIPBOARD: at least 4.An unknown type is refused by name.
The encoders write capabilities (
CLIPBOARD_BY_DEMAND | CLIPBOARD_GRAB_SERIAL),GRAB [serial][UTF8_TEXT],REQUEST,CLIPBOARDandRELEASE, each in chunks of at most 2048 bytes.4. The agent,
/system/bin/vdagent(system/vdagent)The driver (the program) runs over
toyos-pci-claimandtoyos-virtio, with netstack'svirtio_net.rsas the pattern.VERSION_1 | MULTIPORT | ACCESS_PLATFORMand refuses by name a function whosemax_nr_portsis not 2.DRIVER_OKit creates the six queues those two ports name (§5.3.5, §3.1.1), all on the one MSI-X entry the kernel programs.PORT_NAMEiscom.redhat.spice.0.CONSOLE_PORT, aDEVICE_REMOVEof its port, or a used ring it does not believe ends the program by name.PORT_OPEN(0)is answered with the guest'sPORT_OPEN(0), so QEMU can reopen; itsPORT_OPEN(1)is answered withPORT_OPEN(1). The session restarts at the host's next capabilities.Transmit is one buffer, 2 MiB + 4 KiB. It carries what the bridge owes the host, encoded from the bridge's state when the device has taken the previous one back.
hw/char/virtio-serial-bus.c:160-212,util/main-loop.c:307-315), and a host paste needs the same lock (ui/cocoa.m:1799). So no paste can arrive between theGRABand itsCLIPBOARD.ui/clipboard.c:199-208).The bridge is a pure crate nested in the program,
system/vdagent/bridge, host-tested. The owner isNone,HostorGuest(text). It keepslast, the serial of the guest's last grab in this session or none, andnext, the serial its next grab carries.last:= none,next:= 0, and it answers with its own capabilities. A host withoutCLIPBOARD_BY_DEMANDorCLIPBOARD_GRAB_SERIALis named once, and nothing flows. OwnerGuest(t)(a copy came before the port opened):GRAB+CLIPBOARD(t). Otherwise the owner becomesHostand it sendsREQUEST UTF8_TEXT, which QEMU answers from the host's clipboard, or with no data.GRAB slastis none ors ≥ last. A kept grab setsnext:= max(next,s + 1). The owner becomesHost, the guest clipboard is emptied, andREQUEST UTF8_TEXTgoes out if the grab offers it. A grab that is not kept is discarded by name.CLIPBOARDHostand the data is UTF-8. Anything else is refused by name, so a reply that lost to a guest copy cannot land.REQUESTGuestand the type isUTF8_TEXT, no data otherwise.RELEASEHostbecomesNone, and the guest clipboard is emptied. Any other owner ignores it, by name.t: the owner becomesGuest(t), andGRAB [next]+CLIPBOARD(t)go out in one transmit, withlast:=nextandnext+= 1. Empty:RELEASEif the owner wasGuest, and the owner becomesNone. A copy made while the port is closed only sets the owner. The next opening sends it.PORT_OPEN(0)Why the serial rule is right. QEMU keeps a crossing guest grab
giffg ≥ s + 1, that is iffg > s(ui/vdagent.c:342-351,:524-530). The bridge keeps the host's crossing grabsiffs ≥ g. Exactly one side wins, the same on both. Without a crossing, QEMU's grabs carry at least the serial of the last guest grab it kept. So the bridge keeps them, and its next grab carries one past them, which QEMU keeps. Serials compare as QEMU compares them, with no wrap (:524). A session that would passu32::MAXends by name.The loop. One
Pollercovers the claim's interrupt and the compositor connection. The agent connects to the compositor and watches (§5) before it opens the port. It is not restarted: one that ended says why and stays down for the boot.5. The compositor and
sdk/windowToday the compositor holds the clipboard as one
String. Any holder of acompositorconnector sets it, inline up to 4096 bytes and through theCOPY_BEGIN/REGION/COMMITexchange up to 2 MiB (system/compositor/src/session.rs:1051-1127). It is read only by GUI+V. Nothing can learn that it changed.clipboard, with one connection: the watcher.MSG_CLIPBOARD_WATCH. A second connection while the watcher lives is refused by name.toyos_desktop::pending_admits).MAX_WINDOW_SLOTSgoes from 218 to 216 (system/compositor/src/main.rs:46-63).MSG_CLIPBOARD_WATCHasks for the next copy, and it is the only frame that does.unsent: set by a client's set, cleared by the bridge's set and by a delivery.unsentis set, it is sent the clipboard, in the paste's two encodings, empty included. That clears the ask.WATCHdelivers: the clipboard the compositor holds, as soon as a client's set is unsent. At boot that means a copy made before the agent watched.try_sendstill drops the watcher by name under the server doctrine. It cannot happen to a watcher that asked.MSG_CLIPBOARD_SET, orMSG_COPY_BEGIN…MSG_COPY_REGION…MSG_COPY_COMMIT. The region is held by the watcher, not by a waiting connection, and it is the same exchangecopy_begin/copy_commitserve.unsent, are one pure decision intoyos_desktop, host-tested.sdk/windowgetsMSG_CLIPBOARD_WATCHand the watcher's client side besideclipboard_set. It never blocks: every send is atry_send, and the compositor'sMSG_COPY_REGIONarrives as an event on the agent'sPoller. One set is in flight at a time, and a newer host text replaces the one queued behind it.clipboard_set's blocking exchange (sdk/window/src/lib.rs:445-471) stays the apps'.6. Authority
The row is in
[boot] start, and the compositor's row also servesclipboard.clipboardreads every copy made in the session, so one row holds it.build/build.rs'sHELD_ALONEgains (clipboard,vdagent), with the port's name and holder declared intoyos_manifest, as the prompt port's are.compositorconnector (it opens no window), no syscap and no folder.7. Data types
UTF-8 text, and nothing else crosses. QEMU's clipboard has one type, its vdagent maps only
VD_AGENT_CLIPBOARD_UTF8_TEXT, and the compositor's clipboard is aString. A host grab that offers no text empties the guest clipboard and requests nothing, because the last copy wins. Line endings pass untouched: QEMU converts nothing, and the agent announces noGUEST_LINEEND_*.8. Security
cargo runboot pulls in the host's current clipboard. In the guest only the compositor keeps it, and the agent only while it passes it on. A guest program gets it only by GUI+V. A guest program can write the developer's host clipboard; that is the feature, andclipboard=offturns it off.9. Metal
QEMU only. On the T14 the supervisor's mint gets
NotFoundand logsno pci:1af4:1043 on this machine. The agent finds no claim, says so in one line, and exits 0.vendor:devicenamed in adeviceslist is carried by two functions.images/system.toml'spci:entries, with each entry's count of functions. It also carries the boot rows, which show what the feature costs metal: one process start and its binary in the image.10. Tests, under the owner's ruling in #890
Host unit tests:
toyos-vdagent:max_nr_ports, aPORT_NAMEthat is not UTF-8;vd_agent.h's packed layouts and §5.3.6.2's control layout, chunks included.toyos-vdagent-bridge, one test per row of the table, among them:the_answer_to_the_opening_request_lands;the_opening_grabs_a_copy_made_before_the_port_opened;host_text_that_lost_to_a_guest_copy_is_refused;a_crossing_host_grab_older_than_the_guests_is_discarded;a_crossing_host_grab_as_old_as_the_guests_is_kept;a_guest_grab_carries_its_text_in_the_same_transmit;every_request_is_answered;the_hosts_close_clears_the_serials_and_keeps_the_owner.toyos_desktop:a_bridge_set_is_never_sent_back;a_client_set_is_sent_once_asked;only_the_latest_of_two_client_sets_is_sent;nothing_is_sent_unasked;a_bridge_set_made_before_a_delivery_was_seen_is_refused.toyos_pci:the_console_is_the_first_function_without_multiport;multiport_functions_alone_give_no_console;one_undriven_match_among_driven_ones_is_claimed;two_undriven_matches_are_ambiguous;only_driven_matches_are_kernel_driven.The kernel's rule (a) runs on every virtio guest boot, because the agent's function enumerates first (§1). A kernel that took the first function would boot with no console.
One smoke row, on a new boot. No boot the suite runs today has both a compositor and a virtio function: every compositor boot is
Metal-shaped, andHeadlesshas no framebuffer. So a newtests/images/clipboardcase.tomlonProfile::HeadlessVirtioGpustarts logkeeper, the compositor, vdagent and test-runner (for the ready marker). The row waits on the guest for two lines:REQUEST, which is 0, because-display nonegives QEMU's clipboard no peer.Why it needs QEMU: the device exists only under QEMU, so no metal row reaches it. The other end of the handshake and of the request is QEMU's own vdagent and virtio-serial, which no host test runs.
Negative control: the smoke row run on the base, with the agent absent, is red. Each of these mutations must turn the named unit test red:
Hostcheck;Ambiguous.Independent oracles:
The full round trip, copying on the host and pasting in ToyOS and back, is not driven by any test, per the ruling.
Roast
The cost. It is estimated at about 1,300 production lines, an estimate. On top of that come a codec crate, one boot process in every image (metal's included) and one new boot in the guest suite, all for a comfort only
cargo runuses. The cheaper-looking alternatives are worse: the kernel passing on a port of its console, orpci-serial, which is a legacy UART off the published transport, with no open or close for the agent to restart on.Three kernel changes for a QEMU-only feature. Rule (a) reads a policy off a feature bit, and needs a feature read before bus mastering in
virtio.rs. Rule (b) changes what a claim's name means on every machine.Three of the design's steps rest on QEMU's implementation, not on spice-protocol's text:
REQUESTwith no grab before it;CLIPBOARDafter each grab, which leaves the by-demand reading: QEMU stores it, and spice-protocol'sCLIPBOARD"set[s] clipboard data (both directions)" and names no request before it;The module header cites the QEMU lines for each. A QEMU that changes the first breaks the smoke; one that changes either of the others fails with nothing visible.
The eager push moves every guest copy, up to 2 MiB, to QEMU's memory whether or not the host pastes it. That is what removes the freeze: a halted, panicked or debugger-stopped guest no longer holds cocoa's or GTK's paste. No residue of the freeze is left to record.
A host copy past QEMU's 1 MiB never arrives. The guest clipboard is then empty, not stale, and QEMU's stderr log says
buffer full, dropping message.The compositor's stale-set rule refuses a host text that crossed a guest copy. The guest copy then wins on both sides, because the agent grabs it next and QEMU keeps that grab.
Claim slots. A
cargo runboot then claims all four ofpcidev::MAX_FUNCTIONS(kernel/src/pcidev/mod.rs:146): sound, net and disk (images/system.toml:85,:95,:235), and the agent.hw/virtio/virtio-pci.c:2197-2198,:2260-2307). An agent that ended therefore keeps its slot as residue (kernel/pci/src/slot.rs:4-9), as netstack's and soundserver's functions do on that machine.Exhausted.kernel/src/arch/x86_64/idt/user_dev.rs,kernel/src/arch/aarch64/irqchip/its.rs), which is outside the fence. The build round files this inissues/with an exit: acargo runboot leaves a claim slot free.Every development host and CI need a QEMU built with spice-protocol, and
cargo runon a host other than macOS needs one with GTK. The prerequisite check says so by name. On those hosts the display becomes GTK, chosen explicitly.What I am unsure of
-display gtk,clipboard=onand GTK's clipboard peer are unrun: QEMU ships the peer off by default, and this project's one interactive host is a Mac. The build round files that gap inissues/.kernel/src/drivers/virtio.rs(§2), which the ruling's widened fence does not name. It also needstests/images/clipboardcase.tomland two newissues/files.Gates
None run: this is a design round, so nothing is built or tested. Net lines: zero (one empty commit).
🤖 Generated with Claude Code
https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy