Skip to content

クリップボードからの貼り付け時のクラッシュ対応(int32_t)#2453

Open
hpmy-dev wants to merge 9 commits intosakura-editor:masterfrom
hpmy-dev:feature/clipcrash_int32_t
Open

クリップボードからの貼り付け時のクラッシュ対応(int32_t)#2453
hpmy-dev wants to merge 9 commits intosakura-editor:masterfrom
hpmy-dev:feature/clipcrash_int32_t

Conversation

@hpmy-dev
Copy link
Copy Markdown

PR対象

  • アプリ(サクラエディタ本体)
  • テストコード

カテゴリ

  • 不具合修正
  • 改善

PR の背景

PR #2067 において SAKURAClipW 形式のヘッダ型が int から size_t に変更されたことにより、32bit版(size_t = 4バイト)と64bit版(size_t = 8バイト)の間でクリップボードのバイナリレイアウトが不一致となり、クロスビット間のコピー&ペースト時にヘッダの読み取り位置がずれて異常なサイズを確保しようとしクラッシュする問題が発生しています。

関連: #2450, #2067, Issue #2325

仕様・動作説明

1. SAKURAClipW ヘッダ型の int32_t 固定化

ヘッダ型を環境依存の size_t から固定長の int32_t(4バイト)に変更し、v2.4.2 リリース版(int = 4バイト)とのバイナリ互換性を回復します。ヘッダ構造体 SSakuraClipHeader を新設し、#pragma pack(push, 1) + static_assert でレイアウトを保証。

#pragma pack(push, 1)
struct SSakuraClipHeader {
    int32_t cchData;
};
#pragma pack(pop)
static_assert(sizeof(SSakuraClipHeader) == 4, "SSakuraClipHeader must be exactly 4 bytes");

フォーマット名 SAKURAClipW は変更しません(バイナリレイアウトが v2.4.2 と一致するため)。

2. SAKURAClipW メモリレイアウト

オフセット    サイズ        内容
──────────────────────────────────────
0x00          4 bytes      int32_t  cchData(文字数、符号付き)
0x04          N * 2 bytes  wchar_t  szData[cchData](文字データ)
0x04 + N*2    2 bytes      wchar_t  L'\0'(終端ヌル)

3. GlobalSize() による3段階フェイルセーフ(GetText 読み込み時)

SAKURAClipW 形式の読み込み時に、ヘッダの自己申告値を鵜呑みにせず GlobalSize() が返す実際のメモリサイズと突き合わせて検証します。

段階 チェック内容 防御対象
第1段階 pData == nullptr || cbData < sizeof(SSakuraClipHeader) ロック失敗・データ不足
第2段階 cchRaw < 0 符号反転による不正値(32/64bit混在時)
第3段階 cchData > cchMax ヘッダの自己申告値が実メモリ超過 → 破損データとして拒否

フォーマット未指定(デフォルト)の場合、SAKURAClipW の検証が失敗すると CF_UNICODETEXT へ自動フォールバックします。

4. INT32_MAX 超過時の SAKURAClipW スキップ(SetText 書き込み時)

nDataLen > INT32_MAX の場合、SAKURAClipW 形式のみをスキップし、CF_UNICODETEXT・矩形選択フラグ・行選択フラグは正常に書き込みます。

const bool bCanUseSakuraFormat = (nDataLen <= static_cast<size_t>(INT32_MAX));

これにより 2GiB 超のテキストでも CF_UNICODETEXT + 矩形選択フラグが正しく書き込まれ、ペースト側は CF_UNICODETEXT へフォールバックして動作します。

5. SIZE_T オーバーフロー防止(SetText CF_UNICODETEXT 書き込み時)

(nDataLen + 1) * sizeof(wchar_t)SIZE_T の上限を超えてオーバーフローする場合を検出し、CF_UNICODETEXT の GlobalAlloc をスキップします。

6. CLIPBOARD_MAX_CHARS による読み込み上限

CClipboard::CLIPBOARD_MAX_CHARS = INT32_MAX を定義し、CF_UNICODETEXT / CF_OEMTEXT / GetClipboardByFormat の読み込み時にデータ長を上限で切り詰めます。

7. SEH 例外ハンドリング(STATUS_NO_MEMORY 対策)

大容量データのペースト時に Windows ヒープが投げる SEH 例外 STATUS_NO_MEMORY0xC0000017)に対応するため、__try/__except を使用した安全なラッパー関数を導入しました。

CMemory::AllocBuffer 内部の malloc/realloc は C++ 例外を投げず、std::wstring::appendstd::bad_alloc の前に Windows ヒープが SEH 例外を投げるため、C++ の try-catch(std::bad_alloc&) では捕捉できません。MSVC の制約上、__try/__except はデストラクタを持つ C++ オブジェクトと共存できないため、独立した関数に分離しています。

参照: https://learn.microsoft.com/ja-jp/windows/win32/debug/using-an-exception-handler

ヘルパー関数 用途 配置ファイル
SafeAppend IWBuffer::Append の保護 CClipboard.cpp
SafeSetString CNativeW::SetString の保護 CEditView_Mouse.cpp
SafeNewBytes new BYTE[] の保護 CDropTarget.cpp
static bool SafeAppend(IWBuffer* cmemBuf, const wchar_t* pData, size_t nLen)
{
    __try {
        cmemBuf->Append(pData, nLen);
        return true;
    }
    __except( GetExceptionCode() == STATUS_NO_MEMORY
        ? EXCEPTION_EXECUTE_HANDLER
        : EXCEPTION_CONTINUE_SEARCH )
    {
        return false;
    }
}

8. CDropTarget.cpp の安全化

  • SafeNewBytes によるメモリ確保の SEH 保護
  • m_pData 配列のゼロ初期化(goto fail 時の delete[] 未定義動作防止)
  • SAKURAClipW 形式の int32_t 対応(memcpy_raw 使用)
  • nTextLen > INT32_MAX 時の SAKURAClipW スキップ(CF_UNICODETEXT・矩形選択フラグは維持)

9. CEditView_Mouse.cpp(Drop)の安全化

  • SafeSetString による SEH 保護
  • SAKURAClipW 読み込みの int32_t 対応 + GlobalSize() チェック
  • CF_UNICODETEXT / CF_TEXT の CLIPBOARD_MAX_CHARS 切り詰め
  • 破損した SAKURAClipW → CF_UNICODETEXT → CF_TEXT のフォールバックチェーン

仕様・動作説明(ファイル別ロジック詳細)

ファイル別変更詳細


1. sakura_core/_os/CClipboard.h

SSakuraClipHeader 構造体の新設

SAKURAClipW 独自クリップボード形式のバイナリヘッダを、#pragma pack(push, 1) で1バイトアラインメントに固定した構造体として定義。static_assert でサイズが正確に4バイトであることをコンパイル時に保証する。従来は size_t をキャストして直接書き込んでおり、32bit(4バイト)と64bit(8バイト)でレイアウトが不一致になっていた。

オフセット    サイズ        内容
0x00          4 bytes      int32_t  cchData(文字数)
0x04          N * 2 bytes  wchar_t  szData[cchData]
0x04 + N*2    2 bytes      wchar_t  L'\0'(終端ヌル)

CLIPBOARD_MAX_CHARS 定数の追加

CClipboard クラスの static constexpr メンバとして CLIPBOARD_MAX_CHARS = INT32_MAX を定義。ヘッダ末尾にグローバルスコープの static constexpr エイリアスも配置し、無名名前空間内の SafeAppend 等から参照可能にしている。

変更なしの項目

SetText() / GetText() の関数シグネチャ — パラメータ型 size_t nDataLen は PR #2067int から変更されたものだが、関数インターフェースとしては大容量データの受け渡しに必要なため、int には戻さずそのまま維持する。int32_t に固定化したのは SAKURAClipW のバイナリヘッダのみであり、関数の引数型とは独立している。


2. sakura_core/_os/CClipboard.cpp

SafeAppend ヘルパー関数の追加(無名名前空間)

IWBuffer::Append の呼び出しを Windows SEH(__try/__except)で保護する static 関数。STATUS_NO_MEMORY0xC0000017)を捕捉した場合のみ EXCEPTION_EXECUTE_HANDLER を返し false で復帰する。それ以外の例外は EXCEPTION_CONTINUE_SEARCH で上位に伝播させる。C++ の try-catch(std::bad_alloc&) ではなく SEH を使用する理由は、CMemory::AllocBuffer 内部の malloc/realloc が C++ 例外を投げないこと、および std::wstring::appendstd::bad_alloc を投げる前に Windows ヒープが SEH 例外を投げる場合があるため。MSVC の制約上、__try/__except はデストラクタを持つ C++ オブジェクトと同一関数に配置できないため、独立した関数に分離している。

SetText() の変更

  • bCanUseSakuraFormat フラグの導入: nDataLen <= INT32_MAX を関数冒頭で評価し、以降の SAKURAClipW 書き込み判定に使用。return false で関数全体を中断するのではなく、bSakuraText の条件に組み込むことで、CF_UNICODETEXT・矩形選択フラグ・行選択フラグは nDataLen > INT32_MAX でも正常に書き込まれる。
  • CF_UNICODETEXT の SIZE_T オーバーフロー防止: nDataLen + 1 の加算オーバーフローと cchUnicode * sizeof(wchar_t) の乗算オーバーフローを各々検出し、発生時は break で書き込みをスキップ。
  • SAKURAClipW ヘッダの int32_t 書き込み: memcpy(pClip, &header, sizeof(header))SSakuraClipHeader サイズ分を書き込む。sizeof(nDataLen)(64bit 環境で8バイト)ではなく sizeof(SSakuraClipHeader)(固定4バイト)を使用。
  • SAKURAClipW のコメント更新: データレイアウトの説明を SSakuraClipHeader ベースに修正。
  • 関数末尾の成否判定: bCanUseSakuraFormat && !hgClipSakura の場合のみ SAKURAClipW 確保失敗を false とする。nDataLen > INT32_MAX で意図的にスキップした場合は失敗扱いにしない。

GetText(IWBuffer*) の変更

  • SAKURAClipW 読み込み — 3段階フェイルセーフ:
    • 第1段階: pData == nullptr || cbData < sizeof(SSakuraClipHeader) でロック失敗・データ不足を検出。
    • 第2段階: memcpy(&cchRaw, pData, sizeof(cchRaw)) でヘッダを読み取り、cchRaw < 0 で負値(符号反転による不正値)を検出。
    • 第3段階: cchData > cchMaxcchMax = (cbData - sizeof(SSakuraClipHeader)) / sizeof(wchar_t))でヘッダの自己申告値が実メモリを超過するケースを検出し、破損データとして拒否。
    • すべてのエラーパスで GlobalUnlock(hSakura) を呼んでからリターンまたはフォールスルー。
    • フォーマット未指定(uGetFormat == -1)の場合、検証失敗時は CF_UNICODETEXT へフォールスルー。明示指定(uGetFormat == uFormatSakuraClip)の場合は return false
    • 正常パスの AppendSafeAppend で保護。SafeAppendfalse を返した場合も、フォーマット未指定なら CF_UNICODETEXT へフォールスルー。
  • CF_UNICODETEXT 読み込み: GlobalSize() から wcsnlen で実文字数を算出し、std::min(cchTotal, CLIPBOARD_MAX_CHARS) で上限に切り詰め。SafeAppend で SEH を保護し、失敗時は GlobalUnlockreturn false
  • CF_OEMTEXT 読み込み: GlobalSize()CLIPBOARD_MAX_CHARS * sizeof(wchar_t) で切り詰めてから SJIS→UNICODE 変換。変換後の文字数も CLIPBOARD_MAX_CHARS で再切り詰め。SafeAppend で保護。

