<?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: Morgan Ma</title>
    <description>The latest articles on DEV Community by Morgan Ma (@datacpp_3670).</description>
    <link>https://dev.to/datacpp_3670</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%2F4063654%2Fa6a76f1b-0f06-4574-85d8-4edd1ce3af7e.png</url>
      <title>DEV Community: Morgan Ma</title>
      <link>https://dev.to/datacpp_3670</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/datacpp_3670"/>
    <language>en</language>
    <item>
      <title>Why Your C++ Crash Skips Your Laptop: A Clean-Room Debugging Loop on a Disposable Box</title>
      <dc:creator>Morgan Ma</dc:creator>
      <pubDate>Mon, 10 Aug 2026 08:20:13 +0000</pubDate>
      <link>https://dev.to/datacpp_3670/why-your-c-crash-skips-your-laptop-a-clean-room-debugging-loop-on-a-disposable-box-35p2</link>
      <guid>https://dev.to/datacpp_3670/why-your-c-crash-skips-your-laptop-a-clean-room-debugging-loop-on-a-disposable-box-35p2</guid>
      <description>&lt;p&gt;Our CI pipeline failed last Tuesday with a segfault that nobody on the team could reproduce locally. Three developers, three different machines, zero crashes. The binary only fell over on the Linux runner, in release mode, after the cache was warm. That class of failure — environment-dependent memory corruption — is exactly why I stopped debugging C++ crashes on my own hardware and moved the whole loop onto a fresh, discardable server.&lt;/p&gt;

&lt;p&gt;Below is the playbook I now follow: a small program that misbehaves silently on one machine and loudly on another, a short guide for matching sanitizers to symptoms, and the exact loop I run. I'll also cover how I use an AI assistant with free model access during diagnosis, and the places where I refuse to let it drive.&lt;/p&gt;

&lt;h2&gt;
  
  
  A program that lies to you on your dev machine
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;crashme.cpp&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;cstdio&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;cstdlib&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;make_counter&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;start&lt;/span&gt;&lt;span class="p"&gt;)&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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// handing back a pointer into a dead stack frame&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;release&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;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                  &lt;span class="c1"&gt;// caller keeps using the pointer afterward&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"counter = %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// reading reclaimed stack memory&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;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;               &lt;span class="c1"&gt;// freeing the same allocation twice&lt;/span&gt;

    &lt;span class="n"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"survived"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&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;Compile it on a typical desktop with default flags and there's a decent chance it prints &lt;code&gt;counter = 42&lt;/code&gt; followed by &lt;code&gt;survived&lt;/code&gt; and exits cleanly. Two genuine undefined-behavior bugs, no complaint. Ship that confidence to production and the crash arrives later, in a worse place, with a paying customer attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  The case for a clean room
&lt;/h2&gt;

&lt;p&gt;Allocator behavior, compiler defaults, glibc version, optimization level — all of these shift whether a memory bug surfaces. Your workstation is not CI, and CI is not production. The practical answer is to debug inside an environment you build from scratch for each investigation and throw away afterward, configured to mirror the failing target.&lt;/p&gt;

&lt;p&gt;For this kind of throwaway environment I've been using a free server instance from MonkeyCode, spun up when a bug report lands and deleted when the fix is merged. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Nothing in the workflow depends on that specific provider — a local container, a spare VM, or any budget VPS behaves the same. What matters is that the environment starts empty and costs you nothing to reset, so "reproduce from zero" becomes reflex rather than a favor you owe your infrastructure team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Match the instrument to the symptom
&lt;/h2&gt;

&lt;p&gt;The most common mistake I see is reaching for the same tool regardless of the failure. A quick mapping saves hours:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you're observing&lt;/th&gt;
&lt;th&gt;Reach for&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Writes past an allocation, stale pointer use&lt;/td&gt;
&lt;td&gt;AddressSanitizer&lt;/td&gt;
&lt;td&gt;Pinpoints the exact line with allocation history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Garbage values that change between runs&lt;/td&gt;
&lt;td&gt;MemorySanitizer&lt;/td&gt;
&lt;td&gt;Catches uninitialized reads ASan ignores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intermittent crash under concurrency&lt;/td&gt;
&lt;td&gt;ThreadSanitizer&lt;/td&gt;
&lt;td&gt;Detects races via happens-before analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third-party binary you cannot recompile&lt;/td&gt;
&lt;td&gt;Valgrind&lt;/td&gt;
&lt;td&gt;Works on the existing binary, at a heavy slowdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A core file and nothing else&lt;/td&gt;
&lt;td&gt;gdb&lt;/td&gt;
&lt;td&gt;Post-mortem is the only option left&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One caveat that bites people: you cannot combine sanitizers in a single build, so plan for several compile-and-run passes when the symptom is ambiguous.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop, start to finish
&lt;/h2&gt;

