Skip to content

tflite: add verifier regression tests for uint64 external-offset overflow - #10223

Open
cybrdude wants to merge 3 commits into
google-ai-edge:mainfrom
cybrdude:test/verifier-uint64-external-offset-overflow
Open

cybrdude wants to merge 3 commits into
google-ai-edge:mainfrom
cybrdude:test/verifier-uint64-external-offset-overflow

Conversation

@cybrdude

@cybrdude cybrdude commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

What

Regression tests in tflite/core/tools/verifier_test.cc for the uint64_t
wrap in the external-offset bounds checks that tflite::Verify() performs on
Buffer.offset / Buffer.size (VerifyTensors) and
Operator.large_custom_options_offset / large_custom_options_size
(VerifyOperators).

The checks themselves already use the subtraction form on main
(size > len || offset > len - size). This PR pins that behavior so a return
to the offset + size > len form is caught.

Why the verifier

offset + size computed in uint64_t wraps when the pair straddles 2^64, so
the addition form compares a small wrapped value against the model length and
accepts the buffer. tflite::Verify() (or
FlatBufferModel::VerifyAndBuildFromBuffer with a verifier) is the ingress
validation layer for untrusted models, so the tests target it rather than
InterpreterBuilder.

Tests

Two table-driven tests over (offset, size, valid), one per check, each
row run with SCOPED_TRACE:

offset size expected why
8 4 accepted inside the model bytes; the check must not fire
0xFFFFFFFFFFFFFF00 0x100 rejected sum wraps to 0
0xFFFFFFFFFFFFFFF8 0x10 rejected sum wraps to 8; size alone fits in the model, so only the offset > len - size clause can reject it
  • BufferOffsetBoundsCheckHandlesUint64Wrap: Buffer.offset/size via the
    existing AddBufferOffsetTensor helper.
  • LargeCustomOptionsBoundsCheckHandlesUint64Wrap: the same rows on
    Operator.large_custom_options_offset/size, via a new
    AddOperatorWithLargeCustomOptions helper on TfLiteFlatbufferModelBuilder.

Rejected rows assert both Verify overloads return false and match the
specific VerifyTensors / VerifyOperators error string; the accepted row
asserts both return true with no error.

To run: bazel test //tflite/core/tools:verifier_test

History

Re-submission of tensorflow/tensorflow#116631 against this repository, as
requested there by @dmiltr3 (tflite/core/tools/verifier_test.cc is
maintained here and mirrored into TensorFlow). That PR started as a fix in
interpreter_builder.cc and was narrowed to these tests after
review that the validation lives in the verifier. Earlier LiteRT
exports of it: #8183, #9992.

Fixes #8180 (the check itself is on main in tflite/core/tools/verifier.cc;
this PR adds the regression tests for it). Supersedes #8183.

…flow

Covers both external-offset sites that tflite::Verify() bounds-checks in
the subtraction form: Buffer (offset, size) in VerifyTensors and Operator
large_custom_options_(offset, size) in VerifyOperators. Each negative case
uses a pair whose uint64 sum wraps below the model length, which the old
`offset + size > len` form accepted; the positive case pins the in-range
path.

Re-submission of tensorflow/tensorflow#116631 against LiteRT, per review.
@cybrdude cybrdude changed the title tflite: add verifier regression tests for uint64 external-offset over… tflite: add verifier regression tests for uint64 external-offset overflow Sep 24, 2026
Folds the four cases into two table-driven tests over (offset, size,
valid), so every assertion line runs for every row, adds SCOPED_TRACE for
per-row diagnostics, and gives the Buffer check the same in-range control
the large_custom_options check already had. What is verified is unchanged:
both uint64-wrapping pairs are rejected by VerifyTensors and VerifyOperators
with the specific error, the in-range pair is accepted.

Follows the coverage note on tensorflow/tensorflow#116631.
cybrdude added a commit to cybrdude/tensorflow that referenced this pull request Sep 24, 2026
Folds the four cases into two table-driven tests over (offset, size,
valid), so every assertion line runs for every row, adds SCOPED_TRACE for
per-row diagnostics, and gives the Buffer check the same in-range control
the large_custom_options check already had. What is verified is unchanged:
both uint64-wrapping pairs are rejected by VerifyTensors and VerifyOperators
with the specific error, the in-range pair is accepted.

