From 429c28c783fe953e113c83c61118585bf539cf53 Mon Sep 17 00:00:00 2001 From: brianmacy Date: Wed, 4 Feb 2026 09:00:44 -0500 Subject: [PATCH] fix(rust): Zero vtable memory in write_vtable to prevent uninitialized data (#8898) The write_vtable() function's comment claimed to "fill the WIP vtable with zeros" but make_space() only reserves memory without initializing it. When using custom allocators with non-zeroed buffers, unset vtable field entries would contain garbage instead of zero (which indicates "use default value"). This fix explicitly zeros the vtable memory after reserving space, matching the C++ implementation's buf_.fill_big() behavior. Added regression test using a garbage-filled allocator (0xAA) that verifies vtable entries for unset fields are properly zeroed. Fixes #8894 --- rust/flatbuffers/src/builder.rs | 2 + .../tests/vtable_zeroed_test.rs | 164 ++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 tests/rust_usage_test/tests/vtable_zeroed_test.rs diff --git a/rust/flatbuffers/src/builder.rs b/rust/flatbuffers/src/builder.rs index d8a6e81dc..84bb62258 100644 --- a/rust/flatbuffers/src/builder.rs +++ b/rust/flatbuffers/src/builder.rs @@ -611,6 +611,8 @@ impl<'fbb, A: Allocator> FlatBufferBuilder<'fbb, A> { // Write the VTable (we may delete it afterwards, if it is a duplicate): let vt_start_pos = self.head; let vt_end_pos = self.head + vtable_byte_len; + // Zero out the vtable space - make_space only reserves but doesn't initialize + self.allocator[vt_start_pos.range_to(vt_end_pos)].fill(0); { // write the vtable header: let vtfw = diff --git a/tests/rust_usage_test/tests/vtable_zeroed_test.rs b/tests/rust_usage_test/tests/vtable_zeroed_test.rs new file mode 100644 index 000000000..401bc2fcf --- /dev/null +++ b/tests/rust_usage_test/tests/vtable_zeroed_test.rs @@ -0,0 +1,164 @@ +/* + * Copyright 2024 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Regression test for https://github.com/google/flatbuffers/issues/8894 +//! +//! Tests that vtable memory is properly zeroed when building FlatBuffers. + +extern crate alloc; +extern crate flatbuffers; + +use alloc::vec; +use alloc::vec::Vec; +use core::convert::Infallible; +use core::cmp::max; +use core::ops::{Deref, DerefMut}; +use core::ptr::write_bytes; + +use flatbuffers::{Allocator, FlatBufferBuilder}; + +/// Custom allocator that pre-fills buffer with garbage (0xAA) to detect +/// uninitialized memory bugs. +struct GarbageFilledAllocator(Vec); + +impl GarbageFilledAllocator { + fn new(size: usize) -> Self { + Self(vec![0xAA; size]) + } +} + +impl Deref for GarbageFilledAllocator { + type Target = [u8]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for GarbageFilledAllocator { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +// SAFETY: grow_downwards properly moves data and the new space is filled with garbage +// (intentionally, to detect bugs where code assumes zeroed memory) +unsafe impl Allocator for GarbageFilledAllocator { + type Error = Infallible; + + fn grow_downwards(&mut self) -> Result<(), Self::Error> { + let old_len = self.0.len(); + let new_len = max(1, old_len * 2); + + // Resize and fill new space with garbage + self.0.resize(new_len, 0xAA); + + if new_len == 1 { + return Ok(()); + } + + // Move old data to the end + let middle = new_len / 2; + { + let (left, right) = &mut self.0[..].split_at_mut(middle); + right.copy_from_slice(left); + } + // Fill old space with garbage (NOT zeros) + { + let ptr = self.0[..middle].as_mut_ptr(); + unsafe { + write_bytes(ptr, 0xAA, middle); + } + } + Ok(()) + } + + fn len(&self) -> usize { + self.0.len() + } +} + +/// Regression test for https://github.com/google/flatbuffers/issues/8894 +/// +/// The bug: write_vtable() called make_space() which only reserves memory +/// but doesn't zero it. If the allocator's buffer contains garbage data, +/// vtable entries for fields with default values would contain garbage +/// instead of zero (which indicates "use default"). +/// +/// This test uses a garbage-filled allocator to detect if vtable memory +/// is properly zeroed before being written. +#[test] +fn test_vtable_zeroed_with_garbage_allocator() { + // Create a builder with garbage-filled allocator + let allocator = GarbageFilledAllocator::new(256); + let mut builder: FlatBufferBuilder = + FlatBufferBuilder::new_in(allocator); + + // Start a table + let table_start = builder.start_table(); + + // Set a field at a HIGH slot ID (14) to force a larger vtable. + // This leaves slots 4, 6, 8, 10, 12 unset (should be zero). + // VTable layout: [vtable_size:2][table_size:2][field0:2][field1:2][field2:2][field3:2][field4:2][field5:2] + // Offsets: 0 2 4 6 8 10 12 14 + builder.push_slot::(14, 42, 0); // Set field 5 (at vtable offset 14) to 42 + + // End the table - this calls write_vtable() + let table_end = builder.end_table(table_start); + + // Finish the buffer + builder.finish(table_end, None); + + let data = builder.finished_data(); + + // Read the root table offset (first 4 bytes, little-endian) + let root_offset = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize; + let table_pos = root_offset; + + // Read the vtable offset (signed, at table position) + let vtable_offset = i32::from_le_bytes([ + data[table_pos], + data[table_pos + 1], + data[table_pos + 2], + data[table_pos + 3], + ]); + let vtable_pos = (table_pos as i32 - vtable_offset) as usize; + + // Read vtable size (first 2 bytes of vtable) + let vtable_size = u16::from_le_bytes([data[vtable_pos], data[vtable_pos + 1]]) as usize; + + // Verify vtable structure is as expected (16 bytes total for 6 fields + header) + assert_eq!( + vtable_size, 16, + "VTable should be 16 bytes (4 header + 6*2 fields)" + ); + + // Check that unset fields (at offsets 4, 6, 8, 10, 12) are zero. + // Only field at offset 14 was set. + // If the bug exists, unset fields would be 0xAAAA instead of 0. + for i in [4_usize, 6, 8, 10, 12] { + let field_offset = u16::from_le_bytes([data[vtable_pos + i], data[vtable_pos + i + 1]]); + assert_eq!( + field_offset, 0, + "Vtable entry at offset {} should be 0 (default), but was 0x{:04X}. \ + This indicates uninitialized vtable memory (issue #8894).", + i, field_offset + ); + } + + // Verify the field we DID set has a non-zero offset + let field5_offset = u16::from_le_bytes([data[vtable_pos + 14], data[vtable_pos + 14 + 1]]); + assert_ne!(field5_offset, 0, "Field 5 should have a non-zero offset"); +}