{"slug": "the-fix-that-passed-review-and-crashed-in-release", "title": "The Fix That Passed Review and Crashed in Release", "summary": "A developer at MonkeyCode recounts a debugging saga where a C++ worker pool crashed in release builds due to a stack-use-after-scope bug, despite passing both human and AI code review. The root cause was a lambda capturing a local variable by reference, fixed by changing to capture-by-value. The developer emphasizes that reviews are opinions while tests are evidence, and recommends using sanitizers and clean environments for verification.", "body_md": "The fix passed review. It still crashed in release. The reviewer was never tested.\n\nThe reviewer was me. And an AI model. We both approved the same wrong patch.\n\nThis is the retrospective. Symptom, root cause, fix. Plus the debugging loop that actually caught it.\n\nA small C++ worker pool returned garbage. Not always. Only with `-O2`\n\n. Only after a few thousand tasks.\n\nThat is the classic undefined behavior profile. The code looks innocent. The compiler does something legal. Your program does something insane.\n\nHere is the minimal shape of the code:\n\n```\n#include <future>\n#include <iostream>\n#include <vector>\n\nint main() {\n    std::vector<std::future<int>> results;\n    for (int i = 0; i < 4; ++i) {\n        int local = i * 10;\n        results.push_back(std::async(std::launch::async, [&local] {\n            return local + 1;\n        }));\n    }\n    for (auto& r : results) {\n        std::cout << r.get() << '\\n';\n    }\n}\n```\n\nDebug build? Fine. Release build? Random numbers. Sometimes a segfault. Sometimes silence.\n\nMy first instinct: data race. The queue is shared. Threads are racing. Add a mutex.\n\n```\nstd::mutex m;\n{\n    std::lock_guard<std::mutex> lock(m);\n    results.push_back(std::async(std::launch::async, [&local] {\n        return local + 1;\n    }));\n}\n```\n\nThe patch compiled. The patch passed review. The garbage stayed.\n\nWhy did it look right? Because the symptom matched the story. Intermittent. Load-dependent. Multi-threaded. Every checkbox said \"race.\"\n\nCheckboxes lie. Symptoms are not causes.\n\nI asked a free model on MonkeyCode's free server to review the same snippet. MonkeyCode is an open-source AI coding tool. Its current free tier includes model access, a server option, and ten million tokens. (Disclosure: This article was prepared as part of MonkeyCode's product outreach.)\n\nThe model agreed with me. \"Add synchronization,\" it said. Same wrong answer, delivered faster.\n\nThat is the moment I stopped trusting reviews. My own included. The AI's included.\n\nA review is an opinion. A test is evidence.\n\nI moved the reproducer to a disposable environment. The free server worked for this. Clean tools, clean state, no \"works on my machine.\"\n\nThen I minimized. I stripped the worker pool down to the loop above. It still failed.\n\nThen I ran sanitizers:\n\n```\ng++ -std=c++17 -O2 -fsanitize=address,undefined repro.cpp -o repro\n./repro\n```\n\nThe output named the exact line:\n\n```\nERROR: AddressSanitizer: stack-use-after-scope\n    #0 in main::$_0::operator()() const repro.cpp:9\n    #1 in std::__invoke_impl ...\n```\n\n`stack-use-after-scope`\n\n. The lambda captured `local`\n\nby reference. The local died at the end of each loop iteration. The async task ran after that. Dangling reference. Undefined behavior.\n\nThe mutex changed locking. The bug was a lifetime.\n\nTwo different layers. Two different fixes. Only one of them mattered.\n\nCapture by value. One word changed.\n\n```\nresults.push_back(std::async(std::launch::async, [local] {\n    return local + 1;\n}));\n```\n\nVerification:\n\n```\ng++ -std=c++17 -O2 -fsanitize=address,undefined repro.cpp -o repro\n./repro\n# 11, 21, 31, 41 — stable across 10,000 runs\n```\n\nSanitizer clean. Release build clean. The reproducer became a regression test.\n\nThis approach needs a compiler with sanitizer support. Embedded toolchains often lack it. No sanitizers, no shortcut.\n\nIt also needs a reproducible bug. Heisenbugs under TSan need a different loop. So do races inside third-party binaries you cannot rebuild.\n\nAnd the free server is for disposable experiments. Not production workloads. Not sensitive data. Treat it like a scratch box, not a datacenter.\n\nAI turned developers into reviewers. But who reviews the reviewer? Run the experiment. That is the review.\n\nNext time a patch passes review, run it on a clean box first. The free server is a decent place to start. Ten million tokens and one free server are enough for an honest reproducer.", "url": "https://wpnews.pro/news/the-fix-that-passed-review-and-crashed-in-release", "canonical_source": "https://dev.to/datacpp_3670/the-fix-that-passed-review-and-crashed-in-release-j1b", "published_at": "2026-08-26 11:18:02+00:00", "updated_at": "2026-08-26 11:44:11.936935+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["MonkeyCode"], "alternates": {"html": "https://wpnews.pro/news/the-fix-that-passed-review-and-crashed-in-release", "markdown": "https://wpnews.pro/news/the-fix-that-passed-review-and-crashed-in-release.md", "text": "https://wpnews.pro/news/the-fix-that-passed-review-and-crashed-in-release.txt", "jsonld": "https://wpnews.pro/news/the-fix-that-passed-review-and-crashed-in-release.jsonld"}}