<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: R Sai pranav</title>
    <description>The latest articles on DEV Community by R Sai pranav (@pranav-error).</description>
    <link>https://dev.to/pranav-error</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4162872%2F4740ed2b-d797-4c1c-b080-b68e9ee1ff5d.png</url>
      <title>DEV Community: R Sai pranav</title>
      <link>https://dev.to/pranav-error</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pranav-error"/>
    <language>en</language>
    <item>
      <title>Two stack overflows hiding in plain sight</title>
      <dc:creator>R Sai pranav</dc:creator>
      <pubDate>Mon, 05 Oct 2026 05:14:29 +0000</pubDate>
      <link>https://dev.to/pranav-error/two-stack-overflows-hiding-in-plain-sight-1on2</link>
      <guid>https://dev.to/pranav-error/two-stack-overflows-hiding-in-plain-sight-1on2</guid>
      <description>&lt;p&gt;Most memory bugs I've fixed in open-source C weren't clever. They were a buffer sized for the "usual" input, sitting next to code that never promised the usual input. Here are two that got merged, and what each one taught me.&lt;/p&gt;

&lt;p&gt;Bug 1: pgagroal, &lt;code&gt;strcat&lt;/code&gt; into 512 bytes&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/pgagroal/pgagroal" rel="noopener noreferrer"&gt;pgagroal&lt;/a&gt; is a connection pooler for PostgreSQL. Its CLI has a &lt;code&gt;list_limits&lt;/code&gt; command that prints a &lt;code&gt;database=alias,alias,...&lt;/code&gt; column. It built that string like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;db_alias_string&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DB_ALIAS_STRING_LENGTH&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;   &lt;span class="cm"&gt;/* 512 */&lt;/span&gt;
&lt;span class="n"&gt;pgagroal_snprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_alias_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_alias_string&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;strcat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_alias_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"="&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;strcat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_alias_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;","&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;strcat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_alias_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line is bounded. Everything after it isn't: &lt;code&gt;strcat&lt;/code&gt; does no bounds checking, and the loop runs once per alias.&lt;/p&gt;

&lt;p&gt;So the real question is not "is &lt;code&gt;strcat&lt;/code&gt; dangerous" — everyone knows that. It's &lt;strong&gt;how big can this string legally get?&lt;/strong&gt; The answer is in &lt;code&gt;pgagroal.h&lt;/code&gt;, not in &lt;code&gt;cli.c&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MAX_DATABASE_LENGTH = 256, MAX_ALIASES = 8

worst case the config allows : 2304 bytes  -&amp;gt; overflows 512 by 1792
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worst case is easy to dismiss as pathological. What made it worth fixing is that you don't need it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;30-char database + 8 aliases of 60 characters = 519 bytes -&amp;gt; overflows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's an ordinary configuration.&lt;/p&gt;

&lt;p&gt;The fix I didn't make:bump 512 to 2304. It works today and breaks the moment someone raises &lt;code&gt;MAX_ALIASES&lt;/code&gt;. The buffer's size was an assumption about another file's constants, and assumptions like that drift.&lt;/p&gt;

