Skip to content

[Bug]bufput() calls memcpy() with a NULL source for a zero-length append (empty table cell / blockquote) (CWE-476) #822

Open

Description

@1820893135-pixel

Hi,

While fuzzing redcarpet's C renderer sd_markdown_render() I found that rendering the one-byte table document | calls bufput(ob, NULL, 0), whose bufput() then passes the NULL source to memcpy() (a nonnull parameter) -- undefined behaviour. It is reached from the table and blockquote renderers with an empty cell/line.

Summary

bufput() copies unconditionally, even when nothing has to be copied:

Detail

/* ext/redcarpet/buffer.c -- bufput() */
void
bufput(struct buf *buf, const void *data, size_t len)
{
    assert(buf && buf->unit);
    if (buf->size + len > buf->asize && bufgrow(buf, buf->size + len) < BUF_OK)
        return;
    memcpy(buf->data + buf->size, data, len);   /* :169 -- data may be NULL when len == 0 */
    buf->size += len;
}

Root cause: an empty table cell / blockquote passes a NULL data pointer with len == 0; memcpy()'s source is declared nonnull, so memcpy(dst, NULL, 0) is undefined behaviour even though no bytes move (UBSan: null pointer passed as argument 2, which is declared to never be null).

Trigger path

sd_markdown_render() (markdown.c:2960) -> parse_block
  -> parse_table (markdown.c:2462) -> parse_table_header (markdown.c:2433)
       -> parse_table_row (markdown.c:2350) -> rndr_tablerow (html.c:587)
            -> bufput() (buffer.c:169)   <-- memcpy(dst, NULL, 0)

Build the reproducer

The C library has no Ruby dependency on this path: sd_markdown_render()
lives in ext/redcarpet/ and links only 8 translation units
(rc_markdown.c/rc_render.c are the Ruby bindings and are not needed).

git clone https://github.com/vmg/redcarpet.git && cd redcarpet

Save this as driver.c (calls the public C entry point directly):

#include <stdio.h>
#include <stdlib.h>
#include "markdown.h"
#include "html.h"
#include "buffer.h"

int main(int argc, char **argv) {
    FILE *f = fopen(argv[1], "rb"); if (!f) return 2;
    fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
    char *doc = malloc(n > 0 ? n : 1);
    if (n > 0) fread(doc, 1, n, f);
    fclose(f);

    struct buf *ob = bufnew(128);
    struct sd_callbacks cb; struct html_renderopt opt;
    sdhtml_renderer(&cb, &opt, 0);
    struct sd_markdown *md = sd_markdown_new(MKDEXT_AUTOLINK | MKDEXT_TABLES, 16, &cb, &opt);
    sd_markdown_render(ob, (const uint8_t *)doc, (size_t)n, md);
    return 0;
}
D=ext/redcarpet
clang -g -O1 -D_GNU_SOURCE -std=gnu11 -fsanitize=address,undefined   -I$D driver.c   $D/markdown.c $D/html.c $D/html_smartypants.c $D/autolink.c   $D/buffer.c $D/stack.c $D/houdini_href_e.c $D/houdini_html_e.c -o repro

POC (tested on the unmodified source)

A 1-byte markdown document (a single | character). Rebuild it from hex:

printf '%s' '7c' | xxd -r -p > poc.md
sha256sum poc.md
# must print: cbe5cfdf7c2118a9c3d78ef1d684f3afa089201352886449a06a6511cfef74a7
ls -l poc.md     # 1 bytes

Run:

ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1 ./repro poc.md

Trigger result

Verified on unmodified redcarpet 3.6.1 (clang, ASan+UBSan):

ext/redcarpet/buffer.c:169:32: runtime error: null pointer passed as argument 2, which is declared to never be null
    #0 bufput             ext/redcarpet/buffer.c:169:2
    #1 rndr_tablerow      ext/redcarpet/html.c:587:3
    #2 parse_table_row    ext/redcarpet/markdown.c:2350:2
    #3 parse_table_header ext/redcarpet/markdown.c:2433:2
    #4 parse_table        ext/redcarpet/markdown.c:2462:6
    #5 parse_block        ext/redcarpet/markdown.c:2543:9
    #6 sd_markdown_render ext/redcarpet/markdown.c:2960:3
    #7 main
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ext/redcarpet/buffer.c:169:32

Impact

Rendering a one-byte document | performs an undefined memcpy(dst, NULL, 0). The immediate effect is UB, not memory corruption (nothing is copied), but it aborts on UBSan/trapping builds and is a latent hazard: the same call site is a plain memcpy that a future change could easily make size-nonzero.

Suggested fix

Short-circuit a zero-length / NULL-source append before the memcpy:

if (len == 0 || data == NULL)
    return;

(bufput is also called from the blockquote renderer with an empty line, so the guard covers both paths.)

Activity

  1. 1820893135-pixel commented on Oct 24, 2026

    @1820893135-pixel
    Author

    Fixed in #824: bufput() now returns early when len == 0, before the grow and the memcpy(), so an empty table cell no longer passes a NULL source to a nonnull parameter. Verified with an ASan+UBSan build (-fno-sanitize-recover=all): the one-byte document exits 0, and 71 rendered documents are byte-identical before and after.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions