12 KiB
Error Handling & Defensive Programming Guide
Balancing defensiveness with the extreme constraints of microcontrollers (where every byte of Flash/RAM and every clock cycle matters) is a core design tension in TFLM. Since TFLM targets environments that often lack memory protection (no MMU), don't support C++ exceptions, and run on bare metal or simple RTOSs, a "standard" defensive posture would result in unacceptable binary bloat and performance degradation.
This guide provides an architectural framework for writing safe kernels and offers recommendations for code reviews for contributors.
1. Trust Boundaries: Where Data Comes From
To achieve high performance without sacrificing critical stability, TFLM distinguishes between trusted and untrusted data sources.
The C++ API (Trusted)
When a firmware developer uses the public TFLM C++ API (e.g., instantiating a
MicroInterpreter or calling Invoke()), we treat the developer as trusted.
- Recommendation: The core API should not spend runtime cycles or binary
space checking for invalid parameters (e.g., passing
nullptr) or incorrect state-machine sequences. - Mechanism: Rely entirely on
TFLITE_DCHECK. The firmware engineer gets an immediate assertion failure during local debugging if they misuse the API. In release builds, these compile out, leaving zero overhead.
The FlatBuffer Model (Partially Trusted)
When dealing with the .tflite FlatBuffer model, we distinguish between two
types of malformation:
- Corrupted FlatBuffer Files (Structural Malformation): The core
MicroInterpreterassumes the FlatBuffer is structurally valid. We do not perform standard bounds checking on FlatBuffer schema offsets or operator tensor indices (e.g., if an operator requests tensor index999but the model only has10tensors, a production TFLM build will read out of bounds). To aid local debugging, structural checks are considered a "good-to-have" feature but should exclusively useTFLITE_DCHECK. If models are provided via an untrusted channel (e.g., OTA), the application layer is entirely responsible for validating the integrity of the model before passing it to TFLM. - Invalid Model Topologies (Semantic Malformation): The runtime should
defend against TFLite Converter bugs or unsupported configurations (e.g.,
wrong tensor shapes, unsupported types). This validation happens entirely in
the Setup phase. Because developers can use
TF_LITE_STRIP_ERROR_STRINGSto remove the string bloat in production, checking topologies inPrepareprovides safe fallback (e.g., rejecting a bad OTA update) without the severe ROM penalty of embedded strings.
2. Execution Phases: When Code Runs
TFLM distinguishes between setup (which runs once) and execution (which runs continuously).
Phase 1: Setup & Initialization (Prepare)
During the Prepare phase, kernels should validate their inputs and parameters
to ensure Eval can run blindly and safely. We prioritize clear error messages
here (relying on TF_LITE_STRIP_ERROR_STRINGS to mitigate the ROM cost in
production) and we can afford to spend CPU cycles on validation.
What to Validate (Use TF_LITE_ENSURE):
- Model-provided parameters: Check the number of inputs/outputs, tensor types, and tensor shapes.
- Quantization parameters: Explicitly check for invalid quantization
parameters (e.g., a
scaleof0.0or azero_pointout of bounds) that could cause a divide-by-zero or overflow later inEval. - Resource allocations: Check if memory allocations (like
AllocateTempInputTensor) returnnullptr.
Phase 2: Execution (Eval)
During the Eval phase, the runtime is vulnerable to data that causes hardware
traps or memory corruption. We should avoid spending cycles or ROM on redundant
checks.
What to Defend Against (Recommended Validation):
Kernels should actively defend against three specific runtime threats during
Eval:
- Out-of-bounds Memory Access: If an input tensor contains indices or
offsets generated at runtime (e.g., in
GATHER,STRIDED_SLICE), these should be bounds-checked at runtime using a rawif (!valid) return kTfLiteError;. - Hardware Faults (Divide by Zero): Kernels should mathematically protect against divide-by-zero (e.g., if a divisor comes from an input tensor) or explicitly check and return an error.
- Infinite Loops: All loops inside
Evalshould have a guaranteed maximum bound to prevent data-dependent infinite hangs.
What to Ignore (GIGO for Signal Data):
- TFLM kernels are strongly discouraged from explicitly scanning general
input signal data (like images or audio) for
NaN/Inf. For standard math operations, garbage data simply results in garbage output (GIGO).
3. Macro Selection Guide
To strike the right balance between debuggability, code size (ROM), and performance, follow this decision tree.
The Decision Tree
- Are you writing a Unit Test?
- Use GoogleTest-style macros like
EXPECT_EQ,EXPECT_NEAR, etc. for verifying test conditions.
- Use GoogleTest-style macros like
- Are you in
InitorPrepare?- Use
TF_LITE_ENSURE. We want to fail early with a clear log message if the model is incompatible.
- Use
- Are you in
Eval(or a helper called byEval)?- Is it an invariant guaranteed by
Prepare? (e.g., "this pointer cannot be null"). UseTFLITE_DCHECK. It costs nothing in production. - Is it validating control data from an input tensor? (e.g., a dynamic
index). Use a raw
if (!condition) return kTfLiteError;. This safely prevents memory corruption without paying the ROM cost of a string literal. - Are you propagating an error from a helper function? Use
TF_LITE_ENSURE_STATUSorTF_LITE_ENSURE_OK.
- Is it an invariant guaranteed by
Macro Cheat Sheet
| Macro | Cost in Release | When to Use |
|---|---|---|
TFLITE_DCHECKTFLITE_DCHECK_EQ |
Zero (Optimized out) | Default for Eval invariants and FlatBuffer structural bounds checking. |
if (!cond) return kTfLiteError; |
Low (Branch only) | Default for validating control data in Eval. |
TF_LITE_ENSURE_OKTF_LITE_ENSURE_STATUS |
Low (Branch only) | Default for propagating errors from helper functions. |
TF_LITE_ENSURETF_LITE_ENSURE_EQ |
High (Branch + logs __FILE__, __LINE__, #cond) |
Default for Prepare / Setup. Can be used in Eval only if the failure indicates an unrecoverable state-machine corruption; avoid for normal signal data out-of-bounds. |
TF_LITE_ENSURE_MSG |
Highest (Branch + custom string) | Use Sparingly. Only when the default TF_LITE_ENSURE error is too cryptic. |
TFLITE_CHECK |
Fatal (Calls Abort()) |
Avoid in core TFLM. Halts the microcontroller. |
The Hidden Cost of
TF_LITE_ENSURE: This macro expands to anifbranch that callsTF_LITE_KERNEL_LOG. By default, this embeds__FILE__,__LINE__, and the stringified condition directly into the.rodatasection of the binary. Every single invocation permanently consumes precious ROM. If you have multiple preconditions, combine them into a singleTF_LITE_ENSURE(context, a != nullptr && b != nullptr). Note: For production builds where ROM is severely constrained, firmware developers should define theTF_LITE_STRIP_ERROR_STRINGSmacro to compile out these strings, reducingTF_LITE_ENSUREto a simple low-cost branch.
4. Code Review Guidelines & Concrete Examples
How to address common scenarios in PRs:
Validating Framework Pointers
Discouraged. Please avoid wasting ROM checking the validity of the TFLM
framework itself. The context and node pointers passed by the runtime are
typically guaranteed to be valid. Notably, TF_LITE_ENSURE(context, context != nullptr) is logically flawed: if context is actually nullptr, the macro
will dereference it to log the error, causing an immediate hardware fault.
Validating Public API Parameters
Discouraged. This penalizes production deployments for bugs that the
firmware developer should have caught during local testing. We recommend asking
the contributor to use TFLITE_DCHECK(op_resolver != nullptr); instead of
adding if (op_resolver == nullptr) return kTfLiteError; to the
MicroInterpreter API.
Preventing Null Pointer Dereferences
- If in
Prepare(Recommended): It is standard practice to check for nulls when pulling tensors from the context.
TfLiteTensor* operand = micro_context->AllocateTempInputTensor(node, kOperandTensor);
TF_LITE_ENSURE(context, operand != nullptr);
- If in
Eval(Discouraged): Production builds shouldn't pay the cycle cost for checks that can't fail at runtime ifPreparesucceeded. Ask the author to change it toTFLITE_DCHECK.
const TfLiteEvalTensor* input_id = tflite::micro::GetEvalInput(context, node, 0);
TFLITE_DCHECK(input_id != nullptr);
Preventing Buffer Overflows
- If in
Prepare(Recommended): UseTF_LITE_ENSUREto validate that tensor sizes are compatible. - If in
Eval: If the overflow comes from invalid control data (like an input tensor providing indices), it should be validated inEvalto prevent memory corruption. Use raw returns:
// e.g., third_party/tflite_micro/tensorflow/lite/micro/kernels/gather_nd.cc
// Note: use subtraction to prevent integer overflow!
if (from_pos < 0 || from_pos > params_flat_size - slice_size) {
return kTfLiteError; // Halts execution to prevent out-of-bounds memory read
}
If it's just an invariant guaranteed by `Prepare`, ask to discard the check or
use `TFLITE_DCHECK`.
Fixing Fuzzer Crashes
Fuzzer fixes should align with the core philosophy. We evaluate them based on the type of crash:
- Corrupted FlatBuffer Files (Working As Intended): If the fuzzer mutates
the FlatBuffer byte array so a
Tensoroffset points beyond the end of the file (causing a segfault), request changes if the PR usesTF_LITE_ENSUREor rawifstatements. To keep the binary size as small as possible, we prefer to omit the FlatBuffer verifier. However, it is recommended to accept the PR if it exclusively usesTFLITE_DCHECKto catch the out-of-bounds index. This treats the structural check purely as a zero-cost developer aid during debugging. - Invalid Model Topologies (Fix in Prepare): If the fuzzer generates a
valid FlatBuffer but modifies a
CONV_2Doperator to have 0 inputs instead of the expected 3, this is a good fix inPrepareusingTF_LITE_ENSURE_EQ(context, NumInputs(node), 3);. - Static Math Crashes (Fix in Prepare): If the fuzzer sets a quantization
scaleparameter to0.0(causing a hardware trap during inference), this is a good fix inPrepareusingTF_LITE_ENSURE(context, scale != 0.0);. - Dynamic Math/Data Crashes (Fix in Eval): If the fuzzer provides input
data to a
GATHERop with an index of999(causing out-of-bounds corruption), or a divisor tensor evaluates to0(causing a divide-by-zero trap), this is a good fix inEval. Prefer a rawif (index >= 10) return kTfLiteError;to save ROM (avoid usingTF_LITE_ENSUREhere just to log the error; save the ROM).