Skip to content

Clipboard.GetText(TextDataFormat.Rtf) does not retrieve RTF that lacks a null terminating character #14458

@rjacobw

Description

@rjacobw

.NET version

10.0.5

Did it work in .NET Framework?

Yes

Did it work in any of the earlier releases of .NET Core or .NET 5+?

I believe this worked in net6

Issue description

It doesn't seem like the assumption that RTF placed on the clipboard be null-terminated is correct?

// Can't find the explicit docs for CF_RTF, but we've always treated it as null terminated.

RTF is transferred using the registered "Rich Text Format" clipboard format, which is treated as an opaque HGLOBAL buffer. The clipboard contract is size-based, not terminator-based, and only specific standard text formats (e.g., CF_TEXT and CF_UNICODETEXT) guarantee null termination.

https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats

As Raymond notes, registered formats have no structural guarantees beyond the size of the buffer - they are not required to be null-terminated:

https://devblogs.microsoft.com/oldnewthing/20251225-00/?p=111914

In practice, the framework's assumption appears to be invalid: RTF placed on the clipboard by applications like PowerPoint does not include a trailing null and must be read using the buffer size using GlobalSize, not by scanning for a \0.

Steps to reproduce

  1. Copy rich text onto the clipboard from an application like PowerPoint
  2. Author a .NET10 app that calls Clipboard.GetText(TextDataFormat.Rtf)
    -> Note that an empty string is returned

If, instead, you do the following, you'll notice that you get completely valid RTF:

            var clipboardFormatRtf = user32.RegisterClipboardFormat("Rich Text Format");
            if (clipboardFormatRtf == 0 || !user32.OpenClipboard(IntPtr.Zero))
            {
                return string.Empty;
            }

            try
            {
                if (user32.IsClipboardFormatAvailable(clipboardFormatRtf))
                {
                    var hData = user32.GetClipboardData(clipboardFormatRtf);
                    if (hData == IntPtr.Zero)
                    {
                        return string.Empty;
                    }

                    var pData = kernel32.GlobalLock(hData);
                    if (pData == IntPtr.Zero)
                    {
                        return string.Empty;
                    }

                    try
                    {
                        var size = kernel32.GlobalSize(hData);
                        if (size <= 0 || size > int.MaxValue)
                        {
                            return string.Empty;
                        }

                        unsafe
                        {
                            var span = new ReadOnlySpan<byte>((byte*)pData, (int)size);

                            var nullIndex = span.IndexOf((byte)0);
                            if (nullIndex >= 0)
                            {
                                span = span[..nullIndex];
                            }

                            return new string((sbyte*)pData, 0, span.Length);
                        }
                    }
                    finally
                    {
                        kernel32.GlobalUnlock(hData);
                    }
                }
            }
            finally
            {
                user32.CloseClipboard();
            }

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions