[Android] Handle X509ChainContext creation failures#128651
Conversation
Ensure Android X509 chain context creation either returns a valid native context or cleans up partial state before failing. Check the returned SafeHandle in managed code so initialization failures surface as CryptographicException instead of reaching native Build with a null context. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Tagging subscribers to this area: @bartonjs, @vcsjones, @dotnet/area-system-security |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
This PR hardens the Android X.509 chain-building pipeline so that failures during native X509ChainContext initialization don’t later surface as a process-terminating native abort, and instead fail fast with a managed CryptographicException.
Changes:
- Add more consistent JNI exception handling during native
X509ChainContextcreation and pre-create the JavaerrorList. - Validate the returned Android chain context immediately after creation on the managed side and throw a
CryptographicExceptionwhen initialization fails. - Add a new localized resource string for the managed exception message.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/native/libs/System.Security.Cryptography.Native.Android/pal_x509chain.c | Adds additional JNI exception checks and changes how errorList is created/held during chain context initialization. |
| src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Android.cs | Throws early if the returned chain context handle is invalid, preventing later native calls with a bad context. |
| src/libraries/System.Security.Cryptography/src/Resources/Strings.resx | Adds the new error message resource used by the managed exception. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
ebdb42c to
fc5594d
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Note This review was generated by GitHub Copilot. 🤖 Copilot Code Review — PR #128651Holistic AssessmentMotivation: Justified. The PR fixes a real production crash (native abort) on Android caused by passing a null Approach: Sound. The fix adds consistent JNI exception checking in the native Summary: ✅ LGTM. The change is well-scoped, addresses the root cause at both the native and managed layers, and follows existing conventions. Previous review feedback has been addressed. Detailed Findings✅ Native JNI exception handling — Correct and consistentThe added The ✅ Managed interop wrapper — Follows established conventionsMaking the raw ✅ Resource string placement — Alphabetically sortedThe 💡
|
|
/azp run runtime-android |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
/ba-g test failures are unrelated to this PR |
|
/backport to release/10.0 |
|
Started backporting to |
…128984) Backport of #128651 to release/10.0 /cc @simonrozsival ## Customer Impact - [X] Customer reported - [ ] Found internally [Select one or both of the boxes. Describe how this issue impacts customers, citing the expected and actual behaviors and scope of the issue. If customer-reported, provide the issue number.] A customer reported this issue internally. Their Android app crashed unexpectedly without providing useful context about what caused the crash. ## Regression - [ ] Yes - [X] No [If yes, specify when the regression was introduced. Provide the PR or commit if known.] ## Testing [How was the fix verified? How was the issue missed previously? What tests were added?] This change has been verified manually on an Android device. This codepath is already covered by unit and functional tests. ## Risk [High/Medium/Low. Justify the indication by mentioning how risks were measured and addressed.] Low. This change is scoped to Android-specific code and improves error handling. **IMPORTANT**: If this backport is for a servicing release, please verify that: - For .NET 8 and .NET 9: The PR target branch is `release/X.0-staging`, not `release/X.0`. - For .NET 10+: The PR target branch is `release/X.0` (no `-staging` suffix). ## Package authoring no longer needed in .NET 9 **IMPORTANT**: Starting with .NET 9, you no longer need to edit a NuGet package's csproj to enable building and bump the version. Keep in mind that we still need package authoring in .NET 8 and older versions. --------- Co-authored-by: Simon Rozsival <simon@rozsival.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Kevin Jones <kevin@vcsjones.com> Co-authored-by: Steve Pfister <steveisok@users.noreply.github.com>
> [!NOTE] > This PR description was drafted by an AI assistant (GitHub Copilot CLI) based on the actual code changes in this branch. All code in this PR was authored interactively with Simon driving design decisions through multiple review rounds. ## Summary This PR adds missing JNI exception checks across several files in the Android cryptography PAL. The work is based on a static audit of all `Call*Method`/`NewObject`/`GetArrayLength`-style JNI calls and targets sites where a pending Java exception could silently leak back into managed code. Beyond adding the exception checks, the changed functions are tightened to follow three patterns already established elsewhere in the PAL: 1. **`ON_EXCEPTION_PRINT_AND_GOTO(label)` immediately after each fallible JNI call.** The macro both prints and clears the exception, so subsequent JNI calls along the `cleanup`/`error` path are safe. 2. **`INIT_LOCALS(loc, …)` + single `cleanup:`/`RELEASE_LOCALS(loc, env)` tail** for local-ref hygiene, replacing ad-hoc `jobject foo = NULL;` declarations and scattered `DeleteLocalRef` calls. 3. **No partial output state on failure** — functions either defer caller-visible writes until success or clean up/reset partially promoted global refs before returning failure, so the caller never observes a half-populated result. A few smaller correctness fixes surfaced during review ride along with the audit: the shared `GetEnumAsInt` helper no longer deletes the local ref passed to it (each caller now owns and releases its own ref), and `SSLStreamWrite` now null-checks the buffer returned by `EnsureRemaining` before use. Both are described under **Behavior changes worth flagging** below. ## Files changed (11) All under `src/native/libs/System.Security.Cryptography.Native.Android/`: | File | Functions | |---|---| | `pal_cipher.c` | `ReinitializeCipher` | | `pal_dsa.c` | `DsaSizeP`, `GetDsaParameters`, `GetQParameter`, `DsaKeyCreateByExplicitParameters` | | `pal_ecc_import_export.c` | `GetECKeyParameters`, `GetECCurveParameters`, `CreateKeyPairFromCurveParameters`, `ConvertBigIntegerToPositiveInt32`, `EcKeyCreateByExplicitParameters` | | `pal_ecdh.c` | `EcdhDeriveKey` | | `pal_eckey.c` | `EcKeyGetSize` | | `pal_hmac.c` | `HmacCreate` | | `pal_jni.c` | `GetEnumAsInt` | | `pal_ssl.c` | `SSLGetSupportedProtocols` | | `pal_sslstream.c` | `GetHandshakeStatus`, `WrapAndProcessResult`, `Close`, `DoUnwrap`, `FreeSSLStream`, `SSLStreamInitialize`, `AddCertChainToStore`, `SSLStreamWrite` | | `pal_x509.c` | `X509DecodeCollection`, `X509ExportPkcs7` | | `pal_x509store.c` | `ContainsEntryForAlias`, `ContainsMatchingCertificateForAlias`, `X509StoreAddCertificate`, `X509StoreAddCertificateWithPrivateKey`, `X509StoreContainsCertificate`, `X509StoreRemoveCertificate`, `EnumerateCertificates`, `SystemAliasFilter` | ## Behavior changes worth flagging - **`GetEnumAsInt`** (`pal_jni.c`) no longer calls `DeleteLocalRef` on its `enumObj` argument — it is now a pure getter. Each of its five call sites already holds the enum result in a named local that is released through the function's own `cleanup:` path (`RELEASE_LOCALS` / `ReleaseLRef`), so every ref is freed exactly once by its owner. This removes a latent double-`DeleteLocalRef` in `GetHandshakeStatus` and the same footgun for the other callers. - **`AndroidCryptoNative_GetDsaParameters` / `AndroidCryptoNative_GetECKeyParameters`** now zero out their `out` length parameters (`*pLength`, `*qLength`, `*cbQx`, etc.) at function entry. Previously these were set on success or left untouched on failure. No managed caller relies on the prior partial-write behavior. - **`ContainsEntryForAlias` / `ContainsMatchingCertificateForAlias`** (`pal_x509store.c`) — return type changed from `bool` to `int32_t` (SUCCESS/FAIL) with a separate `*contains`/`*matches` out-parameter, so callers can distinguish "lookup failed due to JNI exception" from "lookup succeeded; not contained". Callers are updated accordingly. - **`FreeSSLStream`** now NULL-checks `sslStream->managedContextCleanup` before invoking it. This guards the case where `SSLStreamCreate` succeeded but `SSLStreamInitialize` was never called. - **`SSLStreamInitialize`** now transfers ownership of `managedContextHandle` / `streamReader` / `streamWriter` / `managedContextCleanup` to the native `SSLStream` as soon as initialization begins. Combined with the `FreeSSLStream` NULL-check above, native cleanup skips the callback only when initialization was never called, while partial JNI setup failures still release the managed context handle. - **`GetHandshakeStatus`** now splits `SSLEngine.getHandshakeStatus()` from the enum-ordinal conversion, checks for JNI exceptions/NULL before calling `GetEnumAsInt`, stores the ordinal in a local and only promotes it to the return value once retrieval fully succeeds (otherwise `ret` stays `-1`), and releases the handshake-status local through the standard `INIT_LOCALS` + `cleanup:` + `RELEASE_LOCALS` pattern. - **`AndroidCryptoNative_SSLStreamWrite`** now captures the buffer returned by `EnsureRemaining` in a local and bails out through `cleanup` if it is `NULL`, instead of assigning it straight to `sslStream->appOutBuffer`. On a buffer-expansion failure this avoids both a NULL-object JNI call (`ByteBuffer.put`) and a leaked global ref; the write returns `SSLStreamStatus_Error` (surfaced as `InternalError` by the managed caller) instead of crashing. Mirrors the existing handling in `DoUnwrap`. ## Out of scope A few pre-existing bugs were surfaced during review but intentionally left out of scope to keep this PR focused on the exception-check work: - `pal_x509store.c:115,141,186` — `EntryFlags_HasCertificate & EntryFlags_MatchesCertificate` should be `|` (dead code since 2021). - `pal_ecc_import_export.c:574` — `AddGRef` should be `ToGRef` (intermediate lref leak). - `pal_ecc_import_export.c:577-581` — `CheckJNIExceptions` only runs when `keyPair` is NULL. - `pal_x509.c` `X509DecodeCollection` — per-iteration gref leak on failure. These will be addressed as separate targeted fixes. ## Related Other open PRs in the same area (Android crypto PAL / JNI error handling): - #128747 — [Android] Clear pending JNI exceptions in `pal_rsa.c` to avoid CheckJNI aborts. Same JNI-exception-hygiene theme, applied to `pal_rsa.c` (which this PR does not touch). - #128651 — [Android] Handle `X509ChainContext` creation failures. Related JNI error-handling work in `pal_x509chain.c`. - #124173 — [Android] Respect platform trust manager in `SslStream`. Touches `pal_sslstream.c` and includes an equivalent NULL-check fix to `FreeSSLStream`; the two PRs will need a rebase against whichever lands second. ## Testing | Suite | Result | |---|---| | Android native libraries build (`./src/native/libs/build-native.sh arm64 Debug -os android ninja`) | Passed | | Android native libraries build (`./build.sh libs.native -os android -arch arm64 -c Release`) | Passed | | `System.Security.Cryptography.Tests` (Android arm64 Debug device) | 11715 run / 10738 passed / 0 failed / 977 skipped | | `System.Net.Security.Tests` (Android arm64 Debug device) | 4890 run / 4854 passed / 4 failed / 32 skipped (matches pre-PR baseline — unrelated `DelayedCertificate`/ALPN tests) | CI will provide cross-architecture validation (arm, x86, x64) and additional API levels not covered locally. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
An Android production app reported a native abort while building an X.509 chain on arm64. The available tombstone snippet showed the process aborting in `AndroidCryptoNative_X509ChainBuild` from `pal_x509chain.c`, with the native guard reporting that parameter `ctx` was not a valid pointer. The report did not include a repro or full tombstone, but the observed failure mode means managed code reached the native build entry point with a null `X509ChainContext*`. ``` Thread /__w/1/s/src/native/libs/System.Security.Cryptography.Native.Android/pal_x509chain.c:113 (AndroidCryptoNative_X509ChainBuild): Parameter 'ctx' must be a valid pointer *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** pid: 0, tid: 31609 >>> com.app.name <<< backtrace: #00 pc 0x000000000002232c /system/lib64/libc.so (abort+116) #1 pc 0x0000000000021fe8 [removed]-KwPZdoEumri00C7kBm3pQw==/lib/arm64/libSystem.Security.Cryptography.Native.Android.so #2 pc 0x00000000000220b0 [removed]-KwPZdoEumri00C7kBm3pQw==/lib/arm64/libSystem.Security.Cryptography.Native.Android.so (AndroidCryptoNative_X509ChainBuild+88) #3 pc 0x000000000000cfcc ``` `X509ChainContext` is created by `AndroidCryptoNative_X509ChainCreateContext`. That initialization can fail if Android certificate store setup or PKIX parameter construction throws, or if required JNI global references cannot be created. Previously, the managed Android chain path stored the returned `SafeHandle` without checking whether context creation failed, so a later build could pass a null native context to `AndroidCryptoNative_X509ChainBuild` and terminate the app process. This change makes context creation fail gracefully: - The native create path checks Java exceptions around object creation and method calls more consistently. - Partial native contexts are destroyed if global-reference creation fails. - The Android interop wrapper checks the returned chain context immediately, including a null safe-handle return, and throws `CryptographicException` if initialization failed. No regression test is included because the reliable failure modes depend on Android platform/provider state or artificial fault injection, and a test hook would be fragile and not representative. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Kevin Jones <kevin@vcsjones.com>
> [!NOTE] > This PR description was drafted by an AI assistant (GitHub Copilot CLI) based on the actual code changes in this branch. All code in this PR was authored interactively with Simon driving design decisions through multiple review rounds. ## Summary This PR adds missing JNI exception checks across several files in the Android cryptography PAL. The work is based on a static audit of all `Call*Method`/`NewObject`/`GetArrayLength`-style JNI calls and targets sites where a pending Java exception could silently leak back into managed code. Beyond adding the exception checks, the changed functions are tightened to follow three patterns already established elsewhere in the PAL: 1. **`ON_EXCEPTION_PRINT_AND_GOTO(label)` immediately after each fallible JNI call.** The macro both prints and clears the exception, so subsequent JNI calls along the `cleanup`/`error` path are safe. 2. **`INIT_LOCALS(loc, …)` + single `cleanup:`/`RELEASE_LOCALS(loc, env)` tail** for local-ref hygiene, replacing ad-hoc `jobject foo = NULL;` declarations and scattered `DeleteLocalRef` calls. 3. **No partial output state on failure** — functions either defer caller-visible writes until success or clean up/reset partially promoted global refs before returning failure, so the caller never observes a half-populated result. A few smaller correctness fixes surfaced during review ride along with the audit: the shared `GetEnumAsInt` helper no longer deletes the local ref passed to it (each caller now owns and releases its own ref), and `SSLStreamWrite` now null-checks the buffer returned by `EnsureRemaining` before use. Both are described under **Behavior changes worth flagging** below. ## Files changed (11) All under `src/native/libs/System.Security.Cryptography.Native.Android/`: | File | Functions | |---|---| | `pal_cipher.c` | `ReinitializeCipher` | | `pal_dsa.c` | `DsaSizeP`, `GetDsaParameters`, `GetQParameter`, `DsaKeyCreateByExplicitParameters` | | `pal_ecc_import_export.c` | `GetECKeyParameters`, `GetECCurveParameters`, `CreateKeyPairFromCurveParameters`, `ConvertBigIntegerToPositiveInt32`, `EcKeyCreateByExplicitParameters` | | `pal_ecdh.c` | `EcdhDeriveKey` | | `pal_eckey.c` | `EcKeyGetSize` | | `pal_hmac.c` | `HmacCreate` | | `pal_jni.c` | `GetEnumAsInt` | | `pal_ssl.c` | `SSLGetSupportedProtocols` | | `pal_sslstream.c` | `GetHandshakeStatus`, `WrapAndProcessResult`, `Close`, `DoUnwrap`, `FreeSSLStream`, `SSLStreamInitialize`, `AddCertChainToStore`, `SSLStreamWrite` | | `pal_x509.c` | `X509DecodeCollection`, `X509ExportPkcs7` | | `pal_x509store.c` | `ContainsEntryForAlias`, `ContainsMatchingCertificateForAlias`, `X509StoreAddCertificate`, `X509StoreAddCertificateWithPrivateKey`, `X509StoreContainsCertificate`, `X509StoreRemoveCertificate`, `EnumerateCertificates`, `SystemAliasFilter` | ## Behavior changes worth flagging - **`GetEnumAsInt`** (`pal_jni.c`) no longer calls `DeleteLocalRef` on its `enumObj` argument — it is now a pure getter. Each of its five call sites already holds the enum result in a named local that is released through the function's own `cleanup:` path (`RELEASE_LOCALS` / `ReleaseLRef`), so every ref is freed exactly once by its owner. This removes a latent double-`DeleteLocalRef` in `GetHandshakeStatus` and the same footgun for the other callers. - **`AndroidCryptoNative_GetDsaParameters` / `AndroidCryptoNative_GetECKeyParameters`** now zero out their `out` length parameters (`*pLength`, `*qLength`, `*cbQx`, etc.) at function entry. Previously these were set on success or left untouched on failure. No managed caller relies on the prior partial-write behavior. - **`ContainsEntryForAlias` / `ContainsMatchingCertificateForAlias`** (`pal_x509store.c`) — return type changed from `bool` to `int32_t` (SUCCESS/FAIL) with a separate `*contains`/`*matches` out-parameter, so callers can distinguish "lookup failed due to JNI exception" from "lookup succeeded; not contained". Callers are updated accordingly. - **`FreeSSLStream`** now NULL-checks `sslStream->managedContextCleanup` before invoking it. This guards the case where `SSLStreamCreate` succeeded but `SSLStreamInitialize` was never called. - **`SSLStreamInitialize`** now transfers ownership of `managedContextHandle` / `streamReader` / `streamWriter` / `managedContextCleanup` to the native `SSLStream` as soon as initialization begins. Combined with the `FreeSSLStream` NULL-check above, native cleanup skips the callback only when initialization was never called, while partial JNI setup failures still release the managed context handle. - **`GetHandshakeStatus`** now splits `SSLEngine.getHandshakeStatus()` from the enum-ordinal conversion, checks for JNI exceptions/NULL before calling `GetEnumAsInt`, stores the ordinal in a local and only promotes it to the return value once retrieval fully succeeds (otherwise `ret` stays `-1`), and releases the handshake-status local through the standard `INIT_LOCALS` + `cleanup:` + `RELEASE_LOCALS` pattern. - **`AndroidCryptoNative_SSLStreamWrite`** now captures the buffer returned by `EnsureRemaining` in a local and bails out through `cleanup` if it is `NULL`, instead of assigning it straight to `sslStream->appOutBuffer`. On a buffer-expansion failure this avoids both a NULL-object JNI call (`ByteBuffer.put`) and a leaked global ref; the write returns `SSLStreamStatus_Error` (surfaced as `InternalError` by the managed caller) instead of crashing. Mirrors the existing handling in `DoUnwrap`. ## Out of scope A few pre-existing bugs were surfaced during review but intentionally left out of scope to keep this PR focused on the exception-check work: - `pal_x509store.c:115,141,186` — `EntryFlags_HasCertificate & EntryFlags_MatchesCertificate` should be `|` (dead code since 2021). - `pal_ecc_import_export.c:574` — `AddGRef` should be `ToGRef` (intermediate lref leak). - `pal_ecc_import_export.c:577-581` — `CheckJNIExceptions` only runs when `keyPair` is NULL. - `pal_x509.c` `X509DecodeCollection` — per-iteration gref leak on failure. These will be addressed as separate targeted fixes. ## Related Other open PRs in the same area (Android crypto PAL / JNI error handling): - #128747 — [Android] Clear pending JNI exceptions in `pal_rsa.c` to avoid CheckJNI aborts. Same JNI-exception-hygiene theme, applied to `pal_rsa.c` (which this PR does not touch). - #128651 — [Android] Handle `X509ChainContext` creation failures. Related JNI error-handling work in `pal_x509chain.c`. - #124173 — [Android] Respect platform trust manager in `SslStream`. Touches `pal_sslstream.c` and includes an equivalent NULL-check fix to `FreeSSLStream`; the two PRs will need a rebase against whichever lands second. ## Testing | Suite | Result | |---|---| | Android native libraries build (`./src/native/libs/build-native.sh arm64 Debug -os android ninja`) | Passed | | Android native libraries build (`./build.sh libs.native -os android -arch arm64 -c Release`) | Passed | | `System.Security.Cryptography.Tests` (Android arm64 Debug device) | 11715 run / 10738 passed / 0 failed / 977 skipped | | `System.Net.Security.Tests` (Android arm64 Debug device) | 4890 run / 4854 passed / 4 failed / 32 skipped (matches pre-PR baseline — unrelated `DelayedCertificate`/ALPN tests) | CI will provide cross-architecture validation (arm, x86, x64) and additional API levels not covered locally. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Note
This PR description was drafted with GitHub Copilot.
An Android production app reported a native abort while building an X.509 chain on arm64. The available tombstone snippet showed the process aborting in
AndroidCryptoNative_X509ChainBuildfrompal_x509chain.c, with the native guard reporting that parameterctxwas not a valid pointer. The report did not include a repro or full tombstone, but the observed failure mode means managed code reached the native build entry point with a nullX509ChainContext*.X509ChainContextis created byAndroidCryptoNative_X509ChainCreateContext. That initialization can fail if Android certificate store setup or PKIX parameter construction throws, or if required JNI global references cannot be created. Previously, the managed Android chain path stored the returnedSafeHandlewithout checking whether context creation failed, so a later build could pass a null native context toAndroidCryptoNative_X509ChainBuildand terminate the app process.This change makes context creation fail gracefully:
CryptographicExceptionif initialization failed.No regression test is included because the reliable failure modes depend on Android platform/provider state or artificial fault injection, and a test hook would be fragile and not representative.