GHSA-6R34-94WQ-JHRC
Vulnerability from github – Published: 2026-04-06 17:53 – Updated: 2026-04-06 23:43Summary
A signed length truncation bug causes an out-of-bounds read in the default Markdown parse path. Inputs larger than INT_MAX are truncated to a signed int before entering the native parser, allowing the parser to read past the end of the supplied buffer and crash the process
Details
In both public entry points:
ext/rdiscount.c:97ext/rdiscount.c:136
RSTRING_LEN(text) is passed directly into mkd_string():
MMIOT *doc = mkd_string(RSTRING_PTR(text), RSTRING_LEN(text), flags);
mkd_string() accepts int len:
ext/mkdio.c:174
Document * mkd_string(const char *buf, int len, mkd_flag_t flags)
{
struct string_stream about;
about.data = buf;
about.size = len;
return populate((getc_func)__mkd_io_strget, &about, flags & INPUT_MASK);
}
The parser stores the remaining input length in a signed int:
ext/markdown.h:205
struct string_stream {
const char *data;
int size;
};
The read loop stops only when size == 0:
ext/mkdio.c:161
int __mkd_io_strget(struct string_stream *in)
{
if ( !in->size ) return EOF;
--(in->size);
return *(in->data)++;
}
If the Ruby string length exceeds INT_MAX, the value can truncate to a negative int. In that state, the parser continues incrementing data and reading past the end of the original Ruby string, causing an out-of-bounds read and native crash.
Affected APIs:
RDiscount.new(input).to_htmlRDiscount.new(input).toc_content
PoC
Crash via to_html:
RUBYLIB=lib:ext ruby -e 'require "rdiscount"; n=2_200_000_000; s = "a" * n; warn "built=#{s.bytesize}"; RDiscount.new(s).to_html"'
result:
built=2200000000- Ruby terminates with
[BUG] Segmentation fault - top control frame:
CFUNC :to_html
same result with toc_content
Impact
This is an out-of-bounds read with the main issue being reliable denial-of-service. Impacted is limited to deployments parses attacker-controlled Markdown and permits multi-GB inputs.
Fix
just add a checked length guard before the mkd_string() call in both public entry points:
ext/rdiscount.c:97ext/rdiscount.c:136ex:
VALUE text = rb_funcall(self, rb_intern("text"), 0);
long text_len = RSTRING_LEN(text);
VALUE buf = rb_str_buf_new(1024);
Check_Type(text, T_STRING);
if (text_len > INT_MAX) {
rb_raise(rb_eArgError, "markdown input too large");
}
MMIOT *doc = mkd_string(RSTRING_PTR(text), (int)text_len, flags);
The same guard should be applied in rb_rdiscount_toc_content() before its mkd_string() call.
{
"affected": [
{
"package": {
"ecosystem": "RubyGems",
"name": "rdiscount"
},
"ranges": [
{
"events": [
{
"introduced": "1.3.1.1"
},
{
"fixed": "2.2.7.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-35201"
],
"database_specific": {
"cwe_ids": [
"CWE-125"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-06T17:53:59Z",
"nvd_published_at": "2026-04-06T20:16:27Z",
"severity": "MODERATE"
},
"details": "### Summary\n\nA signed length truncation bug causes an out-of-bounds read in the default Markdown parse path. Inputs larger than `INT_MAX` are truncated to a signed `int` before entering the native parser, allowing the parser to read past the end of the supplied buffer and crash the process\n\n### Details\n\nIn both public entry points:\n\n- `ext/rdiscount.c:97`\n- `ext/rdiscount.c:136`\n\n`RSTRING_LEN(text)` is passed directly into `mkd_string()`:\n\n```c\nMMIOT *doc = mkd_string(RSTRING_PTR(text), RSTRING_LEN(text), flags);\n```\n\n`mkd_string()` accepts `int len`:\n\n- `ext/mkdio.c:174`\n\n```c\nDocument * mkd_string(const char *buf, int len, mkd_flag_t flags)\n{\n struct string_stream about;\n\n about.data = buf;\n about.size = len;\n\n return populate((getc_func)__mkd_io_strget, \u0026about, flags \u0026 INPUT_MASK);\n}\n```\n\nThe parser stores the remaining input length in a signed `int`:\n\n- `ext/markdown.h:205`\n\n```c\nstruct string_stream {\n const char *data;\n int size;\n};\n```\n\nThe read loop stops only when `size == 0`:\n\n- `ext/mkdio.c:161`\n\n```c\nint __mkd_io_strget(struct string_stream *in)\n{\n if ( !in-\u003esize ) return EOF;\n\n --(in-\u003esize);\n\n return *(in-\u003edata)++;\n}\n```\n\nIf the Ruby string length exceeds `INT_MAX`, the value can truncate to a negative `int`. In that state, the parser continues incrementing `data` and reading past the end of the original Ruby string, causing an out-of-bounds read and native crash.\n\nAffected APIs:\n\n- `RDiscount.new(input).to_html`\n- `RDiscount.new(input).toc_content`\n\n### PoC\n\nCrash via `to_html`:\n\n```sh\nRUBYLIB=lib:ext ruby -e \u0027require \"rdiscount\"; n=2_200_000_000; s = \"a\" * n; warn \"built=#{s.bytesize}\"; RDiscount.new(s).to_html\"\u0027\n```\nresult:\n\n- `built=2200000000`\n- Ruby terminates with `[BUG] Segmentation fault`\n- top control frame: `CFUNC :to_html`\n\nsame result with `toc_content`\n\n### Impact\n\nThis is an out-of-bounds read with the main issue being reliable denial-of-service. Impacted is limited to deployments parses attacker-controlled Markdown and permits multi-GB inputs.\n\n### Fix\n\njust add a checked length guard before the `mkd_string()` call in both public entry points:\n\n- `ext/rdiscount.c:97`\n- `ext/rdiscount.c:136`\nex: \n```c\nVALUE text = rb_funcall(self, rb_intern(\"text\"), 0);\nlong text_len = RSTRING_LEN(text);\nVALUE buf = rb_str_buf_new(1024);\nCheck_Type(text, T_STRING);\n\nif (text_len \u003e INT_MAX) {\n rb_raise(rb_eArgError, \"markdown input too large\");\n}\n\nMMIOT *doc = mkd_string(RSTRING_PTR(text), (int)text_len, flags);\n```\n\nThe same guard should be applied in `rb_rdiscount_toc_content()` before its `mkd_string()` call.",
"id": "GHSA-6r34-94wq-jhrc",
"modified": "2026-04-06T23:43:57Z",
"published": "2026-04-06T17:53:59Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/davidfstr/rdiscount/security/advisories/GHSA-6r34-94wq-jhrc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-35201"
},
{
"type": "WEB",
"url": "https://github.com/davidfstr/rdiscount/commit/b1a16445e92e0d12c07594dedcdc56f80b317761"
},
{
"type": "PACKAGE",
"url": "https://github.com/davidfstr/rdiscount"
},
{
"type": "WEB",
"url": "http://github.com/davidfstr/rdiscount/releases/tag/2.2.7.4"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "rdiscount has an Out-of-bounds Read"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.