Skip to content

editor: Fix crash from stale state after prepaint depth exhaustion#49664

Merged
smitbarmase merged 2 commits intozed-industries:mainfrom
jean-humann:fix/editor-prepaint-bounds-checks
Mar 19, 2026
Merged

editor: Fix crash from stale state after prepaint depth exhaustion#49664
smitbarmase merged 2 commits intozed-industries:mainfrom
jean-humann:fix/editor-prepaint-bounds-checks

Conversation

@jean-humann
Copy link
Copy Markdown
Contributor

@jean-humann jean-humann commented Feb 19, 2026

Closes #49662

Summary

Fixes an index-out-of-bounds crash in EditorElement::prepaint that occurs when block decorations oscillate in size, exhausting the recursive prepaint budget (MAX_PREPAINT_DEPTH = 5).

Root cause

When block decorations resize during render_blocks, prepaint recurses to rebuild layout state. Previously, both resize_blocks() and update_renderer_widths() mutated the display map before checking can_prepaint(). At max depth:

  1. resize_blocks() mutated the block map, changing the display row mapping
  2. can_prepaint() returned false, skipping the recursive rebuild
  3. All local state (snapshot, start_row, end_row, line_layouts, row_infos) remained stale
  4. Downstream code — particularly layout_inline_diagnostics which takes a fresh editor.snapshot() — would see the new row mapping but index into the stale line_layouts
  5. row.minus(start_row) produced an index exceeding line_layouts.len() → panic

The crash is most likely to occur when many block decorations appear or resize simultaneously (e.g., unfolding a folder containing many git repos, causing a burst of diff hunk blocks), or when place_near blocks oscillate between inline (height=0) and block (height>=1) mode, burning through all 5 recursion levels without converging.

Fix

Guard both mutation sites with can_prepaint() so the display map is NOT mutated when we cannot recurse to rebuild state:

  • Block resize: move resize_blocks() inside the can_prepaint() guard. At max depth, defer the resize to the next frame via cx.notify(). The resize will be re-detected because the stored height still mismatches the rendered height.

https://github.com/jean-humann/zed/blob/021978ecf939723a6ba4ab9843572b6bcefe7cb7/crates/editor/src/element.rs#L10207-L10239

  • Renderer widths: short-circuit update_renderer_widths() with can_prepaint() so fold widths are not updated at max depth.

https://github.com/jean-humann/zed/blob/021978ecf939723a6ba4ab9843572b6bcefe7cb7/crates/editor/src/element.rs#L10097-L10115

In both cases the next frame starts with a fresh recursion budget (EditorRequestLayoutState::default() resets prepaint_depth to 0) and applies the deferred changes normally. The worst case is one frame of slightly wrong-sized blocks — vastly preferable to a crash.

Test plan

  • cargo check -p editor passes
  • Manual testing with many open files, inline diagnostics, hover popovers, and git diff hunks

Release Notes:

  • Fixed a crash (index out of bounds) during editor rendering when block decorations repeatedly resize, exhausting the recursive prepaint budget.

🤖 Generated with Claude Code

@cla-bot
Copy link
Copy Markdown

cla-bot bot commented Feb 19, 2026

We require contributors to sign our Contributor License Agreement, and we don't have @jean-humann on file. You can sign our CLA at https://zed.dev/cla. Once you've signed, post a comment here that says '@cla-bot check'.

@zed-community-bot zed-community-bot bot added the first contribution the author's first pull request to Zed. NOTE: the label application is automated via github actions label Feb 19, 2026
@jean-humann
Copy link
Copy Markdown
Contributor Author

@cla-bot check'

@cla-bot
Copy link
Copy Markdown

cla-bot bot commented Feb 19, 2026

We require contributors to sign our Contributor License Agreement, and we don't have @jean-humann on file. You can sign our CLA at https://zed.dev/cla. Once you've signed, post a comment here that says '@cla-bot check'.

@cla-bot
Copy link
Copy Markdown

cla-bot bot commented Feb 19, 2026

The cla-bot has been summoned, and re-checked this pull request!

@jean-humann
Copy link
Copy Markdown
Contributor Author

@cla-bot check

