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.
Summary
Fuzzing of the UPB runtime found a reliably reproducible NULL-pointer dereference (SIGSEGV) in
upb_MiniTableEnum_CheckValue()whenupb_Decode()is invoked against a MiniTable produced byupb_MiniTable_Build()that declares a closed-enum field whose sub-enum has not been linked. A 5-byte input crashes the decoder deterministically.514aceb974fbd55031169b79d2bd9f7646157787(branchmain, 2026-04-12)upb_MiniTable_Build()+upb_Decode().Affected function / file
upb_MiniTableEnum_CheckValue()—upb/mini_table/internal/enum.h:30(dereferencee->data[0]whenval < 64) and:34(dereferencee->mask_limitwhenval >= 64)._upb_Decoder_DecodeWireValue()—upb/wire/decode.c:941(varint enum path).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 viaupb_MiniTable_SetSubEnum()/upb_MiniTable_Link(). A mini descriptor declaring a closed-enum field therefore yieldssub->subenum == NULLfor that field.upb_Decode()does not verify this precondition. Atupb/wire/decode.c:940-941:upb_MiniTable_GetSubEnumTable()returns the unchecked NULL pointer, andupb_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 toupb_Decode()(field 1 varint with value 52, hitting the enum-check path).Crash input bytes (hex):
24 34 08 34 00Observed ASAN stack:
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):
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_minitableharness). Happy to share the harness source and full crash corpus on request.