A green debug run does not prove a view owns its bytes. Release can reuse that stack slot before the flush reads it. I start every split-brain crash with that suspicion.
This is a labeled reconstruction, not a ticket I closed. I did not run this harness for the numbers in this draft. The failure mode is still common enough to practice on purpose.
What broke
The exporter looks fine in an unoptimized test binary. A background flush can print junk names under -O2. Sometimes the process dies inside the stream insert for a view.
Why would optimization change a string that tests already checked? Because the view never held storage of its own. The temporary owner died at the end of the capture function.
1. Freeze the symptom
I keep one tiny input and one thread schedule. I do not add logs that allocate before the crash is stable. Extra allocations can hide the exact reuse I need to see.
I record the compiler, the flags, and the sanitizer set. I also record whether the flush ran on another thread. Those three notes stop me from chasing a different bug.
2. Bisect the flags
I rebuild the same source at -O0 and at -O2. I add AddressSanitizer and UndefinedBehaviorSanitizer to both builds. I leave frame pointers on so the stack stays readable.
g++ -std=c++20 -O0 -g -fno-omit-frame-pointer \
-fsanitize=address,undefined repro.cpp -o repro_dbg
g++ -std=c++20 -O2 -g -fno-omit-frame-pointer \
-fsanitize=address,undefined repro.cpp -o repro_rel
./repro_dbg ; echo dbg:$?
./repro_rel ; echo rel:$?
These commands are an unexecuted example for your own machine. I am not claiming a specific sanitizer line from a past run. Run them locally and keep the raw log beside the diff.
3. Draw the lifetime
Ask one blunt question at every return boundary. Who owns the bytes after this function returns to the caller? If the answer is only a view, mark that path as guilty.
Here is the broken capture shape from the lab harness. It is intentionally small so the bug cannot hide in helpers. Read it as a proposal you should compile, not as a trophy.
#include <iostream>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
struct Sample {
std::string_view name; // does not own bytes
};
Sample capture(int id) {
std::string tmp = "job-" + std::to_string(id);
return Sample{tmp}; // view dangles when tmp dies
}
int main() {
std::vector<Sample> batch;
batch.reserve(8);
for (int id = 0; id < 8; ++id) {
batch.push_back(capture(id));
}
std::thread flush([&batch] {
for (const Sample& sample : batch) {
std::cout << sample.name << "\n";
}
});
flush.join();
return 0;
}
The function builds a local string and returns a view into it. The local dies before the vector finishes storing that sample. The flush thread then reads storage that no longer exists.
Why did the unoptimized binary sometimes still look fine? The dead stack slot had not been overwritten yet. Optimization and the second thread reused that slot sooner.
4. Reject the plausible wrong fix
A coding assistant will often suggest a mutex around the flush. That can silence a data race that is not this bug. A lock does not extend the life of a dead temporary.
Another common patch copies the view into a second view. Copying a view copies the pointer, not the characters. You still have no owner after the capture function returns.
I write those wrong patches down so I do not reapply them. Then I ask which object is still alive at the use site. If nothing owns the bytes, the patch is only theater.
5. Fix ownership, then retest both builds
The sample should store a string, not a non-owning view. Views stay at the edges, where a caller still owns memory. I do not keep a view in a container that outlives its source.
struct Sample {
std::string name; // owns its bytes
};
Sample capture(int id) {
Sample out;
out.name = "job-" + std::to_string(id);
return out;
}
I rerun both optimization levels with the same sanitizer flags. I also add a test that reads the batch after capture returns. A test that only prints inside capture will hide this exact bug.
Should the flush still exist after the ownership fix? Yes, but it is no longer the prime suspect. I keep the thread so the schedule stays honest.
6. Where a free assistant and a free server fit
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
I use free model access to list lifetime hypotheses, not to declare a verdict. I ask for three causes ranked by how fast the owner dies. I discard any fix that does not name a living owner.
A free server option can host the two-flag rebuild if your policy allows it. I still treat quota, hardware, and uptime as unknown here. I will not quote a token total I have not verified today.
Paste only a public harness into a hosted assistant. If the code is confidential, run the matrix on your own machine. Would a polished answer alone be enough to merge?
No, I still want the crash or the clean log in my terminal. If you want this checklist on a throwaway sample, start with that free access. Keep the merge on your sanitizer log, not on the model's prose.
7. Limitations
This harness will not catch every dangling use you can write. A custom allocator can confuse AddressSanitizer's default allocation story. Short-string optimization can also mask a nearby but different bug.
Do not use this workflow to support a benchmark claim. I published no timings, no token totals, and no hardware notes. Those numbers change, and I will not invent them here.
Skip the hosted path if your team needs a real air gap. Skip it if policy forbids sending source to a vendor tool. Skip it if you need a guaranteed queue or a named model.
I cannot promise those properties from this article alone. Also skip the drill when the bug is a logic error. Sanitizers will not notice a wrong business rule in the name.
You still need an assertion on the value you meant to export. Who checks that the exported name still matches the id? I do, with one expect after the flush joins.
What I keep
I ask who owns the bytes before I ask why it crashed. I compare unoptimized and optimized builds before I trust a green test. I store owning strings in objects that must outlive the read.
A view is a borrow, and a borrow needs a living owner. If you cannot point at that owner, you do not have a fix. Is this reconstruction enough to ship a production patch alone?
No, it is a drill for the questions, not a certificate of safety. Your tree still needs your log, your flags, and your own review.
Top comments (0)