fix: use Unsafe.allocateInstance for non-static inner class to avoid JDK 25 NPE (#7654) - #7699
fix: use Unsafe.allocateInstance for non-static inner class to avoid JDK 25 NPE (#7654)#7699wenshao wants to merge 9 commits into
Conversation
wenshao
left a comment
There was a problem hiding this comment.
— qwen3.8-max-preview via Qwen Code /review
- ObjectReaderCreatorASM.newObject: only emit the UNSAFE.allocateInstance/checkcast sequence when the enclosing type is public. ldc/checkcast are access-checked against the generated reader, which lives in DynamicClassLoader, so a package-private enclosing type failed with IllegalAccessError (Issue1202) — the actual cause of the CI failures on this PR. - fastjson1-compatible Issue1082: assert a successful parse, matching the new behaviour. - ConstructorSupplier: cache the constructor parameter type instead of cloning getParameterTypes() on every get(). - ObjectReaderAdapter.createInstance: make the parameterCount == 1 assumption explicit and keep new Object[parameterCount] for constructors with 2+ parameters. - Issue7654: add a case whose inner class constructor dereferences the enclosing instance, so the fix is regression-gated on JDK < 25 as well. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- ObjectReaderCreatorASM.newObject: only emit the UNSAFE.allocateInstance/checkcast sequence when the enclosing type is public. ldc/checkcast are access-checked against the generated reader, which lives in DynamicClassLoader, so a package-private enclosing type failed with IllegalAccessError (Issue1202) — the actual cause of the CI failures on this PR. - fastjson1-compatible Issue1082: assert a successful parse, matching the new behaviour. - ConstructorSupplier: cache the constructor parameter type instead of cloning getParameterTypes() on every get(). - ObjectReaderAdapter.createInstance: make the parameterCount == 1 assumption explicit and keep new Object[parameterCount] for constructors with 2+ parameters. - Issue7654: add a case whose inner class constructor dereferences the enclosing instance, so the fix is regression-gated on JDK < 25 as well. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bce14f1 to
0d76b79
Compare
wenshao
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
— qwen3.8-max-preview via Qwen Code /review
…JDK 25 NPE (#7654) JDK 25 adds Objects.requireNonNull for the enclosing instance parameter in inner class constructors. The previous approach of passing null and patching this$0 afterwards now throws NPE before the patch can occur. Use Unsafe.allocateInstance to bypass the constructor entirely for non-static inner classes; the existing this$0 fixup via Unsafe.putObject still sets the enclosing instance correctly after creation.
…to preserve constructor semantics Unsafe.allocateInstance bypassed field initializers (Issue367) and constructor logic (Issue1082). Instead, create a dummy enclosing instance via Unsafe.allocateInstance(declaringClass) and pass it to the inner class constructor, preserving field defaults and validation. The real parent is still patched via Unsafe.putObject afterwards.
- ObjectReaderCreatorASM.newObject: only emit the UNSAFE.allocateInstance/checkcast sequence when the enclosing type is public. ldc/checkcast are access-checked against the generated reader, which lives in DynamicClassLoader, so a package-private enclosing type failed with IllegalAccessError (Issue1202) — the actual cause of the CI failures on this PR. - fastjson1-compatible Issue1082: assert a successful parse, matching the new behaviour. - ConstructorSupplier: cache the constructor parameter type instead of cloning getParameterTypes() on every get(). - ObjectReaderAdapter.createInstance: make the parameterCount == 1 assumption explicit and keep new Object[parameterCount] for constructors with 2+ parameters. - Issue7654: add a case whose inner class constructor dereferences the enclosing instance, so the fix is regression-gated on JDK < 25 as well.
With -Dfastjson2.creator=reflect a non-static inner class goes through beanInfo.creatorConstructor -> ObjectReaderNoneDefaultConstructor -> ConstructorFunction, which the previous commits did not cover: the synthetic enclosing-instance parameter never appears in the JSON, so it fell back to TypeUtils.getDefaultValue() and the constructor ran with a null this$0. ConstructorFunction now recognises that parameter and allocates a bare enclosing instance for it, matching ConstructorSupplier, ObjectReaderAdapter and the ASM path. Found by testInnerClassConstructorDereferencingOuter, which the ASM path passes and the reflect path did not — the Test Reflect CI jobs were failing on it.
…#7654) - ConstructorFunction.defaultArg catches only InstantiationException, so an OutOfMemoryError from allocateInstance is no longer swallowed into a null enclosing instance, which would resurface as the NPE this PR fixes - Issue7654 now iterates TestUtils.readerCreators(), so the ConstructorFunction reflection path is covered as well as the ASM path
- ObjectReaderCreatorASM.newObject: only emit the UNSAFE.allocateInstance/checkcast sequence when the enclosing type is public. ldc/checkcast are access-checked against the generated reader, which lives in DynamicClassLoader, so a package-private enclosing type failed with IllegalAccessError (Issue1202) — the actual cause of the CI failures on this PR. - fastjson1-compatible Issue1082: assert a successful parse, matching the new behaviour. - ConstructorSupplier: cache the constructor parameter type instead of cloning getParameterTypes() on every get(). - ObjectReaderAdapter.createInstance: make the parameterCount == 1 assumption explicit and keep new Object[parameterCount] for constructors with 2+ parameters. - Issue7654: add a case whose inner class constructor dereferences the enclosing instance, so the fix is regression-gated on JDK < 25 as well. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
22ff6a9 to
c7eeba7
Compare
wenshao
left a comment
There was a problem hiding this comment.
— qwen3.8-max-preview via Qwen Code /review (v0.21.3)
wenshao
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
— qwen3.8-max-preview via Qwen Code /review (v0.21.3)
Problem
On JDK 25, deserializing JSON into a class containing a non-static inner class throws
NullPointerException:JDK 25 adds
Objects.requireNonNull(this$0)in inner class constructors. The previous approach of passingnullfor the enclosing instance and patchingthis$0afterwards viaUnsafe.putObjectnow fails because the NPE fires during construction.Fix
Use
Unsafe.allocateInstanceto bypass the constructor entirely for non-static inner classes. The existingthis$0fixup mechanism (viaUnsafe.putObjectin ASM path / reflection inFieldReaderObject) continues to set the enclosing instance correctly after creation.Changes
Unsafe.allocateInstancein bothcreateInstancegeneration andgenCreateObjectUnsafe.allocateInstanceinstead ofconstructor.newInstance(new Object[1])Unsafe.allocateInstancewhen parameterCount > 0Closes #7654