A silenced narrowing cast can still ship a truncated length.
I built a lab repro so you can watch that lie form.
Would you have merged the cast just to keep CI green?
The conclusion first
Reject oversized frames before you store a short length.
A green binary is not proof that the length still fits.
Ask a model for causes, never for a silencing patch.
The symptom I froze
The silenced program should print a stored length of 16.
The buffer should still hold 65552 payload bytes after the pack.
The accept check should return true, which ought to scare you.
How can a short length bless a much longer buffer?
No sanitizer fired, and no exception left the function.
The happy path used 12-byte frames, so it stayed green.
Why did every fixture stay under a 16-bit ceiling?
Nobody sent a payload past 65535, so the lie never printed.
A green suite can be a small-input suite in disguise.
Why the check passed
The check asked whether length was within the buffer.
Sixteen is inside 65552, so the predicate returned true.
Truncation forged a small length, and the guard believed it.
That's the pitfall I want you to remember.
A bounds check can pass because the length was damaged.
Did your last review look for that shape of bug?
The arithmetic, without drama
65552 is 0x10010, and a 16-bit store keeps 0x0010.
The high half vanishes, and 16 remains in the field.
The compiler can warn, unless a cast already hid it.
I'm walking a constructed listing, not a customer outage.
You should compile it, because my confidence is not your proof.
Treat every number below as something you can recalculate.
1. Freeze one bad line
Save the printed line before you rename the function.
I kept the triple stored=16, actual=65552, and accept=1.
If the line changes, you are debugging a different bug.
2. Cut until one file remains
I cut the story down until one main remained.
I kept one oversized size, not a loop of random sizes.
Did the false accept survive that cut, or did it vanish?
If the false accept survives, the bug lives in pack.
That cut should take minutes, and it removes three fake causes.
Threads, disks, and clocks were no longer in the story.
3. Make the narrowing fatal
Compile with -Werror=conversion before you edit a single line.
That flag turns the narrowing conversion into a failed build.
Why was that flag absent from the default debug target?
g++ -std=c++20 -Wall -Wextra -Werror=conversion -c frame_bug.cpp
echo "bug_compile=$?"
g++ -std=c++20 -Wall -Wextra -Werror=conversion -o frame_silenced frame_silenced.cpp
./frame_silenced
If this command fails, read the first diagnostic only.
The diagnostic should mention a conversion that may change the value.
Exact wording varies, so match the meaning, not the spelling.
An explicit static_cast will not trip -Wconversion at all.
That's why the silencing patch looks like a fix.
Don't add a cast just to reach the linker.
The silenced file should compile under that same flag.
Success here is the trap, not the all-clear.
A quiet build is the thing we are trying to distrust.
Listings you can compile
Start with the implicit bug, then build the silenced twin.
The first file should fail, and the second file should link.
The silenced build should print stored=16 actual=65552 accept=1.
That line is arithmetic, so confirm it on your compiler.
If your printout differs, stop and check the integer width.
// frame_bug.cpp: implicit narrowing. Expect -Werror=conversion to fail.
#include <cstdint>
#include <vector>
struct Frame {
uint16_t len;
std::vector<char> bytes;
};
Frame pack(const std::vector<char>& payload) {
Frame frame;
frame.len = payload.size();
frame.bytes = payload;
return frame;
}
int main() {
std::vector<char> big(65552, 'x');
return pack(big).len;
}
// frame_silenced.cpp: the cast hides the warning and keeps the lie.
#include <cstdint>
#include <iostream>
#include <vector>
struct Frame {
uint16_t len;
std::vector<char> bytes;
};
Frame pack(const std::vector<char>& payload) {
Frame frame;
frame.len = static_cast<uint16_t>(payload.size());
frame.bytes = payload;
return frame;
}
bool accept_buggy(const Frame& frame) {
return frame.len <= frame.bytes.size();
}
int main() {
std::vector<char> big(65552, 'x');
Frame frame = pack(big);
std::cout << "stored=" << frame.len
<< " actual=" << frame.bytes.size()
<< " accept=" << accept_buggy(frame) << "\n";
return accept_buggy(frame) ? 0 : 1;
}
4. Ask for causes, not a patch
MonkeyCode is relevant here only as a guess list.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
I used free model access to list causes, not to edit code.
I forbade a patch, a refactor, and a style cleanup.
I did not claim a model name, because none was verified here.
I will not call it open source without a repository link.
Free model access is the only availability claim I will use.
A quota number would be a guess, so I will not print one.
A free server option is not a hardware spec sheet.
The replies were mixed, which is the useful part.
One reply blamed a data race in a single-threaded main.
Another blamed allocator reuse without a dangling pointer.
The tempting reply said to cast the size and continue.
Would you have taken that reply because it compiled?
Which guess can a single-threaded main even support?
I rejected the cast reply before I touched the file.
A helpful tone is not evidence, and it never was.
The printed numbers already pointed at a short store.
5. Put the file on a second machine
I'd place this file on a free server option next.
I want a different box, not a heroic specification.
I will not name a CPU, a region, or an uptime promise.
Those facts were not supplied as stable primary sources.
If the plan page differs, the plan page wins.
Check that page yourself before you script a job.
Copy the file, compile it, and compare the printed line.
If the second box prints a different length, stop.
You may be mixing ABIs, and that is a new bug.
6. Add the failing test before the fix
I wrote the expectation while the bug still passed.
The test sends 65552 bytes and demands rejection.
A test that only sends 12 bytes would rubber-stamp the lie.
Remember that 65535 must pass, and 65536 must fail.
This test is part of the lab listing, not a coverage report.
Run it, and do not trust a screenshot of my terminal.
// frame_fixed.cpp: reject first, then cast.
#include <cassert>
#include <cstdint>
#include <vector>
enum class Status { Ok, TooBig };
Status pack_checked(const std::vector<char>& payload, uint16_t* len) {
if (payload.size() > 65535u) {
return Status::TooBig;
}
*len = static_cast<uint16_t>(payload.size());
return Status::Ok;
}
int main() {
std::vector<char> symptom(65552, 'x');
std::vector<char> edge(65536, 'x');
std::vector<char> max_ok(65535, 'x');
uint16_t len = 0;
assert(pack_checked(symptom, &len) == Status::TooBig);
assert(pack_checked(edge, &len) == Status::TooBig);
assert(pack_checked(max_ok, &len) == Status::Ok);
assert(len == 65535);
return 0;
}
If the assert does not fire on the old function, your test is wrong.
Wire this file to the fixed function, not to the silenced one.
A test that calls pack_silenced will stay green forever.
7. Fix the contract, then allow the cast
I reject any payload larger than UINT16_MAX first.
Only then do I store the size in a uint16_t.
The cast is now a checked conversion, not a gag.
The receiver must not treat a shorter length as a valid frame.
A smaller length than the buffer is a protocol error here.
Equality with the original size is the check I want.
8. Re-run debug, optimized, and sanitized builds
I want three commands, and I want the same rejection.
Optimization must not revive a truncated store through new inlining.
Sanitizers may stay silent, because this bug is not a wild pointer.
Does that silence mean the program is safe?
No, it means you asked the wrong detector.
Warnings were the detector, and the boundary test is the lock.
g++ -std=c++20 -Wall -Wextra -Werror=conversion -O0 -o frame_dbg frame_fixed.cpp
g++ -std=c++20 -Wall -Wextra -Werror=conversion -O2 -o frame_opt frame_fixed.cpp
g++ -std=c++20 -Wall -Wextra -Werror=conversion -fsanitize=address,undefined -o frame_san frame_fixed.cpp
./frame_dbg && ./frame_opt && ./frame_san
echo "all_exit=$?"
A small decision table
I use this table when a warning and a model disagree.
It is a habit, not a score and not a benchmark.
| What you see | Next move |
|---|---|
| Narrowing warning on a length field | Keep that warning fatal |
| Every fixture fits in the short field | Add one oversized payload |
| Model says cast and continue | Reject that reply |
| Bounds check uses the stored short length | Treat the check as suspect |
| Oversized payload returns TooBig | Review the other call sites |
| Debug and -O2 disagree | Stop and diff the checks |
Read the row, then do only that next move.
Do not skip to a rewrite because the table feels slow.
Slow is cheaper than a frame parser that trusts a lie.
Limits you should say out loud
This file is a teaching repro, and your protocol may differ.
A 16-bit length is the stand-in, not a claim about your wire.
This repro assumes size_t is wider than 16 bits.
I did not measure throughput, latency, or cost per token.
I did not verify a token grant, a hardware SKU, or a duration.
Free access can be limited, changed, or removed without this page updating.
A model can sound sure while naming a race you do not have.
A second machine can hide an ABI mismatch if you skip the printout.
If your compiler lacks -Wconversion, do not assume you are safe.
I would not call this a security audit.
I would not paste secrets into the prompt to get a better guess.
Who should not use this flow
Skip it if you need a validated, signed build environment.
Skip it if the failing input contains private customer data.
Skip it if you cannot reduce the bug to a file you can share.
Skip it if the failure needs a logic analyzer, not a compiler.
A free server is a spare desk, not a certified lab.
A chat box is a bad place for a private crash dump.
What I want you to reuse
Freeze the bad line before you debate names.
Cut unrelated code until one translation unit remains.
Turn the relevant warning into an error before you edit.
Ask for three causes, and ban the patch.
Write the boundary test while the bug still passes.
Recompile on a second machine and compare one line.
One invitation, then stop
If you need a spare compile box, MonkeyCode's free server option can start you.
Read the live plan page before you automate against it.
Then run this repro, and keep the warning fatal.
That is the only invitation in this piece.
I will not pretend the tool found the bug.
The arithmetic found it, and the test kept it dead.
Would I ship the silenced file because CI turned green?
I would not ship it, not even with a green badge.
The tool only sped up a list of wrong guesses.
Top comments (0)