Skip to content

fix: harden clipboard handling and unstick Windows screen switch#3

Draft
hsddszjs wants to merge 3 commits into
masterfrom
claude/pull-master-updates-AX390
Draft

fix: harden clipboard handling and unstick Windows screen switch#3
hsddszjs wants to merge 3 commits into
masterfrom
claude/pull-master-updates-AX390

Conversation

@hsddszjs
Copy link
Copy Markdown
Owner

@hsddszjs hsddszjs commented May 8, 2026

Summary

Consolidated bug fixes layered on top of upstream master, addressing
clipboard corruption, large-image transfer crashes, and a cursor lockup
on the Windows server side.

Clipboard protocol & bounds

  • IClipboard::unmarshall: check buffer length before readUInt32 so a truncated header cannot trigger an out-of-bounds read.
  • ClipboardChunk::assemble: validate the toULong parse result; use %zu for size_t in error messages.
  • StreamChunker::sendClipboard: stop the loop on size==0 instead of spinning forever.

Windows clipboard converters

  • AnyText: bounds check before scan[1] access; ensure GlobalUnlock when srcSize <= 1.
  • Bitmap: null-check GetDC / CreateDIBSection / CreateCompatibleDC; guard against integer overflow on huge dimensions; validate biBitCount; handle V4/V5 DIB headers (BITMAPV4HEADER / BITMAPV5HEADER).
  • HTML: bounds check for fragment start/end offsets.

macOS / X11 clipboard

  • BMP fromIClipboard: validate biSize range (40-1024); handle V4/V5 headers consistently with Windows.
  • OSXClipboard: skip file-promise pasteboard data so a Finder copy of files no longer disconnects the macOS client.
  • Text/HTML converters: check CFStringGetBytes buffSize > 0 before allocation.

Windows screen switch (the cursor-stuck bug)

  • MSWindowsScreen::isAnyMouseButtonDown: verify cached m_buttons[] against GetAsyncKeyState before reporting the cursor as locked.

A button-up event from the low-level mouse hook can be silently lost when the hook proc exceeds LowLevelHooksTimeout (~300 ms) during a multi-MB clipboard image transfer; without this check, m_buttons[Left] stays "down" forever and Server::isSwitchOkay() blocks every screen switch (locked by Left Button log spam). Stale entries are now cleared with a WARN so the condition is observable.

Test plan

  • Windows + macOS CI build passes
  • Manual: take screenshot on macOS client (cmd+shift+ctrl+4), paste in DingTalk on Windows server, verify cursor can return to macOS
  • Manual: copy files in Finder on macOS client, verify client does not disconnect
  • Manual: paste a V4/V5 BMP from Windows to another peer, verify it transfers
  • If reproducing the cursor lockup, confirm clearing stuck mouse button state appears in server log

https://claude.ai/code/session_012uK8ms6TCHrBsJGmHRDeWN


Generated by Claude Code

claude added 3 commits May 8, 2026 02:19
Consolidated bug fixes layered on top of upstream master, addressing
clipboard corruption, large-image transfer crashes, and a cursor lockup
on the Windows server side.

Clipboard protocol & bounds:
- IClipboard::unmarshall: check buffer length before readUInt32 so a
  truncated header cannot trigger an out-of-bounds read.
- ClipboardChunk::assemble: validate the toULong parse result and use
  %zu for size_t in error messages.
- StreamChunker::sendClipboard: stop the loop on size==0 instead of
  spinning forever.

Windows clipboard converters:
- AnyText: bounds check before scan[1] access; ensure GlobalUnlock when
  srcSize <= 1 in the text converter.
- Bitmap: null-check GetDC / CreateDIBSection / CreateCompatibleDC;
  guard against integer overflow on huge bitmap dimensions; validate
  biBitCount range; handle V4/V5 DIB headers (BITMAPV4HEADER /
  BITMAPV5HEADER) so non-V1 images no longer crash on conversion.
- HTML: bounds check for fragment start/end offsets.

macOS / X11 clipboard:
- BMP fromIClipboard: validate biSize range (40-1024) before reading
  DIB fields; handle V4/V5 headers consistently with Windows.
- OSXClipboard: skip file-promise pasteboard data so a Finder copy of
  files no longer disconnects the macOS client.
- Text/HTML converters: check CFStringGetBytes buffSize > 0 before
  allocation (was checking the wrong return value).

Windows screen switch:
- MSWindowsScreen::isAnyMouseButtonDown: verify cached m_buttons[]
  against GetAsyncKeyState before reporting the cursor as locked.
  A button-up event from the low-level mouse hook can be silently lost
  when the hook proc exceeds LowLevelHooksTimeout (~300 ms) during a
  multi-MB clipboard image transfer; without this check, m_buttons[Left]
  stays "down" forever and Server::isSwitchOkay() blocks every screen
  switch ("locked by Left Button" log spam). Stale entries are now
  cleared with a WARN so the condition is observable.

https://claude.ai/code/session_012uK8ms6TCHrBsJGmHRDeWN
Both ClientProxy1_3 (server) and ServerProxy (client) only reset their
heartbeat / keep-alive alarm when they receive an explicit kMsgCKeepAlive
echo. Every other inbound message - mouse moves, key events, clipboard
chunks - leaves the watchdog running.

That breaks under bulk clipboard transfer: while the peer ships a
multi-MB clipboard payload to us, its outgoing socket queue is also
where its keep-alive echo has to wait. The echo can be delayed past the
3 * kKeepAliveRate (=9s) deadline even though data is actively flowing
the whole time. Result: "client/server is dead" while the connection is
in fact perfectly healthy and busy. Once the watchdog fires we close
the socket, the cursor cannot return to the peer screen, and the user
has to wait for a reconnect.

Reset the watchdog whenever we successfully parse a frame from the peer
- any recognized inbound traffic is proof the link is alive. The
explicit keep-alive path still works as before; it is just no longer
the only thing that resets the timer.

Repro from logs (2026-05-08T10:46:38 session):
- 10:46:45.139  server: start receiving clipboard data (8 MB x2)
- 10:46:45.252  server: clipboard fully written to Windows
- 10:46:46-51   server: button/key events from client (link clearly alive)
- 10:46:53.358  server: "client \"hikaris-Mac-Pro.local\" is dead"
- 10:46:53.885  client: tls error occurred (server killed the socket)

https://claude.ai/code/session_012uK8ms6TCHrBsJGmHRDeWN
Both OSXScreen::checkClipboards and MSWindowsScreen (checkClipboards
and onClipboardChange) fired ClipboardGrabbed for kClipboardClipboard
*and* kClipboardSelection back-to-back whenever the local clipboard
changed. On these platforms there is only one OS clipboard, so the two
events carry identical content and lead to two identical chunked
transfers on the wire.

For an 8 MB BMP screenshot that meant 16 MB on the network, every
clipboard change. With the keep-alive fix this no longer disconnects
the client, but it still doubles bandwidth, marshalling cost, and the
window during which a peer might mispaste from a stale state.

Drop the kClipboardSelection grab on Mac/Win. Net effect: clipboard
traffic from these platforms is halved.

Caveat: X11 PRIMARY (mouse-selection) sync from a Mac/Win sender is
not supported anymore. Mac/Win never had a PRIMARY-equivalent in the
first place, so the previous "support" was a no-op for the user
(payload 1 was just a copy of payload 0). X11 senders still ship both
clipboards independently, so X11 -> X11 is unchanged.

https://claude.ai/code/session_012uK8ms6TCHrBsJGmHRDeWN
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.

2 participants