Segfaulting on runtime.free on C library pointer -- bindings for CMark-GFM

Hi folks! :wave:

For the little story, I rebuilt my website writing my own custom static site generator using Odin.

It was previously using Pelican, a Python-based static site generator, which works fine, but it’s annoying slow for the “watch for changes & serve” loop and every now and then I would have dependencies issues.

Now I’ve got my own static site generator written in Odin, with a very hacky template engine :sweat_smile: – but it works, and damn it’s fast!

I am hitting a weird issue using the CMark-GFM library, which I’ve installed on my Ubuntu box with sudo apt install libcmark-gfm-dev libcmark-gfm-extensions-dev and I came up with bindings adapting the ones that come vendored in Odin for original CMark.

Anyway, for some reason, my code segfaults if I runtime.free the output of the render_html function.

Here is a self-contained example that reproduces the issue:

package cmark_gfm_example

import "base:runtime"
import "core:c"
import "core:log"
import "core:fmt"
import "core:strings"


foreign import lib "system:cmark-gfm"
foreign import lib_ext "system:cmark-gfm-extensions"


@(default_calling_convention = "c", link_prefix = "cmark_")
foreign lib {
	parser_new :: proc(options: c.int) -> rawptr ---
	parser_feed :: proc(parser: rawptr, text: cstring, length: c.size_t) ---
	parser_finish :: proc(parser: rawptr) -> rawptr ---
	render_html :: proc(document: rawptr, options: c.int, extensions: rawptr) -> cstring ---
	node_free :: proc(node: rawptr) ---
	parser_free :: proc(parser: rawptr) ---
}

@(default_calling_convention = "c", link_prefix = "cmark_")
foreign lib_ext {
	// Extension management
	gfm_core_extensions_ensure_registered :: proc() ---
	find_syntax_extension :: proc(name: cstring) -> rawptr ---
	parser_attach_syntax_extension :: proc(parser: rawptr, extension: rawptr) -> bool ---
}


@(private = "file")
enable_extension :: proc(parser: rawptr, ext_name: cstring) -> bool {
	if table_ext := find_syntax_extension(ext_name); table_ext != nil {
		parser_attach_syntax_extension(parser, table_ext) or_return
	}
	return true
}

markdown_to_html_from_string_extended :: proc(text: string) -> (html: string) {
	options: i32 = 0 // int value of CMARK_OPT_DEFAULT
	gfm_core_extensions_ensure_registered()

	parser := parser_new(options)
	defer parser_free(parser)

	enable_extension(parser, "table")
	enable_extension(parser, "strikethrough")

	// Parse the markdown
	parser_feed(parser, strings.unsafe_string_to_cstring(text), len(text))
	document := parser_finish(parser)
	defer node_free(document)

	// Render to HTML
	html_cstr := render_html(document, options, c.NULL)

	// it's segfaulting here -- but why??
	defer runtime.free(rawptr(html_cstr))

	return strings.clone_from_cstring(html_cstr)
}

main :: proc() {
	context.logger = log.create_console_logger()
	text: string = "This is a ~~strikethrough~~ example.\n\n| Header | Header |\n| ------ | ------ |\n| Cell   | Cell   |"

	fmt.printf("Rendered markdown:\n%s", markdown_to_html_from_string_extended(text))
}

If I uncomment the line with runtime.free, the code works fine.

And the equivalent C example works fine as well:

// Compile with:
// gcc example_c.c -o example_c -lcmark-gfm -lcmark-gfm-extensions
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <cmark-gfm.h>
#include <cmark-gfm-core-extensions.h>

// This is a function that will make enabling extensions easier later on.
void enable_markdown_extension(cmark_parser *parser, char *extName) {
  cmark_syntax_extension *ext = cmark_find_syntax_extension(extName);
  if ( ext )
    cmark_parser_attach_syntax_extension(parser, ext);
}

// A function to convert HTML to markdown
char *markdown_to_html(char *markdown_string)
{
  int options = CMARK_OPT_DEFAULT; // You can also use CMARK_OPT_STRIKETHROUGH_DOUBLE_TILDE to enforce double tilde.

  cmark_gfm_core_extensions_ensure_registered();

  // Modified version of cmark_parse_document in blocks.c
  cmark_parser *parser = cmark_parser_new(options);

  /// Add extensions here
  enable_markdown_extension(parser, "strikethrough");
  enable_markdown_extension(parser, "table");

  cmark_node *doc;
  cmark_parser_feed(parser, markdown_string, strlen(markdown_string));
  doc = cmark_parser_finish(parser);
  cmark_parser_free(parser);

  // Render
  char *html = cmark_render_html(doc, options, NULL);
  cmark_node_free(doc);

  return html;
}

int main() {
  char *markdown = "This is a ~~strikethrough~~ example.\n\n| Header | Header |\n| ------ | ------ |\n| Cell   | Cell   |";
  char *html = markdown_to_html(markdown);
  
  if (html) {
    printf("Converted HTML:\n%s\n", html);
    free(html); // Free the allocated memory for HTML
  } else {
    printf("Conversion failed.\n");
  }
  
  return 0;
}

Any ideas on what might be going on here?
Thanks!

I am not an expert, but my best guess would be that the library is using a different allocator than the Odin runtime allocator (probably libc). If the library does not provide any memory deallocation functions you could try using the Odin core libc procedure to free the memory.

import "core:c/libc"

libc.free(rawptr(html_cstr))
3 Likes

Ohh, but of course!! I had already read about it but had forgotten :see_no_evil:
Thank you, that indeed is the right thing to do and I just checked, it solves the problem! :raised_hands:

Good day to you! :sun_with_face:

The default runtime.heap_allocator uses malloc on Unix & HeapAlloc on Windows (malloc wraps HeapAlloc on Windows).
The issue is that runtime.heap_allocator stores the ptr is gets from the OS just before the one it gives you, src. So when it tries to free a ptr gotten from another allocator, it’ll look behind it & pass whatever’s there to the OS.

2 Likes