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/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)) 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); + } } }