Skip to content

Clipboard copied once into a region the compositor made; copy-once is a type, the compositor forbids unsafe code - #557

Merged
Japabu merged 15 commits into
mainfrom
wt/toyos-desk1
Sep 28, 2026
Merged

Japabu merged 15 commits into
mainfrom
wt/toyos-desk1

Conversation

@Japabu

@Japabu Japabu commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

The compositor no longer receives a handle from a client. The type system rules out reading a copy's region anywhere except once, at the commit, and the compositor crate forbids unsafe code.

What changed, and why

  • The compositor never uses a handle a client sent. MSG_CLIPBOARD_SET_SHM (10) gave the compositor a handle the client chose. SharedMemory::adopt mapped it, and for a pipe the kernel answers WrongType by ending the caller (exit 139), so any GUI client could end the desktop. Message 10 is retired. A client that sends it is dropped with its own reason, DropReason::Retired, and the compositor calls handle_recv nowhere. A handle a client sends stays queued until its connection closes, and then goes back to the kernel unused.

  • The compositor makes the region. A copy past MAX_INLINE_PAYLOAD opens its connection with MSG_COPY_BEGIN, whose payload is exactly one ClipboardShmMsg. Trailing bytes drop the client as out of protocol. The compositor moves the region's handle and then sends a bare MSG_COPY_REGION. The client writes the text and sends a bare MSG_COPY_COMMIT, and the compositor closes the connection. A commit with a payload is refused and the region's text is never read. window::copy_fits is the one bound, and both ends read it.

  • A refused move closes its handle. copy_begin calls syscall::handle_send on its own. When the kernel refuses the move, it restores the handle at its number, and the compositor closes it and drops the client as DropReason::Gone. The compositor never closes a handle after a successful move, because the slot has been reissued. No guest can trigger the refused move deterministically: it needs the client's close to race the compositor's send. The proof is the split itself. deliver_with_handles has the same leak (window creation, paste, resize), which is filed.

  • Copy-once is a type. CopyRegion in client.rs has a private field and one method, take(self) -> Vec<u8>, which reads every byte once through SharedMemory::as_atomic. Pending connections and frames hold Option<CopyRegion>. Reading the region in place does not compile (E0616), and neither does reading it at a paste without consuming it (E0507). set_clipboard validates the copy with String::from_utf8 and refuses non-UTF-8 by name. The inline MSG_CLIPBOARD_SET goes through the same function. The compositor's own mapping of a region it took outlives the client's own handle to it, which is filed with its owner and exit condition.

  • The copy state is one match. dispatch matches on (frame.copy, msg_type). A connection holding a region may send its commit and nothing else, and a commit is only accepted from such a connection; on any other connection, a window's included, it drops the client. If the refusal arm is deleted, the match is no longer exhaustive (E0004).

  • #![forbid(unsafe_code)] on the compositor. The window blit reads SharedMemory::as_slice(). The copy reads SharedMemory::as_atomic(), which is new in toyos/src/shm.rs next to as_slice. The key read decodes each event through ipc::decode_payload, and a torn read asserts. The cursor upload writes through the mapping's slice. Both slices rest on adopt's unchecked size, which is filed.

  • toyos-window. CopyError has two variants: TooLong, and Compositor(CreateError) for everything the exchange shares with window creation. CreateError reports CompositorGone only when the port is closed. The other failures each have their own variant:

    • Kernel(e): the kernel refused a call of the exchange (the connection, a frame, or the region's map), with its error.
    • BrokenOff: the compositor closed the connection, having exited or refused, or sent a frame this client cannot read.
    • NoHandle: the answer came without its handle. The compositor sent none, or this process had no room to take it; recv_handles_exact answers None for both.
    • Protocol(t): an answer this exchange does not allow. Its message names the type without claiming the type was the wrong part.

    IpcError::TooLarge is unreachable!: every frame this crate sends is within MAX_FRAME_LEN, and a const assertion holds MAX_INLINE_PAYLOAD to it.

  • terminal and editor log a refused copy instead of discarding it. An editor cut whose copy was refused keeps its text.

  • issues/isolation/a-received-handle-has-no-knowable-type.md loses its two bullets saying that a hostile client reaches only a closed instance. It gains two instances a client can reach: blockd's Region::adopt, and netd's piped socket and bind.

  • Tests. No guest wait in this PR has a clock: no deadline, no liveness guard, no must-not window and no count over time. Each wait blocks on its event (the connection's hang-up, the reply, the paste, the window's close) and prints waiting for <event> first. An event that never comes reds at the harness's ceiling, and the guest's last line names it. Where a case needs nothing to happen, a later ordered event proves it: the probe the compositor answers after the refusal, or the paste.

    • compositor_hostile_clipboard (new) is registered as metal_sim_hostile_clipboard, in Tier::Fast. Its cases:

      1. A pipe sent on message 10.
      2. A copy of 0xFF bytes; the paste must be the clipboard from before it.
      3. A copy of Cs, overwritten with Ds after its commit; the paste must be the Cs.
      4. A copy that is never committed.
      5. A second MSG_COPY_BEGIN on a copy connection.
      6. A begin with trailing bytes.
      7. A commit with 4 bytes of payload over a region of Es; the paste must be the inline clipboard set before it.
      8. A commit on a window's connection: right after it, the client asks the same connection its resolution — a message the compositor answers from dispatch on any connection it still serves — and the answer must never come, because the window is gone instead. await_hangup reads the connection to EOF rather than watching for a Close event, so a compositor that answered the commit instead of dropping the window reds here instead of passing.

      Cases 5 and 6 must end in a hangup, and bytes where the hangup was due red at once. The guest prints its paste marker once per paste and the host types GUI+V once per marker, so each marker is exactly one paste. The host reds on any handle fault: or exit: compositor record from the kernel, and it requires the named refusals of cases 1, 2 and 4.

    • compositor_client_death's two region cases use the new protocol: a commit with no copy begun, and a copy of u32::MAX bytes. The host requires the named refusal of the second. Its probe loses its 500 polls of 10 ms and blocks on the answer. The window closed from the inside loses its eight polls of 2 s: it waits for Close with no timeout and then polls once more, which a latched window answers None at once and an unlatched one answers Close at once.

    • window_refusal's stand-in compositor gains three answers: a close with no answer (BrokenOff), MSG_WINDOW_CREATED with no buffer (NoHandle), and a port queue the client fills until the kernel refuses the connection (Kernel with the kernel's own error). The client makes every request before it judges any, so a wrong answer reds as its assertion instead of leaving the stand-in blocked in accept on the next case.

Gates (host)

gate head exit
cargo test --lib 988c381 0 (384 passed)
cargo test --workspace --exclude toyos-build 70156f1 0
cargo run -- --clippy 988c381 0
cargo test --test toyos-build -- --list 988c381 0
cargo run -- --build-only 988c381 0

Arms that must not compile

Measured at 74435f1. No file they touch has changed since (git diff --stat 74435f1d 70156f1b -- userland/compositor userland/toyos-window toyos/src/shm.rs is empty). Each patch was applied as a checked patch. The tree was built with cargo run -- --build-only and then restored clean.

arm build exit error
m2: the commit reads the region in place (region.0.as_slice()) 101 E0616, field 0 of struct CopyRegion is private
m3: the region kept past the commit and read at every paste 101 E0507, cannot move out of *r which is behind a shared reference
m6, the refusal arm alone deleted 101 E0004, (Some(_), 0_u32..=13_u32) and (Some(_), 15_u32..=u32::MAX) not covered
m1, the retired arm deleted 101 variant Retired is never constructed

Guest runs: run by the orchestrator

Every run applies its patch with git apply, runs its command, and reverts with git apply -R. Control v3 is the whole change reverted onto the base it merges, git diff HEAD 51fe14c1 -- over its nine paths: tests/toyos-rust-tests/src/bin/compositor_client_death.rs, tests/toyos-rust-tests/src/bin/window_refusal.rs, userland/compositor/src/client.rs, userland/compositor/src/main.rs, userland/compositor/src/render.rs, userland/compositor/src/session.rs, userland/editor/src/main.rs, userland/terminal/src/main.rs, userland/toyos-window/src/lib.rs. It leaves out toyos/src/shm.rs's pure addition — a use plus as_atomic, a new method that changes no existing item and that only the kept test calls on the reverted tree.

Measured at 70156f1, from orch-runs/557r5-*.log, 557r6-fast.log, 557r7-control.log and summary.txt:359-375, 411-412, 485.

run command exit and red line
green cargo test --test toyos-build -- metal_sim_hostile_clipboard 0, PASS metal_sim_hostile_clipboard (4s)
green cargo test --test toyos-build -- window_refusal 0, PASS window_refusal (54ms)
green cargo test --test toyos-build -- --nightly metal_sim_client_death 0, PASS metal_sim_client_death (7s)
control v3, reverted onto 51fe14c metal_sim_hostile_clipboard 1, the kernel ended the compositor: [kernel 0.935 cpu0] handle fault: pid=4 tid=0 syscall=106 a PipeWrite where the call takes a SharedMem
m1, message 10 ignored rather than refused metal_sim_hostile_clipboard 1, the client that sent a pipe where a region went was not refused by name
m3′, the Session keeps the CopyRegion and takes it at the paste metal_sim_hostile_clipboard 1, [a region rewritten after its commit] the paste was 2097152 bytes, UTF-8: true, first byte that is not 'C' at Some(0)
m4, a copy's timeout reported as HandshakeTimeout metal_sim_hostile_clipboard 1, the client that held its region and never committed was not dropped by name
m5, the copy_fits bound removed --nightly metal_sim_client_death 1, a copy longer than any clipboard was not refused by name
m6, a copy connection's non-commit frame served as a fresh one metal_sim_hostile_clipboard 1, [a second begin on a copy] the peer answered where the compositor's refusal was due
m7, a lossy set_clipboard metal_sim_hostile_clipboard 1, [a copy that is not UTF-8] the paste was 6291456 bytes, not the clipboard from before
m8, a begin with trailing bytes accepted metal_sim_hostile_clipboard 1, [a begin with bytes past its length] the peer answered where the compositor's refusal was due
m9, copy_commit's bare-payload check deleted metal_sim_hostile_clipboard 1, [a commit with a payload] the paste was 2097152 bytes, not the clipboard from before
m10, a window's commit falls through to the ordinary dispatch metal_sim_hostile_clipboard 1, [a commit on a window] the peer answered where the compositor closing the window was due
m11, Disconnected mapped to CompositorGone window_refusal 1, reply Close decoded wrongly (left CompositorGone, right BrokenOff), then the client did not survive the refusals
m12, a missing buffer reported as Protocol(MSG_WINDOW_CREATED) window_refusal 1, reply NoBuffer decoded wrongly (left Protocol(1), right NoHandle), then the client did not survive the refusals
m13, EndowError::Refused(_) => Self::CompositorGone window_refusal 1, a full queue decoded wrongly (left Some(CompositorGone)), then the client did not survive the refusals
Fast tier cargo test --test toyos-build 1, 398 passed, 1 failed — log_ring_keeps_the_owners_slots, disabled on origin/main at #569, unrelated: this branch touches no logd or kernel log path

High risk: the two checks

  • Negative control: control v3, above. It reds with the kernel's handle fault on the case this PR closes.
  • Independent oracles:
    • The kernel's handle-type refusal, observed from outside the compositor. The kernel writes the handle fault: and exit: compositor records, whatever the compositor believed it did. The pipe's read end hangs up only once the refused connection's queue is gone.
    • rustc, for copy-once and the copy-state rule. The four compile arms above are its verdicts.

Filed

  • issues/isolation/a-refused-handle-move-leaves-the-compositor-holding-it.md: the same leak in deliver_with_handles.
  • issues/isolation/the-compositor-ignores-a-message-it-does-not-know.md. Owner: Session::dispatch.
  • issues/isolation/a-window-buffer-is-read-while-its-client-writes-it.md. Owner: the compositor's window blit.
  • issues/isolation/sharedmemory-slices-rest-on-a-size-nobody-checks.md: adopt takes an unchecked size, and share plus adopt gives two mappings in one process. Owner toyos::shm.
  • issues/isolation/the-compositor-keeps-a-committed-regions-mapping-for-as-long-as-the-client-does.md: CopyRegion::take drops the compositor's own handle but not its mapping, which the kernel tears down only once every handle to the region is gone anywhere. Owner: kernel::object::shm's handle-driven mapping teardown.
  • issues/design-debt/decode-payload-accepts-bytes-past-its-type.md: one trailing-bytes rule in toyos::ipc.
  • issues/design-debt/a-client-waits-on-the-compositors-answer-with-no-bound.md: clipboard_set and Window::create.
  • issues/build/a-key-being-built-is-waited-for-and-another-key-is-not-reds-under-host-load.md: found running this round's gates, unrelated to this PR. Held by the orchestrator.

🤖 Generated with Claude Code

Japabu and others added 2 commits September 27, 2026 22:57
…ndle is ever used

The compositor took MSG_CLIPBOARD_SET_SHM's region from the client: any handle
the client chose went to shm_map, and a pipe there is WrongType, which the
kernel answers by ending the caller (exit 139). And it validated the region as
UTF-8 in place and then copied it, while the client could still write it.

Now the compositor never receives a handle from a client. A copy past the
inline limit is MSG_COPY_BEGIN{len}, answered with MSG_COPY_REGION and a
region the compositor made; the client writes it and sends MSG_COPY_COMMIT;
the compositor reads every byte once through AtomicU8, validates that copy
with String::from_utf8, drops the region and closes the connection. A copy
never committed is dropped at HANDSHAKE_TIMEOUT by name. Message 10 is
retired and refused by name. The inline clipboard goes through the same
validation instead of from_utf8_lossy.

The compositor denies clippy::undocumented_unsafe_blocks. The key read is a
byte buffer decoded as KeyEvent through IpcPayload instead of a byte view of
an event array; the cursor upload writes the mapping's slice instead of a raw
pointer; the one unsafe block left in render.rs carries its SAFETY clause.

