[Fuzzing] NULL-pointer dereference: upb_MiniTableEnum_CheckValue crash via unlinked closed-enum field in upb_Decode
## 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`:
```c
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`
```bash
# 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):
```c
// 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.
0 条评论