@cla-bot cla-bot bot added the cla-signed The user has signed the Contributor License Agreement label Feb 19, 2026
@cla-bot
Copy link
Copy Markdown

cla-bot bot commented Feb 19, 2026

The cla-bot has been summoned, and re-checked this pull request!

@Veykril
Copy link
Copy Markdown
Member

Veykril commented Feb 20, 2026

This doesn't feel right, any panic here is a bug. Replacing these with fallible fetches will likely lead to hard to debug visual bugs instead

@jean-humann
Copy link
Copy Markdown
Contributor Author

@Veykril I'm digging deeper the root cause trying to replicate the behavior that led to the panic crash

@jean-humann jean-humann force-pushed the fix/editor-prepaint-bounds-checks branch 3 times, most recently from 2e97fdb to 021978e Compare February 20, 2026 09:12
@jean-humann jean-humann changed the title editor: Replace remaining unchecked indexing with bounds-checked .get() in element.rs editor: Fix crash from stale state after prepaint depth exhaustion Feb 20, 2026
@jean-humann
Copy link
Copy Markdown
Contributor Author

@Veykril You're right — the original .get() approach was papering over the bug. I dug deeper and found the actual root cause.

The crash path:

At MAX_PREPAINT_DEPTH (5), resize_blocks() was called before can_prepaint(), mutating the block map (changing the display row mapping) while all local state (line_layouts, row_infos, start_row/end_row) remained stale. The code then fell through past the debug_panic! and continued rendering with this inconsistent state. In particular, layout_inline_diagnostics takes a fresh editor.snapshot() that sees the new row mapping, but indexes into the stale line_layouts — producing an out-of-bounds index.

The fix (updated):

I removed all the .get() band-aids and instead moved both resize_blocks() and update_renderer_widths() inside their respective can_prepaint() guards, so the display map is never mutated when we can't recurse to rebuild state. At max depth for block resize, cx.notify() defers the resize to the next frame (which starts with a fresh depth budget and re-detects the mismatch). The debug_panic! is preserved for the block resize case.

What triggers it:

Block decorations that oscillate in size — particularly place_near blocks that flip between inline (height=0) and block mode depending on available width — can burn through all 5 recursion levels without converging. The reporter's scenario (unfolding folders with many git repos, causing a burst of diff hunk blocks) is a plausible trigger.

jean-humann and others added 2 commits March 19, 2026 12:40
When block decorations resize during rendering or fold element widths
change, `EditorElement::prepaint` recurses up to MAX_PREPAINT_DEPTH (5).
Previously, both `resize_blocks()` and `update_renderer_widths()` mutated
the display map BEFORE checking whether recursion was possible. At max
depth, the mutations proceeded but the rebuild was skipped, leaving all
local state (snapshot, line_layouts, row_infos) stale. Downstream code —
particularly `layout_inline_diagnostics` which takes a fresh snapshot via
`editor.snapshot()` — would see the new row mapping but index into stale
line_layouts, causing index-out-of-bounds panics.

Fix: guard both mutation sites with `can_prepaint()` so the display map
is NOT mutated when we cannot recurse to rebuild state:

- Block resize: move `resize_blocks()` inside the `can_prepaint()` guard.
  At max depth, defer the resize to the next frame via `cx.notify()`.
- Renderer widths: short-circuit `update_renderer_widths()` with
  `can_prepaint()` so fold widths are not updated at max depth.

In both cases the next frame starts with a fresh recursion budget and
applies the deferred changes normally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@smitbarmase smitbarmase force-pushed the fix/editor-prepaint-bounds-checks branch from 021978e to a160ce9 Compare March 19, 2026 07:16
Copy link
Copy Markdown
Member

@smitbarmase smitbarmase left a comment

Choose a reason for hiding this comment

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

I updated it. Thanks!

@smitbarmase smitbarmase merged commit 19c8363 into zed-industries:main Mar 19, 2026
30 checks passed
AmaanBilwar pushed a commit to AmaanBilwar/zed that referenced this pull request Mar 20, 2026
…ed-industries#49664)

Closes zed-industries#49662

## Summary

Fixes an index-out-of-bounds crash in `EditorElement::prepaint` that
occurs when block decorations oscillate in size, exhausting the
recursive prepaint budget (`MAX_PREPAINT_DEPTH = 5`).

## Root cause

When block decorations resize during `render_blocks`, prepaint recurses
to rebuild layout state. Previously, both `resize_blocks()` and
`update_renderer_widths()` mutated the display map **before** checking
`can_prepaint()`. At max depth:

1. `resize_blocks()` mutated the block map, changing the display row
mapping
2. `can_prepaint()` returned false, skipping the recursive rebuild
3. All local state (`snapshot`, `start_row`, `end_row`, `line_layouts`,
`row_infos`) remained stale
4. Downstream code — particularly `layout_inline_diagnostics` which
takes a **fresh** `editor.snapshot()` — would see the new row mapping
but index into the stale `line_layouts`
5. `row.minus(start_row)` produced an index exceeding
`line_layouts.len()` → panic

The crash is most likely to occur when many block decorations appear or
resize simultaneously (e.g., unfolding a folder containing many git
repos, causing a burst of diff hunk blocks), or when `place_near` blocks
oscillate between inline (height=0) and block (height>=1) mode, burning
through all 5 recursion levels without converging.

## Fix

Guard both mutation sites with `can_prepaint()` so the display map is
NOT mutated when we cannot recurse to rebuild state:

- **Block resize**: move `resize_blocks()` inside the `can_prepaint()`
guard. At max depth, defer the resize to the next frame via
`cx.notify()`. The resize will be re-detected because the stored height
still mismatches the rendered height.


https://github.com/jean-humann/zed/blob/021978ecf939723a6ba4ab9843572b6bcefe7cb7/crates/editor/src/element.rs#L10207-L10239

- **Renderer widths**: short-circuit `update_renderer_widths()` with
`can_prepaint()` so fold widths are not updated at max depth.


https://github.com/jean-humann/zed/blob/021978ecf939723a6ba4ab9843572b6bcefe7cb7/crates/editor/src/element.rs#L10097-L10115

In both cases the next frame starts with a fresh recursion budget
(`EditorRequestLayoutState::default()` resets `prepaint_depth` to 0) and
applies the deferred changes normally. The worst case is one frame of
slightly wrong-sized blocks — vastly preferable to a crash.

## Test plan

- [x] `cargo check -p editor` passes
- [ ] Manual testing with many open files, inline diagnostics, hover
popovers, and git diff hunks

Release Notes:

- Fixed a crash (index out of bounds) during editor rendering when block
decorations repeatedly resize, exhausting the recursive prepaint budget.

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
toshmukhamedov pushed a commit to toshmukhamedov/zed that referenced this pull request Mar 20, 2026
…ed-industries#49664)

Closes zed-industries#49662

## Summary

Fixes an index-out-of-bounds crash in `EditorElement::prepaint` that
occurs when block decorations oscillate in size, exhausting the
recursive prepaint budget (`MAX_PREPAINT_DEPTH = 5`).

## Root cause

When block decorations resize during `render_blocks`, prepaint recurses
to rebuild layout state. Previously, both `resize_blocks()` and
`update_renderer_widths()` mutated the display map **before** checking
`can_prepaint()`. At max depth:

1. `resize_blocks()` mutated the block map, changing the display row
mapping
2. `can_prepaint()` returned false, skipping the recursive rebuild
3. All local state (`snapshot`, `start_row`, `end_row`, `line_layouts`,
`row_infos`) remained stale
4. Downstream code — particularly `layout_inline_diagnostics` which
takes a **fresh** `editor.snapshot()` — would see the new row mapping
but index into the stale `line_layouts`
5. `row.minus(start_row)` produced an index exceeding
`line_layouts.len()` → panic

The crash is most likely to occur when many block decorations appear or
resize simultaneously (e.g., unfolding a folder containing many git
repos, causing a burst of diff hunk blocks), or when `place_near` blocks
oscillate between inline (height=0) and block (height>=1) mode, burning
through all 5 recursion levels without converging.

## Fix

Guard both mutation sites with `can_prepaint()` so the display map is
NOT mutated when we cannot recurse to rebuild state:

- **Block resize**: move `resize_blocks()` inside the `can_prepaint()`
guard. At max depth, defer the resize to the next frame via
`cx.notify()`. The resize will be re-detected because the stored height
still mismatches the rendered height.


https://github.com/jean-humann/zed/blob/021978ecf939723a6ba4ab9843572b6bcefe7cb7/crates/editor/src/element.rs#L10207-L10239

- **Renderer widths**: short-circuit `update_renderer_widths()` with
`can_prepaint()` so fold widths are not updated at max depth.


https://github.com/jean-humann/zed/blob/021978ecf939723a6ba4ab9843572b6bcefe7cb7/crates/editor/src/element.rs#L10097-L10115

In both cases the next frame starts with a fresh recursion budget
(`EditorRequestLayoutState::default()` resets `prepaint_depth` to 0) and
applies the deferred changes normally. The worst case is one frame of
slightly wrong-sized blocks — vastly preferable to a crash.

## Test plan

- [x] `cargo check -p editor` passes
- [ ] Manual testing with many open files, inline diagnostics, hover
popovers, and git diff hunks

Release Notes:

- Fixed a crash (index out of bounds) during editor rendering when block
decorations repeatedly resize, exhausting the recursive prepaint budget.

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
AmaanBilwar pushed a commit to AmaanBilwar/zed that referenced this pull request Mar 23, 2026
…ed-industries#49664)

Closes zed-industries#49662

## Summary

Fixes an index-out-of-bounds crash in `EditorElement::prepaint` that
occurs when block decorations oscillate in size, exhausting the
recursive prepaint budget (`MAX_PREPAINT_DEPTH = 5`).

## Root cause

When block decorations resize during `render_blocks`, prepaint recurses
to rebuild layout state. Previously, both `resize_blocks()` and
`update_renderer_widths()` mutated the display map **before** checking
`can_prepaint()`. At max depth:

1. `resize_blocks()` mutated the block map, changing the display row
mapping
2. `can_prepaint()` returned false, skipping the recursive rebuild
3. All local state (`snapshot`, `start_row`, `end_row`, `line_layouts`,
`row_infos`) remained stale
4. Downstream code — particularly `layout_inline_diagnostics` which
takes a **fresh** `editor.snapshot()` — would see the new row mapping
but index into the stale `line_layouts`
5. `row.minus(start_row)` produced an index exceeding
`line_layouts.len()` → panic

The crash is most likely to occur when many block decorations appear or
resize simultaneously (e.g., unfolding a folder containing many git
repos, causing a burst of diff hunk blocks), or when `place_near` blocks
oscillate between inline (height=0) and block (height>=1) mode, burning
through all 5 recursion levels without converging.

## Fix

Guard both mutation sites with `can_prepaint()` so the display map is
NOT mutated when we cannot recurse to rebuild state:

- **Block resize**: move `resize_blocks()` inside the `can_prepaint()`
guard. At max depth, defer the resize to the next frame via
`cx.notify()`. The resize will be re-detected because the stored height
still mismatches the rendered height.


https://github.com/jean-humann/zed/blob/021978ecf939723a6ba4ab9843572b6bcefe7cb7/crates/editor/src/element.rs#L10207-L10239

- **Renderer widths**: short-circuit `update_renderer_widths()` with
`can_prepaint()` so fold widths are not updated at max depth.


https://github.com/jean-humann/zed/blob/021978ecf939723a6ba4ab9843572b6bcefe7cb7/crates/editor/src/element.rs#L10097-L10115

In both cases the next frame starts with a fresh recursion budget
(`EditorRequestLayoutState::default()` resets `prepaint_depth` to 0) and
applies the deferred changes normally. The worst case is one frame of
slightly wrong-sized blocks — vastly preferable to a crash.

## Test plan

- [x] `cargo check -p editor` passes
- [ ] Manual testing with many open files, inline diagnostics, hover
popovers, and git diff hunks

Release Notes:

- Fixed a crash (index out of bounds) during editor rendering when block
decorations repeatedly resize, exhausting the recursive prepaint budget.

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement first contribution the author's first pull request to Zed. NOTE: the label application is automated via github actions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crash: index out of bounds in EditorElement::prepaint (multiple unfixed sites)

3 participants