Skip to content

Panic: nil pointer dereference in inline renderers with unhandled or custom nodes #576

Description

@jdietrich-tc

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:

  1. 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
    }
  2. 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
                }
                // ...
            }
        }
        // ...
    }
  3. 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
 			}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions