| # ArmNN Deserializer β Stack Buffer Overflow via `TensorInfo.dimensionSpecificity` (CWE-121) |
|
|
| ## Summary |
|
|
| `ARM-software/armnn`'s native model-file parser (`armnnDeserializer`, |
| `src/armnnDeserializer/Deserializer.cpp`) deserializes `.armnn` FlatBuffers |
| model files. The free function `armnnDeserializer::ToTensorInfo()` |
| (Deserializer.cpp, around line 743-760) decodes every `TensorInfo` table that |
| appears anywhere in a model file (every layer's input/output tensor |
| descriptors, constant tensors, etc.) and copies the optional |
| `dimensionSpecificity` FlatBuffers vector into a **fixed 5-element stack |
| array**: |
|
|
| ```cpp |
| auto dimensions = tensorPtr->dimensions(); |
| unsigned int size = dimensions->size(); |
| std::vector<unsigned int> outputDims(dimensions->begin(), dimensions->begin() + size); |
| bool dimensionsSpecificity[armnn::MaxNumOfTensorDimensions]; // stack array, size == 5 |
| std::fill_n(dimensionsSpecificity, armnn::MaxNumOfTensorDimensions, true); |
| if (tensorPtr->dimensionSpecificity() != nullptr) |
| { |
| auto dimensionSpecificity = tensorPtr->dimensionSpecificity(); |
| size = dimensionSpecificity->size(); // <-- attacker-controlled length, NOT clamped |
| for (unsigned int i = 0; i < size; ++i) |
| { |
| dimensionsSpecificity[i] = dimensionSpecificity->Get(i); // <-- OOB WRITE once i >= 5 |
| } |
| } |
| ``` |
|
|
| `armnn::MaxNumOfTensorDimensions` is `5` (`include/armnn/Types.hpp`). The loop |
| bound `size` is taken directly from the length of the attacker-supplied |
| `dimensionSpecificity` FlatBuffers vector, with **no check against the fixed |
| stack buffer size**. Any `TensorInfo` in the model file whose |
| `dimensionSpecificity` vector has more than 5 elements causes the loop to |
| write attacker-controlled boolean bytes (each element is the raw byte from |
| the file, coerced to `bool`) past the end of the 5-byte stack array, |
| corrupting adjacent stack memory (other stack locals, saved registers, |
| padding, and β depending on compiler/stack layout/optimization level β |
| potentially the return address) for as many bytes as the attacker declares |
| the vector to be (we tested 40 bytes in the PoC; the vector length field is |
| a FlatBuffers `uint32` count, so far larger overflows are possible in a |
| single call). |
|
|
| `ToTensorInfo()` is called from nearly every layer parser in the file |
| (`ParseFloor`, `ParseAdd`, `ParseActivation`, `ParseConvolution2d`, ... β over |
| 50 call sites), so this is reachable through essentially any `.armnn` model |
| that contains an `Input`/`Output`/any-layer `TensorInfo` with a crafted |
| `dimensionSpecificity` field. This PoC uses the simplest possible layer |
| (`FloorLayer`) to reach it via a single output slot's `TensorInfo`. |
|
|
| ## Attacker input β sink |
|
|
| 1. Application loads an untrusted `.armnn` model file with |
| `armnnDeserializer::IDeserializer::CreateNetworkFromBinary()` β the |
| standard, documented entry point used by `ExecuteNetwork` and any |
| ArmNN-based downstream integrator that accepts serialized ArmNN models. |
| 2. `CreateNetworkFromGraph()` walks `graph->layers()` and dispatches each |
| layer to its parser function (Deserializer.cpp:927-937). |
| 3. The crafted `FloorLayer`'s output slot carries a `TensorInfo` whose |
| `dimensionSpecificity` vector has 40 elements (`dimensions` only has 1). |
| 4. `ParseFloor()` (Deserializer.cpp:2253) calls `ToTensorInfo(outputs[0])`. |
| 5. `ToTensorInfo()` writes 40 bytes into a 5-byte stack array |
| (Deserializer.cpp:756) β stack-buffer-overflow. |
|
|
| ## Reproduction |
|
|
| Built at ArmNN commit (see `ARMNN_COMMIT.txt`), 2026-07-02, current `main` |
| HEAD at time of testing. |
|
|
| ```sh |
| # 1. Generate ArmnnSchema_generated.h and build armnnDeserializer + armnn |
| # (core, no backends needed) with ASan, NDEBUG (Release-like, matches |
| # real deployment: flatbuffers' internal asserts are compiled out): |
| cmake -S armnn -B build -G Ninja \ |
| -DBUILD_ARMNN_DESERIALIZER=ON -DBUILD_UNIT_TESTS=OFF -DBUILD_TESTS=OFF \ |
| -DBUILD_ONNX_PARSER=OFF -DBUILD_TF_LITE_PARSER=OFF -DBUILD_ARMNN_SERIALIZER=OFF \ |
| -DFLATC_DIR=/usr/bin -DFLATBUFFERS_LIBRARY=<path-to-libflatbuffers.so> \ |
| -DFLATBUFFERS_INCLUDE_PATH=<flatbuffers-include-dir> \ |
| -DCMAKE_CXX_FLAGS="-fsanitize=address -g -O1 -DNDEBUG" \ |
| -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" |
| ninja -C build armnnDeserializer |
| |
| # 2. Compile this harness against the just-built libraries (same compiler, |
| # e.g. g++, as used for the library, to avoid mixed ASan runtimes): |
| g++ -std=c++17 -fsanitize=address -g -O1 -DNDEBUG \ |
| -I armnn/include -I armnn/src/armnnUtils -I armnn/generated \ |
| -I <flatbuffers-include-dir> \ |
| harness.cpp -L build -Wl,-rpath,build -larmnnDeserializer -larmnn -o harness |
| |
| # 3. Craft the model file (already provided as poc_floor_stackoverflow.armnn, |
| # built from poc_floor_stackoverflow.json via): |
| flatc -b armnn/src/armnnSerializer/ArmnnSchema.fbs poc_floor_stackoverflow.json |
| |
| # 4. Run: |
| LD_LIBRARY_PATH=build ASAN_OPTIONS=detect_leaks=0 ./harness poc_floor_stackoverflow.armnn |
| ``` |
|
|
| ## Observed result (real evidence, not theoretical) |
|
|
| AddressSanitizer reports a **stack-buffer-overflow WRITE of size 1**, |
| directly at the vulnerable line, with a clean call stack from the public |
| `CreateNetworkFromBinary` API down to the vulnerable write: |
|
|
| ``` |
| ==420219==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7b4d36bf1885 ... |
| WRITE of size 1 at 0x7b4d36bf1885 thread T0 |
| #0 ... armnnDeserializer::ToTensorInfo(armnnSerializer::TensorInfo const*) Deserializer.cpp:756 |
| #1 ... armnnDeserializer::IDeserializer::DeserializerImpl::ParseFloor(...) Deserializer.cpp:2253 |
| #2 ... armnnDeserializer::IDeserializer::DeserializerImpl::CreateNetworkFromGraph(...) Deserializer.cpp:934 |
| #3 ... armnnDeserializer::IDeserializer::DeserializerImpl::CreateNetworkFromBinary(...) Deserializer.cpp:882 |
| #4 ... armnnDeserializer::IDeserializer::CreateNetworkFromBinary(...) Deserializer.cpp:59 |
| #5 ... main harness.cpp:29 |
| ... |
| Address 0x7b4d36bf1885 is located in stack of thread T0 at offset 133 in frame |
| #0 ... armnnDeserializer::ToTensorInfo(...) Deserializer.cpp:679 |
| ... |
| [128, 133) 'dimensionsSpecificity' (line 746) <== Memory access at offset 133 overflows this variable |
| ``` |
|
|
| Full log: `asan_crash_log.txt`. |
|
|
| ## Dedup / prior-art check |
|
|
| * `git blame` shows this exact code has been present, unmodified in the |
| relevant lines, since commit `800b281e50` (2021-02-12) β over 5 years, and |
| is still present at the current `main` HEAD (`2b61cecc9d`, 2026-07-02). |
| * No matching GitHub issue, PR, security advisory, or CVE for ArmNN |
| mentions `dimensionSpecificity`, `ToTensorInfo`, or a deserializer stack |
| overflow. The only related public fix we found (a heap overflow in |
| `SpaceToBatchND` input-type validation) is in the **separate** |
| `armnnTfLiteParser` component (TensorFlow Lite import), not |
| `armnnDeserializer` (ArmNN's own native FlatBuffers format), and concerns |
| input-type validation, not tensor-shape/stack handling. |
| * Distinct root cause from the companion `padList`/`crops` heap-overflow |
| finding filed alongside this one (different function, different bug |
| class β fixed-size stack buffer overflow vs. unsigned-integer-underflow |
| driven heap OOB read). |
|
|
| ## Suggested fix |
|
|
| Clamp the loop bound to `armnn::MaxNumOfTensorDimensions` (and reject/throw |
| a `ParseException` if `dimensionSpecificity->size() > |
| armnn::MaxNumOfTensorDimensions`, mirroring the pattern already used |
| elsewhere in this file for other size-mismatch checks). |
|
|
| ## Files |
|
|
| * `poc_floor_stackoverflow.json` β human-readable FlatBuffers JSON for the |
| crafted model (one `InputLayer`, one `FloorLayer` whose output |
| `TensorInfo.dimensionSpecificity` has 40 entries vs. a 5-element stack |
| buffer). |
| * `poc_floor_stackoverflow.armnn` β the compiled crafted model file |
| (`flatc -b ArmnnSchema.fbs poc_floor_stackoverflow.json`). |
| * `harness.cpp` β minimal driver that calls the real, unmodified |
| `armnnDeserializer::IDeserializer::CreateNetworkFromBinary()` API on a |
| file path given on the command line. |
| * `ArmnnSchema.fbs` β copy of the exact schema used (from the tested |
| commit), needed to regenerate the `.armnn` file with `flatc`. |
| * `asan_crash_log.txt` β full AddressSanitizer report from running the |
| harness against the PoC file. |
| * `ARMNN_COMMIT.txt` β the exact ARM-software/armnn commit hash this was |
| built and tested against. |
|
|