From dce221be5a5e6d36db42008116fbeefdece5a6d1 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 2 Jul 2026 16:18:07 +0800 Subject: [PATCH 1/2] fix(clipboard): make CLIPRDR format-map growth checked (#15493) * fix(clipboard): make CLIPRDR format-map growth checked The Windows CLIPRDR format-list handler relies on map_ensure_capacity() while processing peer-provided formats. The previous helper only attempted growth: if realloc() failed, it returned silently and the caller continued processing. A later iteration could then index past the allocated format_mappings array. Make format-map growth a checked operation. The handler now validates the peer-provided format count, ensures the mapping array is large enough before writing entries, and aborts processing if growth fails. Newly allocated slots are zeroed so existing cleanup can safely run after partial processing. Also bound remote format names before measuring/converting them. The chosen limits follow Windows clipboard/atom constraints: - registered clipboard format IDs use 0xC000..0xFFFF - string atom names are limited to 255 bytes Signed-off-by: fufesou * fix(clipboard): reject invalid remote format-list entries Signed-off-by: fufesou --------- Signed-off-by: fufesou --- libs/clipboard/src/windows/wf_cliprdr.c | 136 ++++++++++++++++++++---- 1 file changed, 114 insertions(+), 22 deletions(-) diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c index 5fd08deebd1..78968cee233 100644 --- a/libs/clipboard/src/windows/wf_cliprdr.c +++ b/libs/clipboard/src/windows/wf_cliprdr.c @@ -41,6 +41,14 @@ /* Maximum number of clipboard streams accepted from a remote peer (integer overflow / DoS guard) */ #define WF_CLIPRDR_MAX_STREAMS 16384 +/* Registered clipboard formats use IDs 0xC000 through 0xFFFF. + * https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclipboardformatw */ +#define WF_CLIPRDR_MAX_FORMATS 0x4000u +/* Registered format names are string atoms; cap the converted WCHAR name. + * https://learn.microsoft.com/en-us/windows/win32/dataxchg/about-atom-tables */ +#define WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS 255u +/* Bound the peer-provided UTF-8 scan separately from the converted Windows name. */ +#define WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES (WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS * 4u) /* Validates the remote descriptor array size after cItems has been read safely. */ static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count) @@ -61,6 +69,25 @@ static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count) return size >= descriptors_size; } +static BOOL wf_cliprdr_bounded_strlen(const char *value, size_t max_len, size_t *len) +{ + size_t i; + + if (!value || !len) + return FALSE; + + for (i = 0; i <= max_len; i++) + { + if (value[i] == '\0') + { + *len = i; + return TRUE; + } + } + + return FALSE; +} + /** * Clipboard Formats */ @@ -1406,25 +1433,35 @@ static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format) return local_format; } -static void map_ensure_capacity(wfClipboard *clipboard) +static BOOL map_ensure_capacity(wfClipboard *clipboard, size_t capacity) { + size_t old_size; + formatMapping *new_map; + if (!clipboard) - return; + return FALSE; - if (clipboard->map_size >= clipboard->map_capacity) - { - size_t new_size; - formatMapping *new_map; - new_size = clipboard->map_capacity * 2; - new_map = - (formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * new_size); + if (!clipboard->format_mappings) + return FALSE; - if (!new_map) - return; + if (capacity <= clipboard->map_capacity) + return TRUE; - clipboard->format_mappings = new_map; - clipboard->map_capacity = new_size; - } + if (capacity > WF_CLIPRDR_MAX_FORMATS || + capacity > ((size_t)-1) / sizeof(formatMapping)) + return FALSE; + + old_size = clipboard->map_capacity; + new_map = + (formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * capacity); + + if (!new_map) + return FALSE; + + memset(new_map + old_size, 0, sizeof(formatMapping) * (capacity - old_size)); + clipboard->format_mappings = new_map; + clipboard->map_capacity = capacity; + return TRUE; } static BOOL clear_format_map(wfClipboard *clipboard) @@ -1451,6 +1488,13 @@ static BOOL clear_format_map(wfClipboard *clipboard) return TRUE; } +static UINT wf_cliprdr_server_format_list_fail(wfClipboard *clipboard) +{ + clear_format_map(clipboard); + clipboard->copied = FALSE; + return ERROR_INTERNAL_ERROR; +} + static UINT cliprdr_send_tempdir(wfClipboard *clipboard) { CLIPRDR_TEMP_DIRECTORY tempDirectory; @@ -2443,6 +2487,16 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context, if (!clear_format_map(clipboard)) return ERROR_INTERNAL_ERROR; + clipboard->copied = FALSE; + + if (formatList->numFormats > WF_CLIPRDR_MAX_FORMATS) + return ERROR_INTERNAL_ERROR; + + if (formatList->numFormats > 0 && !formatList->formats) + return ERROR_INTERNAL_ERROR; + + if (!map_ensure_capacity(clipboard, formatList->numFormats)) + return ERROR_INTERNAL_ERROR; clipboard->copied = TRUE; @@ -2450,19 +2504,58 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context, { format = &(formatList->formats[i]); mapping = &(clipboard->format_mappings[i]); + /* Do not validate the peer-provided formatId as a Windows registered format. + * It is only a remote protocol ID used when requesting data from the peer. + * For named formats, RegisterClipboardFormatW creates the local Windows + * clipboard ID below, and that local ID is checked before publishing. */ mapping->remote_format_id = format->formatId; if (format->formatName) { - int size = MultiByteToWideChar(CP_UTF8, 0, format->formatName, - strlen(format->formatName), NULL, 0); - mapping->name = calloc(size + 1, sizeof(WCHAR)); + size_t name_len; + int size; + + if (!wf_cliprdr_bounded_strlen(format->formatName, + WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES, &name_len)) + { + return wf_cliprdr_server_format_list_fail(clipboard); + } + + if (name_len == 0) + { + return wf_cliprdr_server_format_list_fail(clipboard); + } + + size = MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len, + NULL, 0); + if (size <= 0) + { + return wf_cliprdr_server_format_list_fail(clipboard); + } + + if ((UINT)size > WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS) + { + return wf_cliprdr_server_format_list_fail(clipboard); + } + + mapping->name = calloc((size_t)size + 1, sizeof(WCHAR)); + if (!mapping->name) + { + return wf_cliprdr_server_format_list_fail(clipboard); + } + + if (MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len, + mapping->name, size) != size) + { + free(mapping->name); + mapping->name = NULL; + return wf_cliprdr_server_format_list_fail(clipboard); + } - if (mapping->name) + mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name); + if (mapping->local_format_id == 0) { - MultiByteToWideChar(CP_UTF8, 0, format->formatName, strlen(format->formatName), - mapping->name, size); - mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name); + return wf_cliprdr_server_format_list_fail(clipboard); } } else @@ -2472,7 +2565,6 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context, } clipboard->map_size++; - map_ensure_capacity(clipboard); } if (file_transferring(clipboard)) From a2b79462ab63db2447a4f5c36e6257c74ae47230 Mon Sep 17 00:00:00 2001 From: alonginwind <100897495+alonginwind@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:43:27 +0800 Subject: [PATCH 2/2] fix: auto-close terminal tab/window when shell exits (#15448) --- flutter/lib/desktop/pages/terminal_page.dart | 7 +++++ flutter/lib/mobile/pages/terminal_page.dart | 7 +++++ flutter/lib/models/terminal_model.dart | 6 ++++ src/server/terminal_service.rs | 31 ++++++++++++++++++-- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/terminal_page.dart b/flutter/lib/desktop/pages/terminal_page.dart index d38dc4a8b16..e5e1dbb8dc7 100644 --- a/flutter/lib/desktop/pages/terminal_page.dart +++ b/flutter/lib/desktop/pages/terminal_page.dart @@ -95,6 +95,13 @@ class _TerminalPageState extends State // Register this terminal model with FFI for event routing _ffi.registerTerminalModel(widget.terminalId, _terminalModel); + // Auto-close tab when shell exits + _terminalModel.onClosed = () { + if (mounted) { + widget.tabController.closeBy(widget.tabKey); + } + }; + // Initialize terminal connection WidgetsBinding.instance.addPostFrameCallback((_) { widget.tabController.onSelected?.call(widget.id); diff --git a/flutter/lib/mobile/pages/terminal_page.dart b/flutter/lib/mobile/pages/terminal_page.dart index aff85b40c84..cbf47a7e992 100644 --- a/flutter/lib/mobile/pages/terminal_page.dart +++ b/flutter/lib/mobile/pages/terminal_page.dart @@ -83,6 +83,13 @@ class _TerminalPageState extends State // Register this terminal model with FFI for event routing _ffi.registerTerminalModel(widget.terminalId, _terminalModel); + // Auto-close connection when shell exits + _terminalModel.onClosed = () { + if (mounted) { + closeConnection(id: widget.id); + } + }; + // Web desktop users have full hardware keyboard access, so the on-screen // terminal extra keys bar is unnecessary and disabled. _showTerminalExtraKeys = !isWebDesktop && diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart index 8961d2dd8bf..3374b97826c 100644 --- a/flutter/lib/models/terminal_model.dart +++ b/flutter/lib/models/terminal_model.dart @@ -38,6 +38,10 @@ class TerminalModel with ChangeNotifier { void Function(int w, int h, int pw, int ph)? onResizeExternal; + /// Called when the terminal session ends (shell exits). + /// The listener (typically TerminalPage) can use this to auto-close the tab/page. + VoidCallback? onClosed; + Future _handleInput(String data) async { // Soft keyboards (notably iOS) emit '\n' when Enter is pressed, while a // real keyboard's Enter sends '\r'. Some Android keyboards also emit '\n'. @@ -473,6 +477,8 @@ class TerminalModel with ChangeNotifier { _writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n'); _terminalOpened = false; notifyListeners(); + // Auto-close the tab/page + onClosed?.call(); } void _handleTerminalError(Map evt) { diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index 52a296b7422..8664a99276c 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -1857,15 +1857,33 @@ impl TerminalServiceProxy { // Process each session with its own lock for (terminal_id, session_arc) in sessions { if let Ok(mut session) = session_arc.try_lock() { - // Check if reader thread is still alive and we haven't sent closed message yet + // Check if the session has ended (reader thread finished or child exited). + // On Linux, the PTY reader thread may not return EOF when the shell exits + // (the cloned master fd keeps the read side open), so we also poll the child + // process via try_wait() as a fallback detection mechanism. let mut should_send_closed = false; if !session.closed_message_sent { if let Some(thread) = &session.reader_thread { if thread.is_finished() { should_send_closed = true; - session.closed_message_sent = true; } } + if !should_send_closed { + if let Some(child) = &mut session.child { + match child.try_wait() { + Ok(Some(_)) => { + should_send_closed = true; + } + Ok(None) => {} // still running + Err(e) => { + log::warn!("Terminal {} child wait error: {}", terminal_id, e); + } + } + } + } + if should_send_closed { + session.closed_message_sent = true; + } } // It's Ok to put the closed message here. // Because the `reader_thread` is joined in `stop()`, @@ -2018,7 +2036,8 @@ impl TerminalServiceProxy { } } } else { - // For persistent sessions, just clear the child reference + // For persistent sessions, clear the child reference and remove the session + // if the closed message has been sent (shell has exited). if let Some(session_arc) = sessions.get(&terminal_id) { let mut session = session_arc.lock().unwrap(); if let Some(mut child) = session.child.take() { @@ -2028,6 +2047,12 @@ impl TerminalServiceProxy { } add_to_reaper(child); } + if session.closed_message_sent { + // Shell has exited, remove the dead session + drop(session); + sessions.remove(&terminal_id); + service.lock().unwrap().sessions.remove(&terminal_id); + } } }