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 |
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