Fix int overflow in tensor byte-size calculation#3606
Conversation
`BytesRequiredForTensor`, `TfLiteEvalTensorByteLength`, and
`AllocateOutputDimensionsFromInput` in `tensorflow/lite/micro/memory_helpers.cc`
all multiplied tensor shape dimensions into a signed 32-bit `int element_count`
running product before assigning the result to a `size_t` byte count. Shape
dimensions and the element type size are sourced from the (untrusted)
flatbuffer model, so a model with a shape such as [65536, 65536] over a
4-byte element type produces an element-count product of 2^32, which wraps to
0 in the original code and yields `*bytes = 0`. Subsequent buffer
arithmetic in the allocator then operates on a tensor whose advertised size
is 0 while its dimensions imply ~17 GiB of storage, opening a memory
corruption surface.
This change:
* Performs the running product in `size_t`, matching the result type, and
rejects negative shape dimensions with `kTfLiteError`.
* Guards every multiplication (shape * shape and elements * type_size)
against overflow of `size_t` and returns `kTfLiteError` when the next
multiplication would wrap.
* Propagates the previously discarded status of `TfLiteTypeSizeOf` in
`AllocateOutputDimensionsFromInput`.
Adds three regression tests in `memory_helpers_test.cc`:
* Shape [65536, 65536] float32 reports the correct 17,179,869,184 byte
length on platforms with a 64-bit `size_t` and `kTfLiteError` on
platforms with a 32-bit `size_t` (instead of the pre-fix value of 0).
* A shape that overflows even a 64-bit `size_t` is rejected with
`kTfLiteError`.
* A negative shape dimension is rejected with `kTfLiteError`.
All existing `memory_helpers_test` cases continue to pass.
|
Thank you for the PR. Please read the Error Handling & Defensive Programming Guide and modify this PR accordingly. |
Addresses review feedback on tensorflow#3606: map each edited helper to its execution phase and pick the guide-prescribed macro. - AllocateOutputDimensionsFromInput is the only edited helper that receives a TfLiteContext and runs in Setup/Prepare, so it keeps TF_LITE_ENSURE. Per "The Hidden Cost of TF_LITE_ENSURE", the two per-iteration preconditions (non-negative dimension and no size_t overflow of the running product) are combined into a single TF_LITE_ENSURE so only one error string is emitted into .rodata per call site. The dim >= 0 term is evaluated first, so the division guard is never reached with a wrapped-around udim. - BytesRequiredForTensor and TfLiteEvalTensorByteLength have no TfLiteContext (and the latter is reachable from Eval), so they retain the guide's low-cost `return kTfLiteError;` branch, which carries no embedded __FILE__/__LINE__/#cond strings. - Comments now state the phase/trust-boundary rationale (oversized shape = invalid model topology, validated in the Setup phase). No behavioral change to the tests; the 3 regression cases still pass.
|
Thanks @veblush — I've gone through the Error Handling & Defensive Programming Guide and reworked the PR to match its macro/phase model. My reasoning per touched function:
I'm treating an oversized/negative shape as an Invalid Model Topology (semantic malformation) — "wrong tensor shapes" that the guide says the runtime should defend against in the Setup phase — rather than a structural corruption of the FlatBuffer itself (the shape vector and its offsets are intact; only the product overflows Changes in this revision:
One judgement call I'd like your read on: if you'd instead classify the shape-product overflow as a structural FlatBuffer concern (i.e. the app layer owns validating untrusted/OTA models before handing them to TFLM), then per the guide the correct form is a zero-cost |
…on test Follow-up to 6131b79 that fixes the byte-count/dimension-count coupling @ZX41R flagged on tensorflow#3552, in the existing inline-guard style (no new helper). AllocateOutputDimensionsFromInput accumulated the tensor byte count and then passed that same value to TfLiteIntArrayGetSizeInBytes(), which expects the *number of dimension entries*. For an int32 [5, 5, 5, 5] tensor that requested storage for 2500 int entries to hold a 4-entry dims array. It never crashed the existing test only because the fake allocator ignores the requested size. - Rename the byte accumulator to `bytes` (stored in output->bytes) and pass `dimensions_count` -- already computed one line above -- to TfLiteIntArrayGetSizeInBytes(). No behavioral change to the overflow guard. - Add AllocateOutputDimensionsSizesDimsByDimensionCount, which records the size argument handed to AllocatePersistentBuffer and asserts it equals TfLiteIntArrayGetSizeInBytes(dimensions_count), locking in the fix. BUG=tensorflow#3552
BUG=#3552
Summary
BytesRequiredForTensor,TfLiteEvalTensorByteLength, andAllocateOutputDimensionsFromInputintensorflow/lite/micro/memory_helpers.ccall multiply tensor shape dimensionsinto a signed 32-bit
int element_countrunning product before assigning theresult to a
size_tbyte count. Shape dimensions and the element type sizecome from the (untrusted) flatbuffer model, so a model with a shape such as
[65536, 65536]over a 4-byte element type produces a product of 2^32, whichwraps to 0 in the original code and yields
*bytes = 0. Subsequent bufferarithmetic in the allocator then operates on a tensor whose advertised size
is 0 while its dimensions imply ~17 GiB of storage, opening a memory
corruption surface when a model is loaded from an untrusted source.
This PR:
size_t, matching the result type, andrejects negative shape dimensions with
kTfLiteError.shape * shapeandelements * type_size)against overflow of
size_tand returnskTfLiteErrorwhen the nextmultiplication would wrap.
TfLiteTypeSizeOfinAllocateOutputDimensionsFromInput.Test plan
make -f tensorflow/lite/micro/tools/make/Makefile memory_helpers_testbuilds clean.
gen/linux_x86_64_default_gcc/bin/memory_helpers_test— all 10 casespass, including the 3 new regression tests:
TfLiteEvalTensorByteLengthDoesNotTruncateAcrossInt32Boundary— shape[65536, 65536]float32 now reports17,179,869,184bytes on 64-bitsize_tplatforms (previously:0), andkTfLiteErroron 32-bitsize_tplatforms.TfLiteEvalTensorByteLengthRejectsSizeTOverflow— a shape thatoverflows even a 64-bit
size_tis rejected withkTfLiteError.TfLiteEvalTensorByteLengthRejectsNegativeDimension— a negativeshape dimension is rejected with
kTfLiteError.Notes for review
std::numeric_limits<size_t>::max() / x, which isportable and MSVC-safe — deliberately not
__builtin_mul_overflow, whichwould break the
call-windowsbuild.would you be able to approve the workflow run and take a look when you have a
moment? Happy to switch the manual checks to any form the tree prefers and to
address any other feedback.