Harden Parser::Deserialize and reflection verification input contracts - #9222
Open
Grolar1337 wants to merge 4 commits into
Open
Harden Parser::Deserialize and reflection verification input contracts#9222Grolar1337 wants to merge 4 commits into
Grolar1337 wants to merge 4 commits into
Conversation
…ifier reads Parser::Deserialize(const uint8_t*, size_t) read the FlatBuffer file identifier via BufferHasIdentifier (a 4-byte strncmp at buf + 4/8) before any length or structural validation. A short attacker-supplied buffer made strncmp read past the end of the heap allocation (ASan: heap-buffer-overflow READ of size 1). The caller-supplied size was only used to construct the Verifier, which runs after the identifier check. Add a size < FLATBUFFERS_MIN_BUFFER_SIZE pre-check at the entry so buffers that cannot contain the identifier are rejected before any read. FLATBUFFERS_MIN_BUFFER_SIZE (base.h) covers the longest form (size-prefixed identifier: 2*sizeof(uoffset_t) + kFileIdentifierLength). Regression test: a 10-byte buffer must be rejected without a crash. Signed-off-by: Grolar1337 <blkyakupsait@gmail.com>
…/enums/services loops)
Parser::Deserialize's definition loops used the pattern:
if (symbols.Add(name, def)) { delete def; return false; }
SymbolTable::Add appends the element to its owning vector before checking
for a duplicate, so on a duplicate the just-deleted pointer is still in the
vector and ~SymbolTable deletes it a second time at Parser destruction -- a
heap double-free / use-after-free reachable from a crafted .bfbs containing
duplicate object/enum/service names (which VerifySchemaBuffer does not
reject).
On each Add()==true failure path, pop the element back out of the owning
vector before deleting it (Add appended it last). Applies to the objects,
enums and services loops and their companion Type registrations in
types_.vec. In the services loop, distinguish the two failure modes: a
failed ServiceDef::Deserialize never registered the object (delete without
pop), while a duplicate Add did register it (pop then delete).
SymbolTable::Add itself is unchanged, so no other caller changes behavior.
Regression test: a schema whose second enum is patched to duplicate the
first's qualified name must deserialize to false without a crash.
Signed-off-by: Grolar1337 <blkyakupsait@gmail.com>
Type::Deserialize cast the wire base_type/element values to the internal BaseType enum with an unchecked static_cast. The wire field is a signed byte (reflection::BaseType stored as int8_t); values outside 0..18 (e.g. reflection::MaxBaseType == 19, or any negative value) have no case in the internal set and previously reached SizeOf()/TypeName()/StringOf() FLATBUFFERS_ASSERT(0) defaults in debug builds, or produced wrong size metadata (SizeOf returning 0) in release builds. Reject out-of-range wire values in Type::Deserialize before the cast, using reflection::Vector64 as the largest representable internal type. Fuzzer-found (local target being upstreamed). Regression test: a field type patched to base_type == MaxBaseType (19) must deserialize to false. Signed-off-by: Grolar1337 <blkyakupsait@gmail.com>
VerifyObject's field-type switch asserted on reflection::None. A valid schema never contains a None-typed field, so the branch was treated as unreachable -- but a crafted schema can carry base_type == None through VerifySchemaBuffer (structural verification does not reject it), turning reflection::Verify into an assertion failure (debug) or a silently skipped field (release). Return false for a None-typed field so the verifier reports the schema as invalid instead of aborting. Fuzzer-found (local target being upstreamed). Regression test: a field patched to base_type == None must make reflection::Verify return false without asserting. Signed-off-by: Grolar1337 <blkyakupsait@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four hardening fixes, each with regression tests, on a clean branch from
master(5761d6e). This PR is independent offix/reflection-union-verify(VerifyUnion); it does not touch
VerifyUnion/ union tag handling.Commits
f7863f44Harden Parser::Deserialize entry: validate buffer length before identifier readsLength validation before identifier reads (fixes a reported OOB read — VRP report).
5bf610f5Fix Parser::Deserialize double-free on duplicate .bfbs names (objects/enums/services loops)SymbolTableownership on Deserialize error paths (fixes a reported double-free — VRP report).dd30a3a2Validate base_type/element range in Type::DeserializeFuzzer-found: prevents
SizeOf()'s default case with out-of-range wire values.876a5aa0VerifyObject: reject None-typed fields instead of assertingFuzzer-found: return false instead of assert.
Fixes (3) and (4) close issues found by a local fuzz target being upstreamed
(separate PR).
Summary of changes
src/idl_parser.cppParser::Deserialize(buf,size): rejectsize < FLATBUFFERS_MIN_BUFFER_SIZEbefore identifier reads (commit 1);Parser::Deserialize(Schema*):pop_back()from owningSymbolTablevec beforedeleteon duplicate in the objects/enums/services loops (commit 2);Type::Deserialize: rejectbase_type/elementoutside 0..18 before thestatic_cast(commit 3)include/flatbuffers/idl.hsrc/reflection.cppVerifyObject:reflection::None-typed field →return falseinstead ofFLATBUFFERS_ASSERT(commit 4)tests/reflection_entry_security_test.cpptests/reflection_test.h,tests/test.cpp,CMakeLists.txtReflectionEntrySecurityTest()Fix details
1 — entry length check.
Parser::Deserialize(const uint8_t*, size_t)called
BufferHasIdentifier(a 4-bytestrncmpatbuf + 4/8) before anylength validation. A short attacker-supplied buffer (10 bytes) made
strncmpread past the heap allocation (ASan:
heap-buffer-overflow READ of size 1).Now
size < FLATBUFFERS_MIN_BUFFER_SIZEis rejected up front.2 — Deserialize double-free.
SymbolTable::Addappends before itsduplicate check; on a duplicate the Deserialize error paths
deleted anelement still present in the owning vector, and
~SymbolTablefreed it again.On each
Add()==truefailure path wepop_back()the element (Add appendedit last) before
delete. The services loop now distinguishes a failedServiceDef::Deserialize(never registered, plain delete) from a duplicateAdd(pop then delete).SymbolTable::Additself is unchanged, so no othercaller changes behavior.
3 — base_type range.
Type::Deserializecast the wirebase_type/element(a signed byte, values 0..18 valid) with an uncheckedstatic_cast; out-of-range values (e.g.MaxBaseType== 19, negatives)reached
SizeOf()'sFLATBUFFERS_ASSERT(0)default in debug, or wrong sizemetadata in release. Out-of-range values are now rejected before the cast.
4 — None-typed field.
VerifyObjectasserted on a field whosebase_type == reflection::None(impossible in a valid schema, but craftablethrough
VerifySchemaBuffer). Now treated as a verification failure(
return false).Verification
Debug / ASan (UAF + double-free focus),
detect_leaks=0Output (relevant lines):
No use-after-free / double-free reported by ASan. (
LSANwithdetect_leaks=1reports a pre-existing 48-byte leak inCrossNamespacePackTestthat is also present on pristinemaster(verified in a master worktree); it is unrelated to this PR.)
Release (
-DNDEBUG, default CMake Release)Output:
Regression behavior (pre-fix vs post-fix)
Parser::Deserialize: pre-fix ASanheap-buffer-overflowin
strncmp; post-fix returnsfalse, no crash..bfbstoParser::Deserialize: pre-fix heap double-free at~Parser; post-fix returnsfalse, no crash.base_typeout-of-range (MaxBaseType=19) crafted Type: pre-fixFLATBUFFERS_ASSERT(0)inSizeOf(debug); post-fixDeserializereturnsfalse.None-typed field verified viareflection::Verify: pre-fixFLATBUFFERS_ASSERTinVerifyObject; post-fix returnsfalse.PR body notes for maintainers
harden/deserialize-input-contract(frommaster5761d6e).reflection_verify_fuzzerharness used for fuzzing is being upstreamed in aseparate PR; not included here.