Skip to content

Commit 233aa40

Browse files
authored
Improve clipboard/OLE data handling and test coverage (#14257)
Refactor ReadUtf8StringFromHGLOBAL for safer null-termination and error handling. Prevent double-release of IStream in TryGetIStreamData. Return null instead of corrupted data when GetData fails. Add comprehensive unit tests and new test mocks for edge cases. Update NativeMethods.txt with CLIPBRD_E_CANT_OPEN. Fixes issues #11401 and #11402 in the WPF repo (which also apply to WinForms as the code is shared now).
1 parent 333771d commit 233aa40

5 files changed

Lines changed: 377 additions & 14 deletions

File tree

src/System.Private.Windows.Core/src/NativeMethods.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CFSTR_FILENAMEA
99
CFSTR_INDRAGLOOP
1010
CLIPBOARD_FORMAT
1111
CLIPBRD_E_BAD_DATA
12+
CLIPBRD_E_CANT_OPEN
1213
CloseEnhMetaFile
1314
CLR_*
1415
CoCreateInstance

src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ private static unsafe string ReadStringFromHGLOBAL(HGLOBAL hglobal, bool unicode
226226
}
227227
}
228228

229-
private static unsafe string ReadUtf8StringFromHGLOBAL(HGLOBAL hglobal)
229+
private static string ReadUtf8StringFromHGLOBAL(HGLOBAL hglobal)
230230
{
231231
void* buffer = PInvokeCore.GlobalLock(hglobal);
232232
if (buffer is null)
@@ -237,17 +237,30 @@ private static unsafe string ReadUtf8StringFromHGLOBAL(HGLOBAL hglobal)
237237
try
238238
{
239239
int size = checked((int)PInvokeCore.GlobalSize(hglobal));
240-
return size == 0
241-
? throw new Win32Exception()
242-
: Encoding.UTF8.GetString((byte*)buffer, size - 1);
240+
if (size == 0)
241+
{
242+
throw new Win32Exception();
243+
}
244+
245+
ReadOnlySpan<byte> bytes = new((byte*)buffer, size);
246+
247+
int nullIndex = bytes.IndexOf((byte)0);
248+
if (nullIndex < 0)
249+
{
250+
// Malformed, return empty string.
251+
return string.Empty;
252+
}
253+
254+
bytes = bytes[..nullIndex];
255+
return Encoding.UTF8.GetString(bytes);
243256
}
244257
finally
245258
{
246259
PInvokeCore.GlobalUnlock(hglobal);
247260
}
248261
}
249262

250-
private static unsafe string[]? ReadFileListFromHDROP(HDROP hdrop)
263+
private static string[]? ReadFileListFromHDROP(HDROP hdrop)
251264
{
252265
uint count = PInvokeCore.DragQueryFile(hdrop, iFile: 0xFFFFFFFF, lpszFile: null, cch: 0);
253266
if (count == 0)
@@ -256,7 +269,7 @@ private static unsafe string ReadUtf8StringFromHGLOBAL(HGLOBAL hglobal)
256269
}
257270

258271
Span<char> fileName = stackalloc char[(int)PInvokeCore.MAX_PATH + 1];
259-
string[] files = new string[count];
272+
List<string> files = new((int)count);
260273

261274
fixed (char* buffer = fileName)
262275
{
@@ -268,12 +281,11 @@ private static unsafe string ReadUtf8StringFromHGLOBAL(HGLOBAL hglobal)
268281
continue;
269282
}
270283

271-
string s = fileName[..(int)charactersCopied].ToString();
272-
files[i] = s;
284+
files.Add(fileName[..(int)charactersCopied].ToString());
273285
}
274286
}
275287

276-
return files;
288+
return [.. files];
277289
}
278290

