Skip to content

[Android] Handle X509ChainContext creation failures#128651

Merged
simonrozsival merged 7 commits into
mainfrom
dev/simonrozsival/android-x509-null-context-investigation
Jun 3, 2026
Merged

[Android] Handle X509ChainContext creation failures#128651
simonrozsival merged 7 commits into
mainfrom
dev/simonrozsival/android-x509-null-context-investigation

Conversation

@simonrozsival

@simonrozsival simonrozsival commented May 27, 2026

Copy link
Copy Markdown
Member

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_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)
  #01  pc 0x0000000000021fe8  [removed]-KwPZdoEumri00C7kBm3pQw==/lib/arm64/libSystem.Security.Cryptography.Native.Android.so
  #02  pc 0x00000000000220b0  [removed]-KwPZdoEumri00C7kBm3pQw==/lib/arm64/libSystem.Security.Cryptography.Native.Android.so (AndroidCryptoNative_X509ChainBuild+88)
  #03  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.

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>
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @bartonjs, @vcsjones, @dotnet/area-system-security
See info in area-owners.md if you want to be subscribed.

@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 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 X509ChainContext creation and pre-create the Java errorList.
  • Validate the returned Android chain context immediately after creation on the managed side and throw a CryptographicException when 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.

Comment thread src/libraries/System.Security.Cryptography/src/Resources/Strings.resx Outdated
Comment thread src/libraries/System.Security.Cryptography/src/Resources/Strings.resx Outdated
simonrozsival and others added 2 commits May 28, 2026 09:30
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copilot AI review requested due to automatic review settings May 28, 2026 10:08
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/android-x509-null-context-investigation branch from ebdb42c to fc5594d Compare May 28, 2026 10:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

simonrozsival and others added 2 commits May 30, 2026 21:57
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Note

This review was generated by GitHub Copilot.

🤖 Copilot Code Review — PR #128651

Holistic Assessment

Motivation: Justified. The PR fixes a real production crash (native abort) on Android caused by passing a null X509ChainContext* to AndroidCryptoNative_X509ChainBuild. The reported tombstone clearly shows the failure path.

Approach: Sound. The fix adds consistent JNI exception checking in the native CreateContext function and adds a managed-side guard that throws a CryptographicException instead of allowing a null/invalid handle to propagate. This follows the established pattern used in other Android crypto interop code (e.g., Interop.EvpPkey.X25519.cs).

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 consistent

The added ON_EXCEPTION_PRINT_AND_GOTO(cleanup) calls after every JNI method invocation in AndroidCryptoNative_X509ChainCreateContext are appropriate. Previously, several JNI calls (e.g., NewObject for X509CertSelector, ArrayList, CollectionCertStoreParameters, and calls to setCertificate, add, addCertStore) lacked exception checks, meaning a Java exception could go undetected, leading to subsequent calls operating on invalid state and eventually returning a partially-constructed (or null) context.

The errorList local is now created as a local ref before the allocation, ensuring that if NewObject fails, we goto cleanup without allocating ret — this is the right pattern.

✅ Managed interop wrapper — Follows established conventions

Making the raw LibraryImport private and exposing an internal wrapper that validates the returned SafeHandle follows the same pattern as other crypto interop wrappers in this codebase (confirmed by reviewer @vcsjones's reference to Interop.EvpPkey.X25519.cs). The chainContext is null || chainContext.IsInvalid check with dispose-before-throw is correct.

✅ Resource string placement — Alphabetically sorted

The Cryptography_AndroidX509ChainContextInitializationFailed string is placed after Cryptography_AlgorithmTypesMustBeVisible and before Cryptography_ArgDSARequiresDSAKey, which respects the alphabetical ordering convention.

💡 AddGRef failure not checked after xcalloc

At lines 98-100 of pal_x509chain.c, if AddGRef returns NULL (e.g., extreme OOM where NewGlobalRef fails), ret would be returned with NULL fields. In practice this is extremely unlikely since loc[params] and loc[errorList] are guaranteed non-NULL at that point and global ref exhaustion is a terminal condition. The previous review thread on this was resolved, so this is noted only as a low-priority observation — not a blocker.

Generated by Code Review for issue #128651 · ● 2.8M ·

@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run runtime-android

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Copilot AI review requested due to automatic review settings June 2, 2026 13:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 4/4 changed files
  • Comments generated: 2

@simonrozsival

Copy link
Copy Markdown
Member Author

/ba-g test failures are unrelated to this PR

@simonrozsival
simonrozsival merged commit 9db423a into main Jun 3, 2026
113 of 117 checks passed
@simonrozsival
simonrozsival deleted the dev/simonrozsival/android-x509-null-context-investigation branch June 3, 2026 09:04
@simonrozsival

Copy link
Copy Markdown
Member Author

/backport to release/10.0

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Started backporting to release/10.0 (link to workflow run)

@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-preview6 milestone Jun 17, 2026
steveisok added a commit that referenced this pull request Jun 24, 2026
…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>
simonrozsival added a commit that referenced this pull request Jul 9, 2026
> [!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>
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
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>
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
> [!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>
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 18, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants