
Solidityコンパイラにおける高リスク脆弱性:状態変数の代入が誤って削除される問題
TechFlow厳選深潮セレクト

Solidityコンパイラにおける高リスク脆弱性:状態変数の代入が誤って削除される問題
本稿は、ソースコードのレベルからSolidity(0.8.13≦solidity<0.8.17)コンパイラがコンパイル過程において、Yul最適化メカニズムの欠陥により状態変数への代入操作が誤って削除されてしまう中~高リスクの脆弱性の原理および対策について詳しく解説する。
本稿は、Solidity(0.8.13<=solidity<0.8.17)コンパイラがYul最適化メカニズムの欠陥により、状態変数への代入操作が誤って削除される中~高リスク脆弱性の原理と対策について、ソースコードレベルで詳細に解説しています。
スマートコントラクト開発者が開発時のセキュリティ意識を高め、SOL-2022-7脆弱性によるコード安全性への影響を効果的に回避または緩和することを支援します。
脆弱性の詳細
Yul最適化は、Solidityがコントラクトコードをコンパイルする際のオプション機能であり、冗長な命令を削減することで、コントラクトのデプロイおよび実行時のガス費用を低減できます。詳細なYul最適化の仕組みについては、公式ドキュメントを参照してください。
UnusedStoreEliminatorという最適化ステップでは、「冗長」と判断されたStorageへの書き込み操作が除去されます。しかし、「冗長」の判定に欠陥があるため、特定のユーザー定義関数(内部に呼び出し元ブロックの実行フローに影響しない分岐を持つ)がYul関数ブロックから呼び出され、かつその前後で同一の状態変数に対する書き込み操作がある場合、呼び出し前のすべてのStorage書き込み操作がコンパイル段階で永久に削除されてしまうという問題が発生します。
以下のコードを考えてみましょう:
contract Eocene {
uint public x;
function attack() public {
x = 1;
x = 2;
}
}
UnusedStoreEliminatorによる最適化処理において、x = 1 の代入はattack()関数全体の実行結果に対して明らかに冗長です。そのため、最適化後のYulコードではx = 1が削除され、ガス消費量が削減されます。
次に、中間にカスタム関数の呼び出しを挿入したケースを考えます:
contract Eocene {
uint public x;
function attack(uint i) public {
x = 1;
y(i);
x = 2;
}
function y(uint i) internal{
if (i > 0){
return;
}
assembly { return( 0, 0) }
}
}
明らかに、関数y()の呼び出しにより、それがattack()の実行フローに影響を与えるかどうかを判断する必要があります。もしy()の実行によってメッセージ呼び出しが完全に終了する可能性がある場合(ロールバックではなく、Yulコード中のreturn()で実現可能)、x = 1の代入は削除してはいけません。上記の例では、y()関数内にassembly {return(0, 0)}があるため、メッセージ呼び出しが終了する可能性があり、よってx = 1は削除不可のはずです。
しかし、Solidityコンパイラの実装上の論理的問題により、x = 1が誤ってコンパイル時に削除され、コードの論理が永続的に変更されてしまいます。
実際にコンパイルしたテスト結果は以下の通りです:
驚くべきことに、最適化されてはならないx = 1のYulコードが消失しています!続きは以下で。

SolidityコンパイラのUnusedStoreEliminatorでは、SSA変数追跡と制御フロー追跡を通じて、Storageへの書き込み操作が冗長かどうかを判断しています。カスタム関数に入ったとき、UnusedStoreEliminatorは以下のように動作します:
-
memoryまたはstorageへの書き込み操作:その操作をm_store変数に保存し、初期状態をUndecidedに設定
-
関数呼び出し:関数のmemory/storageの読み書き位置を取得し、m_store内のすべてのUndecided状態の操作と比較
-
m_store内の操作が上書きされる場合:対応する操作の状態をUnusedに変更
-
m_store内の操作が読み取られる場合:対応する操作の状態をUsedに変更
-
関数に引き続き実行可能な分岐がない場合:m_store内のすべてのmemory書き込み操作をUnusedに変更
-
上記条件のもと、関数が実行フローを終了できる場合:m_store内のstateがUndecidedのstorage書き込み操作をUsedに変更。そうでない場合はUnusedに設定
-
関数終了時:すべてのUnused状態の書き込み操作を削除
memoryやstorageへの書き込み操作の初期化コードは以下の通りです:
memoryおよびstorageの書き込み操作がm_storeに保存されていることが確認できます。

