Clipboard owning string copies — 2026-09-22
Pass-5 #5's remaining string-copy audit found a private implementation in
clipboard_store_rtf_data. It measured with the 65,536-byte-capped
guest_strlen, then copied the measured length plus one with a raw linear
memory copy. Longer RTF was truncated and could lose its terminator; a caller
string crossing noncontiguous sparse pages was read from the wrong backing.
The helper now calls guest_strdup, which already sizes the complete string
and copies with guest-aware accesses. Length is measured on the new contiguous
heap-owned copy, not by translating caller memory once. Allocation/copy occurs
before freeing the previous clipboard buffer, including when the source is
that buffer or a suffix of it. NULL input still fails without changing data;
allocation failure now also leaves the previous snapshot intact.
This intentionally replaces the old reusable-capacity policy with a fresh owned allocation per explicit RTF store. The previous allocation is freed on successful replacement; this is not a snapshot leak. Basic RTF synthesis still uses its existing capacity management. No throughput benefit is claimed and OOM failure injection has not been performed.
Verification
The old implementation failed the new 70,000-byte content/terminator check.
test-clipboard-rtf-api.js covers 65,535/65,536/65,537/70,000-byte content and
reported lengths, source independence, self-copy, overlapping suffix input,
NULL preservation, and a string spanning two adjacent guest pages with
nonadjacent backing. The sparse fixture explicitly verifies that mapping
condition. Existing registration, format availability, binary-format and
EmptyClipboard checks remain in the same test.
All 36 checks pass after the change, as do test-crt-strdup.js and
test-ole-clipboard-lifetime.js. A compiler-only transform restoring just the
old RTF helper fails five checks: long-length reporting, 70,000-byte content,
and both sparse-copy assertions (31/36). It does not modify shared source or
build artifacts. Fragment, logical-AND, handler-ESP, silent-inventory, tier and
whitespace checks pass; the quiet inventory remains 248 manual + 22 metadata.
Remaining scope
This is the internal NUL-terminated RTF snapshot path, not a redesign of SetClipboardData's general HGLOBAL transfer contract, embedded-NUL registered formats, delayed rendering, arbitrary invalid pointers, or native Win98 OOM semantics. No browser, game-performance or full clipboard conformance claim.
The broader scan also found capacity-managed control buffers, encoded strings, counted slices, PIDL records and joined paths. They are not interchangeable with whole ANSI NUL-terminated duplication and were not mechanically rewritten. The public text path was subsequently reviewed below; the binary-copy path still needs separate review. This does not close all of Pass-5 #5.
Public SetClipboardData text path
The CF_TEXT/CF_OEMTEXT branch had the same capped length and raw linear copy.
It now duplicates through guest_strdup before freeing the old snapshot,
records the full length from the contiguous owned copy, and clears RichEdit
format state only after allocation succeeds. Return value (the caller's
handle), 12-byte stdcall cleanup, transaction guards and one sequence bump
per successful call are preserved. NULL keeps the existing failure behavior;
it is not an implementation of delayed rendering.
Like the RTF change, this uses a fresh allocation per store and frees the old buffer on success. Allocation failure leaves the old data/format state intact; OOM injection and performance measurements have not been performed.
test/test-clipboard-text-copy.js calls the actual public handler and checks
both text formats at 1/65,535/65,536/65,537/70,000 bytes, exact bytes and NUL,
length, return value, ESP and sequence. It also covers explicitly noncontiguous
sparse backing, internal self/suffix-copy robustness, and NULL/closed-state
preservation. The pre-fix handler fails at 65,537 bytes (reported 65,536).
ASCII-only OEM cases do not certify code-page conversion. Alias cases test
internal copy safety, not valid Windows HGLOBAL transfer patterns.
The new text test, existing open-state and cross-Worker sequence tests, RTF suite (36/36) and OLE lifetime test all pass. Fragment, handler-ESP, logical-AND, silent-inventory, tier and whitespace checks pass. The inventory remains 248 manual + 22 metadata; no full release build or browser run was performed for this change.
Microsoft's SetClipboardData contract describes ownership transfer, movable global allocations, delayed rendering and format conversion. Our current snapshot implementation is not that full contract; this correction does not claim otherwise or establish native Win98 OOM behavior. Unsupported-format inert success, binary sparse copies, Unicode/OEM conversion, empty-string format availability and general HGLOBAL lifetime handling remain candidates for separate behavioral work.
CF_DIB public handle boundary
The next inspection did not establish a sparse-copy bug for legitimate
binary input: our heap allocations are contiguous, and the binary helper
expects a trusted heap allocation, not an arbitrary guest address. Replacing
its copy loop alone would leave the real bug intact: the public CF_DIB branch
passed unchecked input to heap_payload_size_unchecked.
The public branch now uses the existing exact-boundary/provenance validator
heap_global_block_size before calling the trusted copy helper. Invalid input
returns NULL with ERROR_INVALID_HANDLE and leaves the previous snapshot and
sequence untouched. This uses the emulator's existing Global API policy;
the precise invalid-handle error has not been measured on native Win98.
Successful SetClipboardData returns the supplied handle, consistent with the
text/RTF branches, rather than returning the private copied buffer.
The binary snapshot is now marked as a Global allocation before publication, so the handle GetClipboardData returns can actually pass GlobalLock/GlobalSize. Trusted host image injection still accepts ordinary guest heap blocks; it does not pretend those inputs came from the public API. Existing binary snapshot retention for RichEdit images is unchanged.
test-clipboard-dib-handles.js exercises the real GlobalAlloc, Set/GetClipboardData,
GlobalLock, GlobalSize, GlobalFree and sequence handlers. It supplies a minimal
24-bit DIB and checks return handles, copied bytes, extent, stack cleanup and
sequence. Rejection cases include NULL, ordinary heap, forged tagged interior,
freed global, unmapped and interior-global pointers. A host-injected ordinary
heap snapshot must also be lockable. The pre-fix implementation fails the
successful SetClipboardData return-value check.
After the fix, the new public CF_DIB regression, OLE clipboard wrapping (13/13), and the cross-Worker GlobalFlags/size/provenance regression pass. Fragment, ESP, logical-AND, silent-inventory, tier and whitespace gates pass; the silent inventory remains 248 manual + 22 metadata. No native Win98 or browser run was performed for this boundary change.
This is not full HGLOBAL ownership transfer or movable-handle support. It does not enforce DIB content validity, solve concurrent misuse of caller-owned handles, or reclaim retained RichEdit snapshots. Text/RTF snapshot GlobalLock compatibility was left for the publication audit below; the earlier string fixes did not test that contract.
Text/RTF HGLOBAL publication
The follow-up audit found a common publication point: clipboard_get_data_handle
returns the buffers populated by SetClipboardData, native Edit/RichEdit copy
and basic RTF synthesis. It now marks owned text/RTF buffers as Global
allocations immediately before returning them. This avoids scattering marker
maintenance across producers and capacity-growth paths. Repeated retrieval
is idempotent, retains handle identity and does not bump the clipboard sequence.
Binary snapshots retain their creation-time marking because the host injection
helper itself returns that handle.
The public-handle regression now checks GetClipboardData -> GlobalLock -> GlobalSize for CF_TEXT, ASCII CF_OEMTEXT, explicitly stored RTF and synthesized RTF. A larger text replacement followed by RTF synthesis exercises fresh/grown allocations after an earlier handle was published. The synthesis helper is called directly in this unit test; this is not a WordPad UI/end-to-end claim. The pre-fix regression fails because GlobalLock returns NULL for CF_TEXT. After the change, the expanded public-handle test, long/sparse text-copy test and RTF suite (36/36) pass. Fragment, handler-ESP, logical-AND, quiet-inventory, tier and whitespace checks pass; the quiet count remains 248 + 22.
This repairs publication compatibility, not ownership transfer. Guest attempts to free clipboard-owned memory, stale handles after replacement/EmptyClipboard, empty-string format availability, code-page conversion and delayed rendering still require separate behavior and lifetime work.