You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Foundry Local Android 0.1.6 / current Android origin/main
0.14.1
1.26.0
1.2.4 family
0871373716b5b764d24da70ccdb891d5f0497c66
For the 0.13.1 binary, the supplied ARM64 frames are at relative PCs 0x267a98, 0x267fa8, and 0x2bb0ac. For the 0.14.1 binary, the constructor fault at relative PC 0x286688 reads Tokenizer storage before shared_from_this() can establish a strong owner. This is consistent with a null/stale tokenizer handle, use-after-free, disposal/finalizer race, generation replacement race, unsupported concurrent stream creation, or earlier pointer corruption. It does not establish which hypothesis is correct.
The supplied tombstone evidence is not joined to a production Kusto session. Do not claim production prevalence for this exact signature or infer that it caused a specific Android Binder death. A matching BuildId, normalized frames, original exit PID/time, and session correlation are required before making that claim.
Why this belongs in microsoft/Foundry-Local
Android packages prebuilt Core/ORT GenAI binaries and invokes the shared command surface, but the tokenizer registry, managed/native wrapper ownership, C API handles, and model/tokenizer disposal contract are Core/native concerns. Android can provide containment and process evidence; the durable ownership fix must be implemented and validated in the shared Core/runtime path.
Investigation hypotheses
Test these independently:
Request-scoped ownership gap: model/tokenizer lookup returns a borrowed object and a concurrent request, registry update, or cleanup releases/replaces it before stream creation.
Managed wrapper lifetime gap: raw IntPtr ownership permits explicit Dispose or finalization to race a native call. A SafeHandle or equivalent locked add-reference/release lease may be required.
Generation/unload race: unload removes or disposes an old generation while an admitted request still uses it, or a late cleanup disposes a replacement generation.
Live-tokenizer thread-safety: concurrent CreateStream calls on one valid tokenizer are unsupported or mutate shared state.
C API validation gap: null/disposed handles are not rejected before dereference. A null check hardens the boundary but cannot make a stale non-null pointer safe.
Earlier heap corruption: another handle destroy/type mismatch corrupts ownership data before this call.
A runtime upgrade is a comparison cell, not proof of remediation. The critical ownership pattern exists across both observed runtime families.
Reproduction plan
Phase A: one model, concurrent requests, no unload
Start here because the customer uses one model and the crash may not require lifecycle overlap.
Initialize once and load exactly one production-representative model once.
Do not unload, reload, replace, or dispose the model/tokenizer during this phase.
Run streaming and non-streaming cases separately with minimal input and max_tokens=1.
Release 2, 4, 8, and 16 callers from a barrier at the same time.
Run with telemetry/Core side commands enabled and disabled as separate cells.
Record every service/worker PID, process generation, terminal callback, Core/ORT/GenAI version, and ELF BuildId. A hidden service restart is a failure.
Repeat against the exact 0.13.1/e9c42… and 0.14.1/087137… binaries.
Add narrower harnesses to separate ownership layers:
direct OGA: parallel OgaCreateTokenizerStream/destroy-stream calls against one unquestionably live tokenizer;
managed wrapper: parallel CreateStream with forced GC/finalizer activity and explicit dispose tests;
Core command harness: parallel chat commands against one permanently loaded model without Android Binder orchestration.
Phase B: deterministic unload/reload overlap
Run separately after Phase A.
Add a test hook immediately after model/tokenizer lookup and immediately before OgaCreateTokenizerStream.
Pause an admitted request at that hook.
Start unload or replacement of the same model generation.
Release the request into stream creation.
Repeat with reload of the same alias while the old request remains admitted.
Force every ordering: request wins, unload wins, replacement wins, cancellation during wait, and late cleanup from an old generation.
Without the pre-call hook, unload overlap is only probabilistic and cannot qualify a lifetime fix.
Required ownership contract
Model/tokenizer lookup must return a request-scoped strong lease tied to an immutable generation.
Unload removes the generation from new lookup before disposal, then either waits for admitted leases to drain or returns a defined busy/cancelled outcome.
Stream creation must acquire/retain the tokenizer ownership it needs before native dereference.
Existing streams must either retain their required model/tokenizer state or terminate through a documented safe transition.
Managed wrappers must use SafeHandle or equivalent synchronized add-reference/release semantics so native calls cannot race explicit dispose or finalization.
Close/dispose/unload must be idempotent and generation-safe; cleanup for an old generation must never release a replacement.
If live-tokenizer concurrent CreateStream is unsupported even with valid ownership, serialize only that operation and document the constraint.
Cancellation must remain non-blocking and must not create lock inversion through callbacks.
Diagnostics
Emit bounded lifecycle evidence without pointers, prompts, responses, model contents, paths, or credentials:
Core/ORT/ORT GenAI versions and exact ELF BuildIds;
process role/PID/generation and request/model generation;
tokenizer/model opaque handle ID plus generation, state, active lease count, and create/release phase;
create-stream, unload, replacement, dispose, and finalizer begin/end ordering;
typed null/disposed/stale/busy outcomes;
sanitizer finding or normalized native fingerprint when a process exits.
Deterministic acceptance criteria
Phase A completes at least 10,000 full requests across the 2/4/8/16 concurrency matrix with zero native exits, hidden process restarts, missing terminal callbacks, deadlocks, or unbounded object growth.
A direct OGA sanitizer run completes at least 1,000,000 stream create/destroy operations against a live tokenizer with zero ASan/HWASan findings.
A barrier test forces at least 1,000 request-vs-unload/replacement interleavings and 100 full reload cycles with zero UAF, double-free, stale-generation release, or deadlock.
If creation wins admission, it holds a valid strong lease and returns a usable stream. If unload/removal wins, creation returns a typed error before native dereference.
Null, disposed, and stale-generation handles return defined errors and never enter TokenizerStream construction.
Explicit dispose and forced managed finalization cannot release a handle while SafeHandle/equivalent admission is active.
Repeated close/dispose/unload is idempotent and cannot release a replacement generation.
Existing stream behavior during unload is explicitly tested and matches the documented contract.
Malformed tokenizer/config/model artifacts fail through typed parse/integrity errors and never reproduce the lifetime crash.
The patched build passes with any temporary Android global/per-model serialization diagnostic disabled.
Every test artifact records exact Core, ORT, ORT GenAI, wrapper, and BuildId provenance.
If the defect is inside ONNX Runtime GenAI, a minimal reproducer, upstream fix/version, sanitizer regression, and proof that the shipping binary contains the fix are attached.
Non-goals
Claiming the exact signature is prevalent in production telemetry.
Treating model corruption as the established cause of this constructor frame.
Solving Android AEI symbolication or Binder recovery here.
Calling process-wide OgaShutdown() and attempting to reinitialize the runtime.
Ownership
Foundry Local Core: registry generations, request leases, unload/replacement ordering, wrapper lifetime, and deterministic hooks/tests.
Managed wrapper:SafeHandle or equivalent call-vs-dispose/finalizer safety.
ONNX Runtime GenAI: C API lifetime/null contract and any defect reproducible with a valid live handle.
Android: temporary containment experiments, exact process/build evidence, and integration qualification; not the canonical lifetime implementation.
Description
Two shipped Android native runtime families have produced the same process-terminating tokenizer-stream construction stack:
Known binary provenance:
libonnxruntime-genai.soBuildIde9c42fda43224e3104f208390e3fd99ca1d30920origin/main0871373716b5b764d24da70ccdb891d5f0497c66For the 0.13.1 binary, the supplied ARM64 frames are at relative PCs
0x267a98,0x267fa8, and0x2bb0ac. For the 0.14.1 binary, the constructor fault at relative PC0x286688readsTokenizerstorage beforeshared_from_this()can establish a strong owner. This is consistent with a null/stale tokenizer handle, use-after-free, disposal/finalizer race, generation replacement race, unsupported concurrent stream creation, or earlier pointer corruption. It does not establish which hypothesis is correct.The supplied tombstone evidence is not joined to a production Kusto session. Do not claim production prevalence for this exact signature or infer that it caused a specific Android Binder death. A matching BuildId, normalized frames, original exit PID/time, and session correlation are required before making that claim.
Why this belongs in
microsoft/Foundry-LocalAndroid packages prebuilt Core/ORT GenAI binaries and invokes the shared command surface, but the tokenizer registry, managed/native wrapper ownership, C API handles, and model/tokenizer disposal contract are Core/native concerns. Android can provide containment and process evidence; the durable ownership fix must be implemented and validated in the shared Core/runtime path.
Investigation hypotheses
Test these independently:
IntPtrownership permits explicitDisposeor finalization to race a native call. ASafeHandleor equivalent locked add-reference/release lease may be required.CreateStreamcalls on one valid tokenizer are unsupported or mutate shared state.A runtime upgrade is a comparison cell, not proof of remediation. The critical ownership pattern exists across both observed runtime families.
Reproduction plan
Phase A: one model, concurrent requests, no unload
Start here because the customer uses one model and the crash may not require lifecycle overlap.
max_tokens=1.e9c42…and 0.14.1/087137…binaries.Add narrower harnesses to separate ownership layers:
OgaCreateTokenizerStream/destroy-stream calls against one unquestionably live tokenizer;CreateStreamwith forced GC/finalizer activity and explicit dispose tests;Phase B: deterministic unload/reload overlap
Run separately after Phase A.
OgaCreateTokenizerStream.Without the pre-call hook, unload overlap is only probabilistic and cannot qualify a lifetime fix.
Required ownership contract
SafeHandleor equivalent synchronized add-reference/release semantics so native calls cannot race explicit dispose or finalization.CreateStreamis unsupported even with valid ownership, serialize only that operation and document the constraint.Diagnostics
Emit bounded lifecycle evidence without pointers, prompts, responses, model contents, paths, or credentials:
Deterministic acceptance criteria
TokenizerStreamconstruction.SafeHandle/equivalent admission is active.Non-goals
OgaShutdown()and attempting to reinitialize the runtime.Ownership
SafeHandleor equivalent call-vs-dispose/finalizer safety.