279291
/// <summary>
@@ -365,10 +377,17 @@ private static bool TryGetHGLOBALData<T>(
365377
Debug.WriteLineIf(hr == HRESULT.COR_E_SERIALIZATION,
366378
"COR_E_SERIALIZATION returned when trying to get clipboard data, for example, BinaryFormatter threw SerializationException.");
367379

380+
// If GetData failed, don't try to read from the medium - it may contain uninitialized data.
381+
// (This can easily happen when the clipboard content changes between QueryGetData and GetData calls.)
382+
if (hr.Failed)
383+
{
384+
return false;
385+
}
386+
368387
bool result = false;
369388
try
370389
{
371-
if (medium.tymed == Com.TYMED.TYMED_HGLOBAL && !medium.hGlobal.IsNull && hr != HRESULT.COR_E_SERIALIZATION)
390+
if (medium.tymed == Com.TYMED.TYMED_HGLOBAL && !medium.hGlobal.IsNull)
372391
{
373392
result = TryGetDataFromHGLOBAL(medium.hGlobal, in request, out data);
374393
}
@@ -391,7 +410,7 @@ private static bool TryGetHGLOBALData<T>(
391410
return result;
392411
}
393412

394-
private static unsafe bool TryGetIStreamData<T>(
413+
private static bool TryGetIStreamData<T>(
395414
Com.IDataObject* dataObject,
396415
ref readonly DataRequest request,
397416
[NotNullWhen(true)] out T? data)
@@ -420,8 +439,9 @@ private static unsafe bool TryGetIStreamData<T>(
420439
return false;
421440
}
422441

423-
using ComScope<Com.IStream> pStream = new((Com.IStream*)medium.hGlobal);
424-
pStream.Value->Stat(out Com.STATSTG sstg, (uint)Com.STATFLAG.STATFLAG_DEFAULT);
442+
// Don't wrap in ComScope - ReleaseStgMedium will release the stream.
443+
Com.IStream* pStream = (Com.IStream*)medium.hGlobal;
444+
pStream->Stat(out Com.STATSTG sstg, (uint)Com.STATFLAG.STATFLAG_DEFAULT);
425445

426446
hglobal = PInvokeCore.GlobalAlloc(GLOBAL_ALLOC_FLAGS.GMEM_MOVEABLE | GLOBAL_ALLOC_FLAGS.GMEM_ZEROINIT, (uint)sstg.cbSize);
427447

@@ -433,7 +453,7 @@ private static unsafe bool TryGetIStreamData<T>(
433453
}
434454

435455
void* ptr = PInvokeCore.GlobalLock(hglobal);
436-
pStream.Value->Read((byte*)ptr, (uint)sstg.cbSize, null);
456+
pStream->Read((byte*)ptr, (uint)sstg.cbSize, null);
437457
PInvokeCore.GlobalUnlock(hglobal);
438458

439459
return TryGetDataFromHGLOBAL(hglobal, in request, out data);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Windows.Win32.Foundation;
5+
using Windows.Win32.System.Com;
6+
7+
namespace System.Private.Windows.Ole;
8+
9+
/// <summary>
10+
/// A native data object mock that returns success from QueryGetData but failure from GetData.
11+
/// This simulates a race condition where the clipboard changes between QueryGetData and GetData.
12+
/// </summary>
13+
internal unsafe class FailingGetDataNativeDataObject : NativeDataObjectMock
14+
{
15+
private readonly ushort _format;
16+
private readonly HRESULT _failureHResult;
17+
18+
public FailingGetDataNativeDataObject(ushort format, HRESULT failureHResult = default)
19+
{
20+
_format = format;
21+
22+
// Default to CLIPBRD_E_CANT_OPEN which can happen during clipboard contention.
23+
_failureHResult = failureHResult == default ? HRESULT.CLIPBRD_E_CANT_OPEN : failureHResult;
24+
}
25+
26+
public override HRESULT QueryGetData(FORMATETC* pformatetc)
27+
{
28+
if (pformatetc is null)
29+
{
30+
return HRESULT.DV_E_FORMATETC;
31+
}
32+
33+
if (pformatetc->cfFormat != _format)
34+
{
35+
return HRESULT.DV_E_FORMATETC;
36+
}
37+
38+
if (pformatetc->dwAspect != (uint)DVASPECT.DVASPECT_CONTENT)
39+
{
40+
return HRESULT.DV_E_DVASPECT;
41+
}
42+
43+
if (pformatetc->lindex != -1)
44+
{
45+
return HRESULT.DV_E_LINDEX;
46+
}
47+
48+
if (pformatetc->tymed != (uint)TYMED.TYMED_HGLOBAL)
49+
{
50+
return HRESULT.DV_E_TYMED;
51+
}
52+
53+
// QueryGetData succeeds - the data appears to be available
54+
return HRESULT.S_OK;
55+
}
56+
57+
public override HRESULT GetData(FORMATETC* pformatetcIn, STGMEDIUM* pmedium)
58+
{
59+
// But GetData fails - simulating clipboard contention or the data being removed
60+
// between QueryGetData and GetData calls
61+
return _failureHResult;
62+
}
63+
64+
protected override void Dispose(bool disposing) { }
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Windows.Win32;
5+
using Windows.Win32.Foundation;
6+
using Windows.Win32.System.Com;
7+
8+
namespace System.Private.Windows.Ole;
9+
10+
/// <summary>
11+
/// A native data object mock that returns data via <see cref="TYMED.TYMED_ISTREAM"/>.
12+
/// </summary>
13+
internal unsafe class IStreamNativeDataObject : NativeDataObjectMock
14+
{
15+
private readonly Stream _stream;
16+
private readonly ushort _format;
17+
18+
public IStreamNativeDataObject(Stream stream, ushort format)
19+
{
20+
_stream = stream;
21+
_format = format;
22+
}
23+
24+
public override HRESULT QueryGetData(FORMATETC* pformatetc)
25+
{
26+
if (pformatetc is null)
27+
{
28+
return HRESULT.DV_E_FORMATETC;
29+
}
30+
31+
if (pformatetc->cfFormat != _format)
32+
{
33+
return HRESULT.DV_E_FORMATETC;
34+
}
35+
36+
if (pformatetc->dwAspect != (uint)DVASPECT.DVASPECT_CONTENT)
37+
{
38+
return HRESULT.DV_E_DVASPECT;
39+
}
40+
41+
if (pformatetc->lindex != -1)
42+
{
43+
return HRESULT.DV_E_LINDEX;
44+
}
45+
46+
if (pformatetc->tymed != (uint)TYMED.TYMED_ISTREAM)
47+
{
48+
return HRESULT.DV_E_TYMED;
49+
}
50+
51+
return HRESULT.S_OK;
52+
}
53+
54+
public override HRESULT GetData(FORMATETC* pformatetcIn, STGMEDIUM* pmedium)
55+
{
56+
HRESULT result = QueryGetData(pformatetcIn);
57+
if (result.Failed)
58+
{
59+
return result;
60+
}
61+
62+
if (pmedium is null)
63+
{
64+
return HRESULT.E_POINTER;
65+
}
66+
67+
// Reset stream position for each GetData call
68+
_stream.Position = 0;
69+
70+
// Create a ComManagedStream wrapper
71+
ComManagedStream comStream = new(_stream);
72+
73+
// Return the IStream pointer in the STGMEDIUM
74+
// Note: hGlobal is a union with pstm in STGMEDIUM
75+
pmedium->hGlobal = (HGLOBAL)(nint)ComHelpers.GetComPointer<IStream>(comStream);
76+
pmedium->tymed = TYMED.TYMED_ISTREAM;
77+
pmedium->pUnkForRelease = null;
78+
79+
return HRESULT.S_OK;
80+
}
81+
82+
protected override void Dispose(bool disposing) => _stream.Dispose();
83+
}

0 commit comments

Comments
 (0)