compositor_hostile_clipboard is the guest test: a pipe on the retired message,
a region rewritten while it is read, a region rewritten after its commit, and
a copy never committed. compositor_client_death's two region cases move to the
new protocol.

issues/isolation/a-received-handle-has-no-knowable-type.md loses the two
bullets that said a hostile client reaches only a closed instance; blockd is
one it reaches. Filed: the compositor ignores unknown message types, a
window's buffer is read while its client writes it, and no gate runs clippy
over the compositor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu marked this pull request as ready for review September 27, 2026 21:15
@Japabu

Japabu commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 1, of PR #557 at cc3e0c2 (merge base 1808fb8).

Gate state: the host check at cc3e0c2 is queued. There is no conclusion yet. The author's cargo run -- --ci host exited 0 on aafc73a, before the merge. No guest arm has run. Under reviewer.md alone this is NOT READY FOR REVIEW; the review goes ahead because the brief asks for it. The verdict does not depend on this, since BLOCKERs are open.

Net: production +256/−113 (+143), tests +398/−27 (+371), issues +62/−12 (+50).

BLOCKER

  • userland/compositor/src/session.rs:982: a refused handle_send leaks the handle it was meant to move.
    • try_send_with_handles hands theirs to sys_handle_send, and on a refusal the kernel "restores every entry at its own number" (kernel/src/syscall/ipc.rs). copy_begin then returns without closing theirs.
    • A client can drive this at will: send MSG_COPY_BEGIN{2 MiB} and close the connection before the answer comes. The kernel answers Gone, and the compositor keeps one handle slot and a 2 MiB region every time. Nothing bounds it. Once 4096 slots are gone, accept fails for good, and guest RAM runs out before that.
    • Fix: split the two phases. Call syscall::handle_send(conn, &[theirs]) alone and close(theirs) if it refuses, then try_send the frame. Never close theirs after a successful move: the slot has been reissued, and closing a handle the compositor no longer holds ends the compositor.
    • Nothing in a guest triggers this deterministically: it is the client's close racing the compositor's send. The split is its proof; the PR should say so.
  • userland/compositor/src/session.rs:1384 (copy_out), m2: copy-once and "validate the copy" have only a probabilistic red arm.
    • Case 2 reds under m2 only when the compositor's read happens to span a mixed phase.
    • Make the property unrepresentable instead. In client.rs, add pub struct CopyRegion(SharedMemory) with a private field and one method, take(self) -> Vec<u8>, which is the AtomicU8 read. Type PendingConn::copy and ClientFrame::copy as Option<CopyRegion> and delete copy_out. m2 (read the region in place) and m3 (keep the region past the commit) then no longer compile.
    • Delete case 2: rewritten_while_read, the rewriter thread, the all_a branch and the host's conditional "was refused" check.
    • Replace it with a deterministic case. Commit a region filled with 0xFF, wait for the hangup, then paste. The paste must be the previous text, and the host must require compositor: refusing a clipboard from client … it is not UTF-8.
    • The new case's arm, m7: set_clipboard does String::from_utf8_lossy(&bytes).into_owned(). Today m7 is caught only when case 2 happens to be mixed.
  • userland/compositor/src/session.rs:746: nothing tests "a connection holding a region may send MSG_COPY_COMMIT and nothing else".
    • Mutation m6: delete lines 746–749. A second MSG_COPY_BEGIN on a copy connection is then answered with a new region and a new since. A client that re-begins every 2 s holds a pending slot and 2 MiB indefinitely, and 32 such clients (MAX_PENDING_CONNS) lock every new client out.
    • Add a case to compositor_hostile_clipboard: begin_copy, then send COPY_BEGIN again on that connection, then await_hangup. Under m6 the guest fails with "the peer answered where … was due".
  • userland/compositor/src/main.rs:17: nothing enforces deny(clippy::undocumented_unsafe_blocks). The PR's own issue says so. On high-risk code that is a claim no test or gate can fail.
    • Replace it with #![forbid(unsafe_code)], which every build enforces. Two changes make that possible:
      • render.rs:185 is SharedMemory::as_slice() written out by hand, a copy of what the tree already has. Use win.client.shm.as_slice().
      • copy_out's cast moves into one safe SharedMemory::as_atomic(&self) -> &[AtomicU8] in toyos/src/shm.rs, beside as_slice, where CopyRegion::take reads it.
    • Then delete the deny and issues/build/no-gate-lints-the-compositor.md.
    • Cost: toyos is a std dependency (rust/library/std/Cargo.toml:107), so the toolchain's std rebuilds.
    • The clippy route is not a few lines. A Shape in src/clippy.rs needs a toolchain override, because userland/rust-toolchain.toml pins toyos and that toolchain has no clippy. It also needs a per-host --target and -A clippy::all. Even then it lints the compositor under host cfgs, not the x86_64-unknown-toyos build.

NOTE

  • userland/compositor/src/client.rs:240 (deliver_with_handles: paste at session.rs:1361, window buffers at :929 and :1483): this is the same leak as the first BLOCKER, on Full or Gone.
    • It predates this PR, and a client can reach it: MSG_SET_RESOLUTION is open to every app and reallocates every window's buffer. A window that never receives handles fills its 16-batch queue.
    • File it, with the same split as its exit condition.
  • kernel/src/object/shm.rs:3: the kernel tears a mapping down only when the last handle goes. The compositor's mapping of a committed region therefore outlives its drop for as long as the client keeps its handle. Only the client's 4096 slots bound this, at 2 MiB each. The compositor never reads the region again, but "drops the region" is true of the handle only.
  • toyos/src/ipc.rs:428: decode_payload accepts trailing bytes. So MSG_COPY_BEGIN with extra payload is accepted, and the excess is dropped silently. copy_commit checks for a bare frame; copy_begin should check for an exact length too.
  • userland/terminal/src/main.rs:151,178 and userland/editor/src/main.rs:1613,1618: .ok() discards CopyError, so an over-long copy is still dropped silently for the user. The PR body claims otherwise.
    • Report the error. If nothing does, CopyError (lib.rs:381–417) earns nothing: three of its variants and its From<EndowError> are copied from CreateError.
  • userland/toyos-window/src/lib.rs:415: EndowError::Refused(_) and a compositor that refuses by closing (out of memory) both surface as "the compositor is gone", which is false.
  • userland/toyos-window/src/lib.rs:440: the client discards MSG_COPY_REGION's echoed length, and the test is its only reader. Either check it against bytes.len() and refuse by name, or send no payload.
  • userland/compositor/src/session.rs:746 and :999: the copy state is decided in two places. A single match on (frame.copy, msg_type) would state the rule once.
  • tests/toyos.rs:760: Tier::Fast rests on an unmeasured duration. The test includes a guaranteed HANDSHAKE_TIMEOUT wait and three 2 MiB copies, and the tier line is 10 s (src/tiers.rs). Record the green run's wall time and move the test to Nightly if it is over.
  • issues/isolation/a-received-handle-has-no-knowable-type.md:34: netd adopts two handles a client sent as pipes (userland/netd/src/main.rs:432) and a third at :1205. This is the same class, reachable from a client, and the list names blockd but not netd.
  • PR body, negative control: git diff HEAD origin/main names a ref that moves. Name 1808fb8d.
  • userland/compositor/src/session.rs:374: let event = &… followed by (*event).into() borrows and dereferences for nothing.

