Repository navigation
Description
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 redcarpetSave 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 reproPOC (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 bytesRun:
ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1 ./repro poc.mdTrigger 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
1820893135-pixel commentedon Oct 24, 2026
Fixed in #824:
bufput()now returns early whenlen == 0, before the grow and thememcpy(), so an empty table cell no longer passes a NULL source to anonnullparameter. 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.