Agent pane: no system call on the key path to judge who posted an event (P-MAIN S5)

Merged
LA
5d ago
+707-43

Summary

Risk tier: normal, as scripts/ci/risk_tier.py pr --number 28065 grades it (lead decision 2026-11-01 18:03Z: the label follows the repo's risk tool). The change guards permission grants and join consent, so it still has what high would need: an independent SHIP at the exact head and a green fleet compile. risk_tier.py does not list these files as high; that classifier gap goes to cmux-ad.

P-MAIN S5 (found by the S2 stall agent, 2026-10-30). AgentPanePersonInput.isPerson (the agent pane's gesture monitor: every key-down and mouse-down) and PersonInputCheck.isPerson (PersonInputLedger: every key and mouse event of the app) called proc_pidpath on the main thread for each event that another process posted (source pid not 0). That is a system call on the key path.

Why the path is read: only the person may grant in an agent pane (permission Allow, cx-ocdz / cx-1l61) or back a join consent. The HID system's events (pid 0) are the person's. Of the posted events, only the Apple services a person drives the Mac through count (Screen Sharing, Universal Control, Accessibility Keyboard, named by their path under SIP-protected /System), and for the strict join consent check only the WindowServer. A computer-use agent or a script that posts a click must not count. The event itself carries only the pid, not the executable, so the decision cannot come from the event alone (option (a) does not apply).

Fix (option (b)): EventSourceProcesses, one table per app (built in AppServices, injected; no singleton).

  • The key path reads the table under its lock and makes no system call. // perf-hot: marks the three functions.
  • A pid not in the table takes the safe default at once: not the person's. A wrong "no" costs one gesture (the person clicks again); a wrong "yes" would let an automated click grant. The person's own HID input (pid 0) never reaches the table, so local typing and clicks are never affected.
  • The process is read once, on the table's serial queue com.cmuxterm.next.event-source-processes: start time (sysctl(KERN_PROC_PID) p_starttime), path (proc_pidpath), start time again (so both are one process). Neither read has a same-user check, so the WindowServer, launchd and the root Screen Sharing daemon are read as this user (62fc9e9212e; before it, proc_pidinfo(PROC_PIDTBSDINFO) refused them with EPERM). A live process that is still unreadable keeps the safe default until its exit watch fires; it is not read again for each event.
  • An exit watch (a kqueue EVFILT_PROC with NOTE_EXIT | NOTE_EXEC, registered with EV_RECEIPT and read by a dispatch read source on the same serial queue) is armed before the read. It removes the entry when that process ends or runs a new program (exec: same pid and start time, new image). A pid the kernel gives to a new process, or a new image in the same process, is therefore read again. No timer decides anything (R5). One entry per live posting process, no constant cap (R9).

Lead review, second round:

  • Priming. PersonInputLedger also watches posted mouse moves, scrolls, other drags and modifier changes (primingTypes). For an unknown pid, it queues the table read (EventSourceProcesses.prime: a table lookup, no system call). A Screen Sharing session moves the mouse before it clicks, so its first click already counts.
  • SECURITY, exit watch. The watch is a kqueue EVFILT_PROC/NOTE_EXIT registered with EV_RECEIPT, so a refusal is seen. When the kernel refuses the watch, nothing is kept: no verdict can outlive its process or pass to a process that reuses the pid. What the person sees then: a click or key that Screen Sharing, Universal Control or the Accessibility Keyboard posts never counts as their gesture. A permission Allow through Screen Sharing stays unanswered and they must use the Mac's own keyboard or mouse. Their own local input (HID, pid 0) is never affected. Each new event reads the process again, off the main thread.
  • Fleet probe (SSH diagnostic, uid 501, select.kqueue with EV_RECEIPT): on cmux17, cmux19, cmux21 and cmux23 (macOS 27), NOTE_EXIT attached to launchd (pid 1, root) and to the WindowServer (_windowserver). screensharingd starts only on demand and ran on no host; it runs as root like launchd. theKernelExitWatchAttachesAsThisUser prints the same lines from the fleet test step.
  • plans/cmux-next/arch-ratchet.json: unchanged (feat's file). The PR adds no ratchet exception and no // blocking-allow: escape: the start-time sysctl and proc_pidpath run only on the table's serial queue.

Commits: the seam (no behavior change), red test 1, fix 1, red test 2 (priming, no watch), fix 2; after the base merge, 4cc0a03d904 moves the wiring into CmuxNextAgentPane (AgentPaneShared, PersonInputLedger.sources) so CmuxNextApp lines do not rise, and drops the exception.

Performance

Main-thread time per event, measured in EventSourceProcessesTests.perEventMainThreadCost (mean over 20,000 events, one fleet Mac, not an exclusive host).

metricbeforeafterdeltaharness or run link
main-thread time per posted event (person-input check)4333 ns (proc_pidpath per event)323 ns (table read)-4010 nsperEventMainThreadCost, fleet job 5e30898cbc1d8b0a1826e976 at ff83b4e1f5d
main-thread time per mouse move of the person (pid 0), new ledger work0 ns (not watched)208 ns+208 nsperEventMainThreadCost, fleet job 5e30898cbc1d8b0a1826e976 at ff83b4e1f5d

No capture: no visible change; the person-input check gives feat's verdicts for every posting process, including the WindowServer and root Screen Sharing (see the BLOCK at 072f126 and its fix 62fc9e9212e), only off the main thread; an unread or unreadable posting process is refused until it is read.

Review

Review (claude-opus-5.5-merge-review-subagent): SHIP at 97fd6798bb060116e342d07bdf05a5a535085450 (receipt: https://github.com/manaflow-ai/cmux/pull/28065#issuecomment-6520347203; merge of origin/feat-cmux-next, compile c17fa3d30e8e96886973f04c). Before the merge: SHIP at ff83b4e1f5d (https://github.com/manaflow-ai/cmux/pull/28065#issuecomment-6519845847). Earlier round: FIX at 3f13e1d3bec, all findings fixed. Follow-up (non-blocking): drop the default eventSources: argument of AgentPaneView.init and AgentTabStore.init, so that a new caller cannot get an unprimed private table.

Changelog

none

Review round 1 (claude-opus-review-subagent, FIX at 3f13e1d3bec): fixed the exec case (NOTE_EXEC), fail closed unless exactly one EV_RECEIPT receipt has no error, a kernel-wait test that could hang now fails after 10 s, and perEventMainThreadCost prints the main-thread ns per event (R10). The reviewer's finding that the blocking-allow exception is not needed is wrong for the installed hook: its raw-blocking-ext rule (proc_pid*) refused the push without it.

Testing

Regression: EventSourceProcessesTests (CmuxNextAgentPaneTests), run by scripts/ci/package-test-lane.sh suite Packages/macOS/CmuxNext EventSourceProcessesTests.

StepRefJobResult
compile3f13e1d3becbfa3612a880006dfa9828091passed (CmuxNext 275 s, app 50 s, cmux-tui 27 s)
red 1 (per-event read on main)b89a7aaa10518c989cb925e7f1104c5446efailed as expected: theKeyPathReadsOnlyTheTable (101 reads, on main), aReusedPidIsReadAgain, aPidWithNoProcessIsNotKept, theStrictCheckReadsOnlyTheTable
red 2 (no priming, kept without a watch)d9c4ed99a67abffd290d5e174c34c5dcebefailed as expected: only aMouseMoveFillsTheTableBeforeTheFirstClick and aProcessWithNoExitWatchIsNeverKept
green3f13e1d3bec98d9a0eafbca030eb8d0fa807/7 passed on aws-m4pro-6; exit-watch-proof: launchd pid 1 as uid 501: attached, WindowServer pid 164 as uid 501: attached, screensharingd not running
compileff83b4e1f5dddbe9951d28d3c8897f4a6b0passed (CmuxNext 255 s, app 76 s, cmux-tui 39 s)
green at the headff83b4e1f5d5e30898cbc1d8b0a1826e9769/9 passed; theKernelWatchFiresOnExec passed; per event on main: person's mouse move 208 ns (new), posted event from the table 288-323 ns, base proc_pidpath 4333 ns
red 3 (no exec notice)38ce555794aebac231666cc7f1a9e7489d6NO verdict: worker lease lost during compile, 0 test lines
red 3 infra-rerun38ce555794aa74dba670ac66b514e5e0355failed as expected on cmux11s-Mac-mini: only theKernelWatchFiresOnExec (Expectation failed: await Self.fires(fired.stream) after 10.029 s); the other 8 passed

Observed / Not run

Observed: the jobs above; an SSH kqueue probe as uid 501 on cmux17, cmux19, cmux21, cmux23 (macOS 27) attached NOTE_EXIT to launchd (root) and WindowServer (_windowserver).
Not run: the typing bench (perf claim: none); screensharingd ran on no host (it starts on demand; it runs as root like launchd); no local Mac build or test (repo rule).