REMOVE

  • userland/compositor/src/render.rs:182-184: the SAFETY clause says a concurrent write "can tear pixels", but the PR's own issue calls it a data race. It goes with as_slice.
  • userland/toyos-window/src/lib.rs:84-85: remove "this is one 2 MiB page, the smallest region the kernel makes". That was the retired path's reason; the compositor now makes a region of exactly len.
  • issues/build/no-gate-lints-the-compositor.md: the whole file, with the fourth BLOCKER.
  • PR body: the "Unsure" section and every "Expected:" line. Main's record carries measurements.
  • tests/toyos-rust-tests/src/bin/compositor_hostile_clipboard.rs:1,13-16: "Two hostile clipboards" and case 2's paragraph, which go with case 2.
  • tests/toyos.rs metal_sim_hostile_clipboard: the doc's "a region rewritten while the compositor reads it" and the final eprintln's "both rewritten regions read once".

Required guest runs (orchestrator, at the fixed head)

  1. cargo test --test toyos-build -- metal_sim_hostile_clipboard: green, with its wall time recorded.
  2. cargo test --test toyos-build -- --nightly metal_sim_client_death: green. Its binary changed and has no green run.
  3. Negative control: git diff HEAD 1808fb8d -- userland/compositor userland/toyos-window toyos/src/shm.rs tests/toyos-rust-tests/src/bin/compositor_client_death.rs | git apply, then run 1. It must go red at case 1, with handle fault: and exit: compositor … code=139. The test binary keeps its own cast so that it builds on the base.
  4. m1 (the retired arm deleted), then run 1: red with "not refused by name".
  5. m3′ (Session keeps the CopyRegion and takes it in paste), then run 1: deterministically red at case 3.
  6. m4, then run 1: red with "never committed was not dropped by name".
  7. m5, then run 2: red with "a copy longer than any clipboard".
  8. m6 (lines 746–749 deleted), then run 1: red at the second-BEGIN case.
  9. m7 (lossy set_clipboard), then run 1: red at the invalid-UTF-8 case.
  10. cargo test --test toyos-build: green, apart from reds that cargo run -- --known-red names.

m2 is a compile failure once CopyRegion exists. It is shown on the host with cargo run -- --build-only, which must exit non-zero; it needs no guest run. CI host must be green at the head.

SEND BACK

Japabu and others added 2 commits September 27, 2026 23:50
…ve closes its handle

- copy_begin moves the region's handle with handle_send on its own and
  closes it when the move is refused, then sends MSG_COPY_REGION bare. A
  client that begins a copy and closes before the answer used to cost the
  compositor one handle slot and one region per try: the kernel restores a
  refused batch at its own numbers, and nothing closed it. A handle is
  never closed after a successful move. No guest can trigger this
  deterministically, since it is the client's close racing the send. The
  split is the fix. The same leak in deliver_with_handles (window creation,
  paste, resize) is filed.
- CopyRegion (client.rs) wraps the compositor's copy region. Its one method,
  take(self), reads every byte once through SharedMemory::as_atomic. Pending
  connections and frames carry Option<CopyRegion>, and copy_out is gone.
  Reading the region in place or reading it a second time no longer
  compiles.
- The copy state is decided in one match on (frame.copy, msg_type): a
  connection holding a region may send its commit and nothing else, and a
  commit comes only on such a connection. Deleting the refusal arm makes
  the match non-exhaustive.
- MSG_COPY_BEGIN's payload must be exactly one ClipboardShmMsg. Trailing
  bytes drop the client as out of protocol.
- MSG_RETIRED_CLIPBOARD_SET_SHM drops its client with its own reason,
  DropReason::Retired, so the host can tell that refusal from every other
  out-of-protocol drop.
- The compositor is #![forbid(unsafe_code)]. The window blit reads
  SharedMemory::as_slice, and the copy reads SharedMemory::as_atomic,
  added in toyos/src/shm.rs. The clippy deny and its issue are deleted.
- toyos-window: CopyError keeps only what is its own, TooLong and
  Compositor(CreateError). CreateError reports CompositorGone only for a
  closed port. It reports ConnectRefused for a kernel refusal, BrokenOff
  for a connection that failed mid-exchange (the compositor exited, or
  refused by closing), Protocol for an answer that came without its region,
  and Unmappable for a region that would not map.
- terminal and editor report a refused copy instead of discarding it, and
  an editor cut whose copy was refused keeps its text.
- compositor_hostile_clipboard: the rewritten-while-read case is replaced
  by a copy of 0xFF bytes, whose paste must be the clipboard from before
  and whose refusal the host requires by name. The after-commit case
  overwrites with text, so a read at paste would be pasted. New cases: a
  second MSG_COPY_BEGIN on a copy connection, and a begin with trailing
  bytes. Each must end in a hangup.
- Filed: issues/isolation/a-refused-handle-move-leaves-the-compositor-holding-it.md.
  netd is added to a-received-handle-has-no-knowable-type.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu Japabu changed the title Clipboard copied once into a region the compositor made; no client handle is ever used Clipboard copied once into a region the compositor made; copy-once is a type, the compositor forbids unsafe code Sep 27, 2026
@Japabu

Japabu commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 2, of PR #557 at c5d09bb (merge base e5ffe95). Changes since round 1 are reviewed as cc3e0c2..28db806; c5d09bb brings in only main's toyos-net-tcp and toyos-net-wire.

Gate state. This head is not yet ready for review under reviewer.md. The review goes ahead because the brief asks for it.

  • CI host at c5d09bb is QUEUED (run 36354161253) and has no conclusion.
  • The author's host gates at c5d09bb all exit 0 (desk1-r2/gates.log): build-only, --list, --lib, --clippy, and --ci host.
  • orch-runs/summary.txt has no 557r2-* line. queue10.sh waits on QUEUE9, and only QUEUE5 is done.
  • These guest runs are owed:
    • green metal_sim_hostile_clipboard, with its duration;
    • green nightly metal_sim_client_death;
    • the negative control reverted onto 1808fb8;
    • m1-retired-ignored, m3′, m4, m6-second-begin-served, m7, m8, and m5 on client_death;
    • the fast tier.
  • This verdict does not stand without them. If the green run fails, or any arm does not red with its stated line, that is a BLOCKER for round 3.

Net: production +332/−139 (+193), tests +379/−27 (+352), issues +80/−12 (+68).

