The release build crashed every third launch. The debug build never crashed. The team blamed a race condition. The stack trace pointed nowhere useful. The crash moved when two source files swapped order. That last clue changed everything.
Finley inherited a plugin system written in C++. The host application loaded a shared library at runtime. The library exported a stable C API. Inside, forty translation units shared config helpers through headers. Some helpers were inline. Some were not. The code had worked for years. A new compiler flag exposed something old.
The clue in the symbol table
Finley started with nm. The symbol table showed duplicate weak symbols. Two translation units provided Config::normalize(int). One version scaled the value by two. The other added one. Both had the same mangled name. The linker picked one at link time. The choice depended on object file order. That is an ODR violation.
A text search could have found the two definitions. But the same prefix appeared in dozens of files. Reading them all would take days. Finley needed a filter. MonkeyCode provides free model access and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Finley used the model access as a triage step. The model did not prove anything. It only suggested which weak symbols were worth reading first. The free server then ran a clean reproduction that could prove the violation.
The two-stage workflow
Step 1: collect the weak symbols.
The script keeps only symbols marked weak. It sorts them and counts duplicates. The output becomes the triage input.
#!/usr/bin/env bash
set -euo pipefail
nm -C --defined-only libplugin.so \
| awk '$2 ~ /^[WwVv]$/ {print $2, $3}' \
| sort \
| uniq -c \
| sort -rn > weak_symbols.txt
cat weak_symbols.txt
Step 2: ask the free model to classify risk.
The prompt forces a structured answer. It tells the model not to guess. A wrong classification is cheap. A missed one is expensive.
# triage_symbols.py
import json
import os
import urllib.request
def main():
symbols = open('weak_symbols.txt').read()
endpoint = os.environ['MODEL_ENDPOINT']
prompt = (
'Classify C++ weak symbols by ODR risk. '
'Return a JSON array with fields: symbol, risk, reason. '
'Risk is high, medium, or low. '
'Do not claim a violation without seeing the definitions.'
)
payload = {
'messages': [
{'role': 'system', 'content': prompt},
{'role': 'user', 'content': symbols},
]
}
req = urllib.request.Request(
endpoint,
data=json.dumps(payload).encode(),
headers={'Content-Type': 'application/json'},
)
with urllib.request.urlopen(req) as resp:
print(json.dumps(json.loads(resp.read()), indent=2))
if __name__ == '__main__':
main()
The example output flagged Config::normalize as high risk. It also flagged two benign template instantiations. Finley ignored the benign ones. The model made a useful first pass. It did not make a decision.
{
"symbol": "Config::normalize(int)",
"risk": "high",
"reason": "Same symbol appears in two object files. One definition is inline, one is not."
}
Step 3: prove the violation with a two-order build on the free server.
The repro has two translation units. Each defines the same inline function differently. The host prints the result. The script builds the shared library in two different object orders. If the output changes, the ODR is real.
// config_a.cpp
inline int scale(int x) { return x * 2; }
int (*get_a())(int) { return &scale; }
// config_b.cpp
inline int scale(int x) { return x + 1; }
int (*get_b())(int) { return &scale; }
// main.cpp
#include <cstdio>
int (*get_a())(int);
int (*get_b())(int);
int main() {
std::printf("%d %d\n", get_a()(3), get_b()(3));
}
#!/usr/bin/env bash
set -euo pipefail
for order in "config_a.cpp config_b.cpp" "config_b.cpp config_a.cpp"; do
g++ -std=c++20 -O2 -fno-inline -fPIC -shared $order -o librepro.so
g++ -std=c++20 -O2 main.cpp -L. -lrepro -Wl,-rpath,'$ORIGIN' -o repro
echo "$order -> $(./repro)"
done
The same source code produced different results. Changing object file order changed the output. That is a proof. It does not depend on the model's opinion.
Step 4: apply the fix and verify the full build.
Finley moved Config::normalize into a single translation unit. The free server rebuilt with link-time optimization and ODR warnings enabled.
g++ -std=c++20 -O2 -flto -Wodr -Werror=odr -fPIC -shared src/*.cpp -o libplugin.so
Limitations
The model triage is not a proof. It can miss a weak symbol. It can flag a safe one as high risk. The free server may not match the production compiler, libc, or CPU. ODR violations that only appear under LTO need an LTO build. The two-order repro catches one class of ODR bug. It does not catch every one. Multi-threaded crashes may need a different run. The free model may hallucinate a reason for a symbol it has never seen. The fix still requires an engineer to read the two definitions.
Who should skip this
Teams with a fully deterministic build and an existing ODR checker do not need this workflow. Teams that cannot share symbol names with a third-party model should not send them. The workflow is for code that has already been cleared for such use. It is not a replacement for sanitizers or code review.
The useful next step is not to trust a model more. It is to add the two-order build check to the project's CI. If a free model and a free server are already available, start with the weak-symbol triage script before reading forty files.
Top comments (0)