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]

Reply via email to