Files
rustdesk/tests/test_invariant_wf_cliprdr.c
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

218 lines
7.0 KiB
C

#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
static UINT test_client_file_contents_request(
CliprdrClientContext *context,
const CLIPRDR_FILE_CONTENTS_REQUEST *request)
{
CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 };
ck_assert_int_eq(request->haveClipDataId, FALSE);
ck_assert_int_eq(request->clipDataId, 0);
response.msgFlags = CB_RESPONSE_OK;
response.streamId = request->streamId;
return wf_cliprdr_server_file_contents_response(context, &response);
}
START_TEST(test_file_contents_request_initializes_optional_fields)
{
wfClipboard clipboard = { 0 };
CliprdrClientContext context = { 0 };
UINT rc;
clipboard.context = &context;
clipboard.req_f_mutex = CreateMutex(NULL, FALSE, NULL);
clipboard.req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL);
context.Custom = &clipboard;
context.ResponseWaitTimeoutSecs = 1;
context.ClientFileContentsRequest = test_client_file_contents_request;
ck_assert_ptr_nonnull(clipboard.req_f_mutex);
ck_assert_ptr_nonnull(clipboard.req_fevent);
rc = cliprdr_send_request_filecontents(&clipboard, 1, 7, 0, FILECONTENTS_SIZE, 0, 0, 0);
ck_assert_int_eq(rc, CHANNEL_RC_OK);
ck_assert_int_eq(clipboard.req_f_stream_id_expected, 7);
CloseHandle(clipboard.req_fevent);
CloseHandle(clipboard.req_f_mutex);
}
END_TEST
START_TEST(test_file_contents_response_validates_data)
{
wfClipboard clipboard = { 0 };
CliprdrClientContext context = { 0 };
CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 };
UINT rc;
clipboard.context = &context;
clipboard.req_f_mutex = CreateMutex(NULL, FALSE, NULL);
clipboard.req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL);
context.Custom = &clipboard;
context.ResponseWaitTimeoutSecs = 1;
response.msgFlags = CB_RESPONSE_OK;
response.cbRequested = 0;
clipboard.req_f_stream_id_expected = 7;
ck_assert_ptr_nonnull(clipboard.req_f_mutex);
ck_assert_ptr_nonnull(clipboard.req_fevent);
response.streamId = 8;
ck_assert_int_eq(wf_cliprdr_server_file_contents_response(&context, &response),
CHANNEL_RC_OK);
ck_assert_int_eq(WaitForSingleObject(clipboard.req_fevent, 0), WAIT_TIMEOUT);
ck_assert_ptr_null(clipboard.req_fdata);
ck_assert_int_eq(clipboard.req_fsize, 0);
ck_assert_int_eq(clipboard.req_f_response_ok, FALSE);
response.streamId = 7;
ck_assert_int_eq(wf_cliprdr_server_file_contents_response(&context, &response),
CHANNEL_RC_OK);
ck_assert_ptr_null(clipboard.req_fdata);
ck_assert_int_eq(clipboard.req_fsize, 0);
ck_assert_int_eq(clipboard.req_f_response_ok, TRUE);
rc = wait_response_event(0, &clipboard, clipboard.req_fevent,
&clipboard.req_f_received, (void **)&clipboard.req_fdata,
&clipboard.req_f_response_ok);
ck_assert_int_eq(rc, CHANNEL_RC_OK);
response.cbRequested = 1;
clipboard.req_f_size_requested = 1;
ck_assert_int_eq(wf_cliprdr_server_file_contents_response(&context, &response),
ERROR_INTERNAL_ERROR);
ck_assert_ptr_null(clipboard.req_fdata);
ck_assert_int_eq(clipboard.req_fsize, 0);
ck_assert_int_eq(clipboard.req_f_response_ok, FALSE);
rc = wait_response_event(0, &clipboard, clipboard.req_fevent,
&clipboard.req_f_received, (void **)&clipboard.req_fdata,
&clipboard.req_f_response_ok);
ck_assert_int_eq(rc, ERROR_INTERNAL_ERROR);
CloseHandle(clipboard.req_fevent);
CloseHandle(clipboard.req_f_mutex);
}
END_TEST
START_TEST(test_format_data_response_drops_unexpected)
{
wfClipboard clipboard = { 0 };
CliprdrClientContext context = { 0 };
CLIPRDR_FORMAT_DATA_RESPONSE response = { 0 };
clipboard.context = &context;
clipboard.hmem_mutex = CreateMutex(NULL, FALSE, NULL);
clipboard.formatDataRespEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
context.Custom = &clipboard;
ck_assert_ptr_nonnull(clipboard.hmem_mutex);
ck_assert_ptr_nonnull(clipboard.formatDataRespEvent);
// With no outstanding request, an unsolicited response must be dropped: it must
// not signal the event or write the hmem slot.
clipboard.formatDataRespExpected = FALSE;
response.msgFlags = CB_RESPONSE_OK;
response.dataLen = 0;
ck_assert_int_eq(wf_cliprdr_server_format_data_response(&context, &response), CHANNEL_RC_OK);
ck_assert_int_eq(WaitForSingleObject(clipboard.formatDataRespEvent, 0), WAIT_TIMEOUT);
ck_assert_ptr_null(clipboard.hmem);
CloseHandle(clipboard.formatDataRespEvent);
CloseHandle(clipboard.hmem_mutex);
}
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);
tcase_add_test(tc_core, test_file_contents_request_initializes_optional_fields);
tcase_add_test(tc_core, test_file_contents_response_validates_data);
tcase_add_test(tc_core, test_format_data_response_drops_unexpected);
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;
}