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()`:
```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
```