&lt;p&gt;On the fresh box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Pin the compiler so today's run is comparable to next week's&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; clang gdb

&lt;span class="c"&gt;# Keep optimization enabled. Bugs that evaporate at -O0 are real,&lt;/span&gt;
&lt;span class="c"&gt;# and debugging a binary that differs from the failing one wastes time.&lt;/span&gt;
clang++ &lt;span class="nt"&gt;-std&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;c++17 &lt;span class="nt"&gt;-O2&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nt"&gt;-fsanitize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;address,undefined &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-fsanitize-recover&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;address crashme.cpp &lt;span class="nt"&gt;-o&lt;/span&gt; crashme_asan

&lt;span class="c"&gt;# Execute and keep the report&lt;/span&gt;
&lt;span class="nv"&gt;ASAN_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;detect_stack_use_after_return&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 ./crashme_asan 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tee &lt;/span&gt;report.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Against &lt;code&gt;crashme.cpp&lt;/code&gt;, this flags the returned-stack-pointer read inside &lt;code&gt;main&lt;/code&gt; and the double free from &lt;code&gt;release&lt;/code&gt;, each with the stack trace of the original allocation. From an empty server to a complete diagnosis, the whole thing takes minutes, and most of that is package installation.&lt;/p&gt;

&lt;p&gt;The fixes themselves are straightforward: &lt;code&gt;make_counter&lt;/code&gt; should take ownership seriously — return by value, a &lt;code&gt;std::unique_ptr&amp;lt;int&amp;gt;&lt;/code&gt;, or write into a caller-supplied reference — and &lt;code&gt;release&lt;/code&gt; should null out the caller's pointer (or better, take a &lt;code&gt;std::unique_ptr&lt;/code&gt; so the double free becomes unrepresentable).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the AI assistant actually helps
&lt;/h2&gt;

&lt;p&gt;As disclosed above, this article is part of MonkeyCode's product outreach, and I used its free model access for two narrow tasks in this loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Translating sanitizer output into a hypothesis.&lt;/strong&gt; I paste the ASan trace alongside the implicated function and ask the model to rank plausible causes. It reliably converts &lt;code&gt;stack-use-after-return in make_counter&lt;/code&gt; into plain language faster than a junior teammate can. What it cannot do is judge which repair is safe for your codebase's ownership conventions — that call stays human.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sketching the regression test.&lt;/strong&gt; After the root cause is confirmed, I ask for a test that reproduces the failure pre-fix and goes green post-fix. Then I read every line before it enters the repo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice what the model is doing here: reading and drafting. Verification remains mechanical — rerun the sanitizer, watch it go quiet, and only then trust the patch. The instrument is the authority; the assistant is a fast second pair of eyes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this breaks down
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Instrumentation perturbs the binary. Sanitizers rearrange memory layout, and a bug can relocate or disappear under observation. When that happens you're back to Valgrind or post-mortem debugging.&lt;/li&gt;
&lt;li&gt;Free server tiers come with no guarantees about quota, uptime, or hardware specs. Treat them as scratch space, never as something your release pipeline depends on.&lt;/li&gt;
&lt;li&gt;Never feed proprietary source or production secrets into a third-party AI tool without explicit clearance from your security policy. A redacted minimal reproducer — like the one above — is usually all the model needs anyway.&lt;/li&gt;
&lt;li&gt;AI-proposed memory fixes often patch the crash site instead of the root cause. Accepting one without a sanitizer re-run is how bugs get closed and reopened.&lt;/li&gt;
&lt;li&gt;Kernel modules, bare-metal targets, and latency-critical paths where instrumentation is impossible need a different playbook entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The transferable habit isn't any single flag or tool — it's refusing to debug memory corruption in an environment you can't fully control. Rebuild the failing context on a disposable machine, instrument for the symptom you actually have, and let AI handle the reading and drafting while the sanitizer decides what's true. If you'd like a zero-cost way to try this without touching your own infrastructure, MonkeyCode's free server and model access is one option — though a local Docker image gives you the same reproducibility guarantees.&lt;/p&gt;

&lt;p&gt;When a crash refuses to reproduce anywhere except production, what's your first move? I'd genuinely like to compare notes with other C++ developers on how they triage environment-specific failures.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>debugging</category>
      <category>programming</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
