Compare commits

...

26 Commits

Author SHA1 Message Date
RustDesk
8aeafd5401 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-06 15:57:14 +08:00
rustdesk
e264283bf2 fix review 2026-07-06 15:46:41 +08:00
rustdesk
1053b53b57 fix copilot false report 2026-07-06 15:31:12 +08:00
rustdesk
233eb49e4d refactor and simplify, remove mutex which is dangeours 2026-07-06 15:17:06 +08:00
rustdesk
c974514710 remove wf-cliprdr invariant tests
Drop tests/test_invariant_wf_cliprdr.c on this branch as requested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 22:27:56 +08:00
rustdesk
52ff648411 remove the dedicated wf-cliprdr CI workflow
Drop .github/workflows/wf-cliprdr-ci.yml on this branch as requested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 22:16:05 +08:00
rustdesk
55ae167394 harden the format-data path against late/duplicate responses
The format-data rendezvous had the same single-slot race the file-contents
path just fixed: the channel thread rewrote clipboard->hmem with no lock while
explorer-thread consumers read/freed it, nothing serialized concurrent
requests, and no flag told an expected response from a stray one.

- Add format_request_mutex (serializes the whole request/response cycle) and
  hmem_mutex (guards the hmem hand-off and formatDataRespExpected).
- cliprdr_send_data_request now takes ownership of the response buffer under
  hmem_mutex and returns it to the caller, so a later response cannot touch a
  buffer a consumer is using. All three consumers (GetData, WM_RENDERFORMAT,
  DELAYED_RENDERING) and the WM_CLIPBOARDUPDATE cleanup use the returned/taken
  handle instead of the shared slot.
- The response handler drops any response arriving while formatDataRespExpected
  is clear (late/duplicate/unsolicited), consumes the flag on the first
  response, and no longer dereferences a NULL clipboard in the SetEvent path.

Pre-existing issue, not introduced by this branch; generalizes the
file-contents hardening to the format-data path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:23:58 +08:00
rustdesk
ba5a535891 key the responder stream cache on connID as well as streamId
Per-stream ids restart from 1 in each peer process, so two connections can
emit the same streamId. The process-static pStreamStc cache keyed only on
streamId could then serve one peer the IStream cached for another peer (a
different file), silently returning wrong-file bytes. Add connID to the key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:08:55 +08:00
rustdesk
3ab9d4e46d serialize file-contents request state and poison streams after timeout
- Extract lock_mutex() for the WAIT_OBJECT_0/WAIT_ABANDONED idiom shared by
  take_req_fdata, the request-serialization acquire, and the response handler.
- Collapse the acquire/send/take/release cycle into
  cliprdr_request_filecontents_sync(), used by CliprdrStream_Read and the size
  probe in CliprdrStream_New.
- Publish req_f_stream_id_expected/req_f_size_requested under req_f_mutex in the
  sender and read them under the same lock in the response handler, removing the
  cross-thread data race on those fields.
- Poison a stream (m_failed) after a request fails/times out, so a late response
  carrying a previous offset's bytes cannot satisfy a later same-stream read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:07:55 +08:00
rustdesk
bb8ffef7f0 address copilot review findings in wf_cliprdr.c
- Reject a negative FILECONTENTS_SIZE result: m_lSize is unsigned, so a
  negative value became a huge bogus stream size that keeps reads going.
- Use a unique per-stream counter as the CLIPRDR streamId instead of a
  truncated IStream pointer, which could collide or be reused after free
  (and leaked heap addresses to the peer).
- Add req_f_request_mutex to serialize whole file-contents request/response
  cycles, enforcing the previously assumed one-outstanding-request
  invariant when multiple streams are read concurrently. Bounded acquire
  so a wedged request fails the read instead of hanging a consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 17:42:32 +08:00
rustdesk
1c2188f80b add invariant tests for file contents request/response hardening
Cover the zeroed optional request fields, stream ID filtering,
oversized/NULL response rejection and the zero-byte EOF path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 16:31:32 +08:00
rustdesk
ff1ca85827 condense hardening comments, fix style in wf_cliprdr.c
Comment-only cleanup of the review-justification comments; also move
the mutex wait result declaration to the top of the block and fix
continuation-line indentation. No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 16:31:26 +08:00
rustdesk
f695413ee7 fix review 2026-07-04 15:47:53 +08:00
rustdesk
b0555639a2 fix review 2026-07-04 15:20:23 +08:00
RustDesk
cd4ed15214 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-04 14:55:09 +08:00
RustDesk
96ab03aa65 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-04 14:55:00 +08:00
RustDesk
a30af8a321 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-04 14:54:49 +08:00
RustDesk
ed9a423570 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-04 14:54:30 +08:00
rustdesk
5cc7355dd2 fix copilot review 2026-07-04 14:41:16 +08:00
rustdesk
a9eca51ab0 harden wf_cliprdr.c 2026-07-04 14:03:10 +08:00
fufesou
9fdb8410d3 fix: parse exit code of flutter web (#15501)
* fix: parse exit code of flutter web

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: exit-code, debug print

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-07-03 12:49:32 +08:00
alonginwind
a2b79462ab fix: auto-close terminal tab/window when shell exits (#15448) 2026-07-02 16:43:27 +08:00
fufesou
dce221be5a 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 <linlong1266@gmail.com>

* fix(clipboard): reject invalid remote format-list entries

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-07-02 16:18:07 +08:00
rustdesk
9d1ab3fba3 fix the AOM tile-control argument type 2026-07-01 11:47:08 +08:00
rustdesk
b3bd18845d update hbb_common 2026-06-30 11:29:56 +08:00
rustdesk
435f6ec61d update copyright 2026-06-30 11:02:28 +08:00
17 changed files with 278 additions and 232 deletions

View File

@@ -1,85 +0,0 @@
name: wf-cliprdr CI
on:
workflow_dispatch:
pull_request:
paths:
- "libs/clipboard/src/windows/**"
- "tests/test_invariant_wf_cliprdr.c"
- ".github/workflows/wf-cliprdr-ci.yml"
push:
branches:
- master
paths:
- "libs/clipboard/src/windows/**"
- "tests/test_invariant_wf_cliprdr.c"
- ".github/workflows/wf-cliprdr-ci.yml"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: wf_cliprdr invariant test
runs-on: windows-2022
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Set up MSVC
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
with:
arch: x64
- name: Setup vcpkg with GitHub Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
with:
vcpkgDirectory: C:\vcpkg
doNotCache: false
- name: Install vcpkg dependency
shell: pwsh
run: |
& "$env:VCPKG_ROOT\vcpkg.exe" install check:x64-windows --classic --x-install-root="$env:VCPKG_ROOT\installed"
- name: Build test
shell: pwsh
run: |
$testRoot = Join-Path $env:GITHUB_WORKSPACE 'build\wf-cliprdr'
New-Item -ItemType Directory -Force $testRoot | Out-Null
$testSource = (($env:GITHUB_WORKSPACE -replace '\\', '/') + '/tests/test_invariant_wf_cliprdr.c')
$cmakeLists = @(
'cmake_minimum_required(VERSION 3.20)'
'project(test_invariant_wf_cliprdr C)'
''
'set(CMAKE_C_STANDARD 11)'
'set(CMAKE_C_STANDARD_REQUIRED ON)'
'set(CMAKE_C_EXTENSIONS OFF)'
''
'find_package(check CONFIG REQUIRED)'
''
'add_executable(test_invariant_wf_cliprdr'
' "TEST_SOURCE"'
')'
''
'target_link_libraries(test_invariant_wf_cliprdr PRIVATE'
' $<$<TARGET_EXISTS:Check::check>:Check::check>'
' $<$<NOT:$<TARGET_EXISTS:Check::check>>:Check::checkShared>'
')'
) -join [Environment]::NewLine
$cmakeLists.Replace('TEST_SOURCE', $testSource) | Set-Content -NoNewline (Join-Path $testRoot 'CMakeLists.txt')
cmake -S $testRoot -B (Join-Path $testRoot 'out') -G "Visual Studio 17 2022" -A x64 -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows
cmake --build (Join-Path $testRoot 'out') --config Release
- name: Run test
shell: pwsh
run: .\build\wf-cliprdr\out\Release\test_invariant_wf_cliprdr.exe

View File

@@ -213,7 +213,7 @@ exclude = ["vdi/host", "examples/custom_plugin"]
libxdo-sys = { path = "libs/libxdo-sys-stub" }
[package.metadata.winres]
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved."
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
ProductName = "RustDesk"
FileDescription = "RustDesk Remote Desktop"
OriginalFilename = "rustdesk.exe"

View File

@@ -2474,7 +2474,7 @@ class _AboutState extends State<_About> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Tech Pte. Ltd.\n$license',
style: const TextStyle(color: Colors.white),
),
Text(

View File

@@ -95,6 +95,13 @@ class _TerminalPageState extends State<TerminalPage>
// 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);

View File

@@ -83,6 +83,13 @@ class _TerminalPageState extends State<TerminalPage>
// 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 &&

View File

@@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
@@ -38,6 +37,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<void> _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'.
@@ -247,6 +250,33 @@ class TerminalModel with ChangeNotifier {
}
}
static int getExitCodeFromEvt(Map<String, dynamic> evt) {
if (evt.containsKey('exit_code')) {
final v = evt['exit_code'];
if (v is int) {
// Desktop and mobile send exit_code as an int
return v;
} else if (v is String) {
// Web sends exit_code as a string
final parsed = int.tryParse(v);
if (parsed != null) {
return parsed;
} else {
debugPrint(
'[TerminalModel] Failed to parse exit_code as integer: $v. Expected a numeric string.');
return 0;
}
} else {
debugPrint(
'[TerminalModel] Unexpected exit_code type: ${v.runtimeType}, value: $v. Expected int or String.');
return 0;
}
} else {
debugPrint('[TerminalModel] Event does not contain exit_code');
return 0;
}
}
void handleTerminalResponse(Map<String, dynamic> evt) {
final String? type = evt['type'];
final int evtTerminalId = getTerminalIdFromEvt(evt);
@@ -469,10 +499,12 @@ class TerminalModel with ChangeNotifier {
}
void _handleTerminalClosed(Map<String, dynamic> evt) {
final int exitCode = evt['exit_code'] ?? 0;
final int exitCode = getExitCodeFromEvt(evt);
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
_terminalOpened = false;
notifyListeners();
// Auto-close the tab/page
onClosed?.call();
}
void _handleTerminalError(Map<String, dynamic> evt) {

View File

@@ -11,4 +11,4 @@ PRODUCT_NAME = RustDesk
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2025 Purslane Ltd. All rights reserved.
PRODUCT_COPYRIGHT = Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved.

View File

@@ -89,11 +89,11 @@ BEGIN
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "Purslane Ltd" "\0"
VALUE "CompanyName", "Purslane Tech Pte. Ltd." "\0"
VALUE "FileDescription", "RustDesk Remote Desktop" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "rustdesk" "\0"
VALUE "LegalCopyright", "Copyright © 2025 Purslane Ltd. All rights reserved." "\0"
VALUE "LegalCopyright", "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved." "\0"
VALUE "OriginalFilename", "rustdesk.exe" "\0"
VALUE "ProductName", "RustDesk" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"

View File

@@ -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
*/
@@ -205,6 +232,7 @@ struct _CliprdrStream
FILEDESCRIPTORW m_Dsc;
void *m_pData;
UINT32 m_connID;
UINT32 m_streamId; // unique CLIPRDR streamId; avoids leaking a heap pointer
};
typedef struct _CliprdrStream CliprdrStream;
@@ -258,6 +286,9 @@ struct wf_clipboard
char *req_fdata;
HANDLE req_fevent;
BOOL req_f_received;
UINT32 req_f_conn_id_expected; // connID of the outstanding request
UINT32 req_f_stream_id_expected; // streamId of the outstanding request; responses for another are dropped
LONG req_f_stream_id_seq; // source of unique per-stream ids
size_t nFiles;
size_t file_array_size;
@@ -288,7 +319,7 @@ static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format);
static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UINT32 format);
static UINT cliprdr_send_lock(wfClipboard *clipboard);
static UINT cliprdr_send_unlock(wfClipboard *clipboard);
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid,
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId,
ULONG index, UINT32 flag, DWORD positionhigh,
DWORD positionlow, ULONG request);
@@ -371,7 +402,7 @@ static ULONG STDMETHODCALLTYPE CliprdrStream_Release(IStream *This)
static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULONG cb,
ULONG *pcbRead)
{
int ret;
UINT ret;
CliprdrStream *instance = (CliprdrStream *)This;
wfClipboard *clipboard;
@@ -384,12 +415,23 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO
if (instance->m_lOffset.QuadPart >= instance->m_lSize.QuadPart)
return S_FALSE;
ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)This, instance->m_lIndex,
ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId, instance->m_lIndex,
FILECONTENTS_RANGE, instance->m_lOffset.HighPart,
instance->m_lOffset.LowPart, cb);
if (ret < 0)
if (ret != CHANNEL_RC_OK)
{
free(clipboard->req_fdata);
clipboard->req_fdata = NULL;
return E_FAIL;
}
if (clipboard->req_fsize > cb)
{
free(clipboard->req_fdata);
clipboard->req_fdata = NULL;
return STG_E_READFAULT;
}
if (clipboard->req_fdata)
{
@@ -601,6 +643,7 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
instance->m_pData = pData;
instance->m_lOffset.QuadPart = 0;
instance->m_connID = connID;
instance->m_streamId = (UINT32)InterlockedIncrement(&clipboard->req_f_stream_id_seq);
if (instance->m_Dsc.dwFlags & FD_ATTRIBUTES)
{
@@ -611,16 +654,28 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
if (((instance->m_Dsc.dwFlags & FD_FILESIZE) == 0) && !isDir)
{
/* get content size of this stream */
if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)instance,
if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId,
instance->m_lIndex, FILECONTENTS_SIZE, 0, 0,
8) == CHANNEL_RC_OK)
{
success = TRUE;
}
if (clipboard->req_fdata != NULL)
if (clipboard->req_fdata != NULL && clipboard->req_fsize >= sizeof(LONGLONG))
{
LONGLONG sz = 0;
CopyMemory(&sz, clipboard->req_fdata, sizeof(sz));
if (sz < 0)
success = FALSE;
else
instance->m_lSize.QuadPart = sz;
}
else
{
success = FALSE;
}
if (clipboard->req_fdata)
{
instance->m_lSize.QuadPart = *((LONGLONG *)clipboard->req_fdata);
free(clipboard->req_fdata);
clipboard->req_fdata = NULL;
}
@@ -1406,25 +1461,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 +1516,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;
@@ -1729,12 +1801,12 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem);
}
UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index,
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId, ULONG index,
UINT32 flag, DWORD positionhigh, DWORD positionlow,
ULONG nreq)
{
UINT rc;
CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest;
CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest = { 0 };
if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
return ERROR_INTERNAL_ERROR;
@@ -1745,12 +1817,11 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
return rc;
}
clipboard->req_f_received = FALSE;
clipboard->req_f_conn_id_expected = connID;
clipboard->req_f_stream_id_expected = streamId;
fileContentsRequest.connID = connID;
// streamId is `IStream*` pointer, though it is not very good on a 64-bit system.
// But it is OK, because it is only used to check if the stream is the same in
// `wf_cliprdr_server_file_contents_request()` function.
fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid;
fileContentsRequest.streamId = streamId;
fileContentsRequest.listIndex = index;
fileContentsRequest.dwFlags = flag;
fileContentsRequest.nPositionLow = positionlow;
@@ -2443,6 +2514,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 +2531,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 (mapping->name)
if (!wf_cliprdr_bounded_strlen(format->formatName,
WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES, &name_len))
{
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);
}
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);
}
mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name);
if (mapping->local_format_id == 0)
{
return wf_cliprdr_server_format_list_fail(clipboard);
}
}
else
@@ -2472,7 +2592,6 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
}
clipboard->map_size++;
map_ensure_capacity(clipboard);
}
if (file_transferring(clipboard))
@@ -2923,6 +3042,7 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
BOOL bIsStreamFile = TRUE;
static LPSTREAM pStreamStc = NULL;
static UINT32 uStreamIdStc = 0;
static UINT32 uConnIdStc = 0;
wfClipboard *clipboard;
UINT rc = ERROR_INTERNAL_ERROR;
UINT sRc;
@@ -2996,7 +3116,8 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
vFormatEtc.lindex = fileContentsRequest->listIndex;
vFormatEtc.ptd = NULL;
if ((uStreamIdStc != fileContentsRequest->streamId) || !pStreamStc)
if ((uStreamIdStc != fileContentsRequest->streamId) ||
(uConnIdStc != fileContentsRequest->connID) || !pStreamStc)
{
LPENUMFORMATETC pEnumFormatEtc;
ULONG CeltFetched;
@@ -3027,6 +3148,7 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
{
pStreamStc = vStgMedium.pstm;
uStreamIdStc = fileContentsRequest->streamId;
uConnIdStc = fileContentsRequest->connID;
bIsStreamFile = TRUE;
}
@@ -3190,6 +3312,9 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
rc = ERROR_INTERNAL_ERROR;
break;
}
if (fileContentsResponse->connID != clipboard->req_f_conn_id_expected ||
fileContentsResponse->streamId != clipboard->req_f_stream_id_expected)
return CHANNEL_RC_OK;
clipboard->req_fsize = 0;
clipboard->req_fdata = NULL;
@@ -3200,6 +3325,13 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
}
clipboard->req_fsize = fileContentsResponse->cbRequested;
/*
* Keep the zero-size allocation: supported Windows builds use the Microsoft
* CRT, where malloc(0) returns a valid pointer. wait_response_event() also
* uses a non-NULL req_fdata to recognize a successful zero-byte response.
* The Rust FFI derives requestedData and cbRequested from the same Vec, so a
* nonzero length cannot have a NULL data pointer on the normal call path.
*/
clipboard->req_fdata = (char *)malloc(fileContentsResponse->cbRequested);
if (!clipboard->req_fdata)
{

View File

@@ -26,7 +26,7 @@ windows = { version = "0.61", features = [
native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]}
[package.metadata.winres]
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved."
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
ProductName = "RustDesk"
OriginalFilename = "rustdesk.exe"
FileDescription = "RustDesk Remote Desktop"

View File

@@ -79,6 +79,10 @@ mod webrtc {
}
}
fn tile_log2(threads: u32) -> std::os::raw::c_uint {
(threads as f64).log2().ceil() as _
}
fn get_super_block_size(width: u32, height: u32, threads: u32) -> aom_superblock_size_t {
use aom_superblock_size::*;
let resolution = width * height;
@@ -160,8 +164,7 @@ mod webrtc {
} else {
AV1E_SET_TILE_COLUMNS
};
// Failed on android
call_ctl!(ctx, tile_set, (cfg.g_threads as f64 * 1.0f64).log2().ceil());
call_ctl!(ctx, tile_set, tile_log2(cfg.g_threads));
call_ctl!(ctx, AV1E_SET_ROW_MT, 1);
call_ctl!(ctx, AV1E_SET_ENABLE_OBMC, 0);
call_ctl!(ctx, AV1E_SET_NOISE_SENSITIVITY, 0);
@@ -197,6 +200,23 @@ mod webrtc {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::raw::c_uint;
#[test]
fn tile_log2_uses_c_uint_and_rounds_up() {
let one_thread: c_uint = tile_log2(1);
let three_threads: c_uint = tile_log2(3);
let max_threads: c_uint = tile_log2(64);
assert_eq!(one_thread, 0);
assert_eq!(three_threads, 2);
assert_eq!(max_threads, 6);
}
}
}
impl EncoderApi for AomEncoder {

View File

@@ -79,7 +79,7 @@ heading 1;}{\s2\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\li
\ab\af1 \ltrch\fcs0 \b\ul\cf2\lang1033\langfe2052\langnp1033\insrsid1917520
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid8979511 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \hich\af1\dbch\af31505\loch\f1
\hich\f1 This Privacy Policy (hereinafter the \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 Policy}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 ) governs the terms and conditions under which Purslane Ltd. (hereinafter \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 ) governs the terms and conditions under which Purslane Tech Pte. Ltd. (hereinafter \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0
\b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 us}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1
\hich\f1 or \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 we}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1
@@ -300,4 +300,4 @@ b060828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980ae38a38f56e422a3a6
\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5;
\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6;
\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}}
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}}

View File

@@ -85,7 +85,7 @@ def make_parser():
"-m",
"--manufacturer",
type=str,
default="PURSLANE",
default="Purslane Tech Pte. Ltd.",
help="The app manufacturer.",
)
return parser
@@ -499,7 +499,7 @@ def update_license_file(app_name):
license_content = f.read()
license_content = license_content.replace("website rustdesk.com and other ", "")
license_content = license_content.replace("RustDesk", app_name)
license_content = re.sub("Purslane Ltd", app_name, license_content, flags=re.IGNORECASE)
license_content = re.sub(r"Purslane(?: Tech Pte\.)? Ltd", app_name, license_content, flags=re.IGNORECASE)
with open(license_file, "w", encoding="utf-8") as f:
f.write(license_content)

View File

@@ -48,7 +48,7 @@ fn main() {
);
let matches = App::new("rustdesk")
.version(crate::VERSION)
.author("Purslane Ltd<info@rustdesk.com>")
.author("Purslane Tech Pte. Ltd.<info@rustdesk.com>")
.about("RustDesk command line tool")
.args_from_usage(&args)
.get_matches();

View File

@@ -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);
}
}
}

View File

@@ -603,7 +603,7 @@ class MyIdMenu: Reactor.Component {
<div>Fingerprint: " + handler.get_fingerprint() + " \
<div .link .custom-event url='https://rustdesk.com/privacy.html'>" + translate("Privacy Statement") + "</div> \
<div .link .custom-event url='https://rustdesk.com'>" + translate("Website") + "</div> \
<div style='background: #2c8cff; color: white; padding: 1em; margin-top: 1em;'>Copyright &copy; 2025 Purslane Ltd.\
<div style='background: #2c8cff; color: white; padding: 1em; margin-top: 1em;'>Copyright &copy; 2026 Purslane Tech Pte. Ltd.\
<br />" + handler.get_license() + " \
<p style='font-weight: bold'>" + translate("Slogan_tip") + "</p>\
</div>\

View File

@@ -1,92 +0,0 @@
#include <check.h>
#include <stdlib.h>
#include <stddef.h>
#include "../libs/clipboard/src/windows/wf_cliprdr.c"
static SIZE_T descriptor_size(UINT count)
{
return offsetof(FILEGROUPDESCRIPTORW, fgd) + (SIZE_T)count * sizeof(FILEDESCRIPTORW);
}
START_TEST(test_descriptor_size_rejects_buffer_smaller_than_header)
{
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid(0, 1), FALSE);
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid(
offsetof(FILEGROUPDESCRIPTORW, fgd) - 1, 1),
FALSE);
}
END_TEST
START_TEST(test_descriptor_size_rejects_zero_items)
{
ck_assert_int_eq(
wf_cliprdr_file_group_descriptor_size_valid(offsetof(FILEGROUPDESCRIPTORW, fgd), 0),
FALSE);
}
END_TEST
START_TEST(test_descriptor_size_accepts_max_stream_count)
{
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid(
descriptor_size(WF_CLIPRDR_MAX_STREAMS), WF_CLIPRDR_MAX_STREAMS),
TRUE);
}
END_TEST
START_TEST(test_descriptor_size_rejects_stream_count_above_limit)
{
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid(
descriptor_size(WF_CLIPRDR_MAX_STREAMS), WF_CLIPRDR_MAX_STREAMS + 1),
FALSE);
}
END_TEST
START_TEST(test_descriptor_size_rejects_truncated_descriptor_array)
{
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid(descriptor_size(2) - 1, 2),
FALSE);
}
END_TEST
START_TEST(test_descriptor_size_rejects_extreme_count)
{
ck_assert_int_eq(wf_cliprdr_file_group_descriptor_size_valid((SIZE_T)-1, (UINT)-1),
FALSE);
}
END_TEST
Suite *wf_cliprdr_invariant_suite(void)
{
Suite *s;
TCase *tc_core;
s = suite_create("wf_cliprdr_invariants");
tc_core = tcase_create("descriptor_size");
tcase_add_test(tc_core, test_descriptor_size_rejects_buffer_smaller_than_header);
tcase_add_test(tc_core, test_descriptor_size_rejects_zero_items);
tcase_add_test(tc_core, test_descriptor_size_accepts_max_stream_count);
tcase_add_test(tc_core, test_descriptor_size_rejects_stream_count_above_limit);
tcase_add_test(tc_core, test_descriptor_size_rejects_truncated_descriptor_array);
tcase_add_test(tc_core, test_descriptor_size_rejects_extreme_count);
suite_add_tcase(s, tc_core);
return s;
}
int main(void)
{
int number_failed;
Suite *s;
SRunner *sr;
s = wf_cliprdr_invariant_suite();
sr = srunner_create(s);
srunner_run_all(sr, CK_NORMAL);
number_failed = srunner_ntests_failed(sr);
srunner_free(sr);
return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}