gather: bounds-check runtime position values in Eval - #3645
Open
evilgensec wants to merge 1 commit into
Open
Conversation
The GATHER kernel copies each output slice using a position value taken
from the positions input tensor:
input_data + (((batch * outer_size) + outer) * axis_size +
coords_data[batch * coord_size + coord]) * inner_size
Those values were guarded only by TFLITE_DCHECK, which compiles out under
NDEBUG, so a release build used an out-of-range position directly as an
offset and read outside the input tensor. The DCHECKs also read
coords_data[coord] while the memcpy indexes
coords_data[batch * coord_size + coord], so once batch_size is greater
than 1 they did not guard the value actually used.
Read the position once, reject it with a raw branch when it falls outside
[0, axis_size), and use that same value in the memcpy. The error handling
guide calls for a raw branch rather than TF_LITE_ENSURE for runtime index
data in Eval, so no error string is added to .rodata. Cost in release is
one compare and branch per copied slice.
Adds three tests covering a position above the axis size, a negative
position, and an out-of-range position in the second batch, which is the
case the previous DCHECKs never read.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
BUG=GATHER kernel reads out of bounds on out-of-range runtime position values (memory safety)
This reworks #3593 to follow the Error Handling & Defensive Programming Guide, which was the reason that PR was closed.
Problem
Gather()copies each output slice withinput_data + (((batch * outer_size) + outer) * axis_size + coords_data[batch * coord_size + coord]) * inner_sizeThe position values come from the
positionsinput tensor. They are guarded only byThere are two problems with that guard.
TFLITE_DCHECKcompiles out under-DNDEBUG, so in a release build an out-of-range position is used directly as an offset intoinput_dataand thememcpyreads outside the input tensor.The guard reads
coords_data[coord], but thememcpyindexescoords_data[batch * coord_size + coord]. Oncebatch_size > 1these are different elements, so the value actually used as the offset is not the value being checked, even in a debug build.Preparecannot cover this. It validatesaxis,batch_dimsand the tensor shapes, but the position values are only known at invoke time.Approach
The guide, section 2, Phase 2, lists this case directly:
So the check stays in
Eval, but as a raw branch rather thanTF_LITE_ENSURE. Per the macro cheat sheet,TF_LITE_ENSUREinEvalshould be avoided for signal data out-of-bounds because each invocation embeds__FILE__,__LINE__and the stringified condition in.rodata. The raw branch adds no string data.This is the difference from #3593, which used
TF_LITE_ENSUREand also added anaxisre-check inEvalthat duplicated the existingTF_LITE_ENSUREinPrepare. That duplicate is dropped here, sincePreparealready guarantees it and the existingTFLITE_DCHECKs onaxisare the correct choice under the guide.Change
gather.cc: read the position value once intocoord_value, reject it with a raw branch if it falls outside[0, axis_size), and use that same value in thememcpy. This also removes the mismatch in 2 above, because the value checked and the value used are now the same expression.Cost in release is one compare and branch per copied slice, with no added
.rodata.gather_test.cc: three tests, all assertingInitAndPrepare()still returnskTfLiteOkandInvoke()returnskTfLiteError.GatherOp_PositionAboveAxisSizeReturnsError, position 3 against axis size 3.GatherOp_NegativePositionReturnsError, position -1.GatherOp_OutOfRangePositionInSecondBatchReturnsError,batch_dims = 1with the bad value atcoords_data[1]. This is the case the old DCHECK never read, so it fails on the unpatched kernel in a debug build as well as a release build.Testing
Run on macOS arm64. Note that the host
makethere is 3.81 and the Makefile requires 3.82 or later, sogmakeis needed.Both builds pass with this change, 21 tests in each. To confirm the new tests actually exercise the defect, I also ran them against the unpatched kernel:
release_with_logsrelease_with_logsThe last row is the case that matters. With
-DNDEBUGthe DCHECKs are gone, so the unpatched kernel does not abort. It uses the out-of-range position as an offset, reads outside the input tensor, and still returnskTfLiteOk. That is the behaviour this change removes.