Same content as google-ai-edge/LiteRT#10223 (3013ba6).
@cybrdude

Copy link
Copy Markdown
Contributor Author

@dmiltr3 status for this PR, since the Sep 24 internal run on tensorflow/tensorflow#116631 (same diff) came back with two items and this is the copy that should be imported.

Revision 2 (3013ba6), identical content to 1d4fb06 on #116631:

  • the four tests are now two table-driven tests over (offset, size, valid) with SCOPED_TRACE per row, so every assertion line runs for every row;
  • the Buffer check has the same in-range control the large_custom_options check already had;
  • what is verified is unchanged: both uint64-wrapping pairs are rejected by VerifyTensors / VerifyOperators with the specific error, the in-range pair is accepted.

Coverage. That run reported 40% incremental coverage for the test file. I could not reproduce anything unexecuted: building verifier_test from this branch against the pinned flatbuffers 25.9.23 and googletest and running it under gcov, every code line of the addition executes (45 of 45 on this revision, 58 of 58 on the previous one), both arms of the if (c.valid) are taken, and ReportError in each verifier check is taken 4 times. The only zero-count branches on the added lines are gtest's assertion-failure paths, which no passing test can execute. Per-line gcov is under the fold. If the internal number is still low on this revision, it is measuring branches or regions rather than executed lines; in that case please paste the uncovered lines from the report, or apply whatever exemption _test.cc files get, and I will restructure again if the lines show something real.

gcov, the added lines at 3013ba6 (count:line:source; `-` is a line with no code)
     -: 156:  // Adds a custom operator whose options live outside the flatbuffer,
     -: 157:  // addressed by (large_custom_options_offset, large_custom_options_size).
     3: 158:  void AddOperatorWithLargeCustomOptions(const std::vector<int32_t>& inputs,
     -: 159:                                         const std::vector<int32_t>& outputs,
     -: 160:                                         const char* custom_op, uint64_t offset,
     -: 161:                                         uint64_t size) {
     3: 162:    operator_codes_.push_back(
     3: 163:        CreateOperatorCodeDirect(builder_, BuiltinOperator_CUSTOM, custom_op));
     -: 164:    // Vectors have to be serialized before the Operator table is started.
     3: 165:    auto inputs_offset = builder_.CreateVector(inputs);
     3: 166:    auto outputs_offset = builder_.CreateVector(outputs);
     3: 167:    OperatorBuilder op_builder(builder_);
     3: 168:    op_builder.add_opcode_index(operator_codes_.size() - 1);
     3: 169:    op_builder.add_inputs(inputs_offset);
     3: 170:    op_builder.add_outputs(outputs_offset);
     3: 171:    op_builder.add_large_custom_options_offset(offset);
     3: 172:    op_builder.add_large_custom_options_size(size);
     3: 173:    operators_.push_back(op_builder.Finish());
     3: 174:  }
     -: 175:
     -: 697:// (offset, size) pairs for the external-offset bounds checks. The in-range
     -: 698:// pair lies inside the model bytes and must pass. The other two have a uint64
     -: 699:// sum that wraps below any test model's length (to 0, and to 8 with a size
     -: 700:// that fits in the model on its own, so only the `offset > len - size` clause
     -: 701:// can reject it); the old `offset + size > len` form accepted both.
     -: 702:struct ExternalOffsetCase {
     -: 703:  uint64_t offset;
     -: 704:  uint64_t size;
     -: 705:  bool valid;
     -: 706:};
     -: 707:constexpr ExternalOffsetCase kExternalOffsetCases[] = {
     -: 708:    {8, 4, true},
     -: 709:    {0xFFFFFFFFFFFFFF00ULL, 0x100ULL, false},
     -: 710:    {0xFFFFFFFFFFFFFFF8ULL, 0x10ULL, false},
     -: 711:};
     -: 712:
     4: 713:TEST(VerifyModel, BufferOffsetBoundsCheckHandlesUint64Wrap) {
     4: 714:  for (const auto& c : kExternalOffsetCases) {
     3: 715:    SCOPED_TRACE(::testing::Message()
     -: 716:                 << "offset=" << c.offset << " size=" << c.size);
    12: 717:    TfLiteFlatbufferModelBuilder builder({}, {"test"});
     3: 718:    builder.AddOperator({0, 1}, {2}, BuiltinOperator_CUSTOM, "test");
     3: 719:    builder.AddTensor({2, 2}, TensorType_UINT8, {1, 2, 3, 4}, "input");
     3: 720:    builder.AddBufferOffsetTensor({2, 2}, TensorType_UINT8, c.offset, c.size,
     -: 721:                                  "offset_weight");
     3: 722:    builder.AddTensor({2, 2}, TensorType_INT32, {}, "output");
     3: 723:    builder.FinishModel({0}, {2});
     3: 724:    EXPECT_EQ(builder.Verify(), c.valid);
     3: 725:    EXPECT_EQ(builder.VerifyWithOpResolver(), c.valid);
     3: 726:    if (c.valid) {
     2: 727:      EXPECT_EQ("", builder.GetErrorString());
     -: 728:    } else {
     4: 729:      EXPECT_THAT(builder.GetErrorString(),
     -: 730:                  ::testing::ContainsRegex("Tensor offset_weight buffer 2 "
     2: 731:                                           "specified an out of range offset"));
     -: 732:    }
     3: 733:  }
     1: 734:}
     -: 735:
     4: 736:TEST(VerifyModel, LargeCustomOptionsBoundsCheckHandlesUint64Wrap) {
     4: 737:  for (const auto& c : kExternalOffsetCases) {
     3: 738:    SCOPED_TRACE(::testing::Message()
     -: 739:                 << "offset=" << c.offset << " size=" << c.size);
    12: 740:    TfLiteFlatbufferModelBuilder builder({}, {"test"});
     3: 741:    builder.AddTensor({2, 2}, TensorType_UINT8, {1, 2, 3, 4}, "input");
     3: 742:    builder.AddTensor({2, 2}, TensorType_INT32, {}, "output");
     3: 743:    builder.AddOperatorWithLargeCustomOptions({0}, {1}, "test", c.offset,
     3: 744:                                              c.size);
     3: 745:    builder.FinishModel({0}, {1});
     3: 746:    EXPECT_EQ(builder.Verify(), c.valid);
     3: 747:    EXPECT_EQ(builder.VerifyWithOpResolver(), c.valid);
     3: 748:    if (c.valid) {
     2: 749:      EXPECT_EQ("", builder.GetErrorString());
     -: 750:    } else {
     4: 751:      EXPECT_THAT(builder.GetErrorString(),
     -: 752:                  ::testing::ContainsRegex(
     2: 753:                      "Operator's large_custom_options is out of bound"));
     -: 754:    }
     3: 755:  }
     1: 756:}
     -: 757:
gcov, tflite/core/tools/verifier.cc, the two checks under test
       48:  515:    if (op->large_custom_options_offset() > 1) {
       12:  516:      if (op->large_custom_options_size() > len ||
        6:  517:          op->large_custom_options_offset() >
        6:  518:              len - op->large_custom_options_size()) {
        4:  519:        ReportError(error_reporter,
        4:  521:        return false;
...
        8:  710:      } else if (buffer->offset() > 1) {
        8:  717:        if (buffer->size() > len || buffer->offset() > len - buffer->size()) {
        4:  718:          ReportError(error_reporter,
        4:  721:          return false;

ABI check. The change is test-only, one cc_test source with no header, library or exported symbol touched, so tflite_abi_diff_workflow has nothing real to see from it.

Once this one is imported I will close #116631.

@cybrdude

cybrdude commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

@aravindmurali22 @terryheo gentle ping on this one. It is the verifier-side regression test for the external-offset overflow (the check itself landed in tensorflow/tensorflow#123910); the builder-side fix and its tests merged today in tensorflow/tensorflow#123123 and arrived here through #10506, and this PR does not overlap with those files. No conflicts as far as I can see; the workflows are waiting on approval.

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.

uint64 addition overflow bypasses Buffer.offset and large_custom_options bounds checks in tflite/core/interpreter_builder.cc

1 participant