dotnet / dotnet/android

.NET 10 Android runtime libraries ship without .eh_frame, truncating native crash stacks to 1-2 frames

Open
#12,697 4 comments 0 reactions 0 assignees View on GitHub
need-attention
Dominant language
C#
Stars
2.1k
Forks
579
Avg merge
1d 20h
Merged PRs (30d)
257

Description

### Android framework version

net10.0-android

### Affected platform version

.NET 10, Microsoft.Android.Runtime.Mono.36.android-arm64 and Microsoft.Android.Runtime.CoreCLR.36.android-arm64, both 36.1.69, android-arm64

### Description

## Summary

The .NET 10 Android runtime libraries contain **no unwind information at all** — no `.eh_frame`, no
`.eh_frame_hdr`, and no `PT_GNU_EH_FRAME` program header. The equivalent .NET 9 library has all three.

Consequently nothing can unwind out of these libraries. Google Play tombstones for a crash inside the
runtime stop at frame `#00` or `#01`, and no caller is recoverable. Uploading debug symbols does not
help, because what is missing is CFI, not symbol names.

Found whilst investigating #12696

## Evidence

```
net9 Runtime.35 / 35.0.61 PT_GNU_EH_FRAME=YES sections: .eh_frame_hdr, .eh_frame
net10 Runtime.Mono.36 / 36.1.69 PT_GNU_EH_FRAME=NO sections: NONE
net10 Runtime.CoreCLR.36 / 36.1.69 PT_GNU_EH_FRAME=NO sections: NONE
```

Verified independently from both the section header table and the program header table. `.symtab` is
still present in all three (1,430 symbols on net9, 1,594 on net10 Mono), so function names still
resolve — only the ability to walk the stack is gone.

## Impact

A real example. We have a production crash inside the runtime affecting ~1.6% of sessions. The best
of three Play tombstones has two frames; the other two have only frame `#00`. The calling function is
unrecoverable from any crash report, so the crash could not be attributed to a subsystem at all.

We eventually identified the caller by statically scanning `.text` for `BL` instructions targeting the
symbol and mapping call sites back to `.symtab`. That worked, but it should not be the supported route
for diagnosing a crash in a shipped runtime.

This affects anyone triaging a native crash in a .NET 10 Android app, and it silently degrades Play
vitals, Sentry and Crashlytics alike — none of them can do anything about it, because the information
is absent from the binary.

## Possibly related

`--gc-sections` and section-splitting work for Android native release builds landed in this window and
may be the cause, though we have not confirmed that. Whatever the cause, `.eh_frame` ought to survive
it: `--gc-sections` alone does not normally remove unwind tables, so this may be an unintended
consequence of an additional strip step, or of `-fno-asynchronous-unwind-tables`.

## Ask

Ship `.eh_frame`/`.eh_frame_hdr` in the release runtime libraries, as .NET 9 did. The size cost is
modest relative to making every native crash inside the runtime undiagnosable.

### Steps to Reproduce

No app is required. Against the installed workload packs:

readelf -lS /runtimes/android-arm64/native/libmono-android.release.so | grep -E "eh_frame|GNU_EH_FRAME"

1. Run it against Microsoft.Android.Runtime.35 / 35.0.61 — `.eh_frame`, `.eh_frame_hdr` and
`PT_GNU_EH_FRAME` are all present.
2. Run it against Microsoft.Android.Runtime.Mono.36 / 36.1.69 — all three are absent.
3. Same for Microsoft.Android.Runtime.CoreCLR.36 / 36.1.69 (`libnet-android.release.so`).

To see the consequence, take any native crash inside the runtime on a .NET 10 build and read the
tombstone in Play Console: it will stop at frame #00 or #01.

### Did you find any workaround?

None for the missing unwind tables themselves.

The only route we found to a caller was static analysis of the shipped binary: locate the faulting
symbol in `.symtab`, scan `.text` for AArch64 `BL` instructions (top 6 bits 0b100101, sign-extended
imm26 x 4) whose target is that symbol, and map each call site back to its containing function. That
recovers callers without unwinding, but only direct calls, and it cannot recover a runtime call stack.

### Relevant log output

```shell
Section and program header comparison across the three packs:

net9 Runtime.35 / 35.0.61 PT_GNU_EH_FRAME=YES unwind sections: .eh_frame_hdr, .eh_frame
net10 Runtime.Mono.36 / 36.1.69 PT_GNU_EH_FRAME=NO unwind sections: NONE
net10 Runtime.CoreCLR.36 / 36.1.69 PT_GNU_EH_FRAME=NO unwind sections: NONE

A representative truncated tombstone from Google Play (this is the FULL backtrace as reported):

pid: 0, tid: 23507 >>> com.example.app <<<

backtrace:
#00 pc 0x0000000000069c4c /apex/com.android.runtime/lib64/bionic/libc.so (__memcmp_aarch64+12)
#01 pc 0x00000000000afa54 .../split_config.arm64_v8a.apk!libmono-android.release.so
(...robin_hash<...>::find_impl<...>(...) const+208)
(BuildId: ae7ad8820dd75996ecdee692e377f8586298ceb0)

Two of the three sample reports for the same crash contain only frame #00.
```

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by running the provided readelf comparison against the .NET 9 and .NET 10 workload packs, focusing on libmono-android.release.so and libnet-android.release.so. Then trace the Android native release-build changes involving --gc-sections, section splitting, stripping, or unwind-table flags; done means both .NET 10 libraries contain .eh_frame, .eh_frame_hdr, and PT_GNU_EH_FRAME and native tombstones can unwind past runtime frames.

Written by the indexing model from the issue text.

Assessment

Tech stack
android
Domain
build-system, mobile-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.