Skip to content

Commit 56da964

Browse files
Numel and Nbytes Validation (#19121) (#19121)
Summary: Attempt 3 to check numel and nbytes overflow. This time we defer checking dynamic sized inputs until their size is realized. Reviewed By: lucylq Differential Revision: D98148157
1 parent cb94506 commit 56da964

5 files changed

Lines changed: 214 additions & 64 deletions

File tree

runtime/executor/method.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <executorch/runtime/executor/method.h>
1010

1111
#include <c10/util/irange.h>
12+
#include <c10/util/safe_numerics.h>
1213
#include <array>
1314
#include <cinttypes> // @donotremove
1415
#include <cstdint>
@@ -1194,6 +1195,33 @@ Method::set_input(const EValue& input_evalue, size_t input_idx) {
11941195
input_idx,
11951196
executorch::runtime::toString(t_dst.scalar_type()),
11961197
executorch::runtime::toString(t_src.scalar_type()));
1198+
1199+
ssize_t numel = 1;
1200+
for (ssize_t i = 0; i < t_src.dim(); i++) {
1201+
bool overflow = c10::mul_overflows(
1202+
numel, static_cast<ssize_t>(t_src.size(i)), &numel);
1203+
ET_CHECK_OR_RETURN_ERROR(
1204+
!overflow,
1205+
InvalidArgument,
1206+
"Input %" ET_PRIsize_t
1207+
": numel overflowed at dimension %zd with size %zd",
1208+
input_idx,
1209+
(size_t)i,
1210+
(size_t)t_src.size(i));
1211+
}
1212+
size_t nbytes;
1213+
bool nbytes_overflow = c10::mul_overflows(
1214+
static_cast<size_t>(numel),
1215+
executorch::runtime::elementSize(t_src.scalar_type()),
1216+
&nbytes);
1217+
ET_CHECK_OR_RETURN_ERROR(
1218+
!nbytes_overflow,
1219+
InvalidArgument,
1220+
"Input %" ET_PRIsize_t
1221+
": nbytes overflowed: numel %zd with element size %zu",
1222+
input_idx,
1223+
numel,
1224+
executorch::runtime::elementSize(t_src.scalar_type()));
11971225
// Reset the shape for the Method's input as the size of forwarded input
11981226
// tensor for shape dynamism. Also is a safety check if need memcpy.
11991227
ET_CHECK_OK_OR_RETURN_ERROR(

runtime/executor/program_validation.cpp

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
#include <executorch/runtime/platform/log.h>
1515
#include <executorch/schema/program_generated.h>
1616

17-
// #include <c10/util/safe_numerics.h>
17+
#include <c10/util/safe_numerics.h>
1818

1919
namespace executorch {
2020
namespace runtime {
@@ -32,7 +32,8 @@ validate_tensor(const executorch_flatbuffer::Tensor* tensor) {
3232
return Error::InvalidProgram;
3333
}
3434

35-
// ssize_t numel = 1;
35+
ssize_t numel = 1;
36+
bool numel_overflowed = false;
3637
for (flatbuffers::uoffset_t i = 0; i < sizes->size(); i++) {
3738
int32_t size = sizes->Get(i);
3839

@@ -45,16 +46,10 @@ validate_tensor(const executorch_flatbuffer::Tensor* tensor) {
4546
return Error::InvalidProgram;
4647
}
4748

48-
// bool overflow =
49-
// c10::mul_overflows(numel, static_cast<ssize_t>(size), &numel);
50-
// if (overflow) {
51-
// ET_LOG(
52-
// Error,
53-
// "numel overflowed at dimension %u with size %d",
54-
// static_cast<unsigned>(i),
55-
// size);
56-
// return Error::InvalidProgram;
57-
// }
49+
if (!numel_overflowed) {
50+
numel_overflowed =
51+
c10::mul_overflows(numel, static_cast<ssize_t>(size), &numel);
52+
}
5853
}
5954

6055
auto scalar_type =
@@ -64,19 +59,18 @@ validate_tensor(const executorch_flatbuffer::Tensor* tensor) {
6459
return Error::InvalidProgram;
6560
}
6661

67-
// size_t nbytes;
68-
// bool nbytes_overflow = c10::mul_overflows(
69-
// static_cast<size_t>(numel),
70-
// executorch::runtime::elementSize(scalar_type),
71-
// &nbytes);
72-
// if (nbytes_overflow) {
73-
// ET_LOG(
74-
// Error,
75-
// "nbytes overflowed: numel %zd with element size %zu",
76-
// numel,
77-
// executorch::runtime::elementSize(scalar_type));
78-
// return Error::InvalidProgram;
79-
// }
62+
if (numel_overflowed) {
63+
return Error::InvalidProgram;
64+
}
65+
66+
size_t nbytes;
67+
bool nbytes_overflow = c10::mul_overflows(
68+
static_cast<size_t>(numel),
69+
executorch::runtime::elementSize(scalar_type),
70+
&nbytes);
71+
if (nbytes_overflow) {
72+
return Error::InvalidProgram;
73+
}
8074

8175
return Error::Ok;
8276
}
@@ -114,6 +108,27 @@ validate_program(const executorch_flatbuffer::Program* program) {
114108
return Error::InvalidProgram;
115109
}
116110

111+
const auto* inputs = plan->inputs();
112+
auto is_dynamic_input = [&](flatbuffers::uoffset_t idx) -> bool {
113+
if (inputs == nullptr) {
114+
return false;
115+
}
116+
for (flatbuffers::uoffset_t i = 0; i < inputs->size(); i++) {
117+
if (inputs->Get(i) == static_cast<int32_t>(idx)) {
118+
const auto* value = values->Get(idx);
119+
if (value == nullptr) {
120+
return false;
121+
}
122+
const auto* tensor =
123+
static_cast<const executorch_flatbuffer::Tensor*>(value->val());
124+
return tensor != nullptr &&
125+
tensor->shape_dynamism() !=
126+
executorch_flatbuffer::TensorShapeDynamism::STATIC;
127+
}
128+
}
129+
return false;
130+
};
131+
117132
for (flatbuffers::uoffset_t value_idx = 0; value_idx < values->size();
118133
value_idx++) {
119134
const auto* value = values->Get(value_idx);
@@ -128,12 +143,25 @@ validate_program(const executorch_flatbuffer::Program* program) {
128143

129144
Error err = validate_tensor(tensor);
130145
if (err != Error::Ok) {
131-
ET_LOG(
132-
Error,
133-
"Tensor validation failed for value %u in execution plan %u",
134-
static_cast<unsigned>(value_idx),
135-
static_cast<unsigned>(plan_idx));
136-
return err;
146+
// Dynamic input tensors may have upper-bound sizes serialized for
147+
// 64-bit machines that would overflow on 32-bit. Since their actual
148+
// sizes are provided at set_input time, we defer overflow checks
149+
// for those to Method::set_input.
150+
if (is_dynamic_input(value_idx)) {
151+
ET_LOG(
152+
Info,
153+
"Skipping validation failure for dynamic input tensor "
154+
"at value %u in execution plan %u",
155+
static_cast<unsigned>(value_idx),
156+
static_cast<unsigned>(plan_idx));
157+
} else {
158+
ET_LOG(
159+
Error,
160+
"Tensor validation failed for value %u in execution plan %u",
161+
static_cast<unsigned>(value_idx),
162+
static_cast<unsigned>(plan_idx));
163+
return err;
164+
}
137165
}
138166
}
139167

runtime/executor/program_validation.h

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,12 @@ namespace executorch {
2222
namespace runtime {
2323

2424
/**
25-
* Validates that computing numel (number of elements) from the tensor's sizes
26-
* will not overflow. This check should be performed before creating TensorImpl
27-
* objects to prevent undefined behavior from integer overflow.
25+
* Validates that a tensor's metadata is semantically valid: sizes are
26+
* non-negative, scalar type is valid, and computing numel/nbytes will not
27+
* overflow.
2828
*
2929
* @param[in] tensor The flatbuffer Tensor to validate.
30-
* @return Error::Ok if the numel calculation is safe, Error::InvalidProgram
31-
* if computing numel would overflow.
30+
* @return Error::Ok if validation passes, Error::InvalidProgram otherwise.
3231
*/
3332
ET_NODISCARD Error validate_tensor(const executorch_flatbuffer::Tensor* tensor);
3433

runtime/executor/test/method_test.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,31 @@ TEST_F(MethodTest, AliasedIOTest) {
310310
}
311311
}
312312

313+
TEST_F(MethodTest, SetInputRejectsOverflowingSizes) {
314+
// The "cat" model (ModuleDynamicCatUnallocatedIO) has a 2D input.
315+
// set_input validates numel/nbytes overflow before resize_tensor.
316+
ManagedMemoryManager mmm(kDefaultNonConstMemBytes, kDefaultRuntimeMemBytes);
317+
Result<Method> method = programs_["cat"]->load_method("forward", &mmm.get());
318+
ASSERT_EQ(method.error(), Error::Ok);
319+
320+
// Create a 2D tensor with enormous sizes. On 32-bit platforms the numel
321+
// multiplication overflows; on 64-bit the resize bounds check rejects it.
322+
int32_t sizes[2] = {2000000000, 2000000000};
323+
uint8_t dim_order[2] = {0, 1};
324+
int32_t strides[2] = {1, 1};
325+
executorch::aten::TensorImpl impl(
326+
executorch::aten::ScalarType::Float,
327+
2,
328+
sizes,
329+
nullptr,
330+
dim_order,
331+
strides);
332+
333+
auto input_err =
334+
method->set_input(EValue(executorch::aten::Tensor(&impl)), 0);
335+
EXPECT_NE(input_err, Error::Ok);
336+
}
337+
313338
TEST_F(MethodTest, ConstantSegmentTest) {
314339
// Execute model with constants stored in segment.
315340
ManagedMemoryManager mmm(kDefaultNonConstMemBytes, kDefaultRuntimeMemBytes);

runtime/executor/test/program_validation_test.cpp

Lines changed: 98 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,15 @@ struct EValueConfig {
6464
EValueType type;
6565
std::vector<int32_t> tensor_sizes; // For Tensor type.
6666
std::vector<int32_t> tensor_list_items; // For TensorList type (indices).
67+
bool is_dynamic = false; // For Tensor type: if true, uses DYNAMIC_BOUND.
6768
};
6869

6970
// Unified helper to create a minimal valid PTE flatbuffer with configurable
70-
// evalues. Returns a buffer containing the flatbuffer data.
71+
// evalues. Returns a buffer containing the flatbuffer data. input_indices
72+
// specifies which value indices appear in the execution plan's inputs list.
7173
std::vector<uint8_t> CreateTestProgram(
72-
const std::vector<EValueConfig>& configs) {
74+
const std::vector<EValueConfig>& configs,
75+
const std::vector<int32_t>& input_indices = {}) {
7376
flatbuffers::FlatBufferBuilder builder(1024);
7477

7578
std::vector<flatbuffers::Offset<executorch_flatbuffer::EValue>> evalues;
@@ -93,7 +96,9 @@ std::vector<uint8_t> CreateTestProgram(
9396
/*data_buffer_idx=*/0,
9497
/*allocation_info=*/0,
9598
/*layout=*/0,
96-
executorch_flatbuffer::TensorShapeDynamism::STATIC,
99+
config.is_dynamic
100+
? executorch_flatbuffer::TensorShapeDynamism::DYNAMIC_BOUND
101+
: executorch_flatbuffer::TensorShapeDynamism::STATIC,
97102
/*extra_tensor_info=*/0);
98103
evalues.push_back(executorch_flatbuffer::CreateEValue(
99104
builder,
@@ -124,6 +129,7 @@ std::vector<uint8_t> CreateTestProgram(
124129

125130
auto values_vec = builder.CreateVector(evalues);
126131
auto plan_name = builder.CreateString("forward");
132+
auto inputs_vec = builder.CreateVector(input_indices);
127133
auto empty_int_vec = builder.CreateVector(std::vector<int32_t>{});
128134
auto empty_int64_vec = builder.CreateVector(std::vector<int64_t>{0});
129135
auto empty_chain_vec = builder.CreateVector(
@@ -139,8 +145,8 @@ std::vector<uint8_t> CreateTestProgram(
139145
plan_name,
140146
/*container_meta_type=*/0,
141147
values_vec,
142-
empty_int_vec,
143-
empty_int_vec,
148+
/*inputs=*/inputs_vec,
149+
/*outputs=*/empty_int_vec,
144150
empty_chain_vec,
145151
empty_operators_vec,
146152
empty_delegates_vec,
@@ -206,29 +212,93 @@ TEST_F(ProgramValidationTest, InternalConsistencyDetectsTruncatedData) {
206212
ASSERT_EQ(program.error(), Error::InvalidProgram);
207213
}
208214

209-
// TEST_F(ProgramValidationTest, TensorNumelOverflowDetected) {
210-
// std::vector<EValueConfig> configs = {
211-
// {EValueType::Tensor, {2000000000, 2000000000, 2000000000}, {}}};
212-
//
213-
// AlignedBuffer buf(CreateTestProgram(configs));
214-
// auto loader = buf.loader();
215-
//
216-
// Result<Program> program =
217-
// Program::load(&loader, Program::Verification::InternalConsistency);
218-
// EXPECT_EQ(program.error(), Error::InvalidProgram);
219-
// }
220-
221-
// TEST_F(ProgramValidationTest, TensorNumelOverflowNotDetectedWithMinimal) {
222-
// std::vector<EValueConfig> configs = {
223-
// {EValueType::Tensor, {2000000000, 2000000000, 2000000000}, {}}};
224-
//
225-
// AlignedBuffer buf(CreateTestProgram(configs));
226-
// auto loader = buf.loader();
227-
//
228-
// // Minimal verification doesn't run program validation.
229-
// Result<Program> program =
230-
// Program::load(&loader, Program::Verification::Minimal);
231-
// }
215+
TEST_F(ProgramValidationTest, TensorNumelOverflowDetectedForStaticTensor) {
216+
// Static tensors always have their overflow checked at validation time.
217+
std::vector<EValueConfig> configs = {
218+
{EValueType::Tensor,
219+
{2000000000, 2000000000, 2000000000},
220+
{},
221+
/*is_dynamic=*/false}};
222+
223+
AlignedBuffer buf(CreateTestProgram(configs));
224+
auto loader = buf.loader();
225+
226+
Result<Program> program =
227+
Program::load(&loader, Program::Verification::InternalConsistency);
228+
EXPECT_EQ(program.error(), Error::InvalidProgram);
229+
}
230+
231+
TEST_F(ProgramValidationTest, TensorNumelOverflowNotDetectedWithMinimal) {
232+
std::vector<EValueConfig> configs = {
233+
{EValueType::Tensor,
234+
{2000000000, 2000000000, 2000000000},
235+
{},
236+
/*is_dynamic=*/false}};
237+
238+
AlignedBuffer buf(CreateTestProgram(configs));
239+
auto loader = buf.loader();
240+
241+
// Minimal verification doesn't run program validation.
242+
Result<Program> program =
243+
Program::load(&loader, Program::Verification::Minimal);
244+
EXPECT_EQ(program.error(), Error::Ok);
245+
}
246+
247+
TEST_F(ProgramValidationTest, TensorNumelOverflowSkippedForDynamicInput) {
248+
// Dynamic input tensors skip overflow checks at validation time; the check
249+
// is deferred to set_input where actual sizes are known.
250+
std::vector<EValueConfig> configs = {
251+
{EValueType::Tensor,
252+
{2000000000, 2000000000, 2000000000},
253+
{},
254+
/*is_dynamic=*/true}};
255+
256+
// Mark value index 0 as a plan input.
257+
AlignedBuffer buf(CreateTestProgram(configs, /*input_indices=*/{0}));
258+
auto loader = buf.loader();
259+
260+
Result<Program> program =
261+
Program::load(&loader, Program::Verification::InternalConsistency);
262+
EXPECT_EQ(program.error(), Error::Ok);
263+
}
264+
265+
TEST_F(
266+
ProgramValidationTest,
267+
TensorNumelOverflowDetectedForDynamicNonInputTensor) {
268+
// A dynamic tensor that is NOT in the inputs list should still have its
269+
// overflow checked at validation time.
270+
std::vector<EValueConfig> configs = {
271+
{EValueType::Tensor,
272+
{2000000000, 2000000000, 2000000000},
273+
{},
274+
/*is_dynamic=*/true}};
275+
276+
// No input indices — the tensor is not a plan input.
277+
AlignedBuffer buf(CreateTestProgram(configs));
278+
auto loader = buf.loader();
279+
280+
Result<Program> program =
281+
Program::load(&loader, Program::Verification::InternalConsistency);
282+
EXPECT_EQ(program.error(), Error::InvalidProgram);
283+
}
284+
285+
TEST_F(ProgramValidationTest, TensorNumelOverflowDetectedForStaticInputTensor) {
286+
// A static input tensor should still have its overflow checked at
287+
// validation time since its sizes cannot change.
288+
std::vector<EValueConfig> configs = {
289+
{EValueType::Tensor,
290+
{2000000000, 2000000000, 2000000000},
291+
{},
292+
/*is_dynamic=*/false}};
293+
294+
// Mark value index 0 as a plan input.
295+
AlignedBuffer buf(CreateTestProgram(configs, /*input_indices=*/{0}));
296+
auto loader = buf.loader();
297+
298+
Result<Program> program =
299+
Program::load(&loader, Program::Verification::InternalConsistency);
300+
EXPECT_EQ(program.error(), Error::InvalidProgram);
301+
}
232302

233303
TEST_F(ProgramValidationTest, NegativeSizeDetected) {
234304
std::vector<EValueConfig> configs = {{EValueType::Tensor, {10, -5, 10}, {}}};

0 commit comments

Comments
 (0)