
Audit harness testing whether the CVE-2026-0994 Any-unwrapping recursion bug class affects upb's C core in Ruby and PHP protobuf bindings, with ASan/UBSan payloads.
Follow-up to confirmed Any-unwrapping recursion CVEs in protobuf's Python
and JavaScript bindings (most recently CVE-2026-0994, a pure-Python bug in
json_format.py's _ConvertAnyMessage, which recursed via methodcaller()
instead of ConvertMessage() and silently skipped the depth counter).
Question: does the same bug class exist in the C upb core bundled
into the Ruby and PHP native extensions?
Short answer: no. Both the JSON-decode path and the binary wire-decode path enforce their recursion limits correctly under every case tested here, including a 200,000-level stress payload run under a 1MB stack. This is a falsified hypothesis with evidence, not a vulnerability — it's recorded here as a clean negative result, the same way you'd log a falsified hypothesis in any other research thread.
Protobuf's Ruby and PHP native extensions each vendor a single-file
amalgamation of upb's C core (ruby-upb.c, php-upb.c) rather than linking
a shared library. This audit builds that exact file as a standalone C
target — no Ruby or PHP runtime involved — bootstraps a upb_DefPool with
google.protobuf.Any / Struct / Value / ListValue descriptors, and
drives the real decoder entry points (upb_JsonDecode, upb_Decode)
directly with adversarial payloads, under ASan/UBSan.
| Path | Guard | Default limit | Result |
|---|---|---|---|
JSON decode, jsondec_any (Ruby) | d->depth, checked in jsondec_push | 64 | Falsified — clean error at 5000-level payload, byte offset matches ~64 levels exactly |
Binary wire decode, upb_Decode (Ruby) | Decode_LimitDepth | 100 | Falsified — clean kUpb_DecodeStatus_MaxDepthExceeded at 60-level boundary payload and at 200,000-level stress payload, both under an 8MB stack and a 1MB stack (approximating a non-main Ruby thread) |
| PHP binding | same guard functions | same | Falsified by proof of identity, not a separate run — see below |
No ASan or UBSan violation was observed in any run. No crash, no hang, no stack exhaustion.
php-upb.c and ruby-upb.c differ by ~1,964 lines overall, but
jsondec_any, jsondec_push, and the wire depth-limit constants are
byte-identical between the two files (verify_php_identical.sh proves
this, don't take it on faith — run it). Since the guard code itself is
provably the same, the Ruby result transfers without needing a redundant
PHP-specific harness.
JsonParser /
CodedInputStream with their own RecursionLimit handling). Genuinely
untested; this repo doesn't cover it yet.utf8_range.c validation../fetch_source.sh # pins & clones protobuf @ ead3f0029facc43da13588132e9091bf9bd7a26f
./build.sh # compiles both harnesses w/ ASan+UBSan against the fetched source
./run_tests.sh # generates payloads, runs the full matrix, prints results
./verify_php_identical.sh # confirms the PHP claim above instead of asserting it
Requires: gcc, protoc (apt-get install protobuf-compiler), python3,
git. Built and verified against gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 and libprotoc 3.21.12 — different versions may behave
differently; if your results don't match the table above, that's data, not
a bug in your setup. Find out why before assuming it's a tooling mismatch.
third_party/ (fetched by fetch_source.sh) contains Google's
Apache-2.0-licensed protobuf source, pinned to the commit above. It is
never committed to this repo — fetch_source.sh is the reproducibility
anchor instead of vendoring someone else's source tree into this one.