Round-1 BLOCKERs

  • B1, the leak on a refused handle move: CLOSED, by the code, since no guest can race it.
    • session.rs:993 calls syscall::handle_send on its own.
    • sys_handle_send is all-or-nothing and restores the handles at their own numbers on refusal (kernel/src/syscall/ipc.rs:369), so theirs is still the compositor's to close.
    • After a successful move, nothing closes it.
    • The deliver_with_handles leak is filed with an exit condition.
  • B2, copy-once and validation: CLOSED on the compile half, and the guest half is owed.
    • m2 reds with E0616 and m3 with E0507 (build-m2-validate-in-place.log, build-m3-read-at-every-paste.log, both exit 101).
    • The 0xFF case and the D-overwrite case are deterministic by construction. Their arms, m7 and m3′, are owed.
  • B3, a second BEGIN: CLOSED on the compile half, and the guest arm is owed.
    • Deleting the refusal arm reds with E0004 (build-m6-delete-refusal.log, exit 101).
    • m6-second-begin-served is owed.
  • B4, unsafe code in the compositor: CLOSED.
    • #![forbid(unsafe_code)] is at userland/compositor/src/main.rs:17, and build-only exits 0 at the head.
    • git grep unsafe userland/compositor finds only the attribute.
    • The deny and its issue are deleted.

Rulings asked for

  • CreateError's four variants and the Window::create remap: they stay in this PR.
    • They answer round-1 NOTEs: CopyError duplicated three of CreateError's variants, and "the compositor is gone" was false for EndowError::Refused and for a refusal by closing.
    • Folding CopyError into Compositor(CreateError) deletes the duplicate. That makes CompositorGone's narrowed meaning, and a home for everything it used to absorb, necessary in the shared enum.
    • Splitting it out would reintroduce either the duplicate or the false message.
    • git grep finds no reader that matches CompositorGone or any new variant, so the remap changes messages only. The costs are NOTEs 6 to 8.
  • DropReason::Retired is justified.
    • Cases 5 and 6 now produce OutOfProtocol too, so without a reason of its own the host's check for case 1 would pass under m1.
    • Deleting its only construction fails the build (build-m1-delete-arm.log: "variant Retired is never constructed").
  • SharedMemory::as_atomic is sound exactly as far as as_slice is.
    • AtomicU8 has u8's size and alignment. The slice borrows self, and self owns a handle that keeps the mapping.
    • An atomic read is the only in-model way to read bytes a peer process may change.
    • Two things carry the premise, "the mapping is size bytes and nobody else in this process names it". Neither is enforced by the type:
      • adopt is safe and does not check size (its own doc, shm.rs:37-40);
      • shm_map is idempotent, so share() followed by adopt gives two SharedMemory values over one mapping, and as_mut_slice on one aliases as_atomic on the other.
    • Both holes predate this PR, in as_slice and as_mut_slice. The compositor satisfies the premise: every region it reads, it created, and the only one it adopts is the kernel's cursor. See NOTE 1.

BLOCKER

None open.

NOTE

  1. toyos/src/shm.rs:79 (adopt at :43): as_slice, as_mut_slice and now as_atomic are safe functions whose soundness rests on an unchecked size and on one SharedMemory per mapping.
    • A server that adopts a peer region at the peer's declared length reads past it in safe code.
    • forbid(unsafe_code) on the compositor now leans on this.
    • File it with owner toyos::shm. Exit condition: adopt becomes unsafe fn, or it checks the length against the kernel's region and refuses a second mapping.
  2. tests/toyos-rust-tests/src/bin/compositor_hostile_clipboard.rs:173: bytes() repeats SharedMemory::as_atomic.
    • It exists only because the control reverts toyos/src/shm.rs. as_atomic is additive and unread on the base, so the control can leave shm.rs out and the test can call region.as_atomic().
    • Doing this changes the test binary, so the green arm must be re-run.
  3. userland/compositor/src/session.rs:1013: no case reds if the commit's bare-payload check is deleted.
    • The mutation is to delete lines 1013–1016.
    • A connection hangs up after a commit either way, so only the clipboard tells the difference. The case: begin_copy, fill with E, send_bytes(COPY_COMMIT, &[0; 4]), await_hangup, then the paste must not be the Es.
  4. userland/compositor/src/session.rs:752: deleting | (None, window::MSG_COPY_COMMIT) still compiles, because (None, _) covers it, and the commit falls into dispatch's _ => {}.
    • compositor_client_death's "a commit with no copy begun" asserts only that the compositor survives, so nothing reds.
    • For a one-shot connection the consequence is nil. For a window's connection, the frame is ignored instead of being refused.
  5. userland/compositor/src/session.rs:995: ipc::TrySendError::Syscall(e).into() builds an error only to reach DropReason::Gone. Write DropReason::Gone.
  6. userland/toyos-window/src/lib.rs:180: the blanket From<ipc::IpcError> maps Disconnected, Malformed, TooLarge and Syscall(e) all to BrokenOff.
    • It discards Syscall(e), the same collapse that CompositorGone was.
  7. userland/toyos-window/src/lib.rs:450, :454 and :533: a missing handle, or a payload on MSG_COPY_REGION, becomes Protocol(<the expected type>).
    • Its message, "answered with message type 13", is false: the type was right.
    • recv_handles_exact also answers None when this client's own handle table is full. That failure is the client's own, not a protocol violation by the compositor.
  8. userland/toyos-window/src/lib.rs:170-183: no test reads ConnectRefused, BrokenOff or Unmappable.
    • The mutation EndowError::Refused(e) => Self::CompositorGone passes every suite.
    • This is not high-risk code. The name CreateError now also covers copies, and a copy can never produce AtCapacity, TooLarge, NoMemory or Refused.
  9. userland/compositor/src/session.rs:956: copy_begin is the only one of the compositor's five decode_payload sites that refuses trailing bytes.
    • Rect, the cursor style, ResolutionRequest and CreateWindowRequest still accept them.
    • The rule belongs in toyos::ipc, once; file it.
  10. userland/toyos-window/src/lib.rs:449: clipboard_set now blocks on the compositor's answer with no bound. Window::create does the same. A wedged compositor now also wedges a terminal's copy.
  11. tests/toyos.rs:760: Tier::Fast rests on the duration still owed.
    • Take the harness's own per-test time, not queue10's wall=, which includes the build.
    • Move the test to Nightly if it is over 10 s.
  12. Merge commit c5d09bb has the message "x". It cannot be rewritten; this is recorded, and no action is asked.

REMOVE

  • userland/compositor/src/client.rs:253: remove "The handles are moved whether or not the frame lands". The branch's own issue shows it is false for a refused handle_send.
  • issues/isolation/a-refused-handle-move-leaves-the-compositor-holding-it.md:14-15: remove the sentence citing that doc; it goes with the doc.
  • PR body, "Guest arms: run by the orchestrator": every "must print" and "must red with" line. These are round 1's "Expected:" lines rewritten. Main's record carries each arm's exit code and red line from the runs.
  • PR body: remove "is placed in Tier::Fast until the green run measures its duration. It moves to Nightly if that run is over the 10 s tier line." It is a conditional, and the measured tier replaces it.
  • PR body: remove "origin/main has not changed these paths since 1808fb8d." It is true today (checked with git diff 1808fb8d origin/main on those paths: empty), and it rots as main moves.

LAND AFTER NAMED CHANGES

Japabu and others added 5 commits September 28, 2026 01:17
…keeps the kernel's word

The compositor:
- A refused region move drops the client as `DropReason::Gone`, written
  directly rather than built from a send error.
- `deliver_with_handles` loses the sentence saying its handles move whether or
  not the frame lands, which is false for a refused `handle_send`.

`toyos-window`'s `CreateError`:
- `ConnectRefused` and `Unmappable` become one `Kernel(SyscallError)`. Nothing
  distinguished them but the call that was refused, and an `IpcError::Syscall`
  now lands there too instead of being discarded as `BrokenOff`.
- An answer without its handle is `NoHandle`, not `Protocol` of the type that
  was right. Its doc names both causes, since `recv_handles_exact` also answers
  `None` when this process has no room for the handle.
- `IpcError::TooLarge` is unreachable: every frame this crate sends is within
  `MAX_FRAME_LEN`, and a const assertion holds `MAX_INLINE_PAYLOAD` to it.
- `Protocol`'s message no longer says the type was wrong, because a payload on
  `MSG_COPY_REGION` is the right type in the wrong shape.

Tests:
- `compositor_hostile_clipboard` gains two cases. A commit with a payload over
  a region of `E`s must leave the clipboard as it was. A commit on a window's
  connection must lose the window its connection.
- `window_refusal` gains readers for `BrokenOff` (the stand-in closes
  unanswered), `NoHandle` (`MSG_WINDOW_CREATED` with no buffer), and `Kernel`
  (a port queue filled until the kernel refuses the connection).

Filed:
- `issues/isolation/sharedmemory-slices-rest-on-a-size-nobody-checks.md`
- `issues/design-debt/decode-payload-accepts-bytes-past-its-type.md`
- `issues/design-debt/a-client-waits-on-the-compositors-answer-with-no-bound.md`

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #561: the host hands out no guest slots and no build slots. It
touches tests/toyos.rs apart from this branch's hunks, and the merge is clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mes it

compositor_hostile_clipboard loses its 30 s ceiling, its 2 s paste re-mark
and all three Instant deadlines. A hang-up is a blocking read that answers 0,
the region and the probe are blocking header reads, a paste and a window's
close are the window's blocking recv_event. The paste marker is printed once,
so each GUI+V the host types is one paste and no stale-paste skip is needed.

compositor_client_death's probe loses its 500 x 10 ms sleep-poll, and the
window-closed-from-the-inside case its eight 2 s polls: it waits for Close
with no timeout, then asks once more, which a latched window answers None
at once and an unlatched one answers Close at once.

Every wait prints what it waits for first, so an event that never comes is
the harness's ceiling with the missing event on the guest's last line.
window_refusal had no clock; both its sides now name their waits too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
The stand-in serves one request per case. A client that panicked at a
wrong answer left the stand-in blocked in accept on the next case, so a
decoding red was a harness ceiling instead of the client's assertion. Now
every answer is collected first and judged after, so a wrong one reds as
the client's panic and the server's reap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
@Japabu

Japabu commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 3, of PR #557 at a69ee8e (merge base 1ec6daa). Changes since round 2 (c5d09bb) are reviewed as c6613b4, 2a5c94c and a69ee8e. The merges 74435f1 and da3271f bring in only main.

Gate state.

  • CI host at a69ee8e is SUCCESS (run 36384497727).
  • The orchestrator's guest runs at a69ee8e are in orch-runs/557r4-*.log, summary.txt:241-257, with every porcelain clean apart from the arm's own patch.
  • Green, EXIT=0 each: metal_sim_hostile_clipboard (PASS in 5 s), window_refusal, and --nightly metal_sim_client_death.
  • The control patch is byte-identical to git diff HEAD 1ec6daa9 over the ten production and test paths (cmp), so it is the whole change reverted onto the base.

Net: production +197 (340/−143), tests +388 (491/−103), issues +132 (144/−12). The production growth is accepted: a message that let any GUI client end the desktop becomes a protocol in which the compositor makes the region, plus typed errors.

Earlier BLOCKERs

  • r1 B1, B4: CLOSED, as judged in round 2.
  • r1 B2: CLOSED. Its guest half is now measured at a69ee8e:
    • m3′: [a region rewritten after its commit] the paste was 2097152 bytes, UTF-8: true, first byte that is not 'C' at Some(0)
    • m7: [a copy that is not UTF-8] the paste was 6291456 bytes, not the clipboard from before
  • r1 B3: CLOSED. m6: [a second begin on a copy] the peer answered where the compositor's refusal was due.
  • Round 2 had no BLOCKER.

What the brief asked

  • In-guest clocks are gone.
    • Grepping the three binaries for Instant|Duration|sleep|timeout|deadline|elapsed|nonblock finds only doc text and FOREVER = u64::MAX, which means block forever (toyos-abi/src/syscall.rs:2162).
    • window_refusal's fill loop ends on the kernel's refusal, not on a count.
    • On the host, the only clock is the harness ceiling passed to run_test_paced. The frame-batch wait in metal_sim_client_death goes through await_guest, which is bounded by liveness, not by time.
    • Case 4 waits on the compositor's production HANDSHAKE_TIMEOUT, and asserts only the named reason.
  • Each arm reds for its named reason, and none reds on a build failure or an unrelated panic:
    • control: the kernel ended the compositor: … handle fault: pid=4 … a PipeWrite where the call takes a SharedMem, then exit: compositor pid=4 code=139
    • m1: …was not refused by name (the guest itself reached every case survived)
    • m4: …never committed was not dropped by name (the compositor said it never finished its first message)
    • m5: a copy longer than any clipboard was not refused by name (the compositor said no memory for 4294967295 bytes (ResourceExhausted))
    • m8: [a begin with bytes past its length] the peer answered…
    • m9: [a commit with a payload] the paste was 2097152 bytes…
    • m11: reply Close decoded wrongly, CompositorGone against BrokenOff
    • m12: reply NoBuffer decoded wrongly, Protocol(1) against NoHandle
    • m13: a full queue decoded wrongly, Some(CompositorGone) against Some(Kernel(ResourceExhausted))
    • m10 reds at the harness ceiling: timed out after 338s … it was working and did not finish. See the BLOCKER.
  • The Fast tier's one red is not the branch's. It is 395 passed, 1 failed.
  • #![forbid(unsafe_code)] is at the crate root and nothing routes around it.
    • It is at userland/compositor/src/main.rs:17. The crate is a single binary with no build.rs.
    • git grep unsafe userland/compositor finds only the attribute.
    • The only macros the crate invokes are std's.
    • What remains is NOTE 4.
  • The independent oracle is accepted.
    • rustc judges the type claims: E0616, E0507 and E0004 at 74435f1, and no file those arms touch has changed since.
    • For the runtime claim, the kernel's handle-type fault is the oracle. The control reproduces it on the base as exit 139.
  • Merging origin/main carries little conflict risk.

BLOCKER

  • tests/toyos-rust-tests/src/bin/compositor_hostile_clipboard.rs:134-137: case 8 reds only at the harness ceiling, and its verdict cannot tell a drop from a message.
    • m10 costs 340 s of the Fast tier and ends on a ceiling verdict, which is no negative control.
    • while !matches!(committing.recv_event(), Event::Close) also passes when the compositor answers the window with any type decode_event does not know, because of _ => Event::Close in userland/toyos-window/src/lib.rs. A compositor that answered the commit instead of dropping the window would pass.
    • A cheap ordered event exists. MSG_GET_RESOLUTION on the window's own connection is answered from dispatch (session.rs:819, then answer_resolution(handle)), and a dropped connection never answers it.
    • The patch:
      -    let mut committing = Window::create_with_title(64, 64, "commit")
      +    let committing = Window::create_with_title(64, 64, "commit")
      …
           ipc::signal(committing.handle(), COPY_COMMIT)
               .unwrap_or_else(|e| fail(what, &format!("no commit: {e:?}")));
      -    waiting(what, "the compositor closing the window");
      -    while !matches!(committing.recv_event(), Event::Close) {}
      +    ipc::signal(committing.handle(), window::MSG_GET_RESOLUTION)
      +        .unwrap_or_else(|e| fail(what, &format!("no probe: {e:?}")));
      +    await_hangup(committing.handle(), what, "the compositor closing the window");
           probe(what);
      
    • Measure it with the green, and with m10, which must red at once with [a commit on a window] the peer answered where the compositor closing the window was due.
    • If the green shows the compositor sending a fresh window a frame first, skip whole frames by header until EOF, and red on MSG_RESOLUTION_CHANGED. Do not go back to recv_event.

NOTE

  1. The PR body carries no measurement at the head. Its a69ee8e table has an expected column. Replace it with each run's exit code and red line from 557r4-*.log, the lines quoted above.
  2. compositor_hostile_clipboard.rs:211: bytes() still duplicates SharedMemory::as_atomic (round 2, NOTE 2).
  3. kernel/src/object/shm.rs:4: the compositor keeps its mapping of every committed region after CopyRegion::take, for as long as the client keeps its handle. A client that keeps every region leaves one 2 MiB mapping per copy in the compositor, bounded only by its own handle table. Round 1 noted this and it is still not filed. File it with an owner and an exit condition.
  4. userland/compositor/src/render.rs:182: the blit reads a client-written window buffer as &[u8] through the safe as_slice. The crate forbids unsafe, but the unsafety moved into toyos::shm rather than going away. It is filed twice, and it is still true.
  5. issues/isolation/a-window-buffer-is-read-while-its-client-writes-it.md and issues/isolation/the-compositor-ignores-a-message-it-does-not-know.md have no owner line. The other four new issues each name one.
  6. compositor_client_death.rs:153: the second poll_event(FOREVER) prints no waiting line. A hang there would leave "waiting for the window's Close" as the last line, which names the wrong event.

REMOVE

  • PR body: remove the section "Measured at c5d09bb, with the guest tests as they were then", with its Fast-tier line. It measured tests that have since been rewritten, and the runs at a69ee8e supersede it.
  • PR body: remove "## What I am unsure of". Round 1 removed it once already. The green answers its paste bullet, and its m10 bullet goes with the BLOCKER.
  • PR body: remove the expected column; NOTE 1 replaces it.
  • tests/toyos.rs:11317-11319: remove the OVERSIZE comment. It was rewritten from "The one case whose verdict is a line" instead of being deleted.
  • tests/toyos.rs:713: remove "no clock in any verdict". It is a claim nothing checks.
  • compositor_hostile_clipboard.rs:30-32 and compositor_client_death.rs:27-29: remove "No wait here has a clock…". It restates the doc of waiting() in the same file, and a rule the review prompt holds.
  • issues/isolation/a-window-buffer-is-read-while-its-client-writes-it.md:13-14: remove "Nothing bounds the read outside the mapping, so the compositor is not at risk". It reads as its own opposite, and the data race it waves off is the risk the file names.

SEND BACK

Japabu and others added 4 commits September 28, 2026 10:36
…itor

that answers the commit instead of dropping the window reds instead of
passing: a fresh MSG_GET_RESOLUTION on the same connection is answered only
if the connection survives, and await_hangup tells that apart from a drop.

File the compositor's own mapping of a committed region outliving the
client's handle, with its owner and exit, and give the two prior new issues
an owner line in the form the others use. Replace the atomic cast the test
duplicated with SharedMemory::as_atomic, and give the second poll_event its
waiting line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
cargo test --lib reds once on buildlock::tests::a_key_being_built_is_waited_for_and_another_key_is_not
under a loaded host and passes alone straight after; unrelated to this branch's compositor work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
…ly gate

cargo run -- --build-only and cargo test --test toyos-build -- --list both fail after
this branch's merge moved rust/'s pin, reproducing with ./x alone outside this
repository's wrapper; a hand-run cargo build with the same command line and env
succeeds every time, so the fault is in the stale build-dir x drives, not in the
crates or this branch's sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
@Japabu

Japabu commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 4, of PR #557 at 70156f1 (merge base 51fe14c). Changes since round 3 (a69ee8e) are reviewed as 10265bb, 2591a1d and 70156f1. The merge e039fe6 is clean: its diff against its main-side parent is exactly the branch's own 20 paths.

