ITADN

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

#26857Openparasol-aser 创建于 2026-04-12
P
parasol-asercommented
## 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 条评论