YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
TensorRT-OSS β RPROIPlugin::deserialize out-of-bounds heap read (no upfront length validation)
Target: NVIDIA TensorRT OSS (NVIDIA/TensorRT)
Component: plugin/nvFasterRCNN/nvFasterRCNNPlugin.cpp β RPROIPlugin::deserialize(int8_t const* data, size_t length)
Version tested against: TensorRT-OSS 11.1.0.106 (git a892d22)
Class: CWE-125 Out-of-bounds Read (heap) during plugin/engine deserialization
Impact: Loading a crafted/truncated serialized engine or plugin field drives out-of-bounds heap reads before any length check runs β DoS (crash) and potential information disclosure into deserialized plugin state.
Root cause
RPROIPlugin::deserialize reads a full RPROIParams struct and then seven scalar fields
and two variable-length anchor arrays before validating that the input buffer is large
enough. The only guard, PLUGIN_VALIDATE(d == data + length), runs after every read has
already dereferenced memory.
plugin/nvFasterRCNN/nvFasterRCNNPlugin.cpp (lines 84β100):
RPROIPlugin::RPROIPlugin(void const* data, size_t length)
{
// ...
auto const* d{reinterpret_cast<int8_t const*>(data)};
params = *reinterpret_cast<RPROIParams const*>(d); // <-- 40-byte read, NO length check
d += sizeof(RPROIParams);
A = read<int32_t>(d);
C = read<int32_t>(d);
H = read<int32_t>(d);
W = read<int32_t>(d);
mInFeatureType = read<DataType>(d);
mOutFeatureType = read<DataType>(d);
mInFeatureLayout = read<DLayout_t>(d);
copyToHost(d, params.anchorsRatioCount); // <-- element count from attacker-controlled struct
// ...
copyToHost(d, params.anchorsScaleCount); // <-- element count from attacker-controlled struct
// ...
PLUGIN_VALIDATE(d == data + length); // <-- length checked TOO LATE
}
The very first statement, params = *reinterpret_cast<RPROIParams const*>(d), unconditionally
reads sizeof(RPROIParams) (40 bytes) from data. If the serialized blob is shorter than 40
bytes, this reads past the allocation immediately. It then advances d and performs seven
unbounded read<T>(d) scalar reads, followed by two copyToHost(d, params.anchorsRatioCount) /
copyToHost(d, params.anchorsScaleCount) reads whose element counts come from the struct that
was just deserialized from attacker-controlled bytes.
read<>() is a bare memcpy with no bounds check β plugin/common/plugin.h (lines 100β108):
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));
buffer += sizeof(OutType);
return val;
}
deserialize() is reachable through the RPROIPlugin(void const* data, size_t length)
constructor and the creator's deserializePlugin(), both invoked with attacker-controlled
bytes when a serialized engine / plugin field is loaded.
Contrast (correct pattern in a sibling plugin): the legacy ROIAlign plugin validates
PLUGIN_VALIDATE(length == kSERIALIZATION_SIZE) up front before touching the buffer.
RPROIPlugin has no equivalent guard.
PoC
Faithful host repro: poc/rproi_deser_oob.cpp, built with
g++ -std=c++17 -fsanitize=address.
RPROIParamsis copied verbatim frominclude/NvInferPluginUtils.h(lines 67β80).- The
read<>template is copied verbatim fromplugin/common/plugin.h(lines 100β108). - The
deserializebody replaysnvFasterRCNNPlugin.cpplines 84β100 exactly.
A full CUDA/GPU shared-library build is infeasible on the test VM (no nvcc / CUDA toolkit), but
the OOB read is pure host code hit before any CUDA call, so the repro exercises the identical
read sequence, struct layout, and read<> primitive of the real target. copyToHost (a
cudaMemcpy in the real code) is modeled as a host-side read of count * sizeof(float) bytes
from d, exercising the same attacker-controlled length.
Attack input (mode 0, default): an 8-byte malloc'd buffer passed with length = 8 (a
truncated serialized plugin blob). sizeof(RPROIParams) = 40, so the first line reads 32 bytes
past the allocation.
Negative control (mode 1): a well-formed 88-byte buffer with valid small anchor counts
deserializes cleanly and PLUGIN_VALIDATE passes.
Build & run:
g++ -std=c++17 -fsanitize=address -g -o rproi_deser_oob rproi_deser_oob.cpp
./rproi_deser_oob 1 # negative control (well-formed)
./rproi_deser_oob 0 # attack (truncated 8-byte blob) -> ASan heap-buffer-overflow
Captured evidence (verbatim)
########## NEGATIVE CONTROL (well-formed) ##########
[negctrl] well-formed length=88
[negctrl] deserialize OK, no OOB
########## ATTACK (truncated 8-byte blob) ##########
[attack] sizeof(RPROIParams)=40, serialized length=8
=================================================================
==319357==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b99d0fe0010 at pc 0x5590bce16a7a bp 0x7ffe0fc0f520 sp 0x7ffe0fc0f518
READ of size 40 at 0x7b99d0fe0010 thread T0
#0 0x5590bce16a79 in RPROIPluginRepro::deserialize(signed char const*, unsigned long) rproi_deser_oob.cpp:65
#1 0x5590bce16514 in main rproi_deser_oob.cpp:103
0x7b99d0fe0018 is located 0 bytes after 8-byte region [0x7b99d0fe0010,0x7b99d0fe0018)
allocated by thread T0 here:
#0 0x7f79d232435f in malloc
#1 0x5590bce163fe in main rproi_deser_oob.cpp:99
SUMMARY: AddressSanitizer: heap-buffer-overflow rproi_deser_oob.cpp:65 in RPROIPluginRepro::deserialize(signed char const*, unsigned long)
(Re-run on this machine reproduces the same ASan heap-buffer-overflow READ of size 40
"0 bytes after 8-byte region" signature; the PC/address values differ per run/ASLR.)
Suggested fix
Validate the buffer length before the first read, mirroring the ROIAlign plugin:
// minimum fixed-size header = sizeof(RPROIParams) + 4*int32 + 2*DataType + DLayout_t
PLUGIN_VALIDATE(length >= sizeof(RPROIParams) + 4*sizeof(int32_t)
+ 2*sizeof(DataType) + sizeof(DLayout_t));
and, after reading params, verify that
params.anchorsRatioCount / params.anchorsScaleCount are non-negative and that the remaining
length covers (anchorsRatioCount + anchorsScaleCount) * sizeof(float) before the
copyToHost reads.
Dedup note
This is a distinct sink from other TensorRT plugin deserialize OOB findings already filed
(EmbLayerNorm, DecodeBbox3D, PriorBox, Region, FlattenConcat, PillarScatter). Each is a
separate plugin file with its own deserialize() body and its own missing-length-guard root
cause; this report concerns RPROIPlugin in plugin/nvFasterRCNN/nvFasterRCNNPlugin.cpp
specifically, characterized by an unchecked full-struct *reinterpret_cast<RPROIParams const*>(d)
read as the first operation plus attacker-controlled anchor-count array reads. No CVE is known
to cover this specific plugin's deserialize path at the tested commit.