fix: avoid NPE in trustedLookup on environments without IMPL_LOOKUP, for issue #4011#7633
fix: avoid NPE in trustedLookup on environments without IMPL_LOOKUP, for issue #4011#7633wenshao wants to merge 1 commit into
Conversation
…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").
|
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
left a comment
There was a problem hiding this comment.
[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:
- Test
trustedLookupreturns null whenIMPL_LOOKUPis null - Test
createBuildFunctionstill produces a working reflection-based Function whencreateBuildFunctionLambdareturns 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) { |
There was a problem hiding this comment.
[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.
| 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 |
There was a problem hiding this comment.
[Suggestion] The comment claims "All callers wrap usage in a try/catch and fall back to plain reflection" but this is factually inaccurate:
ObjectWriterCreator.lambdaGettercatches Throwable and re-throws asJSONException(no graceful fallback)- Several
PropertyAccessorFactoryLambdamethods (getObject,getBoolean,setInt, etc.) catch Throwable and re-throw asRuntimeException
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
Summary
JDKUtils.IMPL_LOOKUPnull (the Unsafe-based access toMethodHandles.Lookup.IMPL_LOOKUPis intentionally gated to!ANDROID).trustedLookup(Class)then NPE'd atIMPL_LOOKUP.in(...), producing the user-visible "Attempt to invoke virtual methodMethodHandles$Lookup.in(...)on a null object reference" when parsing Kotlin data classes viaJSON.parseObjecton Android 16 (API 36).JDKUtils.trustedLookup: short-circuit and returnnullwhenIMPL_LOOKUPis unavailable. Most existing callers already wrap usage intry/catchand fall back to reflection-based access, so a nullLookupis the cleanest signal.ObjectReaderCreator.lambdaSetter/createBuildFunctionLambda: handle the null lookup explicitly and return null so callers fall back to reflection instead of throwingJSONException(\"create fieldReader error\").Fixes #4011.
Test plan
JDKUtilsTest(usestrustedLookupwith non-nullIMPL_LOOKUP) still passes.ObjectReaderCreator*Test*,LambdaSetterTest*— all 38 tests pass.