trevdatastreams's picture
Publish deterministic Arm NN parser-table OOB PoC
277dba7 verified
|
Raw
History Blame Contribute Delete
3.75 kB
# Arm NN unknown layer discriminator parser-table OOB read
## Summary
Arm NN's native `.armnn` deserializer accepts an out-of-range `AnyLayer`
union discriminator and uses it directly to index `m_ParserFunctions`.
`m_ParserFunctions` has `Layer_MAX + 1` entries, but
`CreateNetworkFromGraph()` performs no range check before indexing it.
The supplied trigger differs from the valid control by one byte:
the `AnyLayer.layer_type` discriminator at file offset 763 changes from
`0x09` (`InputLayer`) to `0x7b` (123, beyond `Layer_MAX`). The outer
FlatBuffer verifier accepts the file. The subsequent parser-table lookup
causes an ASan heap-buffer-overflow read.
## Verified target
- Repository: `ARM-software/armnn`
- Commit: `2b61cecc9df7a43fca1463795062cf359e6be820`
- Date: 2026-07-02
- Library: `libarmnnDeserializer.35.0`
- Entry point: `armnnDeserializer::IDeserializer::CreateNetworkFromBinary`
## Root cause
The constructor allocates the dispatch table at
`Deserializer.cpp:207-210`:
```cpp
m_ParserFunctions(Layer_MAX+1,
&IDeserializer::DeserializerImpl::ParseUnsupportedLayer)
```
`CreateNetworkFromGraph()` then trusts the model discriminator:
```cpp
for (AnyLayer const* layer : *graph->layers())
{
if (layer->layer_type() != Layer_InputLayer &&
layer->layer_type() != Layer_OutputLayer)
{
auto& parserFunction = m_ParserFunctions[layer->layer_type()];
(this->*parserFunction)(graph, layerIndex);
}
}
```
The union discriminator is an eight-bit value. The current generated enum has
`Layer_MAX = Layer_ScatterNdLayer`, while the trigger supplies 123. The
FlatBuffers verifier verifies structural offsets but does not reject this
unknown enum value.
## Differential proof
- `control.armnn`: 908 bytes, discriminator byte `0x09`
- `trigger.armnn`: 908 bytes, discriminator byte `0x7b`
- All other bytes are identical.
Three control runs completed with exit code 0. Three trigger runs terminated
with exit code 134 and the same AddressSanitizer heap-buffer-overflow in
`CreateNetworkFromGraph()`.
SHA-256:
- control: `e1d1d9895e1629eaea453ac60cdc21cd38aef7bea9422aaefb41e23921cfd7ae`
- trigger: `ff1c59432c9de9a631f1df9aab69d077d8b7a31e2e74b82aba4114da66f064bd`
## Reproduction
With an ASan/UBSan Arm NN build in `cyber/huntr-mfv/build-armnn`:
```bash
chmod +x reproduce.sh
./reproduce.sh /absolute/path/to/makemoney
```
`generate-trigger.mjs` checks the control length and original discriminator,
then deterministically applies the one-byte mutation.
## Impact
Loading an untrusted `.armnn` model reads a member-function pointer beyond the
heap allocation that stores the parser dispatch table. The demonstrated
result is a reliable load-time crash. Depending on adjacent heap contents, the
subsequent indirect member-function call may also consume attacker-influenced
data as a code pointer.
The report does not claim code execution; the verified impact is
out-of-bounds read and denial of service.
## Prior-art distinction
Exact searches for `AnyLayer.layer_type`, `m_ParserFunctions`,
`CreateNetworkFromGraph`, source line 933, the fixture hash, and the sanitizer
signature returned no public disclosure. Public native Arm NN findings cover
different semantic fields and sinks. The previously submitted TFLite
`opcode_index` issue is in a different model format, parser, dispatch table,
and fix site.
## Suggested fix
Reject any discriminator outside the generated enum range before indexing:
```cpp
const auto type = layer->layer_type();
if (type <= Layer_NONE || type > Layer_MAX)
{
throw ParseException("Unsupported layer union discriminator");
}
```
The generated FlatBuffer union verifier should also fail closed for unknown
union discriminators.