YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
DecodeBbox3DPlugin deserialize() unbounded readFromBuffer loops driven by attacker-controlled mNumClasses cause heap OOB read
Summary
DecodeBbox3DPlugin::DecodeBbox3DPlugin(void const* data, size_t length) in TensorRT-OSS deserializes a native plugin blob from an engine plan file. It reads a fixed 52-byte header of 13 scalar fields with plugin::readFromBuffer<>(), a raw pointer dereference that performs no bounds check against length. One header field, mNumClasses (int32), is then used directly as the trip count for two unbounded read loops. A crafted engine plan whose inline DecodeBbox3DPlugin blob declares a large mNumClasses but is physically short triggers a heap-buffer-overflow read past the end of the deserialization buffer. The only bounds check (PLUGIN_VALIDATE(d == data + length)) runs after both loops โ after the OOB reads have already occurred.
Target
- Project: NVIDIA TensorRT-OSS (
NVIDIA/TensorRT) - Version: 11.1.0.106 (
VERSIONfile), commita892d22267d9cd2dedc1a0893e6892ac901f6d3d - Vulnerable file:
plugin/decodeBbox3DPlugin/decodeBbox3D.cpp, lines 81-112 (ctor) - Helper file:
plugin/common/templates.h, lines 41-47 (readFromBuffer<>) - Plugin registered via
REGISTER_TENSORRT_PLUGIN(DecodeBbox3DPluginCreator); entry pointDecodeBbox3DPluginCreator::deserializePlugin(decodeBbox3D.cpp:481).
Reachability
IRuntime::deserializeCudaEngine(plan, planLength) (closed TensorRT core)
-> IPluginCreator::deserializePlugin(name, serialData, serialLength)
(DecodeBbox3DPluginCreator, registered via REGISTER_TENSORRT_PLUGIN)
-> new DecodeBbox3DPlugin(serialData, serialLength) <-- vulnerable ctor
serialData / serialLength are copied verbatim out of the attacker-controlled engine plan file (the plugin's serialized blob is stored inline in the engine's layer table). Nothing in the closed core validates that the blob's field values are self-consistent with the number of bytes physically present before this constructor runs. Loading an untrusted .engine/.plan file โ a documented deployment pattern for serving prebuilt engines โ reaches this path.
Root cause
The deserialization helper (verbatim, plugin/common/templates.h:41-47):
// Helper function for deserializing plugin
template <typename ValType, typename BufferType>
ValType readFromBuffer(BufferType const*& buffer)
{
auto val = *toPointer<ValType const>(buffer); // raw deref, NO bounds check
buffer += sizeof(ValType);
return val;
}
The constructor (verbatim, plugin/decodeBbox3DPlugin/decodeBbox3D.cpp:81-112):
DecodeBbox3DPlugin::DecodeBbox3DPlugin(void const* data, size_t length)
{
PLUGIN_VALIDATE(data != nullptr);
auto const* d = reinterpret_cast<uint8_t const*>(data);
mMinXRange = readFromBuffer<float>(d);
mMaxXRange = readFromBuffer<float>(d);
mMinYRange = readFromBuffer<float>(d);
mMaxYRange = readFromBuffer<float>(d);
mMinZRange = readFromBuffer<float>(d);
mMaxZRange = readFromBuffer<float>(d);
mNumDirBins = readFromBuffer<int32_t>(d);
mDirOffset = readFromBuffer<float>(d);
mDirLimitOffset = readFromBuffer<float>(d);
mScoreThreashold = readFromBuffer<float>(d);
mNumClasses = readFromBuffer<int32_t>(d); // attacker-controlled
mFeatureH = readFromBuffer<int32_t>(d);
mFeatureW = readFromBuffer<int32_t>(d);
mAnchorBottomHeight.resize(mNumClasses);
for (int32_t i = 0; i < mNumClasses; i++)
{
mAnchorBottomHeight[i] = readFromBuffer<float>(d); // OOB read
}
mAnchors.resize(mNumClasses * 2 * 4);
for (int32_t i = 0; i < mNumClasses * 2 * 4; i++)
{
mAnchors[i] = readFromBuffer<float>(d); // OOB read
}
PLUGIN_VALIDATE(d == reinterpret_cast<uint8_t const*>(data) + length); // too late
}
The 13-field header is exactly 13 * 4 = 52 bytes. mNumClasses is read at offset 40 and never validated. By supplying a blob that is exactly the 52-byte header (no payload) with a large mNumClasses, the very first mAnchorBottomHeight loop iteration reads a float at data + 52 โ one byte past the end of the heap allocation holding the blob. length is never consulted during the reads; the trailing PLUGIN_VALIDATE bounds check executes only after both loops have already read out of bounds.
Proof of Concept
poc/decodeBbox3D_harness.cpp reproduces the verbatim constructor body from decodeBbox3D.cpp:81-112 plus the verbatim readFromBuffer/toPointer helpers from common/templates.h. PLUGIN_VALIDATE is stubbed to throw (matching the real macro's failure semantics). A malicious plugin blob is built as exactly the 52-byte header (13 scalar fields, no payload) with mNumClasses=4096, heap-allocated at exactly its length via malloc (ASAN-tracked). The constructor's first mAnchorBottomHeight loop iteration reads a float at data+52, one byte past the 52-byte allocation, triggering an ASAN heap-buffer-overflow READ.
Build & run:
clang++ -std=c++17 -O0 -g -fsanitize=address -fno-omit-frame-pointer \
decodeBbox3D_harness.cpp -o decode_asan
./decode_asan # attack
./decode_asan neg # negative control
Negative control: a well-formed 124-byte blob with mNumClasses=2 and a matching 18-float payload (2 anchorBottomHeight + 2*8 anchors) deserializes cleanly with no ASAN error and passes the trailing PLUGIN_VALIDATE.
Captured evidence (verbatim)
[attack] blob length = 52 bytes (header only), mNumClasses=4096
[attack] entering DecodeBbox3DPlugin ctor; expect ASAN heap OOB read...
=================================================================
==322126==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7bb6537e0054 at pc 0x5619f3e7daf7 bp 0x7ffc55a73270 sp 0x7ffc55a73268
READ of size 4 at 0x7bb6537e0054 thread T0
#0 0x5619f3e7daf6 in float nvinfer1::plugin::readFromBuffer<float, unsigned char>(unsigned char const*&) decodeBbox3D_harness.cpp:67:16
#1 0x5619f3e7d6e8 in DecodeBbox3DPlugin::DecodeBbox3DPlugin(void const*, unsigned long) decodeBbox3D_harness.cpp:111:38
#2 0x5619f3e7c659 in main decodeBbox3D_harness.cpp:167:24
0x7bb6537e0054 is located 0 bytes after 52-byte region [0x7bb6537e0020,0x7bb6537e0054)
allocated by thread T0 here:
#0 0x5619f3e369c8 in malloc
#1 0x5619f3e7c93e in buildBlob(int, unsigned long, unsigned long*) decodeBbox3D_harness.cpp:129:42
#2 0x5619f3e7c5cf in main decodeBbox3D_harness.cpp:163:33
SUMMARY: AddressSanitizer: heap-buffer-overflow decodeBbox3D_harness.cpp:67:16 in float nvinfer1::plugin::readFromBuffer<float, unsigned char>(unsigned char const*&)
==322126==ABORTING
--- Negative control ---
[neg] blob length = 124 bytes, mNumClasses=2
[neg] deserialized OK, no OOB (expected)
(The address/PID/PC values differ run to run; the fault site, allocation site, and access size are stable.)
Impact
Heap out-of-bounds read of attacker-influenced size during engine deserialization. Consequences: information disclosure (adjacent heap contents are read into plugin fields and can subsequently influence kernel behavior/outputs), and denial of service (ASAN abort in a hardened build; SIGSEGV when the read walks off a mapped page for a sufficiently large mNumClasses). Trigger requires only that a victim deserialize an untrusted engine plan โ a common pattern when distributing prebuilt TensorRT engines.
Suggested fix
Validate remaining buffer length before every read. Before the loops, require length >= 52 and length == 52 + (mNumClasses + mNumClasses*2*4) * sizeof(float) (and reject negative/oversized mNumClasses) before performing any element reads โ i.e., move the size self-consistency check ahead of the loops rather than after them, and make readFromBuffer bounds-aware against length.
Dedup note
Distinct from the prior TensorRT plugin OOB finding in embLayerNormPlugin (convertAndCopy / WeightsWithConversion::convertAndCopy bulk copy driven by mLd/vocab sizes). This is a different plugin (decodeBbox3DPlugin), a different helper (per-element readFromBuffer<> loop vs. bulk convertAndCopy), and a different attacker-controlled field (mNumClasses vs mLd/vocab sizes). Also distinct from the earlier TensorRT ONNX path-traversal and volume int-overflow findings. No CVE currently maps to DecodeBbox3DPlugin deserialization.