EnigmaConsultant's picture
Upload folder using huggingface_hub
55d216a verified
|
Raw
History Blame Contribute Delete
6.6 kB

TensorFlow tf.io.parse_example / parse_single_example / parse_sequence_example β€” SIGFPE (division-by-zero) Denial of Service

Status: gated PoC, private disclosure in progress via huntr.com (protectai). Do not use against production systems.

Summary

TensorFlow's fast Example/SequenceExample proto parser (tensorflow/core/util/example_proto_fast_parsing.cc) computes an internal per-row element count (elements_per_stride) directly from the inner shape supplied for a variable-length dense feature (tf.io.FixedLenSequenceFeature(shape=..., allow_missing=True), or the equivalent dense_shapes attribute on the raw ParseExampleV2 / ParseSingleExample / ParseSequenceExample ops). If that inner shape has a 0 in it, elements_per_stride is 0, and the parser later divides/modulos by it with no zero-check, crashing the whole process with SIGFPE (signal 8) β€” not a catchable Python exception, not a tf.errors.* status, just an instant process kill.

The crash is independent of the actual serialized Example/SequenceExample bytes β€” it fires even for a syntactically empty/valid proto, and even when the feature in question is entirely absent from the data. It is purely a function of the (attacker-controlled, when the parsing graph ships inside a model file) feature-spec shape. Any model/graph that wires up ParseExampleV2/ParseSingleExample/ParseSequenceExample with such a shape turns every future invocation of that signature into an instant crash of the hosting process β€” a persistent, config-driven Denial of Service against any inference server that loads the model.

Root cause

tensorflow/core/util/example_proto_helper.cc, GetDenseShapes():

elements_per_stride->push_back(dense_shape.num_elements());

dense_shape is the caller-supplied shape with the (optional) leading variable-length dimension stripped. If the remaining static dims multiply to 0 (e.g. shape=[0]), elements_per_stride is 0. Nothing rejects this.

tensorflow/core/util/example_proto_fast_parsing.cc:

  • Batch path, FastParseExample() β†’ MergeDenseVarLenMinibatches lambda:

    const size_t stride_size = config.dense[d].elements_per_stride;
    const size_t max_num_elements = max_num_features / stride_size;   // <-- div by 0
    

    This runs unconditionally once per configured variable-length dense feature, regardless of whether that feature appears anywhere in the batch's data (confirmed by crashing on a batch of empty Example bytes with the feature entirely missing).

  • Per-example path, FastParseSerializedExample():

    if (out.int64_list.size() % num_elements != 0) { ... }   // num_elements == elements_per_stride
    

    (analogous lines for float_list / bytes_list).

  • Single-example path, FastParseSingleExample():

    if (num_elements % num_elements_divisor != 0) { ... }   // num_elements_divisor == elements_per_stride
    
  • SequenceExample dense path, ParseSequenceDenseFeatures():

    if (!c.shape.AsTensorShape(&row_shape) ||
        expected_max_elements != (expected_max_elements / row_shape.num_elements()) *
                                      row_shape.num_elements()) { ... }   // div by 0
    ...
    int64_t expected_max_rows = expected_max_elements / row_shape.num_elements();  // div by 0
    

None of the reachable call sites (tensorflow/core/kernels/example_parsing_ops.cc, tensorflow/core/ops/parsing_ops.cc) validate that a configured dense shape has a non-zero element count before it is used as a divisor.

Verified impact

Tested against a freshly pip install-ed tensorflow-cpu==2.21.0 (current stable release at the time of testing, matching a fresh git clone of tensorflow/tensorflow master). All three PoCs reproducibly kill the Python interpreter with signal 8 (SIGFPE):

$ python3 confirm_signal.py
returncode: -8
Killed by signal: 8 SIGFPE
  • poc_parse_example_minimal.py β€” tf.io.parse_example([b""], {...}) crashes immediately, no TFRecord file, no real feature data at all.
  • poc_parse_sequence_example.py β€” same crash via tf.io.parse_sequence_example.
  • poc_tfrecord_sigfpe.py β€” the realistic end-to-end path: writes one perfectly ordinary tf.train.Example ({"label": int64_list([1])}) to a real .tfrecord file with tf.io.TFRecordWriter, then reads it back with the canonical tf.data.TFRecordDataset(...).batch(1).map(tf.io.parse_example) pipeline. The data is 100% benign; only the feature-spec ("extra": tf.io.FixedLenSequenceFeature(shape=[0], dtype=tf.float32, allow_missing=True)) is malicious. The process is killed by SIGFPE the moment the first batch is materialized.

Attacker-controlled surface (malicious model file)

dense_shapes / the analogous shape attrs are ordinary attributes of the ParseExampleV2, ParseSingleExample, and ParseSequenceExample ops. A GraphDef/SavedModel is free to embed a pre-processing subgraph (e.g. an input-parsing signature) that calls one of these ops with a zero-size inner shape on a variable-length dense feature. Any consumer that loads that model and invokes the corresponding signature β€” regardless of what input tensor it passes β€” will crash on first use. This is a persistent Denial-of-Service purely from loading and calling a malicious model file; no separately malicious dataset/TFRecord is required (though, as poc_tfrecord_sigfpe.py shows, this also fires naturally in the ordinary TFRecordDataset β†’ parse_example data-loading idiom if the feature spec is ever malformed in this specific way).

Suggested fix

Reject a dense_shapes entry whose element count is 0 for a variable-length (allow_missing=True / leading -1) dense feature in GetDenseShapes() (example_proto_helper.cc), or guard every division/modulo by elements_per_stride (and the analogous row_shape.num_elements() in the SequenceExample dense path) with an explicit != 0 check that returns an InvalidArgumentError instead of dividing.

Files

  • poc_parse_example_minimal.py β€” minimal repro via tf.io.parse_example.
  • poc_parse_sequence_example.py β€” minimal repro via tf.io.parse_sequence_example.
  • poc_tfrecord_sigfpe.py β€” realistic end-to-end repro starting from a real, valid .tfrecord file.
  • confirm_signal.py β€” runs the end-to-end PoC as a subprocess and prints the confirmed signal (SIGFPE / 8).

Environment used to verify

$ pip install tensorflow-cpu
$ python3 -c "import tensorflow as tf; print(tf.__version__)"
2.21.0