Skip to content

Add field ownership check to SwapFields to prevent OOB memory access#27289

Open
jortles wants to merge 3 commits into
protocolbuffers:mainfrom
jortles:fix-swapfields-ownership-check
Open

Add field ownership check to SwapFields to prevent OOB memory access#27289
jortles wants to merge 3 commits into
protocolbuffers:mainfrom
jortles:fix-swapfields-ownership-check

Conversation

@jortles
Copy link
Copy Markdown
Contributor

@jortles jortles commented May 8, 2026

Summary

Reflection::SwapFieldsImpl() validates that both messages belong to the correct Reflection object (GetReflection() == this), but does not verify that each field descriptor in the fields vector belongs to the message's own descriptor. This means a field from a different message type can be passed in, and its index() value can exceed the bounds of the target descriptor's offsets_ array.

When this happens, GetFieldOffset() at generated_message_reflection.h:127 reads offsets_[field->index()] out of bounds, producing an arbitrary offset value. The subsequent std::swap then writes 8 bytes at that offset relative to the message pointer, corrupting adjacent heap memory.

Root cause

Every other Reflection write method (SetInt32, SetString, HasField, ClearField, Swap, RemoveLast, MutableRawRepeatedField, etc.) includes USAGE_CHECK_MESSAGE_TYPE which validates field->containing_type() == descriptor_. The SwapFieldsImpl path was the only write method missing this check.

Fix

Add ABSL_CHECK_EQ(field->containing_type(), descriptor_) for each field at the top of the loop in SwapFieldsImpl, before any field operations. This matches the validation pattern used by all other Reflection methods.

Impact

Without this check, passing a foreign field descriptor causes:

  • Out-of-bounds read on offsets_[] array (reads heap data as a field offset)
  • Out-of-bounds write via std::swap at the corrupted offset (overwrites adjacent heap objects)

Test plan

  • Added SwapFieldsForeignFieldCheck death test: passes a ForeignMessage field to SwapFields on a TestAllTypes message, verifies the check fires with "does not belong to message type"
  • Follows the same pattern as the existing UsageErrors death test
  • All existing SwapFields* tests should pass unchanged (they only use fields from the correct descriptor)

jortles added 3 commits May 7, 2026 22:52
SwapFieldsImpl() validates that messages belong to this Reflection
object, but does not check that each field descriptor in the fields
vector actually belongs to the message's descriptor. When a field from
a different message type is passed, its index can exceed the bounds of
the target's offsets_ array, causing an out-of-bounds read that
produces an arbitrary write offset for the subsequent std::swap.

Add an ABSL_CHECK_EQ verifying field->containing_type() == descriptor_
for each field before processing, consistent with USAGE_CHECK_MESSAGE_TYPE
used by all other Reflection write methods (SetInt32, SetString, etc.).

Added regression test SwapFieldsForeignFieldCheck that verifies
SwapFields rejects a field from a different message type.
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