winclip 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -1
- data/ext/winclip/winclip.cpp +95 -22
- data/lib/winclip/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c0b25c458e941f65768b07785e50fd7ee61591ba4fc40dd7fe6571bba6454ab7
|
|
4
|
+
data.tar.gz: 6465565e678f37e773aba98def6cc4b4f13eaa190441c9017225d24f0c7740be
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94c890ec8e6fb145bed87ea71ecbf2c08512435903e343eb128aed777b554eebf42f18c46e4477d887f718ebe8b9f0fb17d60bcdf2d7a47297f9eb3205fe038f
|
|
7
|
+
data.tar.gz: 90702bfc96e92607e6599ea310b562112107542cac28d75789591f70e887f59be36eaefad893d9d3ed1967a6266b9a28fcd4ffd9e90ff2be5b4ad1642a726017
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,43 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.1] - 2026-06-27
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **Intermittent heap-corruption crash (silent `SIGSEGV`) when using the image
|
|
13
|
+
clipboard.** `Winclip.image` and `Winclip.image=` raised their Ruby exceptions
|
|
14
|
+
(`rb_raise`, which uses `longjmp`) from inside a function frame that also
|
|
15
|
+
carried C++ exception-handling state — a `std::vector` and a `try`/`catch`
|
|
16
|
+
around the WIC decode. On MSVC/x64 that `longjmp` unwinds through the frame and
|
|
17
|
+
corrupts the C++ unwind bookkeeping, poisoning the process heap; the corruption
|
|
18
|
+
detonated later in an unrelated allocation (often the next exception raise) as a
|
|
19
|
+
`STATUS_HEAP_CORRUPTION` with no Ruby `[BUG]` dump — so it surfaced as an
|
|
20
|
+
occasional truncated/aborted run (~2 of 3) rather than a counted test failure.
|
|
21
|
+
All C++/exception-handling work now lives in helper functions; the Ruby-facing
|
|
22
|
+
image methods are plain POD frames that `rb_raise` can unwind safely. Verified
|
|
23
|
+
with a stress harness: the previously ~98%-fatal reject path now runs 0 crashes
|
|
24
|
+
across hundreds of invocations, and the full suite is stable across randomized
|
|
25
|
+
ordering. Added `test/test_com_stability.rb` covering the reject path and image
|
|
26
|
+
round-trips.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- COM/WIC is now initialized once per process instead of an apartment
|
|
30
|
+
init/teardown on every image call — robustness hardening that removes
|
|
31
|
+
needless per-call STA churn (not the cause of the crash above, but flagged
|
|
32
|
+
alongside it).
|
|
33
|
+
|
|
34
|
+
### Security / robustness
|
|
35
|
+
- Hardened untrusted-DIB pixel-offset handling in `Winclip.image`: the colour-
|
|
36
|
+
mask gap after a 40-byte `BITMAPINFOHEADER` is now also accounted for under
|
|
37
|
+
`BI_ALPHABITFIELDS` (4 masks), not only `BI_BITFIELDS` (3 masks), so pixels are
|
|
38
|
+
located correctly when the OS does not synthesize a `CF_DIBV5` (e.g. some
|
|
39
|
+
remote-desktop clipboards) and only a raw `CF_DIB` is present.
|
|
40
|
+
- The BGRA row stride (`width * 4`) handed to WIC is now computed and bounds-
|
|
41
|
+
checked in 64-bit, rejecting absurd widths instead of letting a 32-bit
|
|
42
|
+
multiply wrap. Both are defensive (bounded, never a heap over-read in prior
|
|
43
|
+
versions); they keep a malformed/hostile clipboard image decoding correctly or
|
|
44
|
+
failing cleanly rather than producing a wrong image.
|
|
45
|
+
|
|
9
46
|
## [0.1.0] - 2026-05-30
|
|
10
47
|
|
|
11
48
|
### Added
|
|
@@ -27,5 +64,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
27
64
|
returns `nil` rather than raising. Image size math is 64-bit (no overflow).
|
|
28
65
|
- `available?(name)` resolves custom format names without registering them.
|
|
29
66
|
|
|
30
|
-
[Unreleased]: https://github.com/main-path/winclip/compare/v0.1.
|
|
67
|
+
[Unreleased]: https://github.com/main-path/winclip/compare/v0.1.1...HEAD
|
|
68
|
+
[0.1.1]: https://github.com/main-path/winclip/compare/v0.1.0...v0.1.1
|
|
31
69
|
[0.1.0]: https://github.com/main-path/winclip/releases/tag/v0.1.0
|
data/ext/winclip/winclip.cpp
CHANGED
|
@@ -32,8 +32,34 @@
|
|
|
32
32
|
#include <cstring>
|
|
33
33
|
#include <cstdint>
|
|
34
34
|
|
|
35
|
+
#ifndef BI_ALPHABITFIELDS
|
|
36
|
+
#define BI_ALPHABITFIELDS 6 /* 40-byte header + 4 colour masks (R,G,B,A) */
|
|
37
|
+
#endif
|
|
38
|
+
|
|
35
39
|
static VALUE eWinclipError; /* Winclip::Error */
|
|
36
40
|
|
|
41
|
+
/* ---------- COM lifetime -------------------------------------------------
|
|
42
|
+
* WIC runs inside an initialized COM apartment. We initialize the STA ONCE for
|
|
43
|
+
* the life of the process and never CoUninitialize between calls, instead of the
|
|
44
|
+
* old per-call CoInitializeEx/CoUninitialize pair: repeatedly creating and
|
|
45
|
+
* finalizing an apartment around a process-global WIC factory is needless churn,
|
|
46
|
+
* and a single long-lived apartment is the standard model for an in-process WIC
|
|
47
|
+
* consumer. (This is robustness hardening. The intermittent heap-corruption
|
|
48
|
+
* crash itself was a separate bug — rb_raise unwinding through a C++ EH frame —
|
|
49
|
+
* fixed via the C++/EH-confining wc_*_image helpers below, not here.) A plain
|
|
50
|
+
* static flag is safe: every Ruby-facing method runs under the GVL on the main
|
|
51
|
+
* thread. */
|
|
52
|
+
static void wc_ensure_com(void) {
|
|
53
|
+
static bool com_inited = false;
|
|
54
|
+
if (com_inited) return;
|
|
55
|
+
/* S_OK (we initialized it), S_FALSE (already STA) and RPC_E_CHANGED_MODE
|
|
56
|
+
* (thread already MTA) are all acceptable — WIC's in-proc objects work in
|
|
57
|
+
* either apartment model. We deliberately never pair this with
|
|
58
|
+
* CoUninitialize. */
|
|
59
|
+
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
|
|
60
|
+
com_inited = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
37
63
|
/* ---------- UTF-8 <-> UTF-16 (LocalAlloc'd results) ---------------------- */
|
|
38
64
|
|
|
39
65
|
static wchar_t *utf8_to_utf16(const char *utf8, int len /* -1 = NUL-terminated */) {
|
|
@@ -317,9 +343,20 @@ static HRESULT clip_get_image(std::vector<BYTE> &outPng) {
|
|
|
317
343
|
hh = (UINT)(rawH < 0 ? -rawH : rawH);
|
|
318
344
|
if (w == 0 || hh == 0) throw E_FAIL;
|
|
319
345
|
if (biSize < sizeof(BITMAPINFOHEADER) || biSize > dibSize) throw E_FAIL;
|
|
320
|
-
|
|
346
|
+
/* The BGRA stride (w*4) is handed to WIC's WritePixels as a UINT and is
|
|
347
|
+
* used as a row stride below; reject widths whose stride would not fit a
|
|
348
|
+
* 32-bit UINT so the `w * 4` expressions can never wrap. */
|
|
349
|
+
if ((uint64_t)w * 4 > 0xFFFFFFFFu) throw E_FAIL;
|
|
350
|
+
|
|
351
|
+
/* Locate the pixel array. For a plain 40-byte BITMAPINFOHEADER the colour
|
|
352
|
+
* masks sit between the header and the pixels: 3 DWORDs for BI_BITFIELDS,
|
|
353
|
+
* 4 for BI_ALPHABITFIELDS. V4/V5 headers (biSize >= 108) embed their masks,
|
|
354
|
+
* so no gap is added for them. */
|
|
321
355
|
SIZE_T pixOffset = biSize;
|
|
322
|
-
if (
|
|
356
|
+
if (biSize == sizeof(BITMAPINFOHEADER)) {
|
|
357
|
+
if (comp == BI_BITFIELDS) pixOffset += 3 * sizeof(DWORD);
|
|
358
|
+
else if (comp == BI_ALPHABITFIELDS) pixOffset += 4 * sizeof(DWORD);
|
|
359
|
+
}
|
|
323
360
|
if (pixOffset > dibSize) throw E_FAIL;
|
|
324
361
|
BYTE *pixels = dib + pixOffset;
|
|
325
362
|
SIZE_T pixBytes = dibSize - pixOffset;
|
|
@@ -360,7 +397,47 @@ static HRESULT clip_get_image(std::vector<BYTE> &outPng) {
|
|
|
360
397
|
GlobalUnlock(h);
|
|
361
398
|
CloseClipboard();
|
|
362
399
|
if (FAILED(hr)) return hr;
|
|
363
|
-
return bgra_to_png(bgra.data(), w, hh, w * 4, outPng);
|
|
400
|
+
return bgra_to_png(bgra.data(), w, hh, (UINT)((uint64_t)w * 4), outPng);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* ---------- image: C++/EH-confining wrappers ----------------------------
|
|
404
|
+
* The image paths use std::vector and try/catch, which make their enclosing
|
|
405
|
+
* function carry MSVC C++ exception-handling unwind state. rb_raise unwinds via
|
|
406
|
+
* setjmp/longjmp, and on x64 MSVC that longjmp runs frame cleanup as it goes;
|
|
407
|
+
* letting it unwind THROUGH a frame that owns C++ EH state corrupts the unwind
|
|
408
|
+
* bookkeeping and poisons the heap (a later allocation then dies with a silent
|
|
409
|
+
* STATUS_HEAP_CORRUPTION). So all C++ work is confined to these helpers, which
|
|
410
|
+
* NEVER call into Ruby; the Ruby-facing wc_*_image frames below stay pure POD
|
|
411
|
+
* (no destructors, no try/catch) and are the only ones that rb_raise. */
|
|
412
|
+
|
|
413
|
+
/* 0 = ok (result in *hr), 1 = C++ exception (e.g. bad_alloc). Never raises. */
|
|
414
|
+
static int set_image_guarded(const BYTE *png, size_t len, HRESULT *hr) {
|
|
415
|
+
try {
|
|
416
|
+
std::vector<BYTE> in(png, png + len);
|
|
417
|
+
*hr = clip_set_image(in.data(), in.size());
|
|
418
|
+
} catch (...) {
|
|
419
|
+
return 1;
|
|
420
|
+
}
|
|
421
|
+
return 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* 0 = ok (*out is a LocalAlloc'd PNG buffer of *outlen bytes, caller LocalFree's),
|
|
425
|
+
* 1 = C++ exception, 2 = absent/undecodable. Never raises. */
|
|
426
|
+
static int get_image_guarded(BYTE **out, size_t *outlen, HRESULT *hr) {
|
|
427
|
+
*out = NULL; *outlen = 0;
|
|
428
|
+
std::vector<BYTE> png;
|
|
429
|
+
try {
|
|
430
|
+
*hr = clip_get_image(png);
|
|
431
|
+
} catch (...) {
|
|
432
|
+
return 1;
|
|
433
|
+
}
|
|
434
|
+
if (FAILED(*hr)) return 2;
|
|
435
|
+
size_t n = png.size();
|
|
436
|
+
BYTE *buf = (BYTE *)LocalAlloc(LMEM_FIXED, n ? n : 1);
|
|
437
|
+
if (!buf) return 1;
|
|
438
|
+
if (n) memcpy(buf, png.data(), n);
|
|
439
|
+
*out = buf; *outlen = n;
|
|
440
|
+
return 0;
|
|
364
441
|
}
|
|
365
442
|
|
|
366
443
|
/* ---------- format names ------------------------------------------------ */
|
|
@@ -453,38 +530,34 @@ static VALUE wc_get_image(VALUE self) {
|
|
|
453
530
|
if (!IsClipboardFormatAvailable(CF_DIBV5) && !IsClipboardFormatAvailable(CF_DIB))
|
|
454
531
|
return Qnil;
|
|
455
532
|
|
|
456
|
-
|
|
457
|
-
bool com_owned = (cohr == S_OK || cohr == S_FALSE);
|
|
533
|
+
wc_ensure_com(); /* one process-lifetime STA; never CoUninitialize'd */
|
|
458
534
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
catch (...) { threw = true; } /* e.g. bad_alloc in bgra_to_png (clipboard already closed) */
|
|
464
|
-
if (com_owned) CoUninitialize();
|
|
535
|
+
/* Pure-POD frame: all C++/EH is inside get_image_guarded, so the rb_raise
|
|
536
|
+
* below never unwinds through a frame carrying C++ exception state. */
|
|
537
|
+
BYTE *buf = NULL; size_t buflen = 0; HRESULT hr = E_FAIL;
|
|
538
|
+
int rc = get_image_guarded(&buf, &buflen, &hr);
|
|
465
539
|
|
|
466
|
-
if (
|
|
540
|
+
if (rc == 1) rb_raise(eWinclipError, "winclip: out of memory decoding the clipboard image");
|
|
467
541
|
/* Format present but not decodable (palettized / malformed) -> nil, matching
|
|
468
542
|
* the "getters return nil when there's no usable content" contract. */
|
|
469
|
-
if (
|
|
543
|
+
if (rc == 2) return Qnil;
|
|
470
544
|
|
|
471
|
-
VALUE s = rb_str_new((char *)
|
|
545
|
+
VALUE s = rb_str_new((char *)buf, (long)buflen);
|
|
546
|
+
LocalFree(buf);
|
|
472
547
|
rb_enc_associate(s, rb_ascii8bit_encoding());
|
|
473
548
|
return s;
|
|
474
549
|
}
|
|
475
550
|
|
|
476
551
|
static VALUE wc_set_image(VALUE self, VALUE png) {
|
|
477
552
|
StringValue(png);
|
|
478
|
-
|
|
479
|
-
bool com_owned = (cohr == S_OK || cohr == S_FALSE);
|
|
553
|
+
wc_ensure_com(); /* one process-lifetime STA; never CoUninitialize'd */
|
|
480
554
|
|
|
555
|
+
/* Pure-POD frame: the byte-vector and try/catch live in set_image_guarded so
|
|
556
|
+
* the rb_raise calls below never unwind through a C++ EH frame. */
|
|
557
|
+
const BYTE *p = (const BYTE *)RSTRING_PTR(png);
|
|
558
|
+
size_t n = (size_t)RSTRING_LEN(png);
|
|
481
559
|
HRESULT hr = E_FAIL;
|
|
482
|
-
|
|
483
|
-
try {
|
|
484
|
-
std::vector<BYTE> in((BYTE *)RSTRING_PTR(png), (BYTE *)RSTRING_PTR(png) + RSTRING_LEN(png));
|
|
485
|
-
hr = clip_set_image(in.data(), in.size());
|
|
486
|
-
} catch (...) { threw = true; } /* e.g. bad_alloc decoding a huge PNG (no clipboard held) */
|
|
487
|
-
if (com_owned) CoUninitialize();
|
|
560
|
+
int threw = set_image_guarded(p, n, &hr);
|
|
488
561
|
|
|
489
562
|
if (threw) rb_raise(eWinclipError, "winclip: out of memory encoding the clipboard image");
|
|
490
563
|
if (FAILED(hr))
|
data/lib/winclip/version.rb
CHANGED