Skip to content

[CoreCLR] Stop serving BCL p/invokes from the precompiled override#11537

Open
simonrozsival wants to merge 3 commits into
mainfrom
dev/simonrozsival/coreclr-pinvoke-override-bcl
Open

[CoreCLR] Stop serving BCL p/invokes from the precompiled override#11537
simonrozsival wants to merge 3 commits into
mainfrom
dev/simonrozsival/coreclr-pinvoke-override-bcl

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

@simonrozsival simonrozsival commented May 29, 2026

Summary

The p/invoke override's precompiled path (used by the default separate-.so runtime layout) serves the .NET BCL native libraries — libSystem.Native, libSystem.Globalization.Native, libSystem.Security.Cryptography.Native.Android, libSystem.IO.Compression.Native — from a hand-maintained static table (dotnet_pinvokes, generated from hardcoded name lists). That table must be regenerated by hand whenever the runtime adds/renames a p/invoke and silently drifts from the runtime's real exports. A missing entry aborts the app at startup — this is #11530:

Abort message: 'Missing pinvoke SystemNative_IsAtomicNonInheritablePipeCreationSupported@libSystem.Native'
  at Interop+Sys.get_IsAtomicNonInheritablePipeCreationSupported()
  at System.Diagnostics.Process.Start()

In the default layout these libraries ship as standalone .so files in the APK, so CoreCLR can resolve their entry points itself. This PR returns nullptr for them and lets CoreCLR's own DllImport resolution handle them, removing the whole class of drift bugs. The now-dead dotnet_pinvokes symbol list is also deleted from the CoreCLR generator so it can never go stale again.

Commits

  1. Stop serving BCL p/invokes from the precompiled overridemonodroid_pinvoke_override returns nullptr for the four BCL libraries instead of looking them up in dotnet_pinvokes (and aborting on a miss).
  2. Remove dead dotnet_pinvokes generation — deletes the dotnet_pinvoke_names list and the dotnet_pinvokes/dotnet_pinvokes_count emission from the CoreCLR generator, and regenerates pinvoke-tables.include. The four BCL library-name hashes are kept (the override still uses them to identify those libraries and return nullptr); the internal-symbols table is unchanged.
  3. Add PingTest regression guard — a device test exercising Ping.Send("127.0.0.1") (the exact call that hit the missing SystemNative_IsAtomicNonInheritablePipeCreationSupported symbol in CoreCLR Ping causes SIGABRT on Android #11530), so this class of regression is caught regardless of how BCL p/invokes are resolved. Ported from Sync SystemNative pinvoke tables with dotnet/runtime #11531.

Why this is safe (and why it doesn't cost us startup time)

Measured on a physical Samsung SM-A165F (CoreCLR Release, arm64, dotnet new maui --sample-content, 25 cold starts each, back-to-back same session):

Config Median p90
override ON (static table) 1930 ms 1970 ms
override OFF (this PR) 1922 ms 1966 ms
delta +8 ms (toward removal) +4 ms

The +8 ms is well within run-to-run noise. Direct instrumentation explains why: the override is invoked ~70 times during the entire cold start, each native symbol exactly once (CoreCLR caches the resolved pointer per call site). The static table is a one-time path, not a hot one, so it provides no measurable startup benefit. The host shared library also shrinks slightly (1,368,256 → 1,325,936 bytes) as the now-unused dotnet_pinvokes table is dead-stripped.

Correctness check: the override-off build boots and renders identically. During startup the 43 BCL p/invokes that the table used to serve (libSystem.Native ×29, libSystem.Globalization.Native ×13) were resolved by CoreCLR's own resolver — no Missing pinvoke, no cannot locate symbol, no dlopen failed.

Scope — what is intentionally not changed

  • Internal-symbols branch (xa-internal-api, java-interop, liblog): unchanged. These are linked into the host itself and have no separate .so, so the override must keep serving them.
  • handle_other_pinvoke_request (e_sqlite3, app/third-party native libs): unchanged. It resolves arbitrary libraries through dotnet/android's own loader (MonodroidDl::monodroid_dlopen, which loads APK-embedded DSOs and normalizes [DllImport("log")]-style names) — behavior CoreCLR's default resolver does not replicate. It carries no static table, so it does not drift.
  • Unified-DSO layout (dynamic.cc, the generated find_pinvoke table): independent of dotnet_pinvokes and unaffected. There the BCL symbols are hidden in a single DSO and must still be resolved by the override.

Validation status / asking reviewers

  • Compiles clean (Release + Debug, no -Werror).
  • CoreCLR Release arm64 MAUI sample boots; BCL p/invokes resolved by CoreCLR; A/B startup measured.
  • pinvoke-tables.include regenerated via build-tools/scripts/generate-pinvoke-tables.sh; host relinks cleanly.
  • Added PingTest (device regression test). Not run locally in this change; CI device tests will execute it.
  • Not yet exercised at runtime: libSystem.Security.Cryptography.Native.Android (TLS/X509) and libSystem.IO.Compression.Native (Deflate/Zip). They are listed as CoreCLR known runtime native libs and load the same way as libSystem.Native, but a crypto/compression device smoke test would be good to add.
  • CoreCLR Debug startup and extractNativeLibs=false / fast-deploy layouts.
  • Other ABIs (e.g. armeabi-v7a).

Relationship to #11531

#11531 fixes #11530 by re-syncing the SystemNative static table (CLR and MonoVM) and adds PingTest. This PR takes a different, more durable approach for CoreCLR: it removes the static BCL table entirely so CoreCLR resolves those symbols itself, so the CLR table can't drift again. It does not touch MonoVM — MonoVM still needs #11531's sync (or its own follow-up). The PingTest here is ported from #11531 so #11530 keeps a regression guard either way.

Fixes #11530.

The p/invoke override's precompiled path (used by the default separate-`.so`
runtime layout) served the .NET BCL native libraries — `libSystem.Native`,
`libSystem.Globalization.Native`, `libSystem.Security.Cryptography.Native.Android`
and `libSystem.IO.Compression.Native` — from a hand-maintained static table
(`dotnet_pinvokes`). That table has to be regenerated by hand whenever the
runtime adds or renames a p/invoke, and it silently drifts from the runtime's
real exports. A missing entry aborts the application at startup, e.g. #11530:

    Abort message: 'Missing pinvoke
      SystemNative_IsAtomicNonInheritablePipeCreationSupported@libSystem.Native'

In the default layout these libraries are shipped as standalone `.so` files in
the APK, so CoreCLR can resolve their entry points itself. We now return
`nullptr` for them and let CoreCLR's own DllImport resolution handle them,
removing the whole class of drift bugs.

Measured on a physical Samsung SM-A165F (CoreCLR Release, arm64, `dotnet new
maui --sample-content`, 25 cold starts each, back-to-back):

    override ON  (static table): median 1930 ms / p90 1970 ms
    override OFF (this change):  median 1922 ms / p90 1966 ms

The difference (+8 ms, in favour of removing it) is within run-to-run noise:
the override is invoked ~70 times during the entire cold start, each symbol
exactly once, so the static table is a one-time path with no measurable startup
benefit. The host shared library also shrinks slightly (the unused
`dotnet_pinvokes` table is dead-stripped).

The change is scoped to the precompiled path:

* The internal-symbols branch (`xa-internal-api`, `java-interop`, `liblog`) is
  unchanged — those symbols are linked into the host itself and have no separate
  `.so` to resolve.
* `handle_other_pinvoke_request` is unchanged — it resolves arbitrary app and
  third-party native libraries through dotnet/android's own loader
  (`MonodroidDl::monodroid_dlopen`, which loads APK-embedded DSOs), carries no
  static table, and is therefore not subject to drift.
* The unified-DSO layout uses `dynamic.cc` (the generated `find_pinvoke` table),
  which is independent of `dotnet_pinvokes` and unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 29, 2026 11:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the CoreCLR precompiled p/invoke override so it no longer serves BCL native-library p/invokes (libSystem.Native, libSystem.Globalization.Native, libSystem.Security.Cryptography.Native.Android, libSystem.IO.Compression.Native) from the hand-maintained dotnet_pinvokes static table, and instead returns nullptr to let CoreCLR’s default DllImport resolution handle them. This aims to eliminate “table drift” failures like #11530 where missing entries can abort the app at startup.

Changes:

  • Remove the BCL-library fast path that looked up p/invokes in the generated dotnet_pinvokes table and loaded symbols via cached DSO handles.
  • For the four BCL libraries, return nullptr so CoreCLR resolves their symbols through its own resolver in the separate-.so layout.
  • Keep the existing “other libraries” path (handle_other_pinvoke_request) for app/third-party DSOs that require MonodroidDl behaviors (APK-embedded DSOs, name normalization, runtime lib directories).

The previous commit stopped serving the .NET BCL native libraries from
the precompiled override's static `dotnet_pinvokes` table, returning
`nullptr` so CoreCLR resolves them itself. That made the generated table
dead code (it was already dead-stripped from the host binary).

Remove the CoreCLR generator's `dotnet_pinvoke_names` list and the
`dotnet_pinvokes`/`dotnet_pinvokes_count` emission, and regenerate
`src/native/clr/pinvoke-override/pinvoke-tables.include`. This deletes
the hand-maintained symbol list that drifted from the runtime's real
exports (the root cause of #11530),
so it can no longer go stale.

The four BCL library-name hashes are intentionally kept: the precompiled
override still uses them to identify those libraries and return `nullptr`.
The internal-symbols table and all library-name hashes are unchanged.

This only touches the CoreCLR generator; the MonoVM generator and its
generated table are untouched and regenerate byte-for-byte identically.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
Copy link
Copy Markdown
Member Author

/azp run Xamarin.Android-PR

@azure-pipelines
Copy link
Copy Markdown

No pipelines are associated with this pull request.

@simonrozsival
Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

`Ping.Send()` calls `SystemNative_IsAtomicNonInheritablePipeCreationSupported`
in `libSystem.Native`, which was missing from the precompiled override's
static BCL p/invoke table and aborted the app at startup
(#11530).

Add a device test exercising `Ping.Send("127.0.0.1")` so this class of
regression is caught regardless of how the BCL p/invokes are resolved.

Authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

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.

CoreCLR Ping causes SIGABRT on Android

2 participants