Skip to content

[Converter:Bugfix] Fix MNNConvert crash on malformed tflite models (#4796) - #4823

Open
fshuang8299 wants to merge 1 commit into
alibaba:masterfrom
fshuang8299:fix/converter-malformed-tflite-robustness
Open

[Converter:Bugfix] Fix MNNConvert crash on malformed tflite models (#4796)#4823
fshuang8299 wants to merge 1 commit into
alibaba:masterfrom
fshuang8299:fix/converter-malformed-tflite-robustness

Conversation

@fshuang8299

Copy link
Copy Markdown
Contributor

Summary

MNNConvert -f TFLITE segfaults on eight malformed models reported in #4796 (all reproducers + ASAN traces attached there). Every crash is a value taken straight from the model file and used without a check that stops execution.

Root causes

  1. Null builtin_options union: AsConv2DOptions() / AsPool2DOptions() / AsDepthwiseConv2DOptions() / AsTransposeConvOptions() / AsFullyConnectedOptions() return nullptr when the union tag does not match, and the result was dereferenced unconditionally (e.g. fused_activation_function read through null → SEGV at offset 0xc/0x8/0x14).
  2. DCHECK is log-and-continue: logkit.h defines DCHECK(x) as CHECK(x) whose body only prints (no abort), so DCHECK(weightShape.size() == 4) fell through into weightShape[0..3] element loads on empty shapes.
  3. Unchecked arithmetic: co * kh * kw * ci in int wraps for adversarial dims (product of large-but-positive dims wraps to a small positive value), producing an undersized destination followed by an out-of-bounds write inside convertDataFormatTflite (the WRITE SEGV at TfliteUtils.cpp:107).
  4. Unchecked bias buffer: ::memcpy(biasData.data(), biasDataPtr, sizeof(float) * co) reads from the zero page / OOB when the bias buffer is empty or undersized.
  5. ConcatSizeComputer: dereferenced inputs[0] on an empty input list (assert commented out) and trusted a rank that can overflow the 9-slot dim array.

Fix

Reject the malformed operator using the converter's idiomatic dstOp->type = MNN::OpType_MAX pattern (liteConverter.cpp:384 turns it into a clean conversion failure with exit code 1), and fail closed everywhere:

  • Null-check every builtin_options accessor (Conv2D, DepthwiseConv2D, TransposeConv, Pool2D, FullyConnected).
  • Validate weight shape is 4-D, all dims positive, and weightSize computed in int64 does not overflow int — before any allocation or division (also protects the inputShape[3] / ci group calc).
  • Bound-check every bias buffer with size() >= sizeof(T) * count before memcpy / vector range-construction (float, INT8 and UINT8 paths).
  • convertDataFormatTflite now validates dims and src/dst and returns false instead of looping through null pointers (defense in depth for all 4 call sites).
  • ConcatSizeComputer rejects empty inputs, invalid rank (> MNN_MAX_TENSOR_DIM), and out-of-range axis (per input).

Verification

Fixes #4796

…libaba#4796)

MNNConvert -f TFLITE crashed with SIGSEGV on eight malformed models
reported in alibaba#4796. All share the same root cause: values taken straight
from the model file are used without a check that stops execution.

Root causes fixed:
- AsXxxOptions() returns nullptr when the builtin_options union tag does
  not match; the result was dereferenced unconditionally (Conv2D,
  DepthwiseConv2D, TransposeConv, Pool2D, FullyConnected).
- DCHECK only logs and continues (logkit CHECK has no abort), so a
  weight shape that is not 4-D, non-positive dims, or a product that
  wraps in int fell through into element loads/writes.
- Bias buffers were copied with memcpy / vector range-construction
  without verifying the buffer holds sizeof(T)*count bytes (float,
  INT8 and UINT8 paths of Conv2D and DepthwiseConv2D).
- convertDataFormatTflite's DCHECKs were no-ops; it now validates dims
  and src/dst and returns false instead of writing through null.
- ConcatSizeComputer dereferenced inputs[0] with an empty input list
  and trusted a rank that can overflow the 9-slot dim array.

Fix: reject the malformed operator with the converter's idiomatic
dstOp->type = OpType_MAX (liteConverter.cpp turns that into a clean
conversion failure), validate weight shape/size with int64 arithmetic,
bound-check every bias buffer, and make convertDataFormatTflite and
ConcatSizeComputer fail closed.

Notes on policy choices:
- Depthwise INT8 weight with an empty/undersized buffer rejects the op,
  unlike Conv2D INT8 which skips the weight: the depthwise path assumes
  a constant weight buffer and cannot represent the op otherwise.
- ConcatSizeComputer now rejects rank-0 inputs (the old code's
  dim[-1]/dim[axis] access on a scalar input was itself out of bounds).

Verified: all 8 reproducers exit cleanly (8/8 SEGV on master), a legal
conv+depthwise+pool+fc+concat model converts identically to baseline
(only the random model UUID differs), and the full set is clean under
AddressSanitizer.
@fshuang8299
fshuang8299 force-pushed the fix/converter-malformed-tflite-robustness branch from 88abfc7 to 9c35b5b Compare September 2, 2026 01:36
@fshuang8299

Copy link
Copy Markdown
Contributor Author

Verification Report

Complete verification of the fix against the 8 reproducers from #4796, plus regression checks. All tests run locally on x86_64 Linux.

1. Crash reproducers (functional)

All 8 files attached to #4796, run with MNNConvert -f TFLITE --modelFile <file> --MNNModel /dev/null --bizCode check:

Master (baseline) This PR
8/8 SIGSEGV (exit 139) 8/8 clean exit (exit 1 for rejected models)

2. No-regression: legal model conversion

Generated a legal FLOAT32 tflite model (CONV_2D → DEPTHWISE_CONV_2D → AVERAGE_POOL_2D → CONCATENATION, 8 tensors). Converted with master baseline and with this PR:

  • Conversion succeeds on both
  • Output op list identical (same op types / names / input-output indexes)
  • Numeric inference comparison: both converted models run on CPU with the same fixed input → 128 output elements bit-identical (max abs diff = 0)

3. Quantized path (UINT8)

Generated a UINT8-quantized CONV_2D model (with INT32 bias + quantization params). Conversion succeeds identically on baseline and this PR.

4. Memory safety (AddressSanitizer)

ASAN build (-fsanitize=address): all 8 reproducers + the legal model run with zero ASAN reports. The only LeakSanitizer output is a pre-existing 8-byte static registration object (onnxOpConverterRegister<UnaryOnnx>), present on every MNNConvert invocation regardless of this change.

5. MNN test suite

run_test.out (CPU backend, Precision_High): 380/380 passed, 0 failed — covers the touched source/shape/ShapeConcat.cpp (runtime shape computation) and general core regressions.

6. Style checks (matching repo CI)

  • Commit message: [Converter:Bugfix] Fix MNNConvert crash on malformed tflite models (#4796) — matches the CI Check Commit Message Format rule
  • Code format: checked with git clang-format --diff origin/master using clang-format 17.0.6 (the version the CI Check Changed Lines Format job installs) — 0 issues

Notes on policy choices

  • Depthwise INT8 empty/undersized weight → reject the op (OpType_MAX), unlike Conv2D INT8 which skips the weight. The depthwise path assumes a constant weight buffer; without it the op cannot be represented. Rejecting is fail-closed and avoids silently emitting a zero-weight model.
  • ConcatSizeComputer rejects rank-0 inputs and out-of-range axis instead of indexing dim[axis] blindly (the old path was itself out of bounds for scalar inputs / bad axis).
  • Rejected ops use the converter's existing idiom (dstOp->type = MNN::OpType_MAX), which liteConverter.cpp:384 turns into a clean conversion failure (exit code 1) rather than a crash.

@wangzhaode wangzhaode self-assigned this Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] MNNConvert dereferences null builtin_options and empty shape vectors on a malformed tflite model

2 participants