Skip to content

Fix bug in Bytes.lastIndexOf when array is empty and position is not 2²⁵⁶-1 #5797

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/witty-hats-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': patch
---

`Bytes`: Fix `lastIndexOf(bytes,byte,uint256)` with empty buffers and finite position to correctly return `type(uint256).max` instead of accessing uninitialized memory sections.
3 changes: 1 addition & 2 deletions contracts/utils/Bytes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ library Bytes {
function lastIndexOf(bytes memory buffer, bytes1 s, uint256 pos) internal pure returns (uint256) {
unchecked {
uint256 length = buffer.length;
// NOTE here we cannot do `i = Math.min(pos + 1, length)` because `pos + 1` could overflow
for (uint256 i = Math.min(pos, length - 1) + 1; i > 0; --i) {
for (uint256 i = Math.min(Math.saturatingAdd(pos, 1), length); i > 0; --i) {
if (bytes1(_unsafeReadBytesOffset(buffer, i - 1)) == s) {
return i - 1;
}
Expand Down
70 changes: 70 additions & 0 deletions test/utils/Bytes.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,76 @@ import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol";
contract BytesTest is Test {
using Bytes for bytes;

// INDEX OF
function testIndexOf(bytes memory buffer, bytes1 s) public pure {
uint256 result = Bytes.indexOf(buffer, s);

if (buffer.length == 0) {
// Case 0: buffer is empty
assertEq(result, type(uint256).max);
} else if (result == type(uint256).max) {
// Case 1: search value could not be found
for (uint256 i = 0; i < buffer.length; ++i) assertNotEq(buffer[i], s);
} else {
// Case 2: search value was found
assertEq(buffer[result], s);
// search value is not present anywhere before the found location
for (uint256 i = 0; i < result; ++i) assertNotEq(buffer[i], s);
}
}

function testIndexOf(bytes memory buffer, bytes1 s, uint256 pos) public pure {
uint256 result = Bytes.indexOf(buffer, s, pos);

if (buffer.length == 0) {
// Case 0: buffer is empty
assertEq(result, type(uint256).max);
} else if (result == type(uint256).max) {
// Case 1: search value could not be found
for (uint256 i = pos; i < buffer.length; ++i) assertNotEq(buffer[i], s);
} else {
// Case 2: search value was found
assertEq(buffer[result], s);
// search value is not present anywhere before the found location
for (uint256 i = pos; i < result; ++i) assertNotEq(buffer[i], s);
}
}

function testLastIndexOf(bytes memory buffer, bytes1 s) public pure {
uint256 result = Bytes.lastIndexOf(buffer, s);

if (buffer.length == 0) {
// Case 0: buffer is empty
assertEq(result, type(uint256).max);
} else if (result == type(uint256).max) {
// Case 1: search value could not be found
for (uint256 i = 0; i < buffer.length; ++i) assertNotEq(buffer[i], s);
} else {
// Case 2: search value was found
assertEq(buffer[result], s);
// search value is not present anywhere after the found location
for (uint256 i = result + 1; i < buffer.length; ++i) assertNotEq(buffer[i], s);
}
}

function testLastIndexOf(bytes memory buffer, bytes1 s, uint256 pos) public pure {
uint256 result = Bytes.lastIndexOf(buffer, s, pos);

if (buffer.length == 0) {
// Case 0: buffer is empty
assertEq(result, type(uint256).max);
} else if (result == type(uint256).max) {
// Case 1: search value could not be found
for (uint256 i = 0; i <= Math.min(pos, buffer.length - 1); ++i) assertNotEq(buffer[i], s);
} else {
// Case 2: search value was found
assertEq(buffer[result], s);
// search value is not present anywhere after the found location
for (uint256 i = result + 1; i <= Math.min(pos, buffer.length - 1); ++i) assertNotEq(buffer[i], s);
}
}

// SLICES
function testSliceWithStartOnly(bytes memory buffer, uint256 start) public pure {
bytes memory originalBuffer = bytes.concat(buffer);
bytes memory result = buffer.slice(start);
Expand Down
40 changes: 28 additions & 12 deletions test/utils/Bytes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,39 +28,55 @@ describe('Bytes', function () {

describe('indexOf', function () {
it('first', async function () {
expect(await this.mock.$indexOf(lorem, ethers.toBeHex(present))).to.equal(lorem.indexOf(present));
await expect(this.mock.$indexOf(lorem, ethers.toBeHex(present))).to.eventually.equal(lorem.indexOf(present));
});

it('from index', async function () {
for (const start in Array(lorem.length + 10).fill()) {
const index = lorem.indexOf(present, start);
const result = index === -1 ? ethers.MaxUint256 : index;
expect(await this.mock.$indexOf(lorem, ethers.toBeHex(present), ethers.Typed.uint256(start))).to.equal(result);
await expect(
this.mock.$indexOf(lorem, ethers.toBeHex(present), ethers.Typed.uint256(start)),
).to.eventually.equal(result);
}
});

it('absent', async function () {
expect(await this.mock.$indexOf(lorem, ethers.toBeHex(absent))).to.equal(ethers.MaxUint256);
await expect(this.mock.$indexOf(lorem, ethers.toBeHex(absent))).to.eventually.equal(ethers.MaxUint256);
});

it('empty buffer', async function () {
await expect(this.mock.$indexOf('0x', '0x00')).to.eventually.equal(ethers.MaxUint256);
await expect(this.mock.$indexOf('0x', '0x00', ethers.Typed.uint256(17))).to.eventually.equal(ethers.MaxUint256);
});
});

describe('lastIndexOf', function () {
it('first', async function () {
expect(await this.mock.$lastIndexOf(lorem, ethers.toBeHex(present))).to.equal(lorem.lastIndexOf(present));
await expect(this.mock.$lastIndexOf(lorem, ethers.toBeHex(present))).to.eventually.equal(
lorem.lastIndexOf(present),
);
});

it('from index', async function () {
for (const start in Array(lorem.length + 10).fill()) {
const index = lorem.lastIndexOf(present, start);
const result = index === -1 ? ethers.MaxUint256 : index;
expect(await this.mock.$lastIndexOf(lorem, ethers.toBeHex(present), ethers.Typed.uint256(start))).to.equal(
result,
);
await expect(
this.mock.$lastIndexOf(lorem, ethers.toBeHex(present), ethers.Typed.uint256(start)),
).to.eventually.equal(result);
}
});

it('absent', async function () {
expect(await this.mock.$lastIndexOf(lorem, ethers.toBeHex(absent))).to.equal(ethers.MaxUint256);
await expect(this.mock.$lastIndexOf(lorem, ethers.toBeHex(absent))).to.eventually.equal(ethers.MaxUint256);
});

it('empty buffer', async function () {
await expect(this.mock.$lastIndexOf('0x', '0x00')).to.eventually.equal(ethers.MaxUint256);
await expect(this.mock.$lastIndexOf('0x', '0x00', ethers.Typed.uint256(17))).to.eventually.equal(
ethers.MaxUint256,
);
});
});

Expand All @@ -73,8 +89,8 @@ describe('Bytes', function () {
})) {
it(descr, async function () {
const result = ethers.hexlify(lorem.slice(start));
expect(await this.mock.$slice(lorem, start)).to.equal(result);
expect(await this.mock.$splice(lorem, start)).to.equal(result);
await expect(this.mock.$slice(lorem, start)).to.eventually.equal(result);
await expect(this.mock.$splice(lorem, start)).to.eventually.equal(result);
});
}
});
Expand All @@ -89,8 +105,8 @@ describe('Bytes', function () {
})) {
it(descr, async function () {
const result = ethers.hexlify(lorem.slice(start, end));
expect(await this.mock.$slice(lorem, start, ethers.Typed.uint256(end))).to.equal(result);
expect(await this.mock.$splice(lorem, start, ethers.Typed.uint256(end))).to.equal(result);
await expect(this.mock.$slice(lorem, start, ethers.Typed.uint256(end))).to.eventually.equal(result);
await expect(this.mock.$splice(lorem, start, ethers.Typed.uint256(end))).to.eventually.equal(result);
});
}
});
Expand Down