Guard GetValFromStream against out-of-bounds reads in release builds - #3821
Guard GetValFromStream against out-of-bounds reads in release builds#3821mcfnord wants to merge 1 commit into
Conversation
GetValFromStream validated its arguments only with Q_ASSERT, which is compiled out in release builds (the shipped configuration). CVector uses std::vector's unchecked operator[], so a truncated or malformed network message could drive a read past the end of the buffer with no protection. Add a runtime guard alongside the existing asserts: return 0 when iNumOfBytes is out of the 1..4 range or when fewer than iNumOfBytes bytes remain. The asserts are kept so developer errors still fail loudly in debug builds, while release builds no longer perform the out-of-bounds read. Fixes jamulussoftware#3819
| Q_ASSERT ( vecIn.Size() >= iPos + iNumOfBytes ); | ||
|
|
||
| // The asserts above are compiled out in release builds, so also guard at | ||
| // runtime: CVector uses unchecked operator[], and reading past the end of a |
| // truncated/malformed network message would be an out-of-bounds access. | ||
| if ( ( iNumOfBytes < 1 ) || ( iNumOfBytes > 4 ) || ( vecIn.Size() < iPos + iNumOfBytes ) ) | ||
| { | ||
| return 0; |
There was a problem hiding this comment.
Here's the question what it should actually return...
There was a problem hiding this comment.
This is conflating an error indication with a fetched value. In my view, the correct fix would be for the GetValFromStream() and GetCountryFromStream() functions to follow the format of GetStringFromStream(), where the output variable is passed by reference as a parameter, and the return value is a bool indicating error status (true) or success (false). Every fetch would then be validated and could abort the Evaluate function as soon as a malformation, bounds overflow or bad value was detected.
This would require all calls within protocol.cpp to be reworked, but I think it's worth doing, and shouldn't take too long. I'm happy to take that on if agreed,
Also, the runtime checks on iNumBytes are only checking against programmer error, not input data error, since every call to GetValFromStream() passes a constant 1, 2 or 4 as iNumBytes.
There was a problem hiding this comment.
The data from the wire is definitely unchecked at the moment then?
There was a problem hiding this comment.
It looks like some values are checked, and others are just used without checking.
|
It's probably worth understanding where it is called and what would happen if 0 is returned. |
|
Note 📡 STAND BY FOR AN LLM-AUTHORED MESSAGE. Good questions — here's the call-site trace behind returning Where it's called. The call sites come in two shapes:
What happens when On your two inline notes:
|
If this is true, we could probably get away by just adding a |
|
MY LLM WROTE: On the The precondition ( That leaves this guard covering a different failure than the There's no efficiency case for the swap either: the call-site trace above already shows the guard costs nothing on well-formed traffic (never taken) and can't affect the list-loop callers (each already bounds-checks per iteration). It only does anything on a malformed body — where the alternative is the unchecked |
|
MY LLM WROTE: @softins You're right on the The half that does the work is On the larger refactor: I agree it's the correct end state, and I'd rather have it than what I wrote. My earlier comment defended returning One argument for your design that hasn't been raised: So — @ann0see, this wants a decision more than it wants more analysis from me. Two coherent options:
I have no stake in which. softins offered to take the refactor on, and if that's the plan I'm happy to close this PR now so it isn't sitting in the queue implying a decision that's already been made — just say the word. The only outcome I'd argue against is the thread staying open with neither happening, since the unchecked read is live in shipped release builds today either way. |
Fixes #3819
GetValFromStreamvalidates its arguments only withQ_ASSERT, which is compiled out in release builds — the configuration everyone actually ships.CVectorderives fromstd::vectorand uses its uncheckedoperator[], so a truncated or malformed network message can drivevecIn[iPos]past the end of the buffer with no protection at all in release.Approach
Keep the asserts (developer errors still fail loudly in debug builds) and add a runtime guard that returns
0wheniNumOfBytesis outside 1..4 or when fewer thaniNumOfBytesbytes remain:Returning 0 (rather than crashing) keeps the existing "malformed input is tolerated, not fatal" posture of the surrounding parser — the sibling
GetStringFromStreamalready returns an error code on short input rather than aborting. This is the minimal, hot-path-safe change and touches no call sites.Open question for maintainers, per the issue: if a hard failure / disconnect on malformed input is preferred over silently returning 0, that would be a larger change (the function has no error channel today, so it would need a signature change across ~50 call sites). Happy to go that direction if you'd rather.
Testing
clang-format-14clean