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's native-engine plugin deserialization
Plugin: CustomEmbLayerNormPluginDynamic (legacy versions "2" / "3") β embLayerNormVarSeqlenPluginLegacy.cpp
Target: NVIDIA/TensorRT (TensorRT-OSS plugin library, plugin/embLayerNormPlugin/)
Compiled into libnvinfer_plugin.so shipped with every TensorRT release.
Verified commit: a892d22267d9cd2dedc1a0893e6892ac901f6d3d (2026-07-07, current HEAD at time of testing)
Dedup note: This is a distinct bug from the recently patched CVE-2026-24188 (a vaguely described
"out-of-bounds write" in "tensor processing routines," fixed in TensorRT 10.16.1). CVE-2026-24188's
description does not mention this plugin or this code path, and this bug is present in the current
HEAD, which post-dates that patch. This finding is also independent of and unrelated to any prior
onnxtrt-* / ONNX-importer findings β it is in the native engine (.plan/.engine) plugin deserializer,
not the ONNX importer.
Root cause
TensorRT's native engine format (.plan/.engine) supports custom IPluginV2 layers whose
plugin-specific serialized data is deserialized via
IPluginCreator::deserializePlugin(name, serialData, serialLength) during
IRuntime::deserializeCudaEngine().
For the legacy BERT plugin CustomEmbLayerNormPluginDynamic, the deserializing constructor
EmbLayerNormVarSeqlenPluginLegacyBase::EmbLayerNormVarSeqlenPluginLegacyBase(
std::string const& name, void const* data, size_t length)
(plugin/embLayerNormPlugin/embLayerNormVarSeqlenPluginLegacy.cpp:77-108) first reads 6 fixed
scalar fields (mType, mLd, mWordVocabSize, mPosVocabSize, mTokVocabSize, mMaskType) via
deserialize_value() (plugin/common/serialize.hpp). Each of these scalar reads is correctly
bounds-checked against the remaining length and throws std::runtime_error if too short.
Immediately afterward, the constructor calls
WeightsWithOwnership::convertAndCopy(char const*& srcBuf, size_t count, DataType type)
five times β for mBeta, mGamma, mWordEmb, mPosEmb, mTokEmb β passing count derived
directly from the just-deserialized, fully attacker-controlled size_t fields (mLd,
mLd*mWordVocabSize, mLd*mPosVocabSize, mLd*mTokVocabSize).
// plugin/common/bertCommon.h:434-444
void convertAndCopy(char const*& srcBuf, size_t count, DataType type)
{
...
auto const nbBytes = count * getElementSize(type);
...
std::copy_n(srcBuf, nbBytes, destBuf); // <-- no check against remaining `length`
srcBuf += nbBytes;
}
convertAndCopy computes nbBytes = count * getElementSize(type) and unconditionally does
std::copy_n(srcBuf, nbBytes, destBuf). It never consults the remaining length value that
deserialize_value was tracking, so there is no check that nbBytes bytes are actually present
in the plugin's serialized blob.
An attacker who crafts a .plan/.engine file containing a CustomEmbLayerNormPluginDynamic
layer whose plugin-data blob declares an inflated mLd/mWordVocabSize/etc. but supplies a short
physical byte length triggers a massive out-of-bounds heap read on the very first convertAndCopy
call β before any CUDA/GPU code runs.
The try/catch wrapped around deserializePlugin() by callers only catches C++ exceptions (i.e.
the earlier, correctly bounds-checked scalar reads); it does not protect against this raw OOB
memory access. In a non-instrumented production build this manifests as a crash (SIGSEGV, unhandled
by the try/catch) or as disclosure of adjacent heap memory copied into the plugin's weight buffers
β buffers that are subsequently uploaded to the GPU and used in inference, i.e. exposure of
unrelated process memory content to whatever consumes the plugin's outputs.
PoC
Standalone C++ harness β no NVIDIA GPU, no CUDA toolkit, no proprietary libnvinfer.so required β
built entirely from verbatim, unmodified source extracted from the Apache-2.0-licensed
plugin/ tree of github.com/NVIDIA/TensorRT:
plugin/common/serialize.hppcopied unmodified (deserialize_value/Serializer<T>).getElementSize/getWeightsSizecopied verbatim fromplugin/common/bertCommon.h.WeightsWithOwnership::convertAndCopy(char const*&, size_t, DataType)copied verbatim frombertCommon.h:434-444.- The deserializing constructor body copied verbatim from
embLayerNormVarSeqlenPluginLegacy.cpp:77-108β only the trailing CUDAcopyToDevice()calls (which run strictly after the vulnerable reads, and are unreachable once ASan aborts) are stubbed out.
main() crafts a malicious 40-byte plugin blob β exactly what would be embedded inline in a
crafted .plan/.engine file's IPluginV2Layer entry for a CustomEmbLayerNormPluginDynamic
layer β setting mLd = 2^20 while supplying zero payload bytes, then invokes the real constructor
exactly as IPluginCreator::deserializePlugin() does when IRuntime::deserializeCudaEngine() loads
such an engine.
Built with clang++ -std=c++17 -O0 -g -fsanitize=address,undefined.
A negative control (negative_control.cpp, exercising the same real code paths) feeds a
well-formed 168-byte blob whose header fields accurately describe the physically-present payload.
It deserializes cleanly with zero ASan reports and exit code 0, confirming the crash is specific to
the header/length mismatch an attacker controls, not general harness instability.
Files
poc/harness.cppβ malicious-blob PoCpoc/negative_control.cppβ well-formed-blob negative controlpoc/serialize.hppβ verbatim upstream header used by both- Upstream clone used for extraction: TensorRT-OSS commit
a892d22267d9cd2dedc1a0893e6892ac901f6d3d(2026-07-07)
Captured evidence (verbatim)
Malicious blob β triggers heap-buffer-overflow
==240688==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7c03debe0078 at pc 0x55c221b43b57 bp 0x7fff7116b4f0 sp 0x7fff7116acb0
READ of size 4194304 at 0x7c03debe0078 thread T0
#0 0x55c221b43b56 in __asan_memmove
#1 0x55c221b8d4ee in char* std::__copy_n_a<char const*, long, char*>(...) stl_algobase.h:551:8
#2 0x55c221b8d30c in char* std::copy_n<char const*, long, char*>(...) stl_algo.h:698:20
#3 0x55c221b8cfa0 in WeightsWithOwnership::convertAndCopy(char const*&, unsigned long, nvinfer1::DataType) harness.cpp:183:9
#4 0x55c221b8c61e in EmbLayerNormVarSeqlenPluginLegacyBase::EmbLayerNormVarSeqlenPluginLegacyBase(std::string const&, void const*, unsigned long) harness.cpp:222:15
#5 0x55c221b8b499 in main harness.cpp:280:43
0x7c03debe0078 is located 0 bytes after 40-byte region [0x7c03debe0050,0x7c03debe0078)
allocated by thread T0 here:
#0 ... operator new[](unsigned long)
#1 ... in main harness.cpp:260:18
SUMMARY: AddressSanitizer: heap-buffer-overflow ... in __asan_memmove
==240688==ABORTING
Exit code 1; reproduced identically on a second independent run.
Negative control β well-formed 168-byte blob (header=40 + payload=128)
[*] NEGATIVE CONTROL: well-formed plugin blob, 168 bytes total (header=40 + payload=128),
[*] header fields accurately describe the physically-present payload. Expect NO ASan report.
[+] Negative control PASSED: deserialized 168-byte well-formed blob with no memory-safety error.
Exit code 0, no ASan report.
Impact
Loading an attacker-supplied/attacker-modified .plan/.engine file containing a crafted
CustomEmbLayerNormPluginDynamic layer causes TensorRT's engine deserialization to perform a
large out-of-bounds heap read immediately upon IRuntime::deserializeCudaEngine(), prior to any
GPU/CUDA execution and unprotected by the surrounding exception handling. Depending on heap layout
this results in a crash (denial of service) or in adjacent process heap memory being copied into
the plugin's weight tensors, which are then used in subsequent inference β i.e., a potential
information-disclosure primitive via a nominally "just a model file" input.
Suggested fix
convertAndCopy (and the calling constructor) must validate that nbBytes (and the cumulative
total across all five weight copies) does not exceed the remaining unconsumed bytes of the
original length passed to the plugin's deserializing constructor, throwing (as the scalar
deserialize_value reads already do) rather than performing an unchecked std::copy_n.