Skip to content

Fix NULL-pointer dereference in upb_MiniTableEnum_CheckValue for unlinked closed-enum fields#26859

Open
parasol-aser wants to merge 1 commit intoprotocolbuffers:mainfrom
parasol-aser:fix/upb-minitable-enum-null-deref
Open

Fix NULL-pointer dereference in upb_MiniTableEnum_CheckValue for unlinked closed-enum fields#26859
parasol-aser wants to merge 1 commit intoprotocolbuffers:mainfrom
parasol-aser:fix/upb-minitable-enum-null-deref

Conversation

@parasol-aser
Copy link
Copy Markdown

Summary

Fixes a reliably reproducible SIGSEGV in the UPB decoder when upb_Decode() runs against a upb_MiniTable produced by upb_MiniTable_Build() whose closed-enum field has not yet been linked to a sub-enum table. A 5-byte input (24 34 08 34 00) crashes the decoder deterministically by dereferencing a NULL upb_MiniTableEnum* inside upb_MiniTableEnum_CheckValue().

By design, upb_MiniTable_Build() does not populate sub-enum pointers — linking is a separate step via upb_MiniTable_SetSubEnum() / upb_MiniTable_Link(). The decoder failed to account for this on two code paths:

  • _upb_Decoder_DecodeWireValue() — varint (scalar) closed-enum path (upb/wire/decode.c:939)
  • _upb_Decoder_DecodeEnumPacked() — packed closed-enum path (upb/wire/decode.c:351)

Both sites called upb_MiniTable_GetSubEnumTable(field) and passed the (possibly NULL) result straight into upb_MiniTableEnum_CheckValue(), which unconditionally dereferences e->data[0] / e->mask_limit.

Severity: High — remote DoS for deployments that build MiniTables from untrusted mini descriptors and then decode untrusted wire data against them. Not exploitable for code execution (deterministic NULL deref).

Fixes #26857

Fix

Guard both call sites with a NULL check on the sub-enum table, short-circuited with || so upb_MiniTableEnum_CheckValue() is only invoked when e != NULL. When e == NULL, treat the value (or every packed element) as an unknown field — the same behavior as when a value fails the closed-enum value check, and consistent with the existing unlinked-sub-message handling in _upb_Decoder_CheckUnlinked().

The fix is applied at the decode-site boundary rather than inside the hot inline upb_MiniTableEnum_CheckValue(), since its documented precondition is a non-NULL, linked enum table, and callers elsewhere already UPB_ASSERT this. Adding a NULL branch to the hot inline function would regress every correctly-linked caller.

Changes

  • upb/wire/decode.c — add !e || guard at both crash sites.
  • ruby/ext/google/protobuf_c/ruby-upb.c — mirror the same guard in the amalgamated source so the Ruby extension is not left vulnerable.
  • php/ext/google/protobuf/php-upb.c — same, for the PHP extension.
  • upb/wire/decode_test.cc — add a regression test that builds a MiniTable with an unlinked closed-enum field via upb_MiniTable_Build(), decodes both the varint-scalar reproducer and the packed-enum variant, and asserts no crash.
  • upb/wire/BUILD — add //upb/base and //upb/mini_descriptor test deps needed by the new test.

The upb source and both amalgamated mirrors are updated in a single commit so that a Ruby/PHP release is not shipped with a knowingly-vulnerable copy of the decoder.

Test plan

  • bazel test //upb/wire:decode_test passes.
  • bazel test --config=asan //upb/wire:decode_test passes (without the fix, ASAN reports NULL deref at upb/mini_table/internal/enum.h:30).
  • bazel test --config=ubsan //upb/wire:decode_test passes.
  • Full bazel test //upb/... passes unchanged.
  • Ruby extension test suite passes against the patched ruby-upb.c.
  • PHP extension test suite passes against the patched php-upb.c.
  • Manual verification: run the regression test binary against HEAD~1 under ASAN, confirm crash; run against HEAD, confirm clean pass.

🤖 Generated with Claude Code

When upb_MiniTable_Build() produces a table with a closed-enum field,
sub-enum pointers are not populated until a separate link step (e.g.
upb_MiniTable_SetSubEnum / upb_MiniTable_Link). Decoding wire data for
such an unlinked closed-enum field caused a SIGSEGV inside
upb_MiniTableEnum_CheckValue on the varint scalar path
(_upb_Decoder_DecodeWireValue) and the packed path
(_upb_Decoder_DecodeEnumPacked).

Guard both call sites with a NULL check on the sub-enum table. When the
table is missing, treat the value (or every value, for packed) as an
unknown field — the same behavior as when a value fails the closed-enum
check. This matches the existing unlinked-sub-message handling in
_upb_Decoder_CheckUnlinked().

The fix is applied at the decode-site boundary rather than inside the
hot inline upb_MiniTableEnum_CheckValue to avoid regressing every
correctly-linked caller. Amalgamated mirrors in ruby/ext and php/ext
are patched identically so language bindings are not left vulnerable.

Adds a regression test in upb/wire/decode_test.cc covering both the
varint and packed paths using a MiniTable built directly via
upb_MiniTable_Build() without linking the sub-enum.

Fixes protocolbuffers#26857

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@parasol-aser parasol-aser requested review from a team as code owners April 12, 2026 22:38
@parasol-aser parasol-aser requested review from JasonLunn and bshaffer and removed request for a team April 12, 2026 22:38
@google-cla
Copy link
Copy Markdown

google-cla bot commented Apr 12, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

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.

[Fuzzing] NULL-pointer dereference: upb_MiniTableEnum_CheckValue crash via unlinked closed-enum field in upb_Decode

1 participant