Skip to content

Fix HostBuffer.emplace overflow at block boundary - #186539

Closed
soulman-is-good wants to merge 1 commit into
flutter:masterfrom
soulman-is-good:fix/host-buffer-overflow-emplace-check
Closed

soulman-is-good wants to merge 1 commit into
flutter:masterfrom
soulman-is-good:fix/host-buffer-overflow-emplace-check

Conversation

@soulman-is-good

Copy link
Copy Markdown

HostBuffer._allocateEmplacement decided whether to allocate a new block based on the post-padding cursor only, ignoring the size of the data being emplaced. When the cursor landed inside the range [blockLengthInBytes - bytes.lengthInBytes, blockLengthInBytes), the check passed but the subsequent DeviceBuffer.overwrite rejected the write because it would extend past the block end, throwing:

Failed to write range (offset=..., length=...) to HostBuffer-managed
DeviceBuffer (frame=0, buffer=0, offset=...).

This was reproducible in flame_3d 3D apps once per-frame uniform traffic neared 1 MB at certain camera angles.

Include the upcoming write size in the boundary check:

if (_offsetCursor + padding + bytes.lengthInBytes > blockLengthInBytes) {

The strict > (rather than >=) is intentional: a write that exactly fills the remaining space is valid and shouldn't waste a new block.

Adds a regression test that exercises the boundary by configuring a small blockLengthInBytes and emplacing a payload that would overflow the current block under the previous check.

Fixes #186526

Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.

List which issues are fixed by this PR. You must list at least one issue. An issue is not required if the PR fixes something trivial like a typo.

If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

@github-actions github-actions Bot added engine flutter/engine related. See also e: labels. flutter-gpu team-fluttergpu Owned by Flutter GPU team labels May 14, 2026
@github-project-automation github-project-automation Bot moved this to 🤔 Needs Triage in Flutter GPU May 14, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request modifies the HostBuffer allocation logic to include payload size in bounds checks, preventing out-of-bounds writes when the cursor is near the end of a block. It also introduces a regression test to verify this behavior. Review feedback identified a copy-paste error in the test assertions where the view length was incorrectly compared to the total block size instead of the payload size.

// original one, and start at offset 0 of that block.
expect(view.buffer, isNot(equals(fillView.buffer)));
expect(view.offsetInBytes, 0);
expect(view.lengthInBytes, blockLength);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The expectation for view.lengthInBytes appears to be a copy-paste error. It should be checked against overflowingSize (the size of the data emplaced), which is consistent with how fillView.lengthInBytes was checked against fillSize on line 281. Checking against blockLength would imply the view covers the entire block, which is not the behavior of emplace.

Suggested change
expect(view.lengthInBytes, blockLength);
expect(view.lengthInBytes, overflowingSize);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I removed unrelated assertion so the test only checks related things

`HostBuffer._allocateEmplacement` decided whether to allocate a new
block based on the post-padding cursor only, ignoring the size of the
data being emplaced. When the cursor landed inside the range
`[blockLengthInBytes - bytes.lengthInBytes, blockLengthInBytes)`, the
check passed but the subsequent `DeviceBuffer.overwrite` rejected the
write because it would extend past the block end, throwing:

  Failed to write range (offset=..., length=...) to HostBuffer-managed
  DeviceBuffer (frame=0, buffer=0, offset=...).

This was reproducible in flame_3d 3D apps once per-frame uniform traffic
neared 1 MB at certain camera angles.

Include the upcoming write size in the boundary check:

    if (_offsetCursor + padding + bytes.lengthInBytes > blockLengthInBytes) {

The strict `>` (rather than `>=`) is intentional: a write that exactly
fills the remaining space is valid and shouldn't waste a new block.

Adds a regression test that exercises the boundary by configuring a
small `blockLengthInBytes` and emplacing a payload that would overflow
the current block under the previous check.

Fixes flutter#186526
@soulman-is-good
soulman-is-good force-pushed the fix/host-buffer-overflow-emplace-check branch from 9b4b631 to 65b7a68 Compare May 14, 2026 18:40

@bdero bdero left a comment

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.

Thanks for the clear writeup and the platform-independent test sizes. The boundary fix is correct, and the strict > is the right call, since a write that exactly fills the block is valid.

While we're in this branch, there's a second pre-existing bug that your fix now exercises much more often. When _allocateEmplacement rolls over to a fresh block it returns the view with lengthInBytes set to blockLengthInBytes instead of bytes.lengthInBytes, so callers get a view spanning the whole block rather than just the payload. The old bounds check almost never took this branch cleanly (it crashed instead), so the wrong length stayed masked. Now that this is the normal rollover path, every rollover hands back an oversized view. The other two return paths already use bytes.lengthInBytes. Would you mind fixing that line and extending the new test to cover it? Specifics inline.

// cursor lands inside `[blockLengthInBytes - bytes.lengthInBytes,
// blockLengthInBytes)` the check would pass, but the subsequent
// `DeviceBuffer.overwrite` would reject the out-of-bounds write.
if (_offsetCursor + padding + bytes.lengthInBytes > blockLengthInBytes) {

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.

The return BufferView at the end of this branch should use bytes.lengthInBytes, not blockLengthInBytes. This branch writes bytes at offset 0 of the freshly allocated block, so the returned view should describe the payload, matching the oversized branch above and the normal path below. As written, every rollover returns a view spanning the entire block, which then gets bound as an oversized uniform or vertex range.

// The new view must reference a freshly-allocated block, not the
// original one, and start at offset 0 of that block.
expect(view.buffer, isNot(equals(fillView.buffer)));
expect(view.offsetInBytes, 0);

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.

Could you also assert the length here with expect(view.lengthInBytes, overflowingSize);? It pins the view to the payload size, and it currently fails because of the lengthInBytes bug I flagged in buffer.dart, so this one assertion locks in both fixes.

@Piinks

Piinks commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Greetings from stale PR triage! 👋
Is this change still on your radar?

@github-actions

Copy link
Copy Markdown

This pull request is being closed because it has not been updated in the last 21 days after a request for more information.

If you are still working on this, please feel free to reopen it or file a new pull request.

Thanks for your contribution.

@github-actions github-actions Bot closed this Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

engine flutter/engine related. See also e: labels. flutter-gpu team-fluttergpu Owned by Flutter GPU team waiting for response The Flutter team cannot make further progress on this issue until the original reporter responds

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

HostBuffer.emplace overflows DeviceBuffer near block end (off-by-one in size check)

3 participants