Skip to content

Fix unchecked overflow in legacy codegen array size computation#16481

Open
r0qs wants to merge 5 commits intodevelopfrom
fix-unchecked-mul-storage-array-clear
Open

Fix unchecked overflow in legacy codegen array size computation#16481
r0qs wants to merge 5 commits intodevelopfrom
fix-unchecked-mul-storage-array-clear

Conversation

@r0qs
Copy link
Copy Markdown
Member

@r0qs r0qs commented Feb 18, 2026

This PR fixes an unchecked multiplication overflow in ArrayUtils::convertLengthToSize() that could cause delete to silently skip storage clearing when length * storageSize wraps modulo 2^256, leaving stale data in storage.

The correct behavior is to revert (as done in the IR pipeline), because clearing 2^256 slots is fundamentally impossible within gas limits. A delete that returns success but leaves data uncleared is incorrect.

Legacy evmasm codegen now uses overflowCheckedIntMulFunction, matching via-ir behavior.

@r0qs r0qs force-pushed the fix-unchecked-mul-storage-array-clear branch 2 times, most recently from 3aefba0 to 8dabc96 Compare February 18, 2026 17:48
@r0qs r0qs self-assigned this Feb 19, 2026
@r0qs r0qs force-pushed the fix-unchecked-mul-storage-array-clear branch from 8dabc96 to 43c59b5 Compare February 24, 2026 12:51
"summary": "Deleting a dynamic array of large static arrays could silently skip storage clearing due to an unchecked multiplication overflow in the evmasm code generator.",
"description": "When the legacy (evmasm) code generator computes the total number of storage slots occupied by an array, it multiplies the array length by the storage size of its base type. This multiplication was performed without an overflow check, so when the product exceeded ``2**256``, the result would wrap to a small value (or zero). This caused the subsequent clearing loop to process fewer slots than necessary, leaving stale data in storage. The bug could be triggered by using the ``delete`` operator on a dynamic storage array whose base type is large enough for the product to overflow. The via-IR pipeline was not affected, because it already used overflow-checked arithmetic for this computation. With the fix, the legacy code generator now reverts with an arithmetic overflow panic in this situation, matching the via-IR behavior.",
"link": "",
"introduced": "0.1.0",
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The oldest version I could test using Remix was v0.1.3+commit.028f561d, but the bug likely dates back to the introduction of convertLengthToSize in https://github.com/argotorg/solidity/pull/1/changes#diff-bda18cdea6a1adc5907560e2625cd6d135e40fe81a48d0d9e04978d5f0e0586eR626.

@r0qs r0qs force-pushed the fix-unchecked-mul-storage-array-clear branch 4 times, most recently from 78e7b41 to e5d0f63 Compare February 24, 2026 15:51
@r0qs r0qs force-pushed the fix-unchecked-mul-storage-array-clear branch 2 times, most recently from 4ecbda1 to bee5416 Compare February 25, 2026 17:00
@cameel cameel requested review from clonker and matheusaaguiar March 2, 2026 13:53
Copy link
Copy Markdown
Member

@clonker clonker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good to me. Is there any way to check if 0.1.0 was in fact already affected?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could think about changing it a bit so that you push individual elements to the data and show that pushing one element + clear doesn't revert, pushing two + clear reverts.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The elements are so large that even pushing one will revert though. Just for a different reason (out of gas).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The elements are so large that even pushing one will revert though. Just for a different reason (out of gas).

Comment on lines 643 to 652
else
m_context << _arrayType.baseType()->storageSize() << Instruction::MUL;
{
m_context << _arrayType.baseType()->storageSize();
m_context.callYulFunction(
m_context.utilFunctions().overflowCheckedIntMulFunction(*TypeProvider::uint256()),
2,
1
);
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will affect all uses of convertLengthToSize() with storage arrays. Are all of them covered with tests?

Copy link
Copy Markdown
Collaborator

@cameel cameel Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When fixing such issues, we can't stop at fixing just the case that was pointed out in the report. We should take a wider look and consider what other problems of this kind we might have. I think you even mentioned on one of the calls that you've seen places missing overflow checks. This needs a deeper analysis.

Copy link
Copy Markdown
Member Author

@r0qs r0qs Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. Found the same unchecked multiplication in ArrayUtils::accessIndex:

m_context << _arrayType.baseType()->storageSize() << Instruction::MUL;

And unlike the delete case, this one also affects the via-IR pipeline

slot := add(dataArea, mul(index, <storageSize>))

The function YulUtilFunctions::storageArrayIndexAccessFunction uses unchecked mul(index, storageSize). This causes silent data corruption when accessing elements at indices where the product overflows.

Fixed in both pipelines. And added a test that uses assembly to set the array length directly, since push() itself goes through accessIndex() and would trigger the overflow check first. That test was unnecessary, since I managed to reproduce it without inline assembly. So I replaced the test.

But since the underline issue is the same, I'm unsure if we should have two separate bug entries or only one. Any thoughts on that?

For now I added only one bug entry but two changelog entries. Depending on the preferred approach, I can split the bugs in two PRs or update the title and description of this PR accordingly.

@r0qs
Copy link
Copy Markdown
Member Author

r0qs commented Mar 9, 2026

Overall looks good to me. Is there any way to check if 0.1.0 was in fact already affected?

v0.1.3+commit.028f561d was the latest version that I could compile with.

@r0qs r0qs force-pushed the fix-unchecked-mul-storage-array-clear branch 9 times, most recently from 4043584 to 98cb60c Compare March 9, 2026 11:42
@r0qs r0qs force-pushed the fix-unchecked-mul-storage-array-clear branch from 98cb60c to 406b888 Compare March 9, 2026 11:44
@github-actions github-actions bot added the stale The issue/PR was marked as stale because it has been open for too long. label Mar 23, 2026
@argotorg argotorg deleted a comment from github-actions bot Mar 23, 2026
@r0qs r0qs removed the stale The issue/PR was marked as stale because it has been open for too long. label Mar 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants