Skip to content

fix: avoid NPE in trustedLookup on environments without IMPL_LOOKUP, for issue #4011#7633

Open
wenshao wants to merge 1 commit into
mainfrom
fix/issue-4011-android-methodhandles-npe
Open

fix: avoid NPE in trustedLookup on environments without IMPL_LOOKUP, for issue #4011#7633
wenshao wants to merge 1 commit into
mainfrom
fix/issue-4011-android-methodhandles-npe

Conversation

@wenshao

@wenshao wenshao commented Apr 25, 2026

Copy link
Copy Markdown
Member

Summary

  • On Android the static initializer leaves JDKUtils.IMPL_LOOKUP null (the Unsafe-based access to MethodHandles.Lookup.IMPL_LOOKUP is intentionally gated to !ANDROID). trustedLookup(Class) then NPE'd at IMPL_LOOKUP.in(...), producing the user-visible "Attempt to invoke virtual method MethodHandles$Lookup.in(...) on a null object reference" when parsing Kotlin data classes via JSON.parseObject on Android 16 (API 36).
  • JDKUtils.trustedLookup: short-circuit and return null when IMPL_LOOKUP is unavailable. Most existing callers already wrap usage in try/catch and fall back to reflection-based access, so a null Lookup is the cleanest signal.
  • ObjectReaderCreator.lambdaSetter / createBuildFunctionLambda: handle the null lookup explicitly and return null so callers fall back to reflection instead of throwing JSONException(\"create fieldReader error\").

Fixes #4011.

Test plan

  • Existing JDKUtilsTest (uses trustedLookup with non-null IMPL_LOOKUP) still passes.
  • ObjectReaderCreator*Test*, LambdaSetterTest* — all 38 tests pass.
  • Verified on Android (would need an Android device run; reproducer in issue body).

…for issue #4011

On Android, the static initializer leaves IMPL_LOOKUP null because the
Unsafe-based access to MethodHandles.Lookup.IMPL_LOOKUP is gated to
!ANDROID. trustedLookup(Class) then NPE'd at IMPL_LOOKUP.in(...),
producing the user-visible "Attempt to invoke virtual method
MethodHandles$Lookup.in(...) on a null object reference" when parsing
Kotlin data classes via JSON.parseObject on Android 16 (API 36).

- JDKUtils.trustedLookup: short-circuit and return null when IMPL_LOOKUP
  is unavailable. Most existing callers already wrap usage in try/catch
  and fall back to reflection-based access, so a null Lookup is the
  cleanest signal.
- ObjectReaderCreator.lambdaSetter / createBuildFunctionLambda: handle
  the null Lookup explicitly and return null so callers fall back to
  reflection instead of throwing JSONException("create fieldReader error").
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


wenshao seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

⚠️ Downgraded from Request Changes to Comment: self-PR; CI still running.

[Suggestion] PR adds 3 new null-return early-exit paths (trustedLookup, createBuildFunctionLambda, lambdaSetter) but no test verifies behavior when trustedLookup returns null. A test that mocks IMPL_LOOKUP = null would have caught the createBuildFunction regression. Consider adding:

  1. Test trustedLookup returns null when IMPL_LOOKUP is null
  2. Test createBuildFunction still produces a working reflection-based Function when createBuildFunctionLambda returns null

— qwen3.7-max via Qwen Code /review

*/
<T, R> Function<T, R> createBuildFunctionLambda(Method builderMethod) {
MethodHandles.Lookup lookup = JDKUtils.trustedLookup(builderMethod.getDeclaringClass());
if (lookup == null) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Critical] createBuildFunctionLambda returns null on Android, but the caller createBuildFunction (line ~2161) does return createBuildFunctionLambda(builderMethod) inside a try block. A null return completes the try block normally — no exception is thrown — so the catch block is skipped and the reflection-based fallback (after catch) is never reached.

Before this PR, the NPE from trustedLookup was caught by catch (Throwable), which accidentally allowed the reflection fallback to run. After this PR, the reflection fallback is dead code on Android, causing builder-pattern deserialization to silently return the intermediate builder object instead of the final built object.

Suggested change
if (lookup == null) {
if (lookup == null) {
return null; // caller createBuildFunction must check for null and fall back to reflection
}

Fix createBuildFunction to handle null return:

try {
    Function<T, R> result = createBuildFunctionLambda(builderMethod);
    if (result != null) {
        return result;
    }
    // null means trustedLookup unavailable — fall through to reflection below
} catch (Throwable e) {
    jitErrorCount.incrementAndGet();
    jitErrorLast = e;
}

— qwen3.7-max via Qwen Code /review

// IMPL_LOOKUP is null on Android (see static init guard) and may also be
// null on environments where neither the Unsafe-based access nor
// MethodHandles.lookup() were available. All callers wrap usage in a
// try/catch and fall back to plain reflection, so signal that here by

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Suggestion] The comment claims "All callers wrap usage in a try/catch and fall back to plain reflection" but this is factually inaccurate:

  • ObjectWriterCreator.lambdaGetter catches Throwable and re-throws as JSONException (no graceful fallback)
  • Several PropertyAccessorFactoryLambda methods (getObject, getBoolean, setInt, etc.) catch Throwable and re-throw as RuntimeException

Suggested revision:

        // Returns null when IMPL_LOOKUP is unavailable (e.g. Android).
        // Most callers handle this gracefully via try/catch with reflection
        // fallbacks; callers that re-throw rely on their upstream callers
        // to catch and fall back.

— qwen3.7-max via Qwen Code /review

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.

[BUG] NullPointerException: java.lang.invoke.MethodHandles$Lookup java.lang.invoke.MethodHandles$Lookup.in(java.lang.Class)

2 participants