Skip to content

Rust verifier: reject degenerate vtables and verify union values of unknown discriminants - #9290

Open
rodry957 wants to merge 3 commits into
google:masterfrom
rodry957:rust-verifier-union-and-vtable-soundness
Open

rodry957 wants to merge 3 commits into
google:masterfrom
rodry957:rust-verifier-union-and-vtable-soundness

Conversation

@rodry957

Copy link
Copy Markdown

Problem

A buffer that passes the official Rust verifier flatbuffers::root::<T>() can still make the
public safe API read out of bounds. Under Miri this surfaces as
Undefined Behavior: memory access failed ... beyond the end of the allocation.

That violates the core soundness contract of a verifier: once verification succeeds, every
read performed through the safe accessors must stay inside the buffer.

Root cause

Two independent holes in the Rust path.

  1. Degenerate vtables. Verifier::visit_table reads vtable_len from the buffer and then
    calls range_in_buffer(vtable_pos, vtable_len). For vtable_len == 0 that range check
    trivially succeeds (an empty range lies inside any buffer), so the table is accepted. The
    safe Table/VTable API then reads object_inline_num_bytes() at loc + 2 (and
    num_fields, which would underflow) — addresses never covered by the range check, which
    can lie past the end of the buffer.

  2. Union values with an unknown discriminant. The generated verifier for unions ended in
    _ => Ok(()): when the discriminant was not one of the known variants, the union payload
    was never verified. A Rust reader still interprets a union value as
    ForwardsUOffset<Table> — that is what the generated _as_* / union accessors construct —
    so the safe API builds and traverses a Table at an offset the verifier never checked.

Fix (3 changes)

  1. rust/flatbuffers/src/verifier.rs: visit_table now rejects a vtable shorter than the two
    mandatory header VOffsetTs (vtable_len < 2 * SIZE_VOFFSET), returning a
    range-out-of-bounds error before anything is read through that vtable.
  2. rust/flatbuffers/src/verifier.rs: new impl Verifiable for Table<'a>, which verifies a
    table without knowing its schema — the soffset at pos must dereference in bounds and the
    vtable it points to must lie completely inside the buffer.
  3. src/idl_gen_rust.cpp: for unions, the unknown-discriminant arm now emits
    v.verify_union_variant::<::flatbuffers::ForwardsUOffset<::flatbuffers::Table<'static>>>("unknown", pos)
    instead of _ => Ok(()), so the payload is verified as a table before any safe accessor can
    reach it.

How it was found

A differential harness over generated Rust readers: the same inputs are run against the
generated verifier and against the safe accessors under Miri, and cases where verification
succeeds but a safe accessor reads out of bounds are minimized.

Verification status

  • The PoCs that previously produced out-of-bounds reads through the safe API are now rejected
    by verification.
  • cargo test --lib in the rust/flatbuffers crate: 6 passed, 1 failed. That failing test
    fails identically on unpatched upstream master, i.e. it is pre-existing and unrelated to
    this change.

Happy to add regression tests for both cases if you would like them — say the word and I will
push them to this branch.

…nknown discriminants

The Rust verifier accepted vtables shorter than the two mandatory header VOffsetTs, and the generated verifier for unions silently accepted values whose discriminant was unknown. Both could be read out of bounds through the safe API.
@github-actions github-actions Bot added c++ rust codegen Involving generating code from schema labels Sep 29, 2026
@rodry957

Copy link
Copy Markdown
Author

@vglavnyy - flagging this for review since you're listed as a known contributor for the flatbuffers project in OSS-Fuzz.

Context: the corresponding fuzz target for this fix is google/oss-fuzz#16202 - a Rust target that exercises the verifier plus the public safe reader path (root::() -> table -> vtable -> inline size). On the unfixed crate it reproduces an out-of-bounds read (Miri: object_inline_num_bytes accessing 2 bytes past the end of the allocation); with this patch applied the same input is rejected by the verifier and the reader returns cleanly.

Both PRs are small and self-contained: this one adds the verifier hardening plus regression tests, and the OSS-Fuzz one only adds the target for projects/flatbuffers. Would appreciate a look when you have a moment.

@rodry957

Copy link
Copy Markdown
Author

@aardappel - adding a maintainer ping here (I'd pinged @vglavnyy earlier). Summary: the Rust verifier accepts a buffer that then makes the public SAFE API read out of bounds (degenerate vtable / union value with unknown discriminant); the fix rejects those and verifies the union value slot, with 6 regression tests. Corresponding oss-fuzz target: google/oss-fuzz#16202. Full ASan reproduction is on the Buganizer report. Thanks for the look.

@rodry957

rodry957 commented Oct 2, 2026

Copy link
Copy Markdown
Author

A small update that might help move this along: the sibling fix from the same batch just landed - google/filament#10493 (same class of bug: an out-of-range binding reaching a fixed-size descriptor structure) was merged today after three maintainer approvals. This one is the Rust verifier soundness fix, with 6 regression tests, green CLA and green checks - it has been waiting for a reviewer for a while now. Would one of you be able to take a look? Happy to adjust anything you'd like.

@rodry957

rodry957 commented Oct 2, 2026

Copy link
Copy Markdown
Author

@dbaileychess @aardappel - one more nudge here, with some context that may help: the sibling fix from the same batch (google/filament#10493, same class of bug) was merged two days after three maintainer approvals, so this kind of fix does land. This one is a two-file change in the Rust path (verifier.rs + the generator) with six regression tests; CLA and all checks are green, and it has been waiting for a first review since Sept 29. Would one of you have a moment to look? Also, if it is more useful, I am happy to contribute the equivalent coverage as an in-repo Rust fuzz target (we already have a working one that drives the safe verifier path) - just say which you prefer.

@rodry957

Copy link
Copy Markdown
Author

@aardappel @dbaileychess — a status note with a precedent, since this has been waiting for a first review since 2026-09-29.

The same batch of fixes produced google/filament#10493, which landed on 2026-10-02 after three maintainer approvals. Nothing about this one is different in kind or in risk:

  • Two files, +320/−2: rust/flatbuffers/src/verifier.rs (+316) and src/idl_gen_rust.cpp (+4). 6 regression tests included in the same file.
  • CLA green, 7 checks green, mergeable: true. The red-looking status in the UI comes from action_required runs (first-time-contributor gate), not from a failing test.
  • Impact: the Rust verifier accepts buffers that the public safe API then reads out of bounds — a degenerate vtable, and a union value whose discriminant is unknown. Miri reproduces the OOB read on the unpatched crate; with this patch the verifier rejects the same input and the reader returns cleanly.
  • Full ASan reproduction is on the Buganizer report; the corresponding OSS-Fuzz target is flatbuffers: add a Rust fuzz target for the verifier and reader oss-fuzz#16202.

Two sibling PRs from the same batch are also waiting with zero review: #9297 (in-repo fuzz target for the safe verifier path) and #9298 (off-by-one in VTable::get_field). They are independent and small — if reviewing them as a set is easier, that works too.

Happy to reshape anything — naming, test placement, or squashing all three into one PR if that is the preference.

@rodry957

rodry957 commented Nov 1, 2026

Copy link
Copy Markdown
Author

Status on the CI here, since the notification that went out is misleading:

The CI run on the previous head did not fail a test — it never ran a job. Run 37057059631 (head 64ee4c1d) closed as failure today with zero jobs; the API reports total_count: 0 and the UI shows every job stuck at Waiting for pending jobs. Same pattern as the 30-Sep run on 5c2ef307. So there is no compile or test result to read in either case.

What I did: brought all three branches up to date with master (they were 4 commits behind, no conflicts):

On the new heads check-changes, label and cla/google are already green; the build matrix needs one more Approve and run from a maintainer because the branch is a fork. Once it starts, a real result will attach to a commit that is current with master — which is also what a merge would test.

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

Labels

c++ codegen Involving generating code from schema rust

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant