Skip to content

Fix missing bounds checks in Table.VectorLen/Vector (Python, Go, Java) - #9411

Open
behnam1990adel-hash wants to merge 1 commit into
google:masterfrom
behnam1990adel-hash:fix-vectorlen-bounds-check
Open

behnam1990adel-hash wants to merge 1 commit into
google:masterfrom
behnam1990adel-hash:fix-vectorlen-bounds-check

Conversation

@behnam1990adel-hash

Copy link
Copy Markdown

Problem

Table.VectorLen() and Table.Vector() in the Python, Go, and Java implementations read a vector's relative-offset and length fields straight out of the buffer with no bounds checking. If either field is corrupted, nothing stops a crash.

Go panics (slice bounds out of range), Java throws IndexOutOfBoundsException, Python raises struct.error. None of the three languages ship a verifier, so generated code has no way to catch this before it happens.

ByteVector() in go/table.go already got this treatment in #8776. VectorLen() and Vector() are called for every single vector field access and were still unguarded.

Fix

Same pattern as the existing ByteVector fix: validate that both the relative-offset read and the length read land inside the buffer before trusting either value.

  • VectorLen() returns 0 instead of an attacker-controlled length.
  • Vector() returns a position past the end of the buffer instead of a wild address when it can't safely compute the real one.

Same logic, same two-stage check, in all three languages.

Before / after

With the vector's length field corrupted to 0x7FFFFFFF:

  • before: InventoryLength() returns 2147483647 with no error, then Inventory(0) throws struct.error: unpack_from requires a buffer of at least 37 bytes...
  • after: InventoryLength() returns 0

Corrupting the relative-offset field instead reproduces the same class of bug immediately on the call itself: Go panics inside VectorLen() (slice bounds out of range), Java throws IndexOutOfBoundsException, Python raises struct.error.

Standalone repro

No dependencies beyond the Python standard library, does not require the flatbuffers package to be installed:

#!/usr/bin/env python3
"""
Standalone PoC: google/flatbuffers Table.VectorLen() / Table.Vector()
missing bounds checks.

Logic below (Offset/VectorLen/Vector) is a direct, unmodified port of
python/flatbuffers/table.py as of commit
b8431fbcd7a5c71817f314e18b332c0648554efa (google/flatbuffers), using
struct.unpack_from in place of the package's encode.Get/N helpers,
which are themselves thin wrappers around struct.unpack_from.

Run:
    python3 poc.py
"""

import struct

UOFFSET = "<I"   # uint32, matches flatbuffers.packer.uint32
SOFFSET = "<i"   # int32,  matches flatbuffers.packer.int32
VOFFSET = "<H"   # uint16, matches flatbuffers.packer.voffset


class Table:
    def __init__(self, buf, pos):
        self.Bytes = buf
        self.Pos = pos

    def Offset(self, vtable_offset):
        vtable = self.Pos - struct.unpack_from(SOFFSET, self.Bytes, self.Pos)[0]
        vtable_end = struct.unpack_from(VOFFSET, self.Bytes, vtable)[0]
        if vtable_offset < vtable_end:
            return struct.unpack_from(VOFFSET, self.Bytes, vtable + vtable_offset)[0]
        return 0

    def VectorLen(self, off):
        off += self.Pos
        off += struct.unpack_from(UOFFSET, self.Bytes, off)[0]
        return struct.unpack_from(UOFFSET, self.Bytes, off)[0]

    def Vector(self, off):
        off += self.Pos
        x = off + struct.unpack_from(UOFFSET, self.Bytes, off)[0]
        x += 4
        return x


GOOD = bytes.fromhex(
    "140000001000080000000000000000000000040010000000040000000300000001020300"
)
FIELD_SLOT = 14


def mk_evil(pos):
    buf = bytearray(GOOD)
    struct.pack_into(UOFFSET, buf, pos, 0x7FFFFFFF)
    return bytes(buf)