Gate state.

  • CI host at 70156f1 is SUCCESS (run 36402097121).
  • The orchestrator's guest runs at 70156f1 are in orch-runs/557r5-*, 557r6-fast.log and 557r7-control.log, summary.txt:359-375, 411-412, 485.
  • Green, EXIT=0 each: metal_sim_hostile_clipboard (PASS in 4 s), window_refusal, and --nightly metal_sim_client_death.
  • Fast (557r6): 398 passed, 1 failed. The red is log_ring_keeps_the_owners_slots. Main disables it at 225dacb (Disable log_ring_keeps_the_owners_slots behind its filed defect #569), which is an ancestor of origin/main and not of HEAD, and the branch touches no logd or kernel log path. lan_mdns_answer, round 3's red, now passes.

Net against origin/main: production +197 (340/−143), tests +372 (475/−103), issues +221 (233/−12). No production line changed since a69ee8e, so round 3's acceptance of the growth stands.

Earlier BLOCKERs

  • r3 B1 (case 8 red only at the ceiling): CLOSED.
    • 557r5-m10 is EXIT=1 in 4 s with [a commit on a window] the peer answered where the compositor closing the window was due.
    • The green is EXIT=0.
    • The probe cannot be answered in the pass that drops the window:
      • take_frames pumps one frame per window per pass.
      • reap runs after dispatch in the same pass (session.rs:357-359).
      • So MSG_GET_RESOLUTION is answered only on a connection that outlived the commit.
  • r1 B1 to B4: CLOSED, as judged in rounds 2 and 3.

Round 3 NOTEs and REMOVEs

  • NOTE 2 (the duplicated atomic cast) is closed. The test uses SharedMemory::as_atomic.
  • NOTE 3 (the compositor's mapping of a committed region) is closed. It is filed with an owner and an exit condition. Its claim holds: unmap_from has one caller, kernel/src/inbox/mod.rs:73.
  • NOTE 5 (owner lines) is closed for the two files it named.
  • NOTE 6 (the second poll's waiting line) is closed.
  • NOTE 4 (the blit's read) is still true and is filed.
  • NOTE 1 (no measurement at the head in the PR body) is still OPEN. See below.
  • Every REMOVE is done, except that an Expected: came back in the m10 paragraph.

What the brief asked

  • Control v3 is the whole change under test.
    • git diff HEAD 51fe14c1 over its nine paths is byte-identical to control-revert-onto-51fe14c1-v3.patch (cmp).
    • The branch's other three non-issue paths are these:
      • the kept guest test, compositor_hostile_clipboard.rs
      • its host half, tests/toyos.rs
      • toyos/src/shm.rs
    • It reds for the named reason: the kernel ended the compositor: … handle fault: pid=4 … a PipeWrite where the call takes a SharedMem, then exit: compositor pid=4 code=139.
  • Leaving out shm.rs is sound.
    • Its diff is one use plus one new method, as_atomic. It changes no existing item.
    • On the reverted tree, its only caller is the kept test. The base compositor never names it.
    • Reverting it is what made 557r6-control a build failure (EXIT=101, could not compile … compositor_hostile_clipboard), which measures nothing.
  • The arms each red for their named reason. None hits the ceiling:
    • m1: …not refused by name (the guest reached every case survived)
    • m3′: …first byte that is not 'C' at Some(0)
    • m4: …never committed was not dropped by name
    • m5: a copy longer than any clipboard was not refused by name
    • m6 and m8: the peer answered where the compositor's refusal was due
    • m7: 6291456 bytes, not the clipboard from before
    • m9: 2097152 bytes, not the clipboard from before
    • m11: reply Close decoded wrongly
    • m12: reply NoBuffer decoded wrongly
    • m13: a full queue decoded wrongly
  • The compile-arm oracle still applies. git diff --stat 74435f1d 70156f1b -- userland/compositor userland/toyos-window toyos/src/shm.rs is empty.
  • No in-guest clock is left.
    • Grepping the three binaries for Instant|Duration|sleep|timeout|deadline|elapsed|nonblock|try_ finds only FOREVER = u64::MAX and doc text.
    • Case 8's new wait is await_hangup, a blocking read that must return 0.
    • The host's only clock is the harness ceiling, run_test_paced(…, 240 s).
  • #![forbid(unsafe_code)] holds.
    • It is at userland/compositor/src/main.rs:17.
    • git grep unsafe userland/compositor finds only that line.
    • There is no build.rs, macro_rules, include! or #[path].
    • The unsafety sits in toyos::shm (as_slice, and now as_atomic, which production calls at client.rs:137). It is filed.
  • The merge with main carries little textual risk. Behaviourally, the paste path has moved:
    • git merge-tree --write-tree HEAD origin/main exits 0 (tree bde71c7b). tests/toyos.rs is the only file both sides changed, and main's side there touches no harness API the new host function names.
    • Main changed nothing under userland/, toyos/, toyos-abi/ or the kernel's object or IPC code since 51fe14c.
    • Main did change kernel/src/drivers/xhci/ and toyos-xhci/src/port.rs (xHCI: every report that moves a port's belief leaves it to be read #554), and the metal-sim GUI+V that every paste case rides is a USB keyboard on xHCI.
    • The author merges main before landing. At the merged head, re-run metal_sim_hostile_clipboard, window_refusal and --nightly metal_sim_client_death.

BLOCKER

None.

NOTE

  1. PR body, "Guest runs": it still carries round 3's a69ee8e table and an m10 paragraph ending in Expected:. Replace both with the runs at this head. Each row needs its exit code and red line:

  2. PR body, "High risk: the two checks": the negative control it names is "the revert above", and that is the shm.rs-including revert that does not compile. Name v3, as git diff HEAD 51fe14c1 -- its nine paths.

  3. The two new tooling issues have no owner line:

    • issues/build/a-key-being-built-is-waited-for-and-another-key-is-not-reds-under-host-load.md
    • issues/build/bootstraps-own-cargo-build-cant-find-its-crates-through-x.md

    Both sit outside this PR's fence and belong to the orchestrator. Take them out of this branch, or give them owners.

  4. issues/build/a-key-being-built-is-waited-for-and-another-key-is-not-reds-under-host-load.md: the flaky test was re-run and left enabled. CLAUDE.md says a flaky test is disabled at once and never re-run. Hand the disable to the orchestrator; it is not this branch's to make.

  5. After the merge with main, run the three guest tests again at the merged head, because main's xHCI changes carry the injected GUI+V.

REMOVE

  • issues/build/bootstraps-own-cargo-build-cant-find-its-crates-through-x.md: delete the file.
    • Its claim is "blocking this branch's build-only gate", and that cargo test --test toyos-build fails on this worktree after the pin moved.
    • The orchestrator built and ran images with cargo test --test toyos-build in /Users/jan/Dev/jan/toyos-desk1 at 70156f1, 17 times (557r5-*, 557r7-control).
    • The failure is the author's environment's, not the tree's.
  • PR body: the rows marked blocked for --list and --build-only, the paragraph under them, and the "Filed" bullet for that issue. The same orchestrator builds refute them.
  • PR body, the cargo test --lib row: remove the parenthetical about re-running the flaky test.
  • PR body: the control description, "The control is git diff HEAD origin/main -- … toyos/src/shm.rs …" and "The test binary spells its own wire constants and atomic cast". It is false at this head: the test calls as_atomic, and that path list does not build. NOTE 2 replaces it.
  • PR body, the m10 paragraph: remove "If the green run shows a fresh window's compositor sending a frame before the probe's answer, the fallback is …". The green answered it.
  • PR body, line 3: remove the net-lines line. Its issues figure is not the diff's (+221).
  • issues/build/a-key-being-built-is-waited-for-and-another-key-is-not-reds-under-host-load.md:19: remove "widen that wait or". It prescribes a flat wait as an exit.
  • tests/toyos.rs:715: remove "A client's clipboard.", which narrates the test's name.

LAND AFTER NAMED CHANGES

Japabu and others added 2 commits September 28, 2026 15:43
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
…aky buildlock test, and cut a narrating comment

The bootstrap build-cache failure was this worktree's environment, not the
tree's, per the orchestrator's 17 successful `cargo test --test toyos-build`
runs at this head; the file is deleted rather than kept as a false claim.
The buildlock flake is the orchestrator's to disable, not this branch's, so
it gets an owner line and loses the prescribed fix it has no standing to
choose. `tests/toyos.rs`'s clipboard test comment loses the line that only
restates the test's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
@Japabu
Japabu added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit ec0a91a Sep 28, 2026
1 check passed
@Japabu
Japabu deleted the wt/toyos-desk1 branch September 28, 2026 15:57
Japabu added a commit that referenced this pull request Sep 28, 2026
origin/main brought #557 (clipboard fix), which registers
metal_sim_hostile_clipboard at Fast beside the metal_sim_window_drag row
this branch moved to Weekly. Kept #564's move and #557's own row and tier
for the new test, per this branch's stated rule that a row main added
keeps main's tier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
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.

1 participant