File size: 9,608 Bytes
33b540b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | # 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.
|