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:

m_ParserFunctions(Layer_MAX+1,
                  &IDeserializer::DeserializerImpl::ParseUnsupportedLayer)

CreateNetworkFromGraph() then trusts the model discriminator:

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:

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:

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.