ArmNN Deserializer β NULL-pointer dereferences via missing optional FlatBuffers fields (CWE-476)
Status: gated, access on request (huntr / ProtectAI triage). Do not use against production systems you do not own.
Summary
ARM-software/armnn's native model-file parser (armnnDeserializer,
src/armnnDeserializer/Deserializer.cpp) repeatedly calls flatbuffers accessor methods on
fields that are optional in ArmnnSchema.fbs (i.e. legally absent/null from any
well-formed, VerifyModelBuffer-passing .armnn file) without a null check first. Two
independent call sites crash on a .armnn file that is otherwise structurally valid:
IDeserializer::DeserializerImpl::GetOutputs()(Deserializer.cpp:848):TensorRawPtrVector IDeserializer::DeserializerImpl::GetOutputs(const GraphPtr& graphPtr, unsigned int layerIndex) { CHECK_LAYERS(graphPtr, 0, layerIndex); auto layer = GetBaseLayer(graphPtr, layerIndex); const auto& numOutputs = layer->outputSlots()->size(); // <-- outputSlots() may be nullptr ...LayerBase.outputSlotsis an optional[OutputSlot]vector field in the schema. A layer (e.g. aConstantLayer) whosebase.outputSlotsfield is simply omitted from the serialized flatbuffer produceslayer->outputSlots() == nullptr, and->size()on that null pointer segfaults.IDeserializer::DeserializerImpl::SetupInputLayers()(Deserializer.cpp:1059):void IDeserializer::DeserializerImpl::SetupInputLayers(GraphPtr graph) { CHECK_GRAPH(graph, 0); const unsigned int numInputs = graph->inputIds()->size(); // <-- inputIds() may be nullptr ...Graph.inputIdsis an optional[int]vector field. A graph that omits it entirely (as opposed to supplying an empty[], which is legal and handled byCHECK_GRAPH) makesgraph->inputIds()returnnullptr, and->size()segfaults identically.
Both are the same root-cause pattern (CWE-476: calling a flatbuffers accessor on an optional field's raw pointer without a null check), just triggered by omitting two different optional fields, so we treat this as one vulnerability class with two independently reachable PoCs.
Attacker input β sink
nullderef_poc_getoutputs.armnn: oneInputLayer+ oneConstantLayerwhosebase.base(LayerBase) table explicitly omits theoutputSlotsfield. Loading it viaarmnnDeserializer::IDeserializer::CreateNetworkFromBinary()reachesCreateNetworkFromGraph()β dispatch toParseConstant()(Deserializer.cpp:1487) βGetOutputs()(Deserializer.cpp:848) β null-deref onoutputSlots()->size().nullderef_poc_setupinputlayers.armnn: aGraphtable that omits theinputIdsfield entirely.CreateNetworkFromGraph()callsSetupInputLayers()unconditionally (Deserializer.cpp:939) after parsing all layers, which null-derefs oninputIds()->size().
Both fields are declared (optional) (no explicit default forcing presence) in
ArmnnSchema.fbs, so flatbuffers::Verifier/circle-style structural verification
(FlatBuffers' own Verify(), which ArmNN's deserializer does not even call before
walking the buffer) would not reject either file as malformed β omitting an optional field is,
by flatbuffers definition, perfectly valid wire data.
Reproduction
Built at ArmNN commit (see ARMNN_COMMIT.txt), current main HEAD at time of testing, with
ASan (-fsanitize=address -g -O1 -DNDEBUG), reusing the same build/harness pattern as the
companion armnn-heapoverflow/armnn-stackoverflow findings (harness.cpp β minimal driver
that calls the real, unmodified armnnDeserializer::IDeserializer::CreateNetworkFromBinary()).
# harness already built against libarmnnDeserializer.so / libarmnn.so (ASan build)
LD_LIBRARY_PATH=<armnn-build-dir> ASAN_OPTIONS=detect_leaks=0 ./harness nullderef_poc_getoutputs.armnn
LD_LIBRARY_PATH=<armnn-build-dir> ASAN_OPTIONS=detect_leaks=0 ./harness nullderef_poc_setupinputlayers.armnn
Observed result (real evidence, not theoretical)
nullderef_poc_getoutputs.armnn:
==916400==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
#0 ... flatbuffers::Vector<flatbuffers::Offset<armnnSerializer::OutputSlot>, unsigned int>::size() const vector.h:164
#1 ... armnnDeserializer::IDeserializer::DeserializerImpl::GetOutputs(...) Deserializer.cpp:848
#2 ... armnnDeserializer::IDeserializer::DeserializerImpl::ParseConstant(...) Deserializer.cpp:1487
#3 ... armnnDeserializer::IDeserializer::DeserializerImpl::CreateNetworkFromGraph(...) Deserializer.cpp:934
#4 ... armnnDeserializer::IDeserializer::DeserializerImpl::CreateNetworkFromBinary(...) Deserializer.cpp:882
#5 ... armnnDeserializer::IDeserializer::CreateNetworkFromBinary(...) Deserializer.cpp:59
#6 ... main harness.cpp:29
SUMMARY: AddressSanitizer: SEGV vector.h:164 in flatbuffers::Vector<...>::size() const
nullderef_poc_setupinputlayers.armnn:
==916402==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
#0 ... flatbuffers::Vector<int, unsigned int>::size() const vector.h:164
#1 ... armnnDeserializer::IDeserializer::DeserializerImpl::SetupInputLayers(...) Deserializer.cpp:1059
#2 ... armnnDeserializer::IDeserializer::DeserializerImpl::CreateNetworkFromGraph(...) Deserializer.cpp:939
#3 ... armnnDeserializer::IDeserializer::DeserializerImpl::CreateNetworkFromBinary(...) Deserializer.cpp:882
#4 ... armnnDeserializer::IDeserializer::CreateNetworkFromBinary(...) Deserializer.cpp:59
#5 ... main harness.cpp:29
SUMMARY: AddressSanitizer: SEGV vector.h:164 in flatbuffers::Vector<int, ...>::size() const
Full logs: asan_crash_log_getoutputs.txt, asan_crash_log_setupinputlayers.txt. Both are
raw SIGSEGV on a null pointer read β reproducible identically with or without ASan/NDEBUG,
unlike the sibling underflow/OOB-read findings that depend on NDEBUG compiling out an
assert().
Suggested fix
Add explicit null checks (if (layer->outputSlots() == nullptr) { ... } /
if (graph->inputIds() == nullptr) { ... }) before calling ->size()/iterating, matching the
pattern ArmNN already uses elsewhere for genuinely optional fields (e.g.
graph->featureVersions() in GetFeatureVersions(), a few lines above SetupInputLayers()
in the same file).
Files
nullderef_poc_getoutputs.armnnβ crafted model triggering theGetOutputs()null-deref.nullderef_poc_setupinputlayers.armnnβ crafted model triggering theSetupInputLayers()null-deref.harness.cppβ minimal driver that calls the real, unmodifiedarmnnDeserializer::IDeserializer::CreateNetworkFromBinary()API on a file path given on the command line.ArmnnSchema.fbsβ copy of the exact schema used (from the tested commit).asan_crash_log_getoutputs.txt,asan_crash_log_setupinputlayers.txtβ full AddressSanitizer reports from running the harness against each PoC file.ARMNN_COMMIT.txtβ the exact ARM-software/armnn commit hash this was built and tested against.