DEV Community

Morgan Ma
Morgan Ma

Posted on

The View Named a Dead Temporary

A green compile can still hand you a dead view. I hit that lie inside a tiny reconstructed lab. The crash was a plain C++ lifetime bug.

The log line looked fine in the first debugger stop. Two calls later the same line read freed memory. Why did a boring helper kill a calm test?

The view named bytes that had already died. I rebuild this case so you can rerun it. I am not reporting a measured production incident.

The helper that lied

Here is the whole trap in one function. Read the return type before you read the body. Does a view own anything in that signature?

#include <iostream>
#include <string>
#include <string_view>

// Reconstructed lab bug. This return is undefined behavior.
// Do not ship it. Expect ASan to report stack-use-after-return.
std::string_view label_for(int code) {
    std::string tmp = "code=" + std::to_string(code);
    return tmp;
}

int main() {
    std::string_view label = label_for(7);
    std::cout << label << "\n";
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

The local string dies when the function returns. The returned view still holds its old data pointer. The caller prints that pointer and walks off a corpse.

Six moves I reuse

I use six moves, and I keep their order. I do not open with a request for a patch. A patch before a hypothesis just moves the lie.

1. Freeze the symptom

I write the failure as one boring sentence. The print of that returned label reads freed stack memory. I pin that sentence in a comment above the file.

If a later edit does not match it, I stop. The symptom is the contract for the rest of the hunt. A clever patch that misses the sentence is noise.

2. Shrink until the lie is obvious

I delete every line that is not the crash. Can you still fail inside thirty lines of C++? If you cannot, you are debugging a story.

I want one file and one compile command. Anything else can wait until the small file fails. Shrinking is the move I skip when I am tired.

3. Make the sanitizer the judge

I trust AddressSanitizer more than my own eyes here. A quiet run can still be an inlining accident. So I force a frame and I keep symbols.

c++ -std=c++17 -O1 -g -fno-omit-frame-pointer \
  -fsanitize=address,undefined -o label_bug label_bug.cpp
ASAN_OPTIONS=detect_stack_use_after_return=1 ./label_bug
Enter fullscreen mode Exit fullscreen mode

I expect ASan to report a stack use after return. If the run stays silent, I am not finished. I change the optimization level and I try again.

Your compiler may want a different use-after-return switch. Read that switch in your compiler manual before you trust silence. A missing detector is not a clean bill of health.

4. Ask for a hypothesis, not a rewrite

This is the only step that needs a model. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I paste the file plus the sanitizer summary.

I ask which object dies before the read. I do not ask the model to just fix it. A rewrite can hide a second lifetime bug.

A hypothesis is something I can fail in a test. That difference is the whole point of this step. MonkeyCode's free model access covers that one question.

The free server option covers the sanitizer compile. I do not treat either offer as a fixed quota. I read the current limit in the product UI.

A remembered token number is already a risk. Stale limits waste the session before the bug dies. I will not repeat a number I cannot verify today.

I will not call the project open source from memory either. Read the project page for the license and the tree. A remembered license is as stale as a remembered quota.

5. Prove it, then change ownership

I want a check that fails on the dangling return. I want that same check to pass after the fix. If I cannot name that check, I do not patch.

#include <cassert>
#include <string>
#include <string_view>

std::string owned_label(int code) {
    return "code=" + std::to_string(code);
}

int main() {
    std::string owned = owned_label(7);
    std::string_view label = owned;
    assert(label == "code=7");
    // Owner stays alive for the read above.
    // Later mutation must be read through owned, not label.
    owned.push_back('!');
    assert(owned == "code=7!");
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

The factory now returns an owning std::string object. The view is only a local alias of that owner. It must not escape the scope that owns the bytes.

If the caller needs the text later, return the string. If the caller only peeks, pass a view inward. Which of those two jobs is your helper doing?

6. Re-run, then lock the regression

I compile the fixed file with the same sanitizer flags. I also build the fix with no optimization and with O2. Why do I bother with two optimization levels?

Lifetime bugs love to hide behind inlining choices. One green binary is a coincidence, not a conclusion. I want both binaries to pass before I call it fixed.

c++ -std=c++17 -O0 -g -fno-omit-frame-pointer \
  -fsanitize=address,undefined -o label_ok label_ok.cpp
./label_ok
c++ -std=c++17 -O2 -g -fno-omit-frame-pointer \
  -fsanitize=address,undefined -o label_ok_o2 label_ok.cpp
./label_ok_o2
Enter fullscreen mode Exit fullscreen mode

Then I keep the bad pattern in the test notes. Future me needs the trap, not only the patch. A deleted repro is how this bug returns next month.

A table I reuse on the next crash

I do not improvise the first probe anymore. I match the symptom to one row, then I stop. Extra theories are how I waste the afternoon.

Symptom First probe Likely lie Fix shape
Crash just after return ASan stack-use-after-return View of a temporary Return std::string
Crash after a long append ASan heap-use-after-free Pointer across realloc Keep the owner
Garbage only at O2 UBSan plus two opt levels Lifetime or aliasing Narrow the API
Clean sanitizer, bad log Print data() addresses View rebound too early Log the owner

I do not skip the row that matches the crash. I do not add a row just because it sounds clever. The table is a brake, not a brainstorm.

Patches I reject on sight

Models love a short return type of string_view. That signature is fine only when the caller owns the text. A factory that builds the text cannot return a view.

A common suggested fix stores the text in a static. That change can silence AddressSanitizer on this file. It also shares one buffer across every caller.

Is a quieter bug actually a real fix? No, the next call overwrites that shared buffer. Two threads assigning that static string create a data race.

I reject any function that returns a view of a local. I reject a patch I cannot explain in one sentence. If the model cannot name the owner, I discard it.

Where a free server fits

I use the free server as a clean compile box. I want a machine that does not share my local cache. A second toolchain catches a flag I forgot at home.

I upload only the repro file and the compile line. I never upload secrets, tokens, or customer logs. A compile box is not a place to park production data.

If the server image is older than my laptop, I note it. A miss on an old standard library is still data. It is not proof about my release compiler.

I match the language flag before I compare results. I match the sanitizer set before I trust a green run. Otherwise I am comparing two different compiled programs.

Who should skip this

Skip this if the bug needs your real production heap. A thirty-line file will not reproduce that class of failure. Shrink only after you know the crash is local.

Skip this if policy forbids pasting source into a host. Use a local compiler and keep the same six moves. The method does not require a hosted model at all.

Skip this if you need a certified compiler build. A free server image may not be that exact build. Do not pretend a mismatch is a release sign-off.

Skip this for kernel work or a sanitizer-hostile runtime. This lab assumes an ordinary hosted C++ toolchain. If ASan cannot link, this workflow is the wrong tool.

Limits I will not blur

This repro is a reconstruction, not a field report. I did not time the compile, and I did not benchmark a model. I am not naming a model build or a hardware shape.

Free model access and a free server option are the claims I rely on. Those claims can change without a blog post. Check the live page before you plan a long job.

A clean run here does not bless your whole codebase. It blesses one rule about ownership and views. Factories return owning strings, and views stay local.

I still let a model draft the hypothesis sentence. I do not let the model close the bug report. The sanitizer closes it, and the regression keeps it closed.

The cousin that looks like a win

There is a cousin bug that looks like a win. You store data from a heap string, then you append. The append reallocates, and the old pointer dies.

A short append may stay inside the small-string buffer. I force a long append so the heap path is real. If the pointer still looks fine, you missed the realloc.

#include <iostream>
#include <string>

int main() {
    std::string owned(64, 'a'); // already past the small-string buffer
    const char* raw = owned.data();
    owned.append(64, 'b'); // growth, not a tiny append
    // Bug: raw dangles if append reallocated. Read owned instead.
    std::cout << raw << "\n";
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

The sanitizer report now says heap use after free. The debugging moves do not change at all. You still name the owner before you touch the pointer.

I keep both traps in one folder on the compile box. One file returns a view of a temporary string. The other file keeps a pointer across a long append.

Same question every time: who owns the bytes now? If the answer is the temporary, you already lost. If the answer is the string might move, you already lost.

What I want left running

Run the bad file before you run the fixed file. Did ASan fail on the first binary and pass on the second? If not, your flags are wrong and your lesson is fake.

If you already have a free server session, use it for that pair. Paste the failing report back into the hypothesis prompt. Leave secrets off the box, and leave the model off the merge button.

Top comments (1)

Collapse
 
loren_sl profile image
Loren •

Dangling views are hard to catch after the fact. In a minidump from a build without sanitizers, you can often spot one by finding the view's pointer in a freed or recycled stack or heap region.