# 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()`: ```cpp 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: ```cpp 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()`: ```cpp if (out.int64_list.size() % num_elements != 0) { ... } // num_elements == elements_per_stride ``` (analogous lines for `float_list` / `bytes_list`). * Single-example path, `FastParseSingleExample()`: ```cpp if (num_elements % num_elements_divisor != 0) { ... } // num_elements_divisor == elements_per_stride ``` * `SequenceExample` dense path, `ParseSequenceDenseFeatures()`: ```cpp 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 ```