protocolbuffers / protocolbuffers/protobuf
[Fuzzing] NULL-pointer dereference: upb_MiniTableEnum_CheckValue crash via unlinked closed-enum field in upb_Decode
@haberman is already working on this.
Since Jul 16, 2026.
- Dominant language
- C++
- Stars
- 72k
- Forks
- 16.3k
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 140
Description
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(branchmain, 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(dereferencee->data[0]whenval < 64) and:34(dereferencee->mask_limitwhenval >= 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.