Skip to content

Address issue #479: fix crash on empty Markdown headings#482

Merged
schuyler merged 2 commits into
mainfrom
claude/issue-479-87c1hi
Jun 30, 2026
Merged

Address issue #479: fix crash on empty Markdown headings#482
schuyler merged 2 commits into
mainfrom
claude/issue-479-87c1hi

Conversation

@schuyler

@schuyler schuyler commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

Typing a lone setext underline (- or =) on an otherwise-empty line crashed the app. The crash is a C assert() (SIGABRT) inside Hoedown's hoedown_buffer_growassert(buf && buf->unit) — reached from MacDown's patched heading renderer, running on MPRenderer's background render NSOperationQueue (matching the faulting thread in the report).

A bare -/= makes Hoedown emit a heading with empty content. The patched heading renderers built the slug buffer with hoedown_buffer_new(content->size), so an empty heading produced a buffer with a zero growth unit. The fallback HOEDOWN_BUFPUTSL(slug, "section") then grew that buffer and tripped the assert. Minimal reproduction: a document containing only - (or only =); the reported _ - _ - _ -\n-\n\n- ends in exactly such a trailing lone -.

I reproduced this directly by building Hoedown 3.0.7 with the MacDown patch and rendering the reported input — it aborts in hoedown_buffer_put from hoedown_patch_render_header, with the second header callback receiving content->size == 0.

Root cause / origin

Introduced in 0.7-rc.1 by the slug-based heading IDs change (#430). Before #430 there was no custom header callback, so Hoedown's built-in renderer handled empty headings harmlessly. #430 both added the custom renderer and sized its working buffer from the heading content length, which is 0 for an empty heading.

The same slug/heading logic was copied verbatim into the Quick Look renderer in #430, so it carried the identical crash — reachable by previewing a .md file with a bare -/= heading in Finder/Spotlight. That copy is fixed here too.

Changes

  • MacDown/Code/Extension/hoedown_html_patch.c — add a new_growable_buffer() helper that clamps a zero size hint to a non-zero unit; use it in hoedown_patch_render_header, hoedown_patch_render_toc_header, and the analogous block-code info-string buffers.
  • MacDownCore/MPQuickLookRenderer.m — same fix via a mirrored mp_quicklook_new_growable_buffer() helper for mp_quicklook_render_header.
  • MacDownTests/MPMarkdownRenderingTests.m — regression tests rendering a lone -, a lone =, the exact reported input, and an empty heading with TOC enabled, all through the real render path.
  • MacDownTests/MPQuickLookRendererTests.m — regression test rendering a bare - heading through the Quick Look renderer.

After the fix, an empty heading renders as <h2 id="section"></h2> (no crash) and normal headings keep their slugs; verified across the preview and TOC paths and the Quick Look renderer.

Related Issue

Related to #479. The regression originates in #430 (slug-based heading IDs). The reporter arrived here while exercising the selection-count thread in #452, which is a separate, unrelated issue.

Manual Testing Plan

  1. New empty document; type - on the first line → must not crash; preview shows an (empty) heading.
  2. Type = alone on a line → must not crash.
  3. Reproduce the report: type
    _ - _ - _ -
    -
    
    -
    
    typing the final - last → must not crash.
  4. Normal headings (# Hello, ## Foo Bar) still render with correct slug ids (id="hello", id="foo-bar").
  5. Enable TOC ([TOC]) in a document that also contains an empty heading → must not crash; TOC links resolve.
  6. In Finder, select a .md file whose content is just - and press Space (Quick Look) → preview renders without the Quick Look process aborting.

Review Notes

  • Tests run on macOS CI only; the regression tests exercise the real parseMarkdown:/Quick Look render paths, so before the fix they abort the test process and after it they pass.
  • Noted but not changed: Emit slug-based heading IDs for anchor link navigation #430 also switched TOC anchors from #toc_N to #<slug>, so duplicate heading texts collapse to the same id (already pinned by an existing test) — flagged for a possible future slug-dedup, not a crash.

Typing a lone setext underline ('-' or '=') on a new line — e.g. a
trailing '-' after a blank line — made Hoedown emit a heading with empty
content. The patched heading renderers created the slug buffer with
hoedown_buffer_new(content->size), so an empty heading produced a buffer
with a zero growth "unit". The fallback HOEDOWN_BUFPUTSL(slug, "section")
then grew that buffer and tripped Hoedown's assert(buf && buf->unit),
aborting the app on the background render queue (SIGABRT).

Clamp the size hint via a new_growable_buffer() helper so derived buffers
are always growable, and apply it to render_header, render_toc_header,
and the analogous block-code info-string buffers.

Adds regression tests rendering a lone '-', a lone '=', the reported
input, and an empty heading with TOC enabled through the real render
path.

Related to #479
@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Report

Current Coverage: 62.70%

Coverage Details (Summary)
Name                                                                                                                                   Coverage             
-------------------------------------------------------------------------------------------------------------------------------------- -------------------- 
MASPreferences.bundle                                                                                                                  0.00% (0/0)          
MacDown 3000.app                                                                                                                       63.75% (9259/14525)  
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Extension/DOMNode+Text.m                                                   100.00% (111/111)    
        MPAccumulatedTextCountMake                                                                                                     100.00% (7/7)        
        MPAccumulatedTextCountZero                                                                                                     100.00% (3/3)        
        MPAccumulatedTextCountAdd                                                                                                      100.00% (5/5)        
        MPGetStringAccumulatedTextCount                                                                                                100.00% (34/34)      
        __MPGetStringAccumulatedTextCount_block_invoke                                                                                 100.00% (3/3)        
        __MPGetStringAccumulatedTextCount_block_invoke.5                                                                               100.00% (4/4)        
        MPGetChildrenAccumulatedTextCount                                                                                              100.00% (6/6)        
        MPGetNodeAccumulatedTextCount                                                                                                  100.00% (32/32)      
        MPTextCountForString                                                                                                           100.00% (9/9)        
        -[DOMNode(Text) textCount]                                                                                                     100.00% (8/8)        
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Document/MPDocument.m                                                      63.60% (1639/2577)   
        MPEditorPreferenceKeyWithValueKey                                                                                              85.71% (6/7)         
        MPEditorKeysToObserve                                                                                                          100.00% (15/15)      
        __MPEditorKeysToObserve_block_invoke                                                                                           100.00% (10/10)      
        MPEditorPreferencesToObserve                                                                                                   100.00% (14/14)      
        __MPEditorPreferencesToObserve_block_invoke                                                                                    100.00% (9/9)        
        MPRectStringForAutosaveName                                                                                                    100.00% (6/6)        
        MPAreNilableStringsEqual                                                                                                       100.00% (3/3)        
        MPGetWebViewBackgroundColor                                                                                                    0.00% (0/9)          
        -[NSURL(Convert) absoluteBaseURLString]                                                                                        0.00% (0/6)          
        -[WebView(Shortcut) enclosingScrollView]                                                                                       100.00% (3/3)        
        -[MPPreferences(Hoedown) extensionFlags]                                                                                       78.57% (22/28)       
        -[MPPreferences(Hoedown) rendererFlags]                                                                                        75.00% (9/12)        
        MPGetPreviewLoadingCompletionHandler                                                                                           100.00% (30/30)      
        __MPGetPreviewLoadingCompletionHandler_block_invoke                                                                            100.00% (27/27)      
        MPScanFenceMarker                                                                                                              100.00% (28/28)      
        -[MPDocument preferences]                                                                                                      100.00% (3/3)        
        -[MPDocument markdown]                                                                                                         100.00% (3/3)        
        -[MPDocument setMarkdown:]                                                                                                     100.00% (3/3)        
        -[MPDocument html]                                                                                                             0.00% (0/3)          
        -[MPDocument toolbarVisible]                                                                                                   0.00% (0/3)          
        -[MPDocument previewVisible]                                                                                                   100.00% (3/3)        
        -[MPDocument editorVisible]                                                                                                    100.00% (3/3)        
        -[MPDocument needsHtml]                                                                                                        80.00% (4/5)         
        -[MPDocument wordCountTitleForKey:number:]                                                                                     100.00% (5/5)        
        -[MPDocument applyWordsTitle:selected:]                                                                                        100.00% (5/5)        
        -[MPDocument applyCharactersTitle:selected:]                                                                                   100.00% (6/6)        
        -[MPDocument applyCharactersNoSpacesTitle:selected:]                                                                           100.00% (6/6)        
        -[MPDocument setTotalWords:]                                                                                                   100.00% (5/5)        
        -[MPDocument setTotalCharacters:]                                                                                              100.00% (5/5)        
        -[MPDocument setTotalCharactersNoSpaces:]                                                                                      100.00% (5/5)        
        -[MPDocument setAutosaveName:]                                                                                                 100.00% (4/4)        
        -[MPDocument mathJaxRenderGeneration]                                                                                          100.00% (3/3)        
        -[MPDocument init]                                                                                                             90.91% (10/11)       
        -[MPDocument windowNibName]                                                                                                    100.00% (3/3)        

... (3411 more lines truncated)

📊 **Full coverage report available in workflow artifacts**

PR #430 copied the slug/heading-id logic into the Quick Look renderer
(mp_quicklook_render_header), including the same hoedown_buffer_new(
content->size) call. An empty heading (e.g. a bare setext underline)
therefore creates a zero-unit buffer that aborts the Quick Look render
process the first time it is written to — the same defect just fixed in
hoedown_html_patch.c, reachable by previewing a .md file in Finder.

Clamp the size hint with a mp_quicklook_new_growable_buffer() helper
mirroring new_growable_buffer(), and add a regression test rendering a
bare '-' heading through the Quick Look renderer.

Related to #479
@schuyler schuyler merged commit 425f5b3 into main Jun 30, 2026
9 checks passed
@schuyler schuyler added the rc-pending Included in a release candidate awaiting validation label Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rc-pending Included in a release candidate awaiting validation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants