Bug Report: Nil pointer dereference in inline renderers with unhandled or custom nodes
Summary
A nil pointer dereference panic occurs when rendering markdown containing unhandled or custom inline nodes inside inline structures such as emphasis (*...*, **...**), hyperlinks ([...]), or table cells.
This happens because the renderer loops over the children of these elements and invokes child.Render(w, ctx) without performing a nil check. If a child node is not handled in NewElement (which returns a nil ElementRenderer), the interface value in the child slice is nil, leading to a crash.
Root Cause Analysis
In glamour/ansi, the following files iterate over Children slices and call Render directly on each child:
-
ansi/emphasis.go:40:
func (e *EmphasisElement) doRender(w io.Writer, ctx RenderContext, style StylePrimitive) error {
for _, child := range e.Children {
if r, ok := child.(StyleOverriderElementRenderer); ok {
if err := r.StyleOverrideRender(w, ctx, style); err != nil {
return err
}
} else {
if err := child.Render(w, ctx); err != nil { // <--- Panics here if child is nil
return err
}
}
}
return nil
}
-
ansi/link.go:24:
func (e *LinkElement) Render(w io.Writer, ctx RenderContext) error {
for _, child := range e.Children {
if r, ok := child.(StyleOverriderElementRenderer); ok {
// ...
} else {
var b bytes.Buffer
if err := child.Render(&b, ctx); err != nil { // <--- Panics here if child is nil
return err
}
// ...
}
}
// ...
}
-
ansi/table.go:150:
func (e *TableCellElement) Render(_ io.Writer, ctx RenderContext) error {
// ...
for _, child := range e.Children {
if r, ok := child.(StyleOverriderElementRenderer); ok {
// ...
} else {
var bb bytes.Buffer
if err := child.Render(&bb, ctx); err != nil { // <--- Panics here if child is nil
return err
}
// ...
}
}
// ...
}
When Glamour encounters custom or unhandled nodes (e.g. astext.KindTaskCheckBox inside an emphasis block in older Goldmark versions, or custom nodes added by Goldmark parser extensions), NewElement in elements.go falls back to the default case and returns Element{} (whose Renderer is nil). This results in a nil element inside the Children slice.
Minimal Go Reproducer
The following self-contained Go program defines a custom Goldmark inline parser extension that generates a custom node {custom}. When this node is rendered inside an emphasis block (*{custom}*), Glamour panics:
package main
import (
"bytes"
"fmt"
"os"
"github.com/charmbracelet/glamour/ansi"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
type customNode struct {
ast.BaseInline
}
var kindCustomNode = ast.NewNodeKind("CustomNode")
func (n *customNode) Kind() ast.NodeKind { return kindCustomNode }
func (n *customNode) Dump(source []byte, level int) {
ast.DumpHelper(n, source, level, nil, nil)
}
type customExtension struct{}
func (e *customExtension) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(
parser.WithInlineParsers(
util.Prioritized(&customParser{}, 100),
),
)
}
type customParser struct{}
func (p *customParser) Trigger() []byte { return []byte("{") }
func (p *customParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
line, _ := block.PeekLine()
if bytes.HasPrefix(line, []byte("{custom}")) {
block.Advance(8)
return &customNode{}
}
return nil
}
func main() {
markdown := []byte("*{custom}*")
md := goldmark.New(
goldmark.WithExtensions(&customExtension{}),
)
ar := ansi.NewRenderer(ansi.Options{WordWrap: 80})
md.SetRenderer(
renderer.NewRenderer(
renderer.WithNodeRenderers(
util.Prioritized(ar, 1000),
),
),
)
defer func() {
if r := recover(); r != nil {
fmt.Printf("\nPanic captured: %v\n", r)
os.Exit(0)
}
}()
var buf bytes.Buffer
_ = md.Convert(markdown, &buf)
}
Output
Warning: unhandled element CustomNode
Panic captured: runtime error: invalid memory address or nil pointer dereference
Proposed Fix
To fix the panics, Glamour should verify that the child element renderer is non-nil before invoking its methods:
1. In ansi/emphasis.go:
func (e *EmphasisElement) doRender(w io.Writer, ctx RenderContext, style StylePrimitive) error {
for _, child := range e.Children {
+ if child == nil {
+ continue
+ }
if r, ok := child.(StyleOverriderElementRenderer); ok {
if err := r.StyleOverrideRender(w, ctx, style); err != nil {
return err
}
} else {
if err := child.Render(w, ctx); err != nil {
return err
}
}
}
return nil
}
2. In ansi/link.go:
func (e *LinkElement) Render(w io.Writer, ctx RenderContext) error {
for _, child := range e.Children {
+ if child == nil {
+ continue
+ }
if r, ok := child.(StyleOverriderElementRenderer); ok {
st := ctx.options.Styles.LinkText
if err := r.StyleOverrideRender(w, ctx, st); err != nil {
return err
}
} else {
var b bytes.Buffer
if err := child.Render(&b, ctx); err != nil {
return err
}
3. In ansi/table.go:
func (e *TableCellElement) Render(_ io.Writer, ctx RenderContext) error {
var b bytes.Buffer
style := ctx.options.Styles.Table.StylePrimitive
for _, child := range e.Children {
+ if child == nil {
+ continue
+ }
if r, ok := child.(StyleOverriderElementRenderer); ok {
if err := r.StyleOverrideRender(&b, ctx, style); err != nil {
return err
}
} else {
var bb bytes.Buffer
if err := child.Render(&bb, ctx); err != nil {
return err
}
Bug Report: Nil pointer dereference in inline renderers with unhandled or custom nodes
Summary
A
nil pointer dereferencepanic occurs when rendering markdown containing unhandled or custom inline nodes inside inline structures such as emphasis (*...*,**...**), hyperlinks ([...]), or table cells.This happens because the renderer loops over the children of these elements and invokes
child.Render(w, ctx)without performing a nil check. If a child node is not handled inNewElement(which returns anilElementRenderer), the interface value in the child slice isnil, leading to a crash.Root Cause Analysis
In
glamour/ansi, the following files iterate overChildrenslices and callRenderdirectly on each child:ansi/emphasis.go:40:
ansi/link.go:24:
ansi/table.go:150:
When Glamour encounters custom or unhandled nodes (e.g.
astext.KindTaskCheckBoxinside an emphasis block in older Goldmark versions, or custom nodes added by Goldmark parser extensions),NewElementinelements.gofalls back to thedefaultcase and returnsElement{}(whoseRendererisnil). This results in anilelement inside theChildrenslice.Minimal Go Reproducer
The following self-contained Go program defines a custom Goldmark inline parser extension that generates a custom node
{custom}. When this node is rendered inside an emphasis block (*{custom}*), Glamour panics:Output
Proposed Fix
To fix the panics, Glamour should verify that the child element renderer is non-nil before invoking its methods:
1. In
ansi/emphasis.go:func (e *EmphasisElement) doRender(w io.Writer, ctx RenderContext, style StylePrimitive) error { for _, child := range e.Children { + if child == nil { + continue + } if r, ok := child.(StyleOverriderElementRenderer); ok { if err := r.StyleOverrideRender(w, ctx, style); err != nil { return err } } else { if err := child.Render(w, ctx); err != nil { return err } } } return nil }2. In
ansi/link.go:func (e *LinkElement) Render(w io.Writer, ctx RenderContext) error { for _, child := range e.Children { + if child == nil { + continue + } if r, ok := child.(StyleOverriderElementRenderer); ok { st := ctx.options.Styles.LinkText if err := r.StyleOverrideRender(w, ctx, st); err != nil { return err } } else { var b bytes.Buffer if err := child.Render(&b, ctx); err != nil { return err }3. In
ansi/table.go:func (e *TableCellElement) Render(_ io.Writer, ctx RenderContext) error { var b bytes.Buffer style := ctx.options.Styles.Table.StylePrimitive for _, child := range e.Children { + if child == nil { + continue + } if r, ok := child.(StyleOverriderElementRenderer); ok { if err := r.StyleOverrideRender(&b, ctx, style); err != nil { return err } } else { var bb bytes.Buffer if err := child.Render(&bb, ctx); err != nil { return err }