概要
ChromeでuBlock Origin等のManifest V2拡張機能を使い続けるために、MV2廃止に伴う判定を変更するパッチを作成。
対象はWindows x64版のChromium系ブラウザーで、Chromium 151から154 155のバイナリでパッチ対象の検出と変更を確認しています。
<追記:2026/9/15> Chrome Canary 155.0.8058.0へ対応するよう修正
パッチはビルド後のバイナリに対し、MV2拡張機能が有効化やインストールの判定を通過するよう機械語を書き換えます。
以下のdiffは変更後の挙動をC++で表した概念差分です。
関数名とファイル名は対応するChromiumソースを示し、コードは変更点に絞って簡略化しています。
同じ目的で作られたプロジェクトとして次の2件があります。
chrome-mv2-patchはバイナリの数バイトを書き換えるパッチャー、chrome-enable-mv2は読み込み済みのchrome.dllへメモリ上でパッチを適用するランチャーです。
MV2廃止の影響対象を判定する処理
Chromiumは拡張機能のマニフェストバージョン、種別、インストール元からMV2廃止の影響対象かどうかを判定します。
この判定はmanifest_v2_util::IsExtensionAffected()が行います。
条件を簡略化すると次のようになります。
affected =
manifest_version < 3
&& type_is_extension
&& !is_component_extension;
type_is_extensionに当たる種別は通常の拡張機能、ログイン画面用拡張機能、ユーザースクリプトです。
種別とインストール元の条件を満たすMV2拡張機能は、有効化やインストールが拒否され、起動時には無効化されます。
バイナリにはこの判定が複数箇所に現れます。
Clangの最適化とLTO(リンク時最適化)によって、呼び出し元へのインライン展開や関数の複製が行われるためです。
有効化、インストール、起動時処理に現れる判定をまとめて変更します。
jgを同じ飛び先のjmpへ変更する
MV2判定の先頭には、マニフェストバージョンが3以上なら「影響対象外」の処理へ進む分岐があります。
確認したバイナリでは値2との比較と、符号付きの「より大きい」を表す条件分岐命令jgで実装されています。
このjgを無条件分岐命令のjmpへ置き換えます。
MV2もMV3が使っていた既存の「影響対象外」の処理へ進むようになります。
cmp manifest_version, 2
- jg unaffected
+ jmp unaffected
ここでのmanifest_versionとunaffectedは比較対象と飛び先を示す説明用の名前です。
実際の置換バイトは分岐の距離に応じて次の2種類になります。
| 分岐 | 変更前 | 変更後 |
|---|---|---|
| 短距離 | 7F rel8 |
EB rel8 |
| 長距離 | 0F 8F rel32 |
90 E9 rel32 |
rel8とrel32は命令の末尾から飛び先までの相対変位です。
短距離では変更前後とも命令長が2バイトなので、同じ変位で同じ位置へ飛びます。
長距離のjgは6バイト、jmpは5バイトです。
先頭に1バイトのnopである90を置き、続くE9 rel32の末尾を元のjgの末尾に合わせます。
相対変位を維持したまま同じ飛び先を使えます。
命令の特徴を使った検出
パッチ箇所はMV2判定の命令列と、呼び出し側の拒否や無効化処理の特徴から特定します。
比較命令、即値、分岐命令と前後の並びを照合します。
レジスタ割り当てやスタック位置など、ビルドごとに変わる部分はマスクして候補を探します。
この意味ベースの照合は確認済みの命令配置をパターンとして表現する方式です。
命令配置の違いには複数のパターンで対応します。候補の個数と適用状態が各規則の条件を満たせば、対応する箇所を一括変更します。
Chrome 152.0.7977.76の検証では、6規則に対応する8箇所を検出しました。
内訳は以下で説明するMV2判定の6箇所と、保存済みの無効化理由を扱う変換処理の2箇所です。
有効化とインストールの判定
有効化と影響対象の照会
ManifestV2Handlerは、有効化を拒否するかどうかの判定と、MV2廃止の影響対象かどうかの照会を提供します。
どちらも影響対象判定につながるため、それぞれの分岐を既存のfalseを返す処理へ向けます。
diff --git a/extensions/browser/manifest_v2_handler.cc b/extensions/browser/manifest_v2_handler.cc
--- a/extensions/browser/manifest_v2_handler.cc
+++ b/extensions/browser/manifest_v2_handler.cc
@@ ManifestV2Handler::ShouldBlockExtensionEnable(...) @@
- return IsExtensionAffected(extension);
+ return false;
@@ ManifestV2Handler::IsExtensionAffected(...) @@
- return manifest_v2_util::IsExtensionAffected(extension);
+ return false;
この2箇所はallow-enable-and-report規則で扱います。
インストール時の判定
同じハンドラーのShouldBlockExtensionInstallation()はインストール対象のマニフェスト情報を受け取り、MV2廃止による拒否を判定します。
allow-install規則でこの判定の分岐を変更し、falseを返す処理へ進めます。
diff --git a/extensions/browser/manifest_v2_handler.cc b/extensions/browser/manifest_v2_handler.cc
--- a/extensions/browser/manifest_v2_handler.cc
+++ b/extensions/browser/manifest_v2_handler.cc
@@ ManifestV2Handler::ShouldBlockExtensionInstallation(...) @@
- return manifest_v2_util::IsExtensionAffected(
- manifest_version, manifest_type, manifest_location);
+ return false;
管理ポリシー処理に展開された判定
拡張機能の管理ポリシーを扱うStandardManagementPolicyProviderにもMV2判定がインライン展開されています。
MustRemainDisabled()は無効状態の維持を、UserMayInstall()はインストールの可否を判定します。
この2箇所には、それぞれallow-enable-policy-inlineとallow-install-policy-inlineを適用します。
MV2判定の分岐を変更すると、管理ポリシーの処理は次のようにMV2による拒否ブロックを飛び越えます。
diff --git a/chrome/browser/extensions/standard_management_policy_provider.cc b/chrome/browser/extensions/standard_management_policy_provider.cc
--- a/chrome/browser/extensions/standard_management_policy_provider.cc
+++ b/chrome/browser/extensions/standard_management_policy_provider.cc
@@ StandardManagementPolicyProvider::MustRemainDisabled(...) @@
- if (mv2_handler && mv2_handler->ShouldBlockExtensionEnable(*extension)) {
+ if (false) {
if (reason) {
*reason = disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION;
}
return true;
}
@@ StandardManagementPolicyProvider::UserMayInstall(...) @@
- if (mv2_handler && mv2_handler->ShouldBlockExtensionEnable(*extension)) {
+ if (false) {
error = l10n_util::GetStringUTF16(IDS_EXTENSIONS_CANT_INSTALL_MV2_EXTENSION);
std::move(callback).Run({false, error});
return;
}
起動時のMV2一括無効化
起動時にはManifestV2Handler::DisableAffectedExtensions()が有効な拡張機能を走査します。
影響対象の拡張機能を集合へ追加し、走査後にDISABLE_UNSUPPORTED_MANIFEST_VERSIONという理由を付けて無効化します。
skip-startup-disable規則では各拡張機能の判定から次の要素へ進む分岐を常に選択させます。
MV2を理由に無効化する対象の集合は空になります。
diff --git a/extensions/browser/manifest_v2_handler.cc b/extensions/browser/manifest_v2_handler.cc
--- a/extensions/browser/manifest_v2_handler.cc
+++ b/extensions/browser/manifest_v2_handler.cc
@@ ManifestV2Handler::DisableAffectedExtensions() @@
for (const auto& extension : extension_registry->enabled_extensions()) {
- if (!manifest_v2_util::IsExtensionAffected(*extension)) {
+ if (true) {
continue;
}
extensions_to_disable.insert(extension);
}
保存済みの無効化理由8388608の扱い
すでにMV2を理由に無効化された拡張機能には、設定上の無効化理由が残っています。
DISABLE_UNSUPPORTED_MANIFEST_VERSIONの値はdisable_reason.hで1 << 23、すなわち8388608と定義されています。
Chromiumは設定の整数値を読み込み、CollapseUnknownDisableReasons()で実行時に参照する無効化理由の集合へ変換します。
ignore-mv2-disable-reason-at-runtime規則は、この変換ループで8388608を読み飛ばします。
diff --git a/extensions/browser/extension_prefs.cc b/extensions/browser/extension_prefs.cc
--- a/extensions/browser/extension_prefs.cc
+++ b/extensions/browser/extension_prefs.cc
@@ CollapseUnknownDisableReasons(...) @@
for (int reason : stored_reasons) {
- if (IsValidDisableReason(reason))
- effective_reasons.insert(reason);
- else
- effective_reasons.insert(DISABLE_UNKNOWN);
+ if (reason == DISABLE_UNSUPPORTED_MANIFEST_VERSION)
+ continue;
+
+ effective_reasons.insert(reason);
}
Chromeはメッセージ認証コード(MAC)を使って拡張機能の保存設定を検証しています。
保存値だけを書き換えると、MACとの不一致によって設定のリセット対象になる場合があります。
保存値と対応するMACをともに正しく更新すれば、整合性を維持できます。
対応する実装は、拡張機能設定の保護設定と読み込み時のMAC検証処理です。
本パッチでは保存設定と保護情報の更新をChromeに任せ、MV2の無効化理由を実行時に除外します。
保存値を保持したまま読み出し後の理由集合から8388608を除外するため、パッチャーがMACの計算仕様や暗号化ハッシュの管理方法に依存する部分を減らせます。
その分、Chromeの更新時には読み出し処理の命令配置を確認し、変換後の理由集合を参照する処理にもパッチが適合するかを調べる必要があります。
保存された理由が8388608だけなら実行時の集合は空になります。ほかの理由も保存されていれば、その理由で無効状態が維持されます。
| 設定に保存された理由 | パッチ後の実行時の理由 |
|---|---|
{8388608} |
{} |
{8388608, 別の理由} |
{別の理由} |
{別の理由} |
{別の理由} |
機械語では元のIsValidDisableReason()呼び出し部分を8388608との比較に置き換えます。
8388608以外の整数はその値のまま実行時の集合へ入ります。
DISABLE_UNKNOWNへ集約していた未知の整数も、そのまま無効化理由として扱います。
有効化や起動時の判定に加えて保存済みの理由を読み出す判定も変更し、MV2拡張機能を有効化できる状態にします。