High-risk vulnerability in Solidity compiler: Accidentally delete state variable assignment
Eocene
2023-05-06 12:49
本文约2022字,阅读全文需要约8分钟
This article explains in detail from the source code level the solidity (0.8.13<=solidity<0.8.17) compiler in the compilation process, due to the defect of the Yul optimization mechanism, the state variable assignment operation is mistakenly delete

This article explains in detail from the source code level the solidity (0.8.13<=solidity<0.8.17) compiler in the compilation process, due to the defect of the Yul optimization mechanism, the state variable assignment operation is mistakenly deleted. corresponding precautions.

first level title

1. Vulnerability details

The Yul optimization mechanism is an option for Solidity to compile the contract code. Through the optimization mechanism, some redundant instructions in the contract can be reduced, thereby reducing the gas cost during contract deployment and execution. For the specific Yul optimization mechanism, please refer toofficial document

In the UnusedStoreEliminator optimization step of the compilation process, the compiler will remove the "redundant" Storage write operation, but due to the identification defect of "redundancy", when a Yul function block calls a specific user-defined function (function There is a branch inside that does not affect the execution flow of the calling block), and there is a write operation to the same state variable before and after the called function in the Yul function block, which will cause the user-defined function in the block to be called by the Yul optimization mechanism all beforeStorage write operations are permanently deleted from the compilation level

Consider the following code:

contract Eocene {

        uint public x;

        function attack() public {

                x = 1;

                x = 2;

        }

}

x=1 is obviously redundant for the entire execution of function attack() when UnusedStoreEliminator optimizes. Naturally, the optimized Yul code will delete x= 1; to reduce the gas consumption of the contract.

Next consider inserting a call to a custom function in the middle:

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) }

}

}

Obviously, due to the call of the y() function, we need to judge whether the y() function will affect the execution of the function attack(), if the y() function can cause the entire function execution flow to terminate (note that it is not a rollback, the Yul code return() function), then x= 1 obviously cannot be deleted, so for the above contract, the existence of assembly {return(0, 0)} in the y() function can lead to the termination of the entire message call, x= 1 naturally cannot be deleted.

But in the Solidity compiler, due to code logic problems, x= 1 was deleted by mistake during compilation, which permanently changed the code logic.

The actual compilation test results are as follows:

Shock! The Yul code with x=1 that should not be optimized is lost! If you want to know what happened next, please read below.

In the UnusedStoreEliminator of the solidiry compiler code, SSA variable tracking and control flow tracking are used to determine whether a Storage write operation is redundant. When entering a custom function, if UnusedStoreEliminator encounters:

  1. memory or storage write operations: store memory and storage write operations into the m_store variable, and set the initial state of the operation to Undecided;

  2. Function call: Get the memory or storage read and write operation location of the function, and compare it with all operations in the Undecided state stored in the m_store variable:

    1. If it is a write overwrite of the storage operation in m_store, change the corresponding operation status in m_store to Unused

    2. If it is a read of the storage operation in m_store, change the corresponding operation status in the corresponding m_store to Used

    3. If the function does not have any branches that can continue to execute message calls, change all memory write operations in m_store to Unused

    1. Under the appeal condition, if the function can terminate the execution flow, change the storage write operation whose status is Undecided in m_store to Used; otherwise, mark it as Unused

  3. End of function: delete all write operations marked as Unused

The initialization code for writing to memory or storage is as follows:

It can be seen that the encountered memory and storage write operations are stored in m_store

The processing logic code when a function call is encountered is as follows:

Among them, operationFromFunctionCall() and applyOperation() implement the processing logic of 2.1 and 2.2 of the appeal. The If statement that is judged based on the function canContinue and canTerminate below implements the 2.3 logic.

It should be noted that it is the defect of the If judgment below that leads to the existence of the loophole! ! !

operationFromFunctionCall() to obtain all memory or storage read and write operations of this function. It should be noted here that there are many built-in functions in Yul, such as sstore(), return(). Here you can see that there are different processing logics for built-in functions and user-defined functions.

The applyOperation() function compares all read and write operations obtained from operationFromFuncitonCall() to determine whether the data stored in m_store is read or written in this function call, and modifies the corresponding operation status in m_store.

Consider the processing of the above-mentioned UnusedStoreEliminator optimization logic on the attack() function of the Eocene contract:

Store x= 1 into the m_store variable and set the status to Undecided

1. Encounter the y() function call, get all the read and write operations of the y() function call

2. Traverse the m_store variable and find that all read and write operations caused by the y() call have nothing to do with x= 1, and the state of x= 1 is still Undecided

1. Obtain the control flow logic of the y() function, because the y() function has a branch that can return normally, so canContinue is True, and does not enter the If judgment. x= 1 status is still Undecided! ! !

3. Encountered x= 2 storage operation:

1. Traverse the m_store variable and find that x= 1 in the Undecided state, the operation x= 2 overrides x= 1, and sets the state of x= 1 to Unused.

2. Store the x= 2 operation into m_store, and the initial state is undecided.

4. Function ends:

1. Change the operation state of undecided state in all m_stores to Used

2. Delete all operations in the Unused state in m_store

Obviously, when the function is called, if the called function can terminate the execution of the message, all the writing operations of the Undecided state before the called function should be changed to Used, instead of remaining as Undecided, resulting in the write operation before the called function Action was deleted by mistake.

exist

existSolidity, an example of the contract code that will not be affected under basically the same logic. However, the code is not affected by this vulnerability not because there are other possibilities in the processing logic of UnusedStoreEliminator, but in the Yul optimization step before UnusedStoreEliminator, there is a FullInliner optimization process that will embed the called function with a small or only one call into the In the call function, user-defined functions in the vulnerability trigger conditions are avoided.

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) }

    }

}

The compilation result is as follows:

first level title

2. Solutions

The most fundamental solution is to compile without using the solidity compiler in the affected range. If you need to use a vulnerable version of the compiler, you can consider removing the UnusedStoreEliminator optimization step during compilation.

If you want to mitigate the vulnerability from the contract code level, considering the complexity of multiple optimization steps and the complexity of the actual function call flow, please find professional security personnel to conduct code audits to help discover the vulnerabilities in the contract caused by this vulnerability Security Question.

Eocene
作者文库