&lt;p&gt;The fix that got merged (&lt;a href="https://github.com/pgagroal/pgagroal/pull/1036" rel="noopener noreferrer"&gt;#1036&lt;/a&gt;): use the project's existing &lt;code&gt;pgagroal_append()&lt;/code&gt; / &lt;code&gt;pgagroal_append_char()&lt;/code&gt; helpers, which grow the allocation as they go. There's no fixed buffer left to overrun, and the &lt;code&gt;DB_ALIAS_STRING_LENGTH&lt;/code&gt; constant disappears with it. This was also the direction the maintainer had already asked for in that area, which mattered more than my preference.&lt;/p&gt;

&lt;p&gt;One honest caveat I put in the PR: I didn't trigger the overflow. The test suite needs a live PostgreSQL, so the evidence is the arithmetic from the configuration limits. Saying that plainly was better than implying a test I hadn't run.&lt;/p&gt;

&lt;p&gt;Bug 2: GRASS GIS, &lt;code&gt;"%.8f"&lt;/code&gt; into 30 bytes&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/OSGeo/grass" rel="noopener noreferrer"&gt;GRASS GIS&lt;/a&gt; formats map-region coordinates with a helper in &lt;code&gt;lib/gis/wind_format.c&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;format_double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;full_prec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_prec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"%.15g"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="n"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"%.8f"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;%.8f&lt;/code&gt; looks bounded because of the &lt;code&gt;.8&lt;/code&gt;. It isn't. The precision limits digits &lt;em&gt;after&lt;/em&gt; the decimal point; the digits before it are unbounded. For &lt;code&gt;DBL_MAX&lt;/code&gt; the result is &lt;strong&gt;318 characters&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Meanwhile four callers passed buffers sized for a normal coordinate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;raster&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;print_hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;   &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;north&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;south&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;east&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;west&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;raster&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;north&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;ps&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;map_info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;        &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;east&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;     &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;buf1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;buf2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Can a user actually get a huge value there? Yes: &lt;code&gt;g.region n=1e20&lt;/code&gt; in a non-lat/lon location is parsed by a bare &lt;code&gt;sscanf&lt;/code&gt; with no magnitude check, and the region's north then flows into those buffers when any of the four programs prints its header.&lt;/p&gt;

&lt;p&gt;Reproducing it without a multi-GB build.I copied &lt;code&gt;format_double()&lt;/code&gt; verbatim into a small harness and compiled it with &lt;code&gt;-fsanitize=address&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;                  &lt;span class="cm"&gt;/* same as r.coin */&lt;/span&gt;
&lt;span class="n"&gt;format_double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1e20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mi"&gt;10937&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AddressSanitizer&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;overflow&lt;/span&gt;
&lt;span class="n"&gt;WRITE&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The boundary was exact: &lt;code&gt;9e19&lt;/code&gt; produces 29 characters and is clean; &lt;code&gt;1e20&lt;/code&gt; produces 30, one more than 30 bytes can hold with the terminator, and overflows.&lt;/p&gt;

&lt;p&gt;Picking the fix (&lt;a href="https://github.com/OSGeo/grass/pull/7943" rel="noopener noreferrer"&gt;#7943&lt;/a&gt;). The "right" fix is arguably a bounded &lt;code&gt;snprintf&lt;/code&gt; inside &lt;code&gt;format_double()&lt;/code&gt;. But it's called from many places with no size parameter, so that means changing an API across the tree — a bigger decision than the bug. I sized the four buffers to 320 bytes (sign + 318 digits + terminator), commented why, and offered the API change as a follow-up if the maintainers preferred it. They merged the small fix.&lt;/p&gt;

&lt;p&gt;What both bugs had in common&lt;/p&gt;

&lt;p&gt;1.The bound lived somewhere else.In pgagroal it was a header's configuration limits; in GRASS it was what &lt;code&gt;sscanf&lt;/code&gt; would accept. Reading the buffer's line tells you nothing. Follow the data back to where its size is actually decided.&lt;br&gt;
2."Reachable from an ordinary input" is the bar. A worst case gets waved away; 519 bytes from a normal config, or &lt;code&gt;n=1e20&lt;/code&gt; from a normal command, doesn't.&lt;br&gt;
3.Match the fix to the decision you're allowed to make. Removing the buffer (pgagroal) and resizing it (GRASS) were both right, because of what each project's maintainers wanted and how far each change reached.&lt;br&gt;
4.Say exactly what you verified. "Proven from limits, not triggered" and "reproduced in an ASan harness, not the full build" made both PRs easier to trust, not harder.&lt;/p&gt;

&lt;p&gt;These two are part of 50+ patches I've had merged across pgmoneta, pgagroal, pgvictoria, GRASS GIS, JabRef, GNU Radio and the Kubernetes docs. More at &lt;a href="https://saipranav.me" rel="noopener noreferrer"&gt;saipranav.me&lt;/a&gt; and &lt;a href="https://github.com/Pranav-error" rel="noopener noreferrer"&gt;github.com/Pranav-error&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>c</category>
      <category>opensource</category>
      <category>security</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
