
Harnais d'audit testant si la classe de bug de récursion de déballage Any CVE-2026-0994 affecte le cœur C d'upb dans les bindings protobuf Ruby et PHP, avec des charges utiles ASan/UBSan.
Statut : Terminé. Six vérifications de garde de récursion réparties sur trois implémentations indépendantes (cœur upb partagé Ruby/PHP, implémentation managée séparée de C#), toutes falsifiées sauf la CVE Python déjà publique et déjà corrigée par rapport à laquelle cet audit était mené. Aucune nouvelle vulnérabilité n'a été trouvée.
Suite aux CVE de récursion de déballage Any confirmées dans les bindings Python et JavaScript de protobuf (plus récemment CVE-2026-0994, un bug purement Python dans _ConvertAnyMessage de json_format.py, qui effectuait une récursion via methodcaller() au lieu de ConvertMessage() et ignorait silencieusement le compteur de profondeur).
Question : la même classe de bug existe-t-elle dans le cœur C upb intégré aux extensions natives Ruby et PHP ?
Réponse courte : non. Le chemin de décodage JSON, le chemin de décodage binaire sur le fil, et l'implémentation managée C# indépendante appliquent tous correctement leurs limites de récursion dans chaque cas testé ici, y compris une charge de stress à 200 000 niveaux exécutée avec une pile de 1 Mo. Il s'agit d'une hypothèse falsifiée avec preuves, pas d'une vulnérabilité — elle est consignée ici comme un résultat négatif propre, de la même manière que l'on consignerait une hypothèse falsifiée dans n'importe quel autre axe de recherche.
Les extensions natives Ruby et PHP de protobuf embarquent chacune une amalgamation mono-fichier du cœur C de upb (ruby-upb.c, php-upb.c) plutôt que de lier une bibliothèque partagée. Cet audit compile ce fichier exact comme cible C autonome — sans runtime Ruby ni PHP impliqué — initialise un upb_DefPool avec les descripteurs google.protobuf.Any / Struct / Value / ListValue, et pilote directement les vrais points d'entrée du décodeur (upb_JsonDecode, upb_Decode) avec des charges adverses, sous ASan/UBSan.
| Chemin | Garde | Limite par défaut | Résultat |
|---|---|---|---|
Décodage JSON, jsondec_any (Ruby) | d->depth, vérifié dans jsondec_push | 64 | Falsifié — erreur propre sur une charge à 5000 niveaux, l'offset d'octet correspond exactement à ~64 niveaux |
Décodage binaire sur le fil, upb_Decode (Ruby) | Decode_LimitDepth | 100 | Falsifié — kUpb_DecodeStatus_MaxDepthExceeded propre à la charge limite de 60 niveaux et à la charge de stress de 200 000 niveaux, toutes deux sous une pile de 8 Mo et une pile de 1 Mo (approximant un thread Ruby non principal) |
| Binding PHP | mêmes fonctions de garde | idem | Falsifié par preuve d'identité, pas par une exécution séparée — voir ci-dessous |
Décodage JSON, JsonParser.MergeAny (C#, Google.Protobuf 3.36.1) | tokenizer.RecursionDepth, vérifié dans Merge() | 100 | Falsifié — confirmé sur une charge à 200 niveaux ; MergeAny construit un nouveau JsonTokenizer via FromReplayedTokens à chaque déballage d'Any, mais la comptabilisation de profondeur se propage correctement à travers |
Décodage binaire sur le fil, CodedInputStream/Value.Parser (C#, Google.Protobuf 3.36.1) | recursionDepth/recursionLimit sur le flux | 64 | Falsifié — InvalidProtocolBufferException propre à la charge limite de 60 niveaux (mêmes octets de charge que le test binaire Ruby, réutilisés directement) |
Aucune violation ASan ou UBSan n'a été observée dans quelque exécution que ce soit. Aucun crash, aucun blocage, aucun épuisement de pile.
php-upb.c et ruby-upb.c diffèrent d'environ 1 964 lignes au total, mais jsondec_any, jsondec_push et les constantes de limite de profondeur sur le fil sont identiques octet pour octet entre les deux fichiers (verify_php_identical.sh le prouve, ne le prenez pas pour argent comptant — exécutez-le). Comme le code de garde lui-même est prouvablement le même, le résultat Ruby se transpose sans nécessiter de harnais spécifique à PHP redondant.
Quatre bindings testés (Ruby, PHP, C#, plus la CVE déjà publique de Python), tous falsifiés sauf celui qui était déjà corrigé en amont. Axes restants ouverts, aucun d'entre eux ne relevant de cette classe de bug spécifique :
utf8_range.c embarquée.dotnet run -- wire <payload>) n'est pas encore intégré à la CI — seul le test C# en mode JSON est automatisé../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
Nécessite : gcc, protoc (apt-get install protobuf-compiler), python3,
git. Compilé et vérifié avec gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 et libprotoc 3.21.12 — des versions différentes peuvent se
comporter différemment ; si vos résultats ne correspondent pas au tableau
ci-dessus, c'est une donnée, pas un bug dans votre configuration. Cherchez
la cause avant de supposer qu'il s'agit d'une incompatibilité d'outillage.
third_party/ (récupéré par fetch_source.sh) contient le code source
protobuf de Google sous licence Apache-2.0, épinglé au commit ci-dessus.
Il n'est jamais commité dans ce dépôt — fetch_source.sh constitue
l'ancrage de reproductibilité plutôt que de vendorer l'arborescence source
d'autrui dans celle-ci.