editor: Fix crash from stale state after prepaint depth exhaustion#49664
Conversation
|
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 check' |
|
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'. |
|
The cla-bot has been summoned, and re-checked this pull request! |
|
@cla-bot check |
|
The cla-bot has been summoned, and re-checked this pull request! |
|
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 |
|
@Veykril I'm digging deeper the root cause trying to replicate the behavior that led to the panic crash |
2e97fdb to
021978e
Compare
|
@Veykril You're right — the original The crash path: At The fix (updated): I removed all the What triggers it: Block decorations that oscillate in size — particularly |
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>
021978e to
a160ce9
Compare
…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>
…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>
…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>
Closes #49662
Summary
Fixes an index-out-of-bounds crash in
EditorElement::prepaintthat 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, bothresize_blocks()andupdate_renderer_widths()mutated the display map before checkingcan_prepaint(). At max depth:resize_blocks()mutated the block map, changing the display row mappingcan_prepaint()returned false, skipping the recursive rebuildsnapshot,start_row,end_row,line_layouts,row_infos) remained stalelayout_inline_diagnosticswhich takes a fresheditor.snapshot()— would see the new row mapping but index into the staleline_layoutsrow.minus(start_row)produced an index exceedingline_layouts.len()→ panicThe 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_nearblocks 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:resize_blocks()inside thecan_prepaint()guard. At max depth, defer the resize to the next frame viacx.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
update_renderer_widths()withcan_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()resetsprepaint_depthto 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 editorpassesRelease Notes:
🤖 Generated with Claude Code