GetClipboardByFormat() の変更

  • GetLengthByMode() で取得した nLength を、モードに応じた上限値(バイナリモードは CLIPBOARD_MAX_CHARS、それ以外は CLIPBOARD_MAX_CHARS * sizeof(wchar_t))で切り詰め。
  • nEndMode == -1 で再取得した nLength にも同じ上限を適用。

3. sakura_core/_os/CDropTarget.cpp

SafeNewBytes ヘルパー関数の追加(無名名前空間)

new BYTE[]__try/__except で保護する static 関数。STATUS_NO_MEMORY 捕捉時は *ppOut = nullptr を設定して false を返す。

CDataObject::SetText() の変更

  • bUseSakuraFormat フラグの導入: nTextLen <= INT32_MAX を評価。false の場合は SAKURAClipW エントリを除外して m_nFormat を調整(2 or 3)し、CF_UNICODETEXT・CF_TEXT・矩形選択フラグは維持。
  • m_pData 配列のゼロ初期化: new DATA[m_nFormat] の直後に全エントリの datanullptr に初期化。goto fail 時に未初期化ポインタを delete[] する未定義動作を防止。
  • new BYTE[]SafeNewBytes に置き換え: CF_UNICODETEXT、CF_TEXT、SAKURAClipW、MSDEVColumnSelect の各確保を SEH で保護。失敗時は goto fail で全エントリをクリーンアップ。
  • SAKURAClipW ヘッダの int32_t 書き込み: memcpy_raw(m_pData[i].data, &cchData, sizeof(cchData))SSakuraClipHeader サイズ分を書き込む。

4. sakura_core/view/CEditView_Mouse.cpp

SafeSetString ヘルパー関数の追加(無名名前空間)

CNativeW::SetString__try/__except で保護する static 関数。STATUS_NO_MEMORY 捕捉時は false を返す。

Drop() 関数の変更 — ドロップデータ取得ループ内

  • SAKURAClipW パス: memcpy_raw(&header, pData, sizeof(header)) でヘッダを読み取り(R1: エイリアシング安全)。header.cchData >= 0 かつ cchData <= cchMaxcchMax = (nSize - sizeof(SSakuraClipHeader)) / sizeof(wchar_t))で検証。検証失敗時は GlobalUnlockGlobalFree → CF_UNICODETEXT / CF_TEXT へフォールバック。
  • CF_UNICODETEXT パス: wcsnlen で文字数を算出し、CLIPBOARD_MAX_CHARS で切り詰め。SafeSetString で SEH を保護。失敗時は GlobalUnlockGlobalFreeE_OUTOFMEMORY を返す。
  • CF_TEXT パス: CLIPBOARD_MAX_CHARS * sizeof(wchar_t) でバイト数を切り詰めてから SJIS→UNICODE 変換。変換後の文字数も CLIPBOARD_MAX_CHARS で再切り詰め。

5. src/test/cpp/tests1/test-cclipboard.cpp

新規追加テスト

テスト名 検証内容
ClipboardMaxCharsConstant CLIPBOARD_MAX_CHARS == INT32_MAX かつ正値であることの確認
SetText7 nDataLen > INT32_MAX でサクラ形式指定時に SetClipboardData が呼ばれず false を返す
SetText8 nDataLen > INT32_MAX でもデフォルト指定で CF_UNICODETEXT と矩形選択フラグが書き込まれ true を返す。SAKURAClipW は Times(0) で呼ばれないことを検証
SetTextSizeTOverflow nDataLen = size_t::max で SIZE_T オーバーフロー防御が機能し false を返す
SakuraFormatNegativeLength ヘッダ cchData = -1GetText がフォーマット指定なしで false を返す
SakuraFormatOverflowLength ヘッダ cchData = 2 だが実データ1文字分で GetTextfalse を返す
CorruptedSakuraFallsBackToUnicode ヘッダ負値の SAKURAClipW があってもデフォルト取得で CF_UNICODETEXT にフォールバックし true を返す

既存テストの変更

  • SakuraFormatInGlobalMemory マッチャ: ヘッダの読み取りを SSakuraClipHeader 構造体経由に変更し、cchData < 0 を検出。
  • フィクスチャ CClipboardGetText: sakuraMemory の確保サイズを sizeof(SSakuraClipHeader) + ... に変更。ヘッダ書き込みを ((SSakuraClipHeader*)p)->cchData = static_cast<int32_t>(...) に変更。
  • #include <limits> を追加(std::numeric_limits<size_t>::max() 使用のため)。

PR の影響範囲

修正対象ファイル

ファイル 変更内容
sakura_core/_os/CClipboard.h SSakuraClipHeader 構造体新設、CLIPBOARD_MAX_CHARS 定数追加
sakura_core/_os/CClipboard.cpp SafeAppend 追加、SetText/GetText の int32_t 対応、GlobalSize() チェック、SEH ハンドリング
sakura_core/_os/CDropTarget.cpp SafeNewBytes 追加、int32_t 対応、ゼロ初期化、SAKURAClipW スキップ
sakura_core/view/CEditView_Mouse.cpp SafeSetString 追加、Drop() の int32_t 対応、GlobalSize() チェック
src/test/cpp/tests1/test-cclipboard.cpp SAKURAClipW ヘッダ検証テスト、フォールバックテスト等追加

変更しないもの

  • フォーマット名 SAKURAClipW(バイナリレイアウトが v2.4.2 と一致するため)
  • SetText() / GetText() の関数シグネチャ — パラメータ型 size_t nDataLen は PR INT_MAXより大きなバイト数のテキストのクリップボードへのコピーが行えるようにする変更 #2067int から変更されたものだが、関数インターフェースとしては大容量データの受け渡しに必要なため、int には戻さずそのまま維持する。int32_t に固定化したのは SAKURAClipW のバイナリヘッダのみであり、関数の引数型とは独立している。
  • CF_UNICODETEXT 関連の基本的な処理フロー
  • SetHtmlText() / HDROP 処理(大容量データに該当しない)

テスト内容

ユニットテスト(test-cclipboard.cpp)

SetText 系

テスト 内容
SetText1〜6 既存テスト(回帰確認)
SetText7 nDataLen > INT32_MAX でサクラ形式指定時に false を返す
SetText8 nDataLen > INT32_MAX でも矩形選択フラグが書き込まれる

GetText 系

テスト 内容
NoSpecifiedFormat1〜6 既存テスト(回帰確認)
SakuraFormatNegativeLength ヘッダ負値で拒否
SakuraFormatOverflowLength ヘッダ値が実メモリ超過で拒否
CorruptedSakuraFallsBackToUnicode 破損 SAKURAClipW → CF_UNICODETEXT フォールバック
SakuraFormatSuccess / Failure サクラ形式の正常取得・失敗
UnicodeTextSuccess / Failure CF_UNICODETEXT の正常取得・失敗
OemTextSuccess / Failure CF_OEMTEXT の正常取得・失敗

動作テスト

# テストシナリオ 期待結果
1 64bit版で文字列をコピー → 64bit版でペースト 正常にペーストされる
2 32bit版(v2.4.2)で文字列をコピー → 64bit版でペースト 正常にペーストされる(互換性回復)
3 64bit版で文字列をコピー → 32bit版(v2.4.2)でペースト 正常にペーストされる(互換性回復)
4 外部アプリから CF_UNICODETEXT でペースト 正常にペーストされる
5 64bit版でドラッグ&ドロップ(SAKURAClipW形式) 正常にドロップされる
6 不正なクリップボードデータ(ヘッダ負値) クラッシュせず、ペースト失敗として処理
7 不正なクリップボードデータ(ヘッダ値 > 実メモリサイズ) クラッシュせず、CF_UNICODETEXT へフォールバック
8 nDataLen > INT32_MAX の矩形選択コピー → ペースト SAKURAClipW は無し、CF_UNICODETEXT + 矩形選択フラグで動作
9 64bit版で 2GiB 超テキストをコピー → 32bit版でペースト クラッシュせず、STATUS_NO_MEMORY を SafeAppend が捕捉して false を返す

ビルド確認

構成 確認内容
Win32 Release ビルド成功、警告なし(C4018, C4267, C4244)
x64 Release ビルド成功
Win32 Debug ビルド成功
x64 Debug ビルド成功
ユニットテスト 全テスト PASS

関連 issue, PR

参考資料

@beru beru added the 🐛bug🦋 ■バグ修正(Something isn't working) label Apr 29, 2026
@beru
Copy link
Copy Markdown
Contributor

beru commented Apr 29, 2026

差分を確認しましたがタイトル「クリップボードからの貼り付け時のクラッシュ対応(int32_t)」以外の変更も行われています。

@hpmy-dev
Copy link
Copy Markdown
Author

hpmy-dev commented May 1, 2026

はい、下記理由の為です。
・grepのマルチスレッド有りきで動作できる事を前提にしてますので含んでます。
・X64でビルドエラーを解消しないとクリップボードのX64版の動作確認が出来ませんので否応なしに取り込んでます。

・既にPRでsrc/test/cpp/tests1/test-cclipboard.cppは修正が入ってましたので、どっちみちMERGEが発生する。

上記2点はこの分です。
5236f20

@berryzplus
Copy link
Copy Markdown
Contributor

もとの実装がだいぶおかしいので、色んな切り口で検討してみる、も価値あることだと思っています。

ざっくり。

  • 生クリップボード クリップボードの生APIを直接操作する方式。クリップボードの所有者ウインドウの生ハンドルが必要あので、使い勝手はあまりよくない。ただ、指定したクリップボード形式「だけ」を扱える側面を持つのでマクロ専用として使えないことはない存在と考えられる。
  • OLEクリップボード COMインターフェース IDataObject を実装して「クリップボードデータ」を定義する Windows95 で導入された新方式。「このアプリでコピーできるデータとは?」と「特定のクリップボード形式をどうレンダリングするか」を切り離して実装できるのが特徴。

マクロ専用関数 Get / Set ClipboardByFormat の不審点

  • クリップボード形式を文字列で受け取っている
  • nMode でエンコーディング方法を受け取っている
    • -1 がバイナリモード
    • -2 がエディタと同じモード
    • それ以外なら ECodeType が指定されたとみなす
  • nEndMode で NUL終端の個数を受け取っている
    • 1, 2, 4 のいずれかを指定すると指定されたバイト数を付加した値を拡張する

テキストエディタの機能なのに、バイナリモードがある。
「エディタと同じ」のはずなのに、処理が違う。

@hpmy-dev
Copy link
Copy Markdown
Author

hpmy-dev commented May 4, 2026

確かに同様に文字コードの自動判定処理もオリジナルになってますね。最後はタイプ別設定に紐づけられるって笑

クリップボードに関してnotepadは重たい感じがありますが、notepad++などいろいろ参考にされてはどうですかね。
あとはクラッシュを優先して直されるのか、書かれている対応を含めて改良されるのか次第だと思います。

私としてはPRしている32bit版でgrepのマルチスレッドを早く32bit版ででも
リリースして頂ければと思っているぐらいです。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛bug🦋 ■バグ修正(Something isn't working)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants