Skip to content

Commit 9222453

Browse files
Amxxernestognw
andauthored
Merge pull request from GHSA-9vx6-7xxf-x967
* add tests for the encode reads dirty data issue * Fix the encode reads dirty data issue * add changeset * trigger the issue without assembly * rename mock * gas optimization * Apply suggestions from code review Co-authored-by: Ernesto García <[email protected]> * alternative fix: cheaper * update comment * fix lint --------- Co-authored-by: Ernesto García <[email protected]>
1 parent 8b4b7b8 commit 9222453

File tree

4 files changed

+51
-9
lines changed

4 files changed

+51
-9
lines changed

.changeset/warm-geese-dance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': patch
3+
---
4+
5+
`Base64`: Fix issue where dirty memory located just after the input buffer is affecting the result.

contracts/mocks/Base64Dirty.sol

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {Base64} from "../utils/Base64.sol";
6+
7+
contract Base64Dirty {
8+
struct A {
9+
uint256 value;
10+
}
11+
12+
function encode(bytes memory input) public pure returns (string memory) {
13+
A memory unused = A({value: type(uint256).max});
14+
// To silence warning
15+
unused;
16+
17+
return Base64.encode(input);
18+
}
19+
}

contracts/utils/Base64.sol

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,26 +58,32 @@ library Base64 {
5858
let tablePtr := add(table, 1)
5959

6060
// Prepare result pointer, jump over length
61-
let resultPtr := add(result, 32)
61+
let resultPtr := add(result, 0x20)
62+
let dataPtr := data
63+
let endPtr := add(data, mload(data))
64+
65+
// In some cases, the last iteration will read bytes after the end of the data. We cache the value, and
66+
// set it to zero to make sure no dirty bytes are read in that section.
67+
let afterPtr := add(endPtr, 0x20)
68+
let afterCache := mload(afterPtr)
69+
mstore(afterPtr, 0x00)
6270

6371
// Run over the input, 3 bytes at a time
6472
for {
65-
let dataPtr := data
66-
let endPtr := add(data, mload(data))
73+
6774
} lt(dataPtr, endPtr) {
6875

6976
} {
7077
// Advance 3 bytes
7178
dataPtr := add(dataPtr, 3)
7279
let input := mload(dataPtr)
7380

74-
// To write each character, shift the 3 bytes (18 bits) chunk
81+
// To write each character, shift the 3 byte (24 bits) chunk
7582
// 4 times in blocks of 6 bits for each character (18, 12, 6, 0)
76-
// and apply logical AND with 0x3F which is the number of
77-
// the previous character in the ASCII table prior to the Base64 Table
78-
// The result is then added to the table to get the character to write,
79-
// and finally write it in the result pointer but with a left shift
80-
// of 256 (1 byte) - 8 (1 ASCII char) = 248 bits
83+
// and apply logical AND with 0x3F to bitmask the least significant 6 bits.
84+
// Use this as an index into the lookup table, mload an entire word
85+
// so the desired character is in the least significant byte, and
86+
// mstore8 this least significant byte into the result and continue.
8187

8288
mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F))))
8389
resultPtr := add(resultPtr, 1) // Advance
@@ -92,6 +98,9 @@ library Base64 {
9298
resultPtr := add(resultPtr, 1) // Advance
9399
}
94100

101+
// Reset the value that was cached
102+
mstore(afterPtr, afterCache)
103+
95104
if withPadding {
96105
// When data `bytes` is not exactly 3 bytes long
97106
// it is padded with `=` characters at the end

test/utils/Base64.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,13 @@ describe('Strings', function () {
4747
expect(await this.mock.$encodeURL(buffer)).to.equal(expected);
4848
});
4949
});
50+
51+
it('Encode reads beyond the input buffer into dirty memory', async function () {
52+
const mock = await ethers.deployContract('Base64Dirty');
53+
const buffer32 = ethers.id('example');
54+
const buffer31 = buffer32.slice(0, -2);
55+
56+
expect(await mock.encode(buffer31)).to.equal(ethers.encodeBase64(buffer31));
57+
expect(await mock.encode(buffer32)).to.equal(ethers.encodeBase64(buffer32));
58+
});
5059
});

0 commit comments

Comments
 (0)