Fix integer overflow in heap_opt range walk on top-of-memory access (… - #582
Fix integer overflow in heap_opt range walk on top-of-memory access (…#582kvpanch wants to merge 3 commits into
Conversation
…581) The word-stepping loops in `taint_range`, `mark_escaping_range`, and `mark_escaping_and_tainted_range` walked a static range with `while word < end { word += 32 }`. When an access sits near the top of the address space, `end = address.saturating_add(size)` saturates at `u64::MAX`, and the final `word += 32` steps past `u64::MAX`, panicking with "attempt to add with overflow". Under `--newyork` this is reachable from valid Yul: folding `add(mul(MAX_U256, 1), 0x41)` to `0x40` lets an mstore corrupt the free-memory-pointer word with `u64::MAX`; mem_opt then forwards `mload(0x40)` to that literal, turning a later `mstore(fmp, _)` into a static unaligned store at `u64::MAX` that taints its covered words. Iterate the already-bounded `num_words` count with a saturating step so the walk covers the same words without overflowing.
xermicus
left a comment
There was a problem hiding this comment.
Could you please add the MRE from the bug report as integration test?
| let range = end.saturating_sub(first_word); | ||
| let num_words = | ||
| range.saturating_add(BYTE_LENGTH_WORD as u64 - 1) / BYTE_LENGTH_WORD as u64; | ||
| // Records each covered word; same range walk and corner cases as `taint_range`. |
There was a problem hiding this comment.
Please remove this and make a doc-comment instead
| .saturating_sub(first_word) | ||
| .saturating_add(BYTE_LENGTH_WORD as u64 - 1) | ||
| / BYTE_LENGTH_WORD as u64; | ||
| // `[address, address + size)` spans `num_words` 32-byte words starting at |
There was a problem hiding this comment.
Same here, please remove and make a proper doc comment instead.
elle-j
left a comment
There was a problem hiding this comment.
Could you add a changelog entry as well, referencing this PR? 👍
| self.escaping_regions.insert(word); | ||
| self.tainted_regions.insert(word); | ||
| word += BYTE_LENGTH_WORD as u64; | ||
| word = word.saturating_add(BYTE_LENGTH_WORD as u64); |
There was a problem hiding this comment.
From seeing how similar the updated sections are, we could consider adding e.g. a fn range_words() helper to extract the identical logic (let end = .., let first_word = .., let num_words = .., if num_words > MAX_RANGE_WORDS { .., etc.), having it return the range of words (an iterator over the words).
Other than easier maintenance and the stepping being handled in one place, I think the small differences at each call site would also be a bit clearer then (e.g. how they all handle if num_words > MAX_RANGE_WORDS being true or false a bit differently. The helper could return None when the range is wider than the max for instance). Let me know what you think.
| let range = end.saturating_sub(first_word); | ||
| let num_words = | ||
| range.saturating_add(BYTE_LENGTH_WORD as u64 - 1) / BYTE_LENGTH_WORD as u64; | ||
| // Records each covered word; same range walk and corner cases as `taint_range`. |
There was a problem hiding this comment.
If by the "corner cases" you mean e.g. the size == 0 case then at all locations they do handle this somewhat differently, so I'd drop the "same" wording here.
| // unaligned store through a corrupted free-memory pointer). Stepping the | ||
| // bounded `num_words` count with a saturating add keeps the final increment | ||
| // from overflowing `u64` — a plain `word += 32` panics here. | ||
| // - `size == 0` is excluded by the match guard (an empty range taints nothing). |
There was a problem hiding this comment.
This last comment line needs an update. The (Some(address), _) arm adds a tainted region for size == 0.
| /// `taint_range` taints exactly the word-aligned words a static range covers, | ||
| /// including the partial leading word when the start is unaligned. | ||
| #[test] | ||
| fn taint_range_covers_spanned_words() { |
There was a problem hiding this comment.
Currently only the taint_range() is tested. Could consider adding similar ones for the other updates.
| /// must not overflow the word walk. `add(mul(MAX_U256, 1), 0x41)` folds to `0x40`, corrupting the | ||
| /// free-memory-pointer word with `u64::MAX`; `mem_opt` then forwards a store to that literal and | ||
| /// the walk panicked with "attempt to add with overflow". Compiling is the regression; the call | ||
| /// pins runtime behaviour against solc-EVM. |
There was a problem hiding this comment.
Nit:
| /// pins runtime behaviour against solc-EVM. | |
| /// pins runtime behavior against solc-EVM. |
|
|
||
| /// An access at the top of the address space must not overflow the word walk. | ||
| #[test] | ||
| fn taint_range_top_of_memory_does_not_overflow() { |
There was a problem hiding this comment.
We could add an assertion on the analysis.has_dynamic_access in this test as well.
|
|
||
| /// The overflowing range reaches this caller through the same helper. | ||
| #[test] | ||
| fn mark_escaping_range_top_of_memory_does_not_overflow() { |
There was a problem hiding this comment.
We could add an assertion on the analysis.has_dynamic_escapes in this test as well.
|
|
||
| /// The overflowing range through the both-sets walk. | ||
| #[test] | ||
| fn mark_escaping_and_tainted_range_top_of_memory_does_not_overflow() { |
There was a problem hiding this comment.
We could add an assertion on the analysis.has_dynamic_escapes in this test as well.
| ## Unreleased | ||
|
|
There was a problem hiding this comment.
| ## Unreleased | |
| ## Unreleased | |
| Supported `polkadot-sdk` rev: `2604.2.0` | |
…#581)
The word-stepping loops in
taint_range,mark_escaping_range, andmark_escaping_and_tainted_rangewalked a static range withwhile word < end { word += 32 }. When an access sits near the top of the address space,end = address.saturating_add(size)saturates atu64::MAX, and the finalword += 32steps pastu64::MAX, panicking with "attempt to add with overflow".Under
--newyorkthis is reachable from valid Yul: foldingadd(mul(MAX_U256, 1), 0x41)to0x40lets an mstore corrupt the free-memory-pointer word withu64::MAX; mem_opt then forwardsmload(0x40)to that literal, turning a latermstore(fmp, _)into a static unaligned store atu64::MAXthat taints its covered words.Iterate the already-bounded
num_wordscount with a saturating step so the walk covers the same words without overflowing.