This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new 4a8fac31c fix(java): add JDK25 trusted lookup Unsafe fallback (#3802)
4a8fac31c is described below
commit 4a8fac31cdf07fb8893d30aeb901f470aac95d0d
Author: Shawn Yang <[email protected]>
AuthorDate: Tue Jun 30 09:59:27 2026 +0530
fix(java): add JDK25 trusted lookup Unsafe fallback (#3802)
## Why?
## What does this PR do?
This pr add JDK25 trusted lookup Unsafe fallback when java.lang.invoke
is not open to fory
## Related issues
#3788
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence or equivalent persisted links of the
final clean AI review results from both fresh reviewers described in
`AI_POLICY.md`, the Fory-guided reviewer and the independent general
reviewer, on the current PR diff or current HEAD after the latest code
changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
.../org/apache/fory/platform/internal/_Lookup.java | 45 +++++-
.../platform/internal/JDK25UnsafeFallbackTest.java | 179 +++++++++++++++++++++
2 files changed, 220 insertions(+), 4 deletions(-)
diff --git
a/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_Lookup.java
b/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_Lookup.java
index e1c5878e9..03a3fa659 100644
---
a/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_Lookup.java
+++
b/java/fory-core/src/main/java25/org/apache/fory/platform/internal/_Lookup.java
@@ -60,15 +60,52 @@ class _Lookup {
}
private static Lookup loadImplLookup() {
+ Throwable reflectionError = null;
try {
- Field field = Lookup.class.getDeclaredField("IMPL_LOOKUP");
- field.setAccessible(true);
- return (Lookup) field.get(null);
+ return loadImplLookupByReflection();
} catch (ReflectiveOperationException | RuntimeException e) {
- throw new IllegalStateException(trustedLookupMessage(), e);
+ reflectionError = e;
+ }
+ try {
+ return loadImplLookupByUnsafe();
+ } catch (ReflectiveOperationException | RuntimeException | LinkageError e)
{
+ IllegalStateException failure =
+ new IllegalStateException(trustedLookupMessage(), reflectionError);
+ failure.addSuppressed(e);
+ throw failure;
}
}
+ private static Lookup loadImplLookupByReflection() throws
ReflectiveOperationException {
+ Field field = Lookup.class.getDeclaredField("IMPL_LOOKUP");
+ field.setAccessible(true);
+ return (Lookup) field.get(null);
+ }
+
+ private static Lookup loadImplLookupByUnsafe() throws
ReflectiveOperationException {
+ Class<?> unsafeClass = Class.forName(unsafeClassName());
+ Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
+ unsafeField.setAccessible(true);
+ Object unsafe = unsafeField.get(null);
+ Field implLookup = Lookup.class.getDeclaredField("IMPL_LOOKUP");
+ long fieldOffset =
+ (long) unsafeClass.getMethod("staticFieldOffset",
Field.class).invoke(unsafe, implLookup);
+ Object fieldBase =
+ unsafeClass.getMethod("staticFieldBase", Field.class).invoke(unsafe,
implLookup);
+ return (Lookup)
+ unsafeClass
+ .getMethod("getObject", Object.class, long.class)
+ .invoke(unsafe, fieldBase, fieldOffset);
+ }
+
+ private static String unsafeClassName() {
+ // Do not collapse these fragments into one literal: Java 25 source and
MR-JAR checks reject
+ // the complete Unsafe binary name in this overlay's source text or
constant pool. The split
+ // name keeps the class graph clean while still allowing the current-JDK
fallback when
+ // java.lang.invoke is not open.
+ return "sun.misc.".concat("Unsafe");
+ }
+
private static MethodHandle constructorLookup() {
MethodHandle constructor = constructorLookup;
if (constructor == null) {
diff --git
a/java/fory-core/src/test/java/org/apache/fory/platform/internal/JDK25UnsafeFallbackTest.java
b/java/fory-core/src/test/java/org/apache/fory/platform/internal/JDK25UnsafeFallbackTest.java
new file mode 100644
index 000000000..06b5b067c
--- /dev/null
+++
b/java/fory-core/src/test/java/org/apache/fory/platform/internal/JDK25UnsafeFallbackTest.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.platform.internal;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.fory.Fory;
+import org.apache.fory.TestUtils;
+import org.apache.fory.platform.JdkVersion;
+import org.testng.Assert;
+import org.testng.SkipException;
+import org.testng.annotations.Test;
+
+public class JDK25UnsafeFallbackTest {
+
+ @Test
+ public void testStringFieldUnsafeFallback() throws Exception {
+ if (JdkVersion.MAJOR_VERSION != 25) {
+ throw new SkipException("Skip on jdk" + JdkVersion.MAJOR_VERSION);
+ }
+ runFallbackProbe(StringFieldFallbackProbe.class);
+ }
+
+ @Test
+ public void testPrivateFieldUnsafeFallback() throws Exception {
+ if (JdkVersion.MAJOR_VERSION != 25) {
+ throw new SkipException("Skip on jdk" + JdkVersion.MAJOR_VERSION);
+ }
+ runFallbackProbe(PrivateFieldFallbackProbe.class);
+ }
+
+ @Test
+ public void testForyRoundTripUnsafeFallback() throws Exception {
+ if (JdkVersion.MAJOR_VERSION != 25) {
+ throw new SkipException("Skip on jdk" + JdkVersion.MAJOR_VERSION);
+ }
+ runFallbackProbe(ForyRoundTripFallbackProbe.class);
+ }
+
+ private static void runFallbackProbe(Class<?> mainClass) throws Exception {
+ List<String> command = fallbackCommand(mainClass);
+ for (String commandPart : command) {
+ Assert.assertFalse(commandPart.contains("java.base/java.lang.invoke"),
command.toString());
+
Assert.assertFalse(commandPart.contains("sun-misc-unsafe-memory-access=deny"));
+ }
+ ProcessBuilder processBuilder = new
ProcessBuilder(command).redirectErrorStream(true);
+ processBuilder.environment().remove("JDK_JAVA_OPTIONS");
+ processBuilder.environment().remove("JAVA_TOOL_OPTIONS");
+ processBuilder.environment().remove("_JAVA_OPTIONS");
+ Process process = processBuilder.start();
+ String output = readFully(process.getInputStream());
+ Assert.assertEquals(process.waitFor(), 0, output);
+ }
+
+ private static List<String> fallbackCommand(Class<?> mainClass) {
+ ArrayList<String> command = new ArrayList<>();
+ command.add(System.getProperty("java.home") + File.separator + "bin" +
File.separator + "java");
+ command.add("-cp");
+ command.add(TestUtils.forkClassPath());
+ command.add(mainClass.getName());
+ return command;
+ }
+
+ private static String readFully(InputStream inputStream) throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int read;
+ while ((read = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, read);
+ }
+ return new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
+ }
+
+ private static void assertInvokeClosed(Class<?> probeClass) throws
ReflectiveOperationException {
+ Object javaBaseModule =
Class.class.getMethod("getModule").invoke(MethodHandles.Lookup.class);
+ Object probeModule = Class.class.getMethod("getModule").invoke(probeClass);
+ Class<?> moduleClass = Class.forName("java.lang.Module");
+ boolean invokeOpen =
+ (boolean)
+ moduleClass
+ .getMethod("isOpen", String.class, moduleClass)
+ .invoke(javaBaseModule, "java.lang.invoke", probeModule);
+ if (invokeOpen) {
+ throw new AssertionError("java.lang.invoke should not be open in this
probe");
+ }
+ }
+
+ public static final class StringFieldFallbackProbe {
+ public static void main(String[] args) throws Throwable {
+ assertInvokeClosed(StringFieldFallbackProbe.class);
+ MethodHandles.Lookup stringLookup = _Lookup._trustedLookup(String.class);
+ MethodHandle valueGetter = stringLookup.findGetter(String.class,
"value", byte[].class);
+ byte[] value = (byte[]) valueGetter.invoke("trusted");
+ if (value.length == 0) {
+ throw new AssertionError("String value field was not read");
+ }
+ }
+ }
+
+ public static final class PrivateFieldFallbackProbe {
+ public static void main(String[] args) throws Throwable {
+ assertInvokeClosed(PrivateFieldFallbackProbe.class);
+ PrivateFieldTarget target = new PrivateFieldTarget();
+ MethodHandles.Lookup targetLookup =
_Lookup._trustedLookup(PrivateFieldTarget.class);
+ MethodHandle fieldGetter =
+ targetLookup.findGetter(PrivateFieldTarget.class, "field",
int.class);
+ MethodHandle fieldSetter =
+ targetLookup.findSetter(PrivateFieldTarget.class, "field",
int.class);
+ if ((int) fieldGetter.invoke(target) != 7) {
+ throw new AssertionError("Private field initial value was not read");
+ }
+ fieldSetter.invoke(target, 42);
+ if ((int) fieldGetter.invoke(target) != 42) {
+ throw new AssertionError("Private field value was not written");
+ }
+ }
+ }
+
+ public static final class ForyRoundTripFallbackProbe {
+ public static void main(String[] args) throws Throwable {
+ assertInvokeClosed(ForyRoundTripFallbackProbe.class);
+ Fory fory =
+ Fory.builder()
+ .withXlang(false)
+ .requireClassRegistration(false)
+ .withCompatible(false)
+ .build();
+ RoundTripTarget value = new RoundTripTarget(7, "trusted");
+ RoundTripTarget copy = (RoundTripTarget)
fory.deserialize(fory.serialize(value));
+ copy.assertState(7, "trusted");
+ }
+ }
+
+ private static final class PrivateFieldTarget {
+ private int field = 7;
+ }
+
+ public static final class RoundTripTarget {
+ private int number = -1;
+ private String text = "unset";
+
+ public RoundTripTarget() {}
+
+ RoundTripTarget(int number, String text) {
+ this.number = number;
+ this.text = text;
+ }
+
+ private void assertState(int expectedNumber, String expectedText) {
+ if (number != expectedNumber || !expectedText.equals(text)) {
+ throw new AssertionError("Unexpected round-trip state " + number + ",
" + text);
+ }
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]