protobuf / EVIDENCE.md
testamentaria's picture
Upload 11 files
33b540b verified
|
Raw
History Blame Contribute Delete
9.61 kB

Evidencia β€” focus-02-protobuf-mlmodel-deserialization

Verificacion empirica realizada 2026-07-22 por verify-agent, FASE 4. Todo lo de aqui se ejecuto en targets/coremltools/verify/venv-* (venvs locales, pip install coremltools/pip install protobuf==X), nunca contra infraestructura de Apple ni de terceros. Ningun dato real de vendor implicado β€” todos los .mlmodel son sintaticamente validos pero triviales, construidos por script.

Entorno

  • targets/coremltools/verify/venv-protobuf/ β€” pip install coremltools limpio, tal cual lo obtendria hoy cualquiera (coremltools==9.0, arrastra protobuf==7.35.1, backend por defecto upb (acelerado)).
  • targets/coremltools/verify/venv-cve-old/ β€” mismo coremltools==9.0, protobuf forzado a 4.25.7 (ANTES del fix de CVE-2025-4565/GHSA-8qvm-5x2c-j2w7).
  • targets/coremltools/verify/venv-cve-new/ β€” mismo coremltools==9.0, protobuf forzado a 4.25.8 (DESPUES del fix).
  • Python 3.12.10 (py -3.12), Windows 11.

Confirmacion de backend/version (Paso 2 del plan del operador):

$ ./verify/venv-protobuf/Scripts/python.exe -m pip show coremltools protobuf
Name: coremltools   Version: 9.0
Name: protobuf      Version: 7.35.1
$ ./verify/venv-protobuf/Scripts/python.exe -c "from google.protobuf.internal import api_implementation; print(api_implementation.Type())"
upb

-> Confirmado: un pip install coremltools normal hoy (2026-07-22) resuelve protobuf 7.35.1 con el backend acelerado upb por defecto. Esto es la condicion mas comun para cualquier usuario que instale desde PyPI en una plataforma con wheel prebuilt (x86_64/arm64, Windows/Linux/macOS estandar).

Scripts de PoC

  • build_payloads.py β€” construye, con los stubs *_pb2.py de coremltools.proto (sin protoc), tres familias de payload con profundidad de anidamiento configurable:
    • pipeline: Model.pipeline.models[0] -> Model -> pipeline.models[0] -> ...
    • mil_block: Function.block_specializations["CoreML5"].operations[i] -> Operation.blocks[j] -> Block.operations[i] -> ... (ciclo MILSpec.Operation.blocks/Block.operations)
    • nn_branch: NeuralNetwork.layers[i].branch.ifBranch (que es de nuevo un mensaje NeuralNetwork completo) encadenado -> ciclo NeuralNetwork.BranchLayer
    • Nota tecnica documentada en el propio script: construir/serializar cadenas muy profundas (>=~2000-5000 niveles) con el backend acelerado upb puede hacer CRASHEAR nativamente el proceso Python entero (sin traceback) durante la CONSTRUCCION, un fenomeno DISTINTO al que se esta verificando (que es el comportamiento del PARSEO/deserializacion sobre bytes ya serializados, que es el escenario real: el atacante construye el fichero una vez, offline, por el medio que sea, y la victima solo llama ParseFromString/load_spec sobre bytes ya hechos). Para que la fase de CONSTRUCCION nunca sea el cuello de botella bajo prueba, el script fuerza PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python + un hilo con pila de 64MB + sys.setrecursionlimit alto SOLO para construir los payloads; los bytes resultantes son formato wire estandar, independientes del backend que los genero.
  • test_parse.py β€” carga un .mlmodel con coremltools.utils.load_spec() (o, con --raw, directamente Model_pb2.Model().ParseFromString() para aislar el parseo puro) en un hilo worker con pila de tamano POR DEFECTO (deliberadamente NO agrandada, para reproducir fielmente lo que le pasaria a un hilo normal de una aplicacion que procese un fichero de un atacante), e imprime backend/version de protobuf activos y el resultado exacto (OK / DecodeError / RecursionError / excepcion generica).

Payloads generados (out/*.mlmodel): profundidades 1, 5, 10, 20, 30, 40, 45, 48, 49, 50, 150, 1000, 5000 para pipeline; 50, 150, 1000, 5000 para mil_block y nn_branch. Todos entre 400 bytes y ~102KB β€” confirma la afirmacion del hallazgo original: "una cadena de unos pocos cientos/miles de mensajes es minuscula en bytes serializados".

Resultado A β€” Backend por defecto (upb, protobuf 7.35.1) β€” caso comun pip install

Log completo: results/A_default_backend_upb.log y busqueda de frontera exacta en results/A2_boundary_search.log.

depth (pipeline) resultado
1..49 OK, parsea sin error
50 DecodeError: ... Exceeded upb_DecodeOptions_MaxDepth
150, 1000, 5000 mismo DecodeError manejable

Mismo resultado (DecodeError: Exceeded upb_DecodeOptions_MaxDepth) en las 3 familias (pipeline, mil_block, nn_branch) en todas las profundidades >=50.

-> Confirmado exactamente lo que predecia el hallazgo: el backend acelerado impone un limite de anidamiento de facto en torno a 100 niveles del arbol protobuf (aqui, como cada "depth" de mi constructor produce 2 niveles de anidamiento -- el campo pipeline/branch y el mensaje Model/NeuralNetwork anidado -- la frontera cae en depth=50, es decir, ~99-100 niveles reales), y la excepcion resultante (google.protobuf.message.DecodeError) es manejable/capturable por cualquier codigo llamador razonable. No hay DoS real en este escenario, que es el mas comun.

Resultado B β€” Backend puro-Python FORZADO + protobuf 4.25.7 (VULNERABLE, antes del fix)

Log completo: results/B_vulnerable_protobuf_4.25.7_pure_python.log.

depth pipeline mil_block nn_branch
50 OK OK OK
150 OK OK OK
1000 RecursionError: maximum recursion depth exceeded RecursionError RecursionError
5000 RecursionError: maximum recursion depth exceeded RecursionError RecursionError

-> Confirmado el mecanismo central del hallazgo: con el backend puro-Python (forzado via PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python) y una version de protobuf sin el fix de CVE-2025-4565, load_spec() deja escapar un RecursionError no capturado por protobuf ni por coremltools (no es subclase de DecodeError; ningun except DecodeError en codigo de la victima lo atrapa) en las 3 familias de ciclo recursivo identificadas en el hallazgo original. Control adicional (results/E_raw_parsefromstring_vs_load_spec.log): el mismo RecursionError se reproduce con Model_pb2.Model().ParseFromString() puro, sin pasar por load_spec() β€” confirma que coremltools no aΓ±ade ninguna mitigacion propia (ni try/except, ni limite de profundidad propio) alrededor del parseo.

Resultado C β€” Backend puro-Python FORZADO + protobuf 4.25.8 (PARCHEADO, con el fix)

Log completo: results/C_patched_protobuf_4.25.8_pure_python.log.

depth pipeline mil_block nn_branch
50 DecodeError: too many levels of nesting idem idem
150 idem idem idem
1000 idem idem idem
5000 idem idem idem

-> Control positivo confirmado: la MISMA condicion (backend puro-Python forzado) pero con protobuf 4.25.8 convierte SIEMPRE el RecursionError no capturado en un DecodeError manejable, incluso a profundidad 50 (el limite DEFAULT_RECURSION_LIMIT=100 del parche cuenta los niveles de forma distinta al backend upb, pero el resultado -- excepcion manejable -- es el mismo). Esto aisla la causa raiz exactamente donde predecia el hallazgo: el fix vive en la libreria protobuf (decoder.py), no en coremltools, y coremltools no hace nada propio para garantizarlo (no fija una version minima de protobuf, no fuerza el backend acelerado).

Resultado D β€” Backend por defecto (upb) es indiferente a la version de protobuf

Log completo: results/D_default_backend_control_both_protobuf_versions.log.

Con protobuf==4.25.7 (vulnerable) Y protobuf==4.25.8 (parcheado), SIN forzar la variable de entorno, ambos venvs resuelven backend upb y ambos producen el mismo DecodeError manejable a depth=1000. Confirma la afirmacion de la advisory oficial: los wheels binarios (backend acelerado) nunca estuvieron afectados por CVE-2025-4565, independientemente del numero de version exacto de protobuf instalado β€” lo que importa es el BACKEND, no la version, para el caso comun.

Resumen de la matriz completa

Backend Version protobuf Resultado a profundidad alta (1000/5000) Manejable?
upb (acelerado, DEFAULT de pip install) 7.35.1 (hoy) DecodeError a partir de depth~50 SI
upb (acelerado) 4.25.7 (vulnerable) DecodeError (control D) SI
upb (acelerado) 4.25.8 (parcheado) DecodeError (control D) SI
puro-Python (forzado) 4.25.7 (vulnerable) RecursionError NO capturado NO β€” crash/excepcion no manejada
puro-Python (forzado) 4.25.8 (parcheado) DecodeError SI

Las 3 familias de ciclo recursivo (pipeline, mil_block/MILSpec, nn_branch/NeuralNetwork) se comportan de forma IDENTICA en todas las filas de la matriz β€” el mecanismo no es especifico de un solo esquema, es generico del parser protobuf subyacente.

Codigo de coremltools: sin mitigacion propia

grep -rn "RecursionError\|SetRecursionLimit\|recursion" coremltools/ (excluyendo proto/) no encuentra ninguna proteccion relacionada con el parseo de specs. La unica ocurrencia relevante, converters/mil/frontend/tensorflow/tf_graph_pass/delete_asserts.py:14: sys.setrecursionlimit(5000), hace justo lo contrario de mitigar: SUBE el limite global de Python (para poder convertir grafos TensorFlow grandes), lo que en el escenario vulnerable ampliaria aun mas la profundidad necesaria para disparar el RecursionError, no lo evita.