| # Arm NN TFLite DetectionPostProcess malformed FlexBuffer PoC |
|
|
| This repository demonstrates a load-time heap-buffer-overflow read in Arm NN's |
| TFLite parser. A verifier-valid `.tflite` model can provide a one-byte |
| `custom_options` vector for the `TFLite_Detection_PostProcess` custom operator. |
| Arm NN passes that vector directly to `flexbuffers::GetRoot(...).AsMap()` |
| without first validating the FlexBuffer. |
|
|
| ## Tested version |
|
|
| - Arm NN commit: |
| `2b61cecc9df7a43fca1463795062cf359e6be820` |
| - Commit date: 2026-07-02 |
| - Parser library: `libarmnnTfLiteParser.24.7.dylib` |
| - FlatBuffers compiler: 24.3.25 |
| - Instrumentation: Clang AddressSanitizer and UndefinedBehaviorSanitizer |
|
|
| ## Root cause |
|
|
| `src/armnnTfLiteParser/TfLiteParser.cpp` copies attacker-controlled custom |
| options and parses them without a FlexBuffers verifier: |
|
|
| ```cpp |
| auto custom_options = operatorPtr->custom_options; |
| const flexbuffers::Map& m = |
| flexbuffers::GetRoot(custom_options.data(), custom_options.size()).AsMap(); |
| ``` |
|
|
| `flexbuffers::GetRoot` reads the root byte-width and packed type from the end |
| of the supplied buffer. A one-byte vector has no room for both values, so the |
| first access reads one byte before the allocation. |
|
|
| The outer TFLite FlatBuffer is valid. The TFLite verifier checks the |
| `custom_options` vector as a byte vector but does not validate its nested |
| FlexBuffer encoding. |
|
|
| ## Differential fixtures |
|
|
| - `control.tflite` contains the upstream test's valid DetectionPostProcess |
| FlexBuffer and loads successfully. |
| - `trigger.tflite` is the same model structure with `custom_options` reduced |
| to the single byte `0x00`. |
|
|
| SHA-256: |
|
|
| ```text |
| b1828036d5ace9f157f1a1b34a0ad94614989d8b227f127f146af5c3e332e704 control.tflite |
| af9c5e86ab6973b87eb78ad17573f79739e01dcf7e17e0d50348ce95a82a185f trigger.tflite |
| ``` |
|
|
| ## Reproduction |
|
|
| Build Arm NN's TFLite parser with ASan/UBSan, then run: |
|
|
| ```bash |
| ./reproduce.sh |
| ``` |
|
|
| Expected control result: |
|
|
| ```text |
| loaded 1280 bytes |
| ``` |
|
|
| Expected trigger result: |
|
|
| ```text |
| ERROR: AddressSanitizer: heap-buffer-overflow |
| READ of size 1 |
| ... in armnnTfLiteParser::TfLiteParserImpl::ParseDetectionPostProcess |
| 0x... is located 1 bytes before 1-byte region |
| ``` |
|
|
| The included `repeated-results.txt` records three independent runs. Every |
| control exited `0`; every trigger exited `134` with the same ASan signature. |
|
|
| ## Impact |
|
|
| Any service, model inspection pipeline, or application that imports an |
| attacker-supplied TFLite DetectionPostProcess model through Arm NN can be |
| terminated during model loading. The invalid nested encoding reaches native |
| out-of-bounds memory access before inference. |
|
|
| This PoC demonstrates denial of service through a native out-of-bounds read. |
| It does not claim code execution or data disclosure. |
|
|
| ## Suggested remediation |
|
|
| Validate `custom_options` with the FlexBuffers verifier before calling |
| `GetRoot`, and reject buffers that are too short to contain a root reference. |
| The parser should also validate that the root is a map and that required keys |
| have the expected scalar types before reading descriptor fields. |
|
|
| ## Research safety |
|
|
| The fixtures are synthetic and operate only on a local instrumented build. |
| No hosted service or third-party system is contacted. |
|
|