DeFiHardhat
21,000 USDC
View results
Submission Details
Severity: medium
Invalid

`toString` function doesn't manage memory properly

Summary

The toString function in LibStrings.sol does not adequately manage the memory used for string conversion, leading to potential residual data issues. This can result in incorrect string handling and interpretation by other parts of the contract or external systems.

Impact

The presence of residual or "dirty" data in memory after the string data can lead to security vulnerabilities, incorrect data processing, and potential exploitation where memory content is critical. This issue is capable of damaging the integrity of data handling within other contracts and could lead to incorrect outputs or behaviors in systems that rely on this contract.

Proof of Concept

Take a look at this function. It assumes that the free memory is clean, i.e., does not explicitly zero out used memory

https://github.com/Cyfrin/2024-05-Beanstalk-3/blob/662d26f12ee219ee92dc485c06e01a4cb5ee8dfb/protocol/contracts/libraries/LibStrings.sol#L16-L37

function toString(uint256 value) internal pure returns (string memory) {
// Inspired by OraclizeAPI's implementation - MIT licence
// https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol
if (value == 0) {
return "0";
}
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
uint256 index = digits - 1;
temp = value;
while (temp != 0) {
buffer[index--] = bytes1(uint8(48 + temp % 10));
temp /= 10;
}
return string(buffer);
}

Here is a test Function in TestLibStrings.sol

function testToStringDirtyMemory() public {
uint256 testValue = 123; // Example number to convert to string
// Dirty the memory with a specific pattern before calling toString
uint256 freeMemPointer;
assembly {
freeMemPointer := mload(0x40) // Load the free memory pointer
for { let i := 0 } lt(i, 128) { i := add(i, 32) } {
mstore(add(freeMemPointer, i), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
}
}
// Call toString function
string memory output = LibStrings.toString(testValue);
// Log the string data
bytes memory outputBytes = bytes(output);
emit LogStringData("Output String Data", outputBytes);
// Check the memory after the string
bytes32 postStringMemory;
assembly {
postStringMemory := mload(add(freeMemPointer, 32)) // Load 32 bytes after where the string should end
}
emit LogStringData("Memory After String", abi.encodePacked(postStringMemory));
}

Log Result from Remix Terminal:

logs: [
{
"from": "0xf8e81D47203A594245E36C48e151709F0C19fBe8",
"topic": "0x98da8f9f470ffa9cedb4a8b53d6b99e4112603790dba07e80753d45d60b50efd",
"event": "LogStringData",
"args": {
"0": {
"_isIndexed": true,
"hash": "0xa3177c80f33000f4feff1c9b891b3b26b8fdd9a90e19a74622bf83dbc7883da6"
},
"1": "0x313233"
}
},
{
"from": "0xf8e81D47203A594245E36C48e151709F0C19fBe8",
"topic": "0x98da8f9f470ffa9cedb4a8b53d6b99e4112603790dba07e80753d45d60b50efd",
"event": "LogStringData",
"args": {
"0": {
"_isIndexed": true,
"hash": "0x50bc6aedbcf4fd5a447e2d38afd70347c19b7b6c88d0b30893bde2ca7e4ebeb1"
},
"1": "0x313233ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
}
}
]

Let's look at the logs:

  1. First Log Entry:

    • Event: LogStringData

    • Args:

      • 0x313233 (Hexadecimal representation of the string data)

    • This log corresponds to the "Output String Data" emitted by our test function. The hexadecimal 0x313233 translates to the ASCII string "123", which is the correct string representation of the number 123 that was passed to the toString function.

  2. Second Log Entry:

    • Event: LogStringData

    • Args:

      • 0x313233ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff (Hexadecimal representation of the memory after the string)

    • This log corresponds to the "Memory After String" emitted by our test function. The presence of 0x313233 is expected as it represents the string "123". Albeit, the trailing ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff indicates that the memory immediately following the string contains the "dirty" data that was intentionally written to memory before calling the toString function.

The key observation here is that the memory after the string still contains the "dirty" data (0xFF...). This confirms the concern about the toString function not managing memory cleanly. Specifically, it shows that:

  • The function does not clear or zero out the memory after the string data, leaving residual data intact.

  • This residual data could potentially lead to errors or unexpected behavior if the memory is interpreted as part of the string by other parts of the contract or by external systems that interact with the contract.

In some cases, high level solidity will not have issues decoding values as this region in memory is meant to be
empty. However, certain ABI decoders, notably Etherscan, will have trouble decoding them.
Note: It is likely that the use of toString() in Beanstalk will not be impacted by the above issue. However,
the issue can become severe if LibString is used as a generic string library.

Tools Used

  • Remix

  • Manual Review

Recommendation

To address this issue, you should modify the toString function to ensure that all memory used to store the string is initialized to zero before the string data is written. Additionally, after writing the string data, any unused portion of the allocated memory should be explicitly set to zero. This can be done by adjusting the loop that writes the string data to also cover the entire allocated memory space, or by adding a separate loop to zero out the memory after the string data has been written.

Here's a conceptual modification to the toString function:

function toString(uint256 value) internal pure returns (string memory) {
if (value == 0) {
return "0";
}
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
uint256 index = digits - 1;
temp = value;
while (temp != 0) {
buffer[index--] = bytes1(uint8(48 + temp % 10));
temp /= 10;
}
+ // Zero out any remaining memory in the buffer
+ for (uint256 i = index; i < buffer.length; i++) {
+ buffer[i] = 0;
+ }
return string(buffer);
}

This way, we can ensure that the entire buffer is initialized and any unused memory after the string is explicitly cleared, mitigating the risk of residual data affecting the contract's behavior.

Updates

Lead Judging Commences

giovannidisiena Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.