fix(reader): prevent use-after-free when rendering nested list items#316
Merged
Conversation
Issue: Opening reader mode (Cmd+R) on a session whose scrollback contained a nested list crashed the app with SIGSEGV. The bug only surfaced after enough activity for the freed allocator slot to land on an unmapped page. Solution: The indent buffer for list items was heap-allocated inside an inner `if` block whose `defer` freed it before the wrapping loop consumed it. Replaced the allocation with a module-level static padding slice, since the parser caps indent depth at 12 levels. Also moved the ordered-list marker buffer up to the case scope so its address does not outlive its declaring branch.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Pull request overview
Fixes a crash in the markdown reader overlay by removing a use-after-free in list item indentation/marker handling within the markdown renderer.
Changes:
- Replace per-list-item heap allocation for indentation padding with a safe static padding slice, avoiding dangling pointers during wrapping.
- Move the ordered-list marker buffer to a broader scope to avoid passing references to stack data with an insufficient lifetime.
- Add a regression test using a PoisonAllocator to catch use-after-free behavior when rendering nested list items.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Issue: PR #316 review pointed out that the renderer's indent cap sat next to its static padding while the parser kept its own `12` literal inside `parseListItem`. If one side changed, the renderer would silently clamp deeper lists. Solution: Export `max_indent_level` from `markdown_parser.zig` and reference it from both `parseListItem` and the renderer's `indent_padding`, so the cap lives in one place.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Solution
Pressing Cmd+R on a session whose scrollback contained a nested markdown list crashed Architect 0.65.3 with
EXC_BAD_ACCESS. The crash report pointed atui.components.markdown_renderer.buildLines + 3516, called fromReaderOverlayComponent.rebuildLinesand ultimately fromrefreshFromSession.The culprit lived in the
.list_itembranch ofbuildLines:The
deferran when the innerifexited, beforeappendWrappedLinesiteratedrun.textbyte by byte. The crash registers line up exactly with that loop:LDP x24, x25, [sp, #56]thenLDRB w23, [x26]reading a freed 2-byte slice (indent_level=1 → indent_spaces=2). It usually went unnoticed because the freed slot kept the original space bytes for a while, but on a long-running process the page eventually got unmapped.While in the same block, the ordered-list marker buffer (
var marker_buf: [32]u8 = undefined) was declared inside anelse ifbranch and its address handed off throughrun_inputs. That is undefined behaviour even when it happens to work today, so it moves up to the case scope as part of the fix.The replacement uses a module-level static
indent_paddingarray of 24 spaces (max_list_indent_level * 2). The parser already capsindent_levelat 12, andRunInput.textis borrowed (the eventualRenderRunis duplicated downstream inappendWrappedLines), so a const slice into static data is both safe and avoids the allocation entirely.Verification
A regression test wraps
std.testing.allocatorin aPoisonAllocatorthat fills freed memory with0xAAbefore delegating to the backing allocator. The new test (renderer handles nested list items without use-after-free) feeds" - nested item\n"throughparser.parseandbuildLines. Against the previous code it segfaults at exactly the line from the production crash; against the fix all 24 markdown renderer and parser tests pass.zig build,zig build test,just lint, andzig fmt --checkare all green.Test plan
cat <<EOF\n- top level\n - nested item\n - deeper item\nEOF).