Extend ~TypeInfo scribble to cover ClassData function pointers - #29505
Open
jortles wants to merge 13 commits into
Open
Extend ~TypeInfo scribble to cover ClassData function pointers#29505jortles wants to merge 13 commits into
jortles wants to merge 13 commits into
Conversation
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
force-pushed
the
fix-dynamic-message-scribble-classdata
branch
from
August 28, 2026 22:02
f43ba39 to
b1f351e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The existing scribble in
~TypeInfo(dynamic_message.cc:467-477) coversoffsets[]andhas_bits_indices[]to catch use-after-free bugs where the factory is destroyed butDynamicMessageinstances are still used. The comment on line 470 notes this is "a common bug with DynamicMessageFactory." However, theClassDatafunction pointers are not scribbled:After factory destruction,
type_info_points to freed memory.GetClassData()returns a pointer into the freedTypeInfo'sClassData, 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
~TypeInfowas added to detect the factory-outlives-messages bug pattern, but only covers theoffsets[]andhas_bits_indices[]arrays. TheClassDatafunction pointers (is_initialized,merge_to_from,message_creator,tc_table) embedded inTypeInfo(or inglobalsinPROTOBUF_MESSAGE_GLOBALSbuilds) are left intact in freed memory.Downstream impact
IsInitialized()(message_lite.cc:101): Callsdata->is_initialized(*this)— indirect call through freed pointer.MergeFrom()(message.cc:176): Callsdata->merge_to_from— indirect call through freed pointer.New()(message_lite.h:439-445): Callsdata->message_creator.AllocateMessage()andPlacementNew()— indirect calls through freed pointer._InternalParse()(message_lite.cc:106): Readstc_tablefrom freed memory, passes toTcParser::ParseLoop.~DynamicMessage(): Readstype_info_->GetClassDataFull()during destruction — reads freed memory.With the existing scribble, only
GetInt32/GetStringetc. viaoffsets[]are caught (they read the scribbled0xCDCDCDCDsentinel). The function pointer paths silently use stale freed data.Fix
Null out the
ClassDatafunction pointers (tc_table,is_initialized,merge_to_from) and default-initializemessage_creatorbefore callingSizedDelete. InPROTOBUF_CUSTOM_VTABLEbuilds, also nulldestroy_message,clear,byte_size_long, andserialize. Cacheallocation_size()before scribblingmessage_creator, since theSizedDeletecall needs it.This must happen before
SizedDeletebecause inPROTOBUF_MESSAGE_GLOBALSbuilds,SizedDeletefrees theglobalsallocation which containsClassData.After this change, any accidental use of a
DynamicMessagewhose 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
DynamicMessage*tests pass — no regression.