関数呼び出し時の処理ロジックは以下の通りです:
operationFromFunctionCall()とapplyOperation()が上記2.1および2.2の処理を実行します。下部にあるcanContinueおよびcanTerminateに基づくif文が2.3の処理を担当します。
ここで注目すべきは、この下部のif文のバグがまさにこの脆弱性の原因となっている点です!!!

operationFromFunctionCall()は、関数内のすべてのmemory/storageの読み書き操作を取得します。Yulにはsstore()やreturn()のような多くの組み込み関数が存在し、組み込み関数とユーザー定義関数では異なる処理が行われていることに注意してください。

applyOperation()関数は、operationFromFunctionCall()で取得したすべての読み書き操作を比較し、m_storeに保存された操作が今回の関数呼び出しで読み書きされたかを判断し、対応する操作の状態を更新します。

上述のUnusedStoreEliminatorの最適化ロジックが、Eoceneコントラクトのattack()関数にどのように適用されるかを検討します:
x = 1のストレージ操作をm_storeに保存し、状態をUndecidedに設定
1. y()関数呼び出しを検出。y()のすべての読み書き操作を取得
2. m_storeを走査。y()呼び出しによる読み書き操作はx = 1と無関係であるため、x = 1の状態は依然としてUndecided
- y()関数の制御フローを解析。y()には正常にリターンする分岐があるため、canContinueはTrueとなり、if文に入らず。x = 1の状態は依然としてUndecided!!!
3. x = 2のストレージ操作を検出:
-
m_storeを走査。Undecided状態のx = 1が存在。x = 2はx = 1を上書きするため、x = 1の状態をUnusedに設定
-
x = 2の操作をm_storeに登録。初期状態はundecided
4. 関数終了:
-
m_store内のすべてのundecided状態の操作をUsedに変更
-
m_store内のすべてのUnused状態の操作を削除
明らかに、関数呼び出し時に、呼び出し先関数がメッセージ実行を終了できる場合、呼び出し前のすべてのUndecided状態の書き込み操作をUsedに変更すべきです。しかし、現状ではUndecidedのまま残され、結果として誤って削除されてしまいます。
さらに注意すべきは、各ユーザー定義関数の制御フロー識別情報は伝播するため、複数の関数が再帰的に呼び合う場合、最も深い層の関数が上記条件を満たしていても、x = 1が削除されてしまう可能性がある点です。
Solidityのブログでは、ほぼ同じロジックを持つにもかかわらず、この脆弱性の影響を受けないコード例が紹介されています。しかし、このコードが影響を受けない理由は、UnusedStoreEliminatorの処理ロジックが異なるためではなく、UnusedStoreEliminatorよりも前のYul最適化ステップにFullInlinerという最適化工程があり、非常に小さい、あるいは一度しか呼び出されない関数を呼び出し元にインライン展開してしまうため、ユーザー定義関数という脆弱性のトリガー条件自体が回避されているからです。
contract Normal {
uint public x;
function f(bool a) public {
x = 1;
g(a);
x = 2;
}
function g(bool a) internal {
if (!a)
assembly { return( 0, 0) }
}
}
コンパイル結果は以下の通りです:
関数g(bool a)がf()関数内にインライン展開され、ユーザー定義関数という脆弱性の条件が回避され、結果として脆弱性は発生しません。

解決策
根本的な解決策は、影響範囲内のSolidityコンパイラを使用しないことです。どうしても該当バージョンのコンパイラを使用する必要がある場合は、コンパイル時にUnusedStoreEliminatorの最適化ステップを無効化することを検討してください。
コントラクトコード側で脆弱性の緩和を図りたい場合は、複数の最適化工程や実際の関数呼び出しフローの複雑さを考慮し、専門のセキュリティ担当者によるコード監査を依頼して、本脆弱性に起因するセキュリティ問題を特定することをお勧めします。
TechFlow公式コミュニティへようこそ
Telegram購読グループ:https://t.me/TechFlowDaily
Twitter公式アカウント:https://x.com/TechFlowPost
Twitter英語アカウント:https://x.com/BlockFlow_News