EVIL_OFFSET = mk_evil(24)
EVIL_LENGTH = mk_evil(28)


def load(buf):
    pos = struct.unpack_from(UOFFSET, buf, 0)[0]
    return Table(buf, pos)


def main():
    t = load(GOOD)
    print("good buffer: VectorLen =", t.VectorLen(t.Offset(FIELD_SLOT)))

    try:
        t = load(EVIL_OFFSET)
        n = t.VectorLen(t.Offset(FIELD_SLOT))
        print("evil_offset: VectorLen() returned", n, "(no exception)")
    except Exception as e:
        print("evil_offset: VectorLen() CRASHED ->", type(e).__name__, "-", e)

    try:
        t = load(EVIL_OFFSET)
        x = t.Vector(t.Offset(FIELD_SLOT))
        print("evil_offset: Vector() returned", x, "(no exception, out-of-bounds address)")
    except Exception as e:
        print("evil_offset: Vector() CRASHED ->", type(e).__name__, "-", e)

    try:
        t = load(EVIL_LENGTH)
        o = t.Offset(FIELD_SLOT)
        n = t.VectorLen(o)
        print("evil_length: VectorLen() returned", n, "(bogus, no exception yet)")
        start = t.Vector(o)
        for i in range(n):
            _ = t.Bytes[start + i]
    except Exception as e:
        print("evil_length: element access CRASHED ->", type(e).__name__, "-", e)


if __name__ == "__main__":
    main()

Testing

  • Added regression tests in tests/py_test.py, tests/go_test.go, java/src/test/java/JavaTest.java covering both the corrupted-length and corrupted-offset cases.
  • Full existing Python suite still passes (129/129).
  • No behavior change for well-formed buffers.

Related work

A University of Hamburg master's thesis ("Security Evaluation and Vulnerability Assessment of FlatBuffers", Finn Sell) independently documented this exact crash behavior for Go, Java, and Python as part of a broader language comparison. This PR doesn't claim to be the first to notice the gap — it's a concrete fix for it, following the same approach already merged for ByteVector.

@google-cla

google-cla Bot commented Oct 14, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces bounds checking to vector length and vector data retrieval functions across Go, Java, and Python implementations of FlatBuffers to prevent crashes and out-of-bounds reads from corrupted or malicious buffers, along with corresponding regression tests. However, the feedback highlights critical security vulnerabilities in the new checks across all three languages, where additions like off += t.Pos or negative offsets can cause integer overflows or bypasses, allowing malicious buffers to still evade the bounds checks. The reviewer has provided robust, overflow-safe suggestions to resolve these issues.

Comment thread go/table.go
Comment thread go/table.go
Comment thread java/src/main/java/com/google/flatbuffers/Table.java
Comment thread java/src/main/java/com/google/flatbuffers/Table.java
Comment thread python/flatbuffers/table.py
Comment thread python/flatbuffers/table.py
@behnam1990adel-hash
behnam1990adel-hash force-pushed the fix-vectorlen-bounds-check branch from 92a6986 to 3fbda24 Compare October 16, 2026 11:46
@behnam1990adel-hash

Copy link
Copy Markdown
Author

Good catch — confirmed the bypass and fixed it in Go and Java with the overflow-safe pattern you suggested (compare with subtraction before any addition). Python doesn't have this exploitable gap since it has no fixed-width wraparound, but I applied the same pattern there too for consistency. Added a dedicated regression test for the wraparound case in all three languages. Pushed.

@behnam1990adel-hash

behnam1990adel-hash commented Oct 17, 2026 •

Copy link
Copy Markdown
Author

Heads-up: #9146 (by @owvr27) fixes the Go side of the same issue. This PR covers Go, Java and Python and adds regression tests for all three. I ran the Python, Go and Java tests locally after the last push and they pass. Happy to rebase or split it up if the maintainers prefer a different order.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant