Skip to content

fix(ios): zero-initialize TfLiteCoreMlDelegateOptions - #196

Open
jslok wants to merge 2 commits into
margelo:mainfrom
jslok:fix/coreml-delegate-options-zero-init
Open

fix(ios): zero-initialize TfLiteCoreMlDelegateOptions#196
jslok wants to merge 2 commits into
margelo:mainfrom
jslok:fix/coreml-delegate-options-zero-init

Conversation

@jslok

@jslok jslok commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

What

getCoreMLDelegate() declares TfLiteCoreMlDelegateOptions delegateOptions; without initialization. It's a plain C struct with no ...OptionsDefault() helper, so all four fields (enabled_devices, coreml_version, max_delegated_partitions, min_nodes_per_partition) are indeterminate stack garbage.

Depending on what happens to be on the stack, this poisons the delegate's graph partitioning — we've seen it produce EXC_BAD_ACCESS inside the first TfLiteInterpreterInvoke on some device/OS combinations, and it can also silently change which devices the delegate enables.

Fix

= {}. Zero values are the documented defaults (ANE-only devices, newest CoreML version, unlimited partitions).

History

This is the same bug #165 fixed — that PR was closed because it targeted the pre-Nitro TensorflowPlugin.cpp and you invited a re-roll against the current codebase ("if this is still relevant in the latest version, we can open another PR and get it merged 👍"). It is still relevant: the Nitro migration carried the uninitialized declaration into cpp/TfliteHelpers.cpp.

We've been shipping this as a patch-package fix in production.

🤖 Generated with Claude Code

TfLiteCoreMlDelegateOptions is a plain C struct with no ...OptionsDefault()
helper, and it was declared without any initialization - so enabled_devices,
coreml_version, max_delegated_partitions and min_nodes_per_partition were
stack garbage. Depending on what values happened to be on the stack, this
poisons the delegate's graph partitioning and can crash (EXC_BAD_ACCESS)
inside the first TfLiteInterpreterInvoke.

Zero values are the documented defaults (ANE-only devices, newest CoreML
version, unlimited partitions), so '= {}' is the correct initialization.

Same fix as margelo#165, re-rolled against the Nitro codebase per maintainer
feedback there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DaveyEke

DaveyEke commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Hey @jslok, thanks for the PR. What bug does this PR intend to fix though? Do you know of any github issue? Asking so I can repro and understand the situation better

@jslok

jslok commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Hi @DaveyEke, fair question. There's no dedicated issue; the history is #165, which fixed the same line in the codebase before the Nitro migration and was closed as out of sync, with an invitation to reopen it against the current code. This PR is that reroll: the Nitro migration carried the uninitialized declaration into TfliteHelpers.cpp.

To set expectations honestly: this is an uninitialized memory bug (undefined behavior), so there's no deterministic repro to hand you. delegateOptions contains whatever happens to be on the stack when getCoreMLDelegate() runs. On most stacks the garbage is benign and everything works, which is why nobody files an issue, but all four fields directly control delegate behavior:

  • enabled_devices: a garbage nonzero value silently changes which devices the delegate enables (ANE only vs. all)
  • coreml_version: garbage can request a nonexistent CoreML version
  • max_delegated_partitions / min_nodes_per_partition: garbage changes graph partitioning, i.e. which ops go to CoreML vs. CPU

The easiest way to observe it is not a crash but the values themselves: breakpoint (or log) the struct fields right before TfLiteCoreMlDelegateCreate. They're arbitrary, not the documented defaults.

In our production app we've seen nondeterministic first inference EXC_BAD_ACCESS inside TfLiteInterpreterInvoke with the CoreML delegate on some device/OS combos. I can't prove uninitialized partitioning params are the cause, since UB doesn't leave that kind of receipt, but they're exactly the class of input that can produce it, and initializing to zero is unambiguously correct regardless: per coreml_delegate.h, zero is the documented default for every field, so = {} just makes the code do what it already assumes it's doing.

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.

2 participants