google / google/flatbuffers

Null Pointer Dereference in `JsonPrinter::PrintOffset()` for Union Types

Open
#9,033 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

*Note: This report was discovered through fuzzing and reproduced and polished by AI for clarity.*

| Field | Value |
|---|---|
| **CWE** | CWE-476 (NULL Pointer Dereference) |
| **Affected file** | `src/idl_gen_text.cpp`, lines 187-188 |
| **Tested on** | commit `e223d69b` (`flatc version 25.12.19`) |
| **Reproducible with** | Stock `flatc` binary (release build) |

## Description

`JsonPrinter::PrintOffset()` handles `BASE_TYPE_UNION` fields by
dereferencing `prev_val` — a pointer to the union type discriminator byte
in the serialised table. The only guard is `FLATBUFFERS_ASSERT(prev_val)`,
which is removed in release builds (`-DNDEBUG`).

When a malformed FlatBuffer has the union **value** field present but the
union **type** discriminator field absent (vtable offset zeroed), `prev_val`
is `nullptr`. Dereferencing it causes an immediate segmentation fault,
crashing `flatc` or any application that converts a FlatBuffer to JSON.

### Affected source (`src/idl_gen_text.cpp:184-188`)

```cpp
case BASE_TYPE_UNION: {
// If this assert hits, you have an corrupt buffer, a union type field
// was not present or was out of range.
FLATBUFFERS_ASSERT(prev_val); // gone in release builds
auto union_type_byte = *prev_val; // NULL DEREF
```

The comment on lines 185-186 acknowledges the corrupt-buffer scenario but
relies solely on an assert that does not survive release compilation.

## Confirmed Call Chain

Crash observed in the stock `flatc` binary with this exact stack trace:

```
main
flatbuffers::FlatCompiler::Compile()
flatbuffers::TextCodeGenerator::GenerateCode() [idl_gen_text.cpp]
flatbuffers::GenTextFile() [idl_gen_text.cpp:438]
flatbuffers::GenText() [idl_gen_text.cpp:419]
flatbuffers::GenerateTextImpl() [idl_gen_text.cpp:383]
flatbuffers::JsonPrinter::GenStruct() [idl_gen_text.cpp:315]
flatbuffers::JsonPrinter::GenFieldOffset() [idl_gen_text.cpp:282]
flatbuffers::JsonPrinter::PrintOffset() [idl_gen_text.cpp:188]
*prev_val → SIGSEGV (address 0x0)
```

`GenText()` does not call `Verifier` before traversing the buffer.
The public APIs `GenTextFromTable()` and `GenTextFile()` are equally affected.

## Step-by-Step Reproduction (using `flatc` binary)

This crash is reproducible with the stock `flatc` binary and a 40-byte
crafted file. **No custom C++ compilation required.**

### Prerequisites

```bash
git clone https://github.com/google/flatbuffers.git
cd flatbuffers
git checkout e223d69b # or any recent commit
cmake -S . -B build -DFLATBUFFERS_BUILD_FLATC=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build --target flatc
```

### Step 1 — Create a schema with a union field

Save as `union_crash.fbs`:

```fbs
table Inner { x: int; }
union MyUnion { Inner }
table Root { u: MyUnion; }
root_type Root;
```

### Step 2 — Generate a valid binary FlatBuffer

Save as `valid.json`:

```json
{ "u_type": "Inner", "u": { "x": 42 } }
```

Compile to binary:

```bash
build/flatc -b union_crash.fbs valid.json
# produces valid.bin (40 bytes)
```

### Step 3 — Corrupt the vtable (2-byte edit)

The corruption zeroes the vtable entry for the `u_type` field, making it
appear absent while the union value `u` is still present.

Save as `corrupt.py`:

```python
#!/usr/bin/env python3
import struct

with open("valid.bin", "rb") as f:
buf = bytearray(f.read())

# Follow root offset → root table → vtable
root_pos = struct.unpack_from("= 0) {
```

### Option B — Verify buffers before text generation

Add `Verifier::VerifyBuffer()` at the top of `GenText()` / `GenTextFile()`
to reject corrupt buffers before traversal. This would defend against this
bug and other unverified-buffer issues, but changes the public API contract:

```diff
--- a/src/idl_gen_text.cpp
+++ b/src/idl_gen_text.cpp
@@ -418,6 +418,10 @@ const char* GenText(const Parser& parser, const void* flatbuffer,
std::string* _text) {
FLATBUFFERS_ASSERT(parser.root_struct_def_);
+ flatbuffers::Verifier verifier(
+ static_cast(flatbuffer),
+ parser.builder_.GetSize());
+ if (!parser.root_struct_def_->Verify(verifier)) return "buffer verification failed";
auto root = parser.opts.size_prefixed
```

## Included Files

| File | Description |
|---|---|
| `reproduce.sh` | One-command reproducer: `bash reproduce.sh [/path/to/flatc]` |
| `corrupt.py` | Python script: corrupts `valid.bin` → `corrupt.bin` |
| `union_crash.fbs` | Schema with union field |
| `valid.json` | Valid JSON input |
| `valid.bin` | Valid 40-byte binary FlatBuffer (pre-built from schema + JSON) |
| `corrupt.bin` | Pre-corrupted binary (vtable u_type zeroed) — crashes flatc directly |
| `poc.cpp` | Standalone C++ PoC (alternative to the flatc reproduction) |

Attachment: [report1.zip](https://github.com/user-attachments/files/26581652/report1.zip)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.