Skip to content

Extend ~TypeInfo scribble to cover ClassData function pointers - #29505

Open
jortles wants to merge 13 commits into
protocolbuffers:mainfrom
jortles:fix-dynamic-message-scribble-classdata
Open

Extend ~TypeInfo scribble to cover ClassData function pointers#29505
jortles wants to merge 13 commits into
protocolbuffers:mainfrom
jortles:fix-dynamic-message-scribble-classdata

Conversation

@jortles

@jortles jortles commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

The existing scribble in ~TypeInfo (dynamic_message.cc:467-477) covers offsets[] and has_bits_indices[] to catch use-after-free bugs where the factory is destroyed but DynamicMessage instances are still used. The comment on line 470 notes this is "a common bug with DynamicMessageFactory." However, the ClassData function pointers are not scribbled:

// dynamic_message.cc:857
const internal::ClassData* DynamicMessage::GetClassData() const {
    return type_info_->GetClassDataFull().base();  // dereferences raw type_info_
}

After factory destruction, type_info_ points to freed memory. GetClassData() returns a pointer into the freed TypeInfo's ClassData, and downstream operations call through its function pointers:

  • IsInitialized() / SerializeToString()data->is_initialized(*this) (message_lite.cc:101)
  • MergeFrom()data->merge_to_from(to, from) (message_lite.cc:119)
  • New()data->message_creator.PlacementNew(...) (message_lite.h:444)
  • _InternalParse()TcParser::ParseLoop(this, ..., GetTcParseTable()) (message_lite.cc:106)

Root cause

The scribble mitigation in ~TypeInfo was added to detect the factory-outlives-messages bug pattern, but only covers the offsets[] and has_bits_indices[] arrays. The ClassData function pointers (is_initialized, merge_to_from, message_creator, tc_table) embedded in TypeInfo (or in globals in PROTOBUF_MESSAGE_GLOBALS builds) are left intact in freed memory.

Downstream impact

  • IsInitialized() (message_lite.cc:101): Calls data->is_initialized(*this) — indirect call through freed pointer.
  • MergeFrom() (message.cc:176): Calls data->merge_to_from — indirect call through freed pointer.
  • New() (message_lite.h:439-445): Calls data->message_creator.AllocateMessage() and PlacementNew() — indirect calls through freed pointer.
  • _InternalParse() (message_lite.cc:106): Reads tc_table from freed memory, passes to TcParser::ParseLoop.
  • ~DynamicMessage(): Reads type_info_->GetClassDataFull() during destruction — reads freed memory.

With the existing scribble, only GetInt32/GetString etc. via offsets[] are caught (they read the scribbled 0xCDCDCDCD sentinel). The function pointer paths silently use stale freed data.

Fix

Null out the ClassData function pointers (tc_table, is_initialized, merge_to_from) and default-initialize message_creator before calling SizedDelete. In PROTOBUF_CUSTOM_VTABLE builds, also null destroy_message, clear, byte_size_long, and serialize. Cache allocation_size() before scribbling message_creator, since the SizedDelete call needs it.

This must happen before SizedDelete because in PROTOBUF_MESSAGE_GLOBALS builds, SizedDelete frees the globals allocation which contains ClassData.

After this change, any accidental use of a DynamicMessage whose factory has been destroyed will hit a null function pointer (immediate crash with a clear signal) instead of calling through stale freed memory.

Test plan

  • All existing DynamicMessage* tests pass — no regression.
  • The scribble is a mitigation, not a behavioral change. It converts stale-pointer calls into null dereferences for the factory-outlives-messages bug pattern that the existing scribble already targets.

jortles and others added 13 commits May 23, 2026 21:35
The existing scribble in ~TypeInfo covers offsets[] and
has_bits_indices[] to catch use-after-free bugs where the factory is
destroyed but DynamicMessage instances are still used (noted as "a
common bug" in the existing comment). However, the ClassData function
pointers (is_initialized, merge_to_from, message_creator, tc_table)
are not scribbled.

DynamicMessage::GetClassData() dereferences the raw type_info_ pointer
and returns a pointer into TypeInfo's ClassData. Operations like
IsInitialized(), MergeFrom(), New(), and _InternalParse() then call
through these function pointers. After factory destruction, these
pointers point to freed memory — if the memory is reallocated, the
stale pointers may point to arbitrary data.

Null out the ClassData function pointers before freeing the globals
allocation. This must happen before SizedDelete, which frees globals
(and class_data within it) in PROTOBUF_MESSAGE_GLOBALS builds.
Also cache allocation_size() before scribbling message_creator.
tc_table and prototype are only fields of ClassData when
PROTOBUF_MESSAGE_GLOBALS is not defined. The unconditional access
caused compilation failures in builds that define this macro (GCC,
ASAN, Python, PHP, Rust, upb CI targets).

Also null prototype in non-globals builds since it is a raw pointer
into freed memory that could be dereferenced through a stale
DynamicMessage.
Replace std::memset on non-trivial MessageCreator with
default-constructor assignment. GCC 10.5+ rejects memset
on types with non-trivial constructors under -Werror.

The default constructor zeros all fields (allocation_size=0,
tag=kZeroInit, alignment=0, func=nullptr), which still
ensures an immediate crash on use-after-free — same safety
guarantee as the 0xCD scribble pattern.
@jortles
jortles force-pushed the fix-dynamic-message-scribble-classdata branch from f43ba39 to b1f351e Compare August 28, 2026 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant