Skip to content

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

@parasol-aser

Description

@parasol-aser

Summary

Fuzzing of the UPB runtime found a reliably reproducible NULL-pointer dereference (SIGSEGV) in upb_MiniTableEnum_CheckValue() when upb_Decode() is invoked against a MiniTable produced by upb_MiniTable_Build() that declares a closed-enum field whose sub-enum has not been linked. A 5-byte input crashes the decoder deterministically.

  • Severity: High — remote denial of service for deployments that build MiniTables from untrusted mini descriptors and then decode untrusted wire data against them.
  • CVSS estimate: ~7.5 (network-exploitable DoS; no integrity/confidentiality impact)
  • Exploitability: Not exploitable for code execution (deterministic NULL deref at address 0x0 or 0x8). Exploitable for DoS.
  • Commit tested: 514aceb974fbd55031169b79d2bd9f7646157787 (branch main, 2026-04-12)
  • Discovered by: libFuzzer + AddressSanitizer harness targeting upb_MiniTable_Build() + upb_Decode().

Affected function / file

  • upb_MiniTableEnum_CheckValue()upb/mini_table/internal/enum.h:30 (dereference e->data[0] when val < 64) and :34 (dereference e->mask_limit when val >= 64).
  • Called unguarded from _upb_Decoder_DecodeWireValue()upb/wire/decode.c:941 (varint enum path).
  • Same missing guard at upb/wire/decode.c:351 (packed enum path).

Root cause

upb_MiniTable_Build() is documented to produce a MiniTable whose enum sub-tables are not yet linked — linking is a separate step via upb_MiniTable_SetSubEnum() / upb_MiniTable_Link(). A mini descriptor declaring a closed-enum field therefore yields sub->subenum == NULL for that field.

upb_Decode() does not verify this precondition. At upb/wire/decode.c:940-941:

const upb_MiniTableEnum* e = upb_MiniTable_GetSubEnumTable(field);
if (!upb_MiniTableEnum_CheckValue(e, val->uint64_val)) {

upb_MiniTable_GetSubEnumTable() returns the unchecked NULL pointer, and upb_MiniTableEnum_CheckValue() dereferences it unconditionally.

The two observed crash addresses (0x0 and 0x8) correspond to the two branches inside CheckValue — they are the same underlying bug: a missing NULL check at the boundary between the (untrusted) mini-descriptor build step and the (untrusted) wire-format decode step.

Related but distinct from prior fixes (not covered by them):

  • 90065d39a (2022-01) — proto2 enum Ruby crash, different code path.
  • ececc2162 (2022-07) — proto2 enum extension parsing, not MiniTable build/decode.
  • 0a2b39bfd (2025-09) — map-enum MiniTable rejection, did not add a NULL guard at the decode site.

Reproduction steps

5-byte reproducer: the first half is the mini descriptor passed to upb_MiniTable_Build() (builds a table with a closed-enum field); the second half is wire data fed to upb_Decode() (field 1 varint with value 52, hitting the enum-check path).

Crash input bytes (hex): 24 34 08 34 00

# Minimal libFuzzer-style harness (excerpt of what the fuzzer runs):
#   upb_MiniTable* mt = upb_MiniTable_Build(desc_ptr, desc_len, kUpb_MiniTablePlatform_Native, arena, &status);
#   upb_Decode(wire_ptr, wire_len, msg, mt, NULL, 0, arena);

printf '\x24\x34\x08\x34\x00' > /tmp/mt_crash
./fuzz_minitable /tmp/mt_crash

Observed ASAN stack:

#0 upb_MiniTableEnum_CheckValue   upb/mini_table/internal/enum.h:30
#1 _upb_Decoder_DecodeWireValue   upb/wire/decode.c:941
#2 _upb_Decoder_DecodeFieldData   upb/wire/decode.c:1078
#3 _upb_Decoder_DecodeFieldNoFast upb/wire/decode.c:1115
#4 _upb_Decoder_DecodeField       upb/wire/decode.c:1170
#5 _upb_Decoder_DecodeMessage     upb/wire/decode.c:1181
#6 _upb_Decoder_DecodeTop         upb/wire/decode.c:1194
#7 upb_Decoder_Decode             upb/wire/decode.c:1206
#8 upb_Decode                     upb/wire/decode.c:1231

All 1,016 crash files produced during the fuzzing run deduplicate to this single root cause.

Suggested fix

Minimal patch — add a NULL guard at both decode sites, treating the field as unknown when the sub-enum is unlinked (matches existing unknown-field semantics, lowest-risk change):

// upb/wire/decode.c:940-941 (varint enum path)
const upb_MiniTableEnum* e = upb_MiniTable_GetSubEnumTable(field);
if (!e || !upb_MiniTableEnum_CheckValue(e, val->uint64_val)) {
    *op = kUpb_DecodeOp_UnknownField;
    return ptr;
}

// upb/wire/decode.c:351 (packed enum path)
const upb_MiniTableEnum* e = upb_MiniTable_GetSubEnumTable(field);
// ...
if (!e || !upb_MiniTableEnum_CheckValue(e, elem.uint64_val)) {
    // treat as unknown
}

Defense-in-depth alternative: have upb_MiniTable_Build() reject mini descriptors that declare closed-enum fields, since the builder cannot link enum sub-tables on its own — only the explicit linking API can. Either fix resolves the crash; the decode-site guard is lower-risk and preserves current semantics for callers that build-then-link.


Reported by an automated fuzzing engagement (libFuzzer + ASAN, ~1 CPU-hour on the fuzz_minitable harness). Happy to share the harness source and full crash corpus on request.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions