YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Heap-buffer-overflow OOB read in TensorRT-OSS FlattenConcat_TRT plugin deserialize constructor (attacker-controlled mNumInputs element count)
Target
- Project: NVIDIA TensorRT (TensorRT-OSS plugin library, compiled into
libnvinfer_plugin.soshipped with every TensorRT release) - Repo: https://github.com/NVIDIA/TensorRT
- Commit:
a892d22267d9cd2dedc1a0893e6892ac901f6d3d(2026-07-07),VERSIONfile11.1.0.106, HEAD ofmain - Vulnerable file:
plugin/flattenConcat/flattenConcat.cppβFlattenConcat::FlattenConcat(void const* data, size_t length), lines 60β79 - Supporting code:
plugin/common/plugin.hread<>()(lines 100β108) - Class: CWE-125 Out-of-bounds Read (heap-buffer-overflow READ)
Trigger path
IRuntime::deserializeCudaEngine() parses an attacker-supplied .plan/.engine
file that contains an IPluginV2Layer of type "FlattenConcat_TRT" (registered via
REGISTER_TENSORRT_PLUGIN). The engine reader hands the plugin's inline
serialized-data blob verbatim to the plugin creator:
FlattenConcatPluginCreator::deserializePlugin(name, serialData, serialLength)
-> new FlattenConcat(serialData, serialLength)
-> FlattenConcat::FlattenConcat(void const* data, size_t length) <-- vulnerable
serialData / serialLength come verbatim from the attacker-controlled plan file.
Root cause
The constructor reads a 32-bit element count mNumInputs directly out of the
attacker blob and then loops that many times pulling values via the unchecked
read<> helper. The only length check runs at the very end of the
constructor β after every read loop has already executed:
// plugin/flattenConcat/flattenConcat.cpp:60-79 (VERBATIM)
FlattenConcat::FlattenConcat(void const* data, size_t length)
{
char const* d = static_cast<char const*>(data);
char const* const a = d;
mIgnoreBatch = read<bool>(d);
mConcatAxisID = read<int32_t>(d);
PLUGIN_VALIDATE(mConcatAxisID >= 1 && mConcatAxisID <= 3);
mOutputConcatAxis = read<int32_t>(d);
mNumInputs = read<int32_t>(d); // <-- attacker-controlled count
mInputConcatAxis.resize(mNumInputs);
std::for_each(mInputConcatAxis.begin(), mInputConcatAxis.end(),
[&](int32_t& inp) { inp = read<int32_t>(d); }); // OOB read loop
mCHW = read<nvinfer1::Dims3>(d);
mCopySize.resize(mNumInputs);
std::for_each(mCopySize.begin(), mCopySize.end(),
[&](size_t& inp) { inp = read<size_t>(d); }); // 2nd OOB read loop
PLUGIN_VALIDATE(d == a + length); // length check AFTER OOB reads
}
// plugin/common/plugin.h:100-108 (VERBATIM)
template <typename OutType, typename BufferType>
OutType read(BufferType const*& buffer)
{
static_assert(sizeof(BufferType) == 1, "BufferType must be a 1 byte type.");
OutType val{};
std::memcpy(&val, static_cast<void const*>(buffer), sizeof(OutType)); // no bound
buffer += sizeof(OutType);
return val;
}
mNumInputs is never validated against the number of bytes physically remaining in
the blob. read<int32_t>() is an unconditional 4-byte memcpy that advances the
cursor with no bound check. The first std::for_each reads mNumInputs consecutive
int32 values; a second std::for_each reads mNumInputs size_t values into
mCopySize the same way. The sole PLUGIN_VALIDATE(d == a + length) runs only after
both loops complete β i.e. after the out-of-bounds reads have already occurred. By
inflating mNumInputs while supplying a blob that only contains the 13-byte header,
the read<int32_t> loop walks off the end of the heap allocation holding the plugin
blob β heap-buffer-overflow READ.
PoC
A faithful standalone ASan harness reproduces the bug using the real, unmodified
TensorRT-OSS code for read<> (plugin/common/plugin.h) and the verbatim body of
the FlattenConcat deserialize constructor (flattenConcat.cpp lines 60β79). Field
writes are retargeted to layout-compatible static mirrors so the verbatim constructor
body compiles standalone; every read<> call is byte-for-byte the real code.
- Harness:
flattenConcat_harness.cpp - Negative control:
flattenConcat_negctl.cpp
The harness heap-allocates a 13-byte blob containing only the header
(mIgnoreBatch=0, mConcatAxisID=1 to pass the [1,3] validate, mOutputConcatAxis=3,
mNumInputs=100000) with no per-input payload. Calling the constructor makes the
mInputConcatAxis for_each read loop walk read<int32_t>() off the end of the
13-byte allocation.
Build & run
clang++ -std=c++17 -O0 -g -fsanitize=address -fno-omit-frame-pointer \
flattenConcat_harness.cpp -o flattenConcat_asan
./flattenConcat_asan
clang++ -std=c++17 -O0 -g -fsanitize=address -fno-omit-frame-pointer \
flattenConcat_negctl.cpp -o flattenConcat_negctl
./flattenConcat_negctl
Captured evidence (verbatim)
############ POSITIVE (attack) ############
[*] blob heap alloc = 13 bytes; mNumInputs (attacker) = 100000
[*] concat-axis loop will read 100000 int32 (400000 bytes) from a 13-byte blob
[*] calling FlattenConcat::FlattenConcat(serialData, serialLength) ...
=================================================================
==305646==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b84fbfe001d at pc 0x561d9a6de68f bp 0x7ffeed94b900 sp 0x7ffeed94b0c0
READ of size 4 at 0x7b84fbfe001d thread T0
#0 0x561d9a6de68e in __asan_memcpy
#1 0x561d9a727545 in int nvinfer1::plugin::read<int, char>(char const*&) flattenConcat_harness.cpp:68:5
#2 0x561d9a727049 in deserializeFlattenConcat(void const*, unsigned long)::$_0::operator()(int&) const flattenConcat_harness.cpp:114:95
#3 0x561d9a726c7a in deserializeFlattenConcat(void const*, unsigned long)::$_0 std::for_each<...>(...) stl_algo.h:3798:2
#4 0x561d9a7268b8 in deserializeFlattenConcat(void const*, unsigned long) flattenConcat_harness.cpp:114:5
#5 0x561d9a72632b in main flattenConcat_harness.cpp:159:9
0x7b84fbfe001d is located 0 bytes after 13-byte region [0x7b84fbfe0010,0x7b84fbfe001d)
allocated by thread T0 here:
#0 0x561d9a6e0a38 in malloc
#1 0x561d9a7261f4 in main flattenConcat_harness.cpp:141:43
SUMMARY: AddressSanitizer: heap-buffer-overflow in __asan_memcpy
==305646==ABORTING
############ NEGATIVE CONTROL ############
[*] well-formed blob = 73 bytes; mNumInputs = 2 (matches payload)
[*] deserialize completed cleanly, no ASan report (expected)
The negative control feeds a well-formed 73-byte blob whose mNumInputs=2 matches the
actual payload and whose length is exact: the constructor completes cleanly, no ASan
report, final PLUGIN_VALIDATE(d == a + length) passes. This isolates the defect to the
count/length mismatch, not an artifact of the harness.
Impact
An attacker who supplies a crafted TensorRT engine/plan file (a common untrusted input:
models are routinely downloaded, shared, and served) triggers an out-of-bounds heap read
during deserializeCudaEngine(). Depending on heap layout this is a crash (DoS) or an
information disclosure of adjacent heap memory into the plugin's mInputConcatAxis /
mCopySize arrays, which may subsequently influence execution. The second loop reading
mNumInputs size_t values compounds the over-read.
Dedup
- No public CVE currently describes this
FlattenConcat_TRTdeserialize-constructor count/length mismatch. - Distinct plugin, file, and function from prior findings in this audit: the
PriorBox_TRT(priorBoxPlugin.cpp),DecodeBbox3D,Region, andembLayerNormPluginTensorRT findings. This is theFlattenConcatplugin's constructor, driven by an unchecked serializedmNumInputscount. - Same root-cause family as the other TensorRT plugin deserialize OOB reads
(unbounded
read<>loop driven by an attacker-controlled serialized count with the length check placed after the loops), but a separate plugin and code path.
Suggested fix
Before resize/loop, validate that the requested bytes remain in the blob, e.g.
PLUGIN_VALIDATE(mNumInputs >= 0 && (d - a) + mNumInputs * (sizeof(int32_t) + sizeof(size_t)) + sizeof(nvinfer1::Dims3) <= length);
so the length check precedes any read<>() in the loops.