From 04944a2603811d4c2e9193de6f5a904c481c2672 Mon Sep 17 00:00:00 2001 From: FxRayHughes Date: Sat, 22 Aug 2026 19:43:28 +0800 Subject: [PATCH] fix(incision): prevent native delegate linkage recursion --- module/incision/src/main/c/incision_jvmti.c | 17 +++++ .../izzel/incision/bridge/IncisionBridge.java | 76 ++++++++++++++----- .../bridge/IncisionBridgeNativeRoutingTest.kt | 47 ++++++++++++ 3 files changed, 120 insertions(+), 20 deletions(-) diff --git a/module/incision/src/main/c/incision_jvmti.c b/module/incision/src/main/c/incision_jvmti.c index fd5e591d2..a1eefd43e 100644 --- a/module/incision/src/main/c/incision_jvmti.c +++ b/module/incision/src/main/c/incision_jvmti.c @@ -64,6 +64,20 @@ static jint g_extract_len = 0; /* 抽取请求绑定目标 jclass,避免并发或嵌套钩子复制到无关类字节码。 */ static jclass g_extract_target = NULL; +/* + * Incision 协议接口不是业务目标。它的嵌套 BackendToken 可能正处于定义过程中; + * 若此时再次回调 Java delegate,隔离 ClassLoader 会把同一个接口递归定义两次。 + * 在 JNI 回调边界提前放行,比 Java 层收到回调后再判断更早、更安全。 + */ +static int is_backend_protocol_name(const char *name) { + static const char suffix[] = "/Backend$BackendToken"; + size_t name_len; + size_t suffix_len = sizeof(suffix) - 1; + if (name == NULL) return 0; + name_len = strlen(name); + return name_len >= suffix_len && strcmp(name + name_len - suffix_len, suffix) == 0; +} + /* djb2 字符串哈希 */ static unsigned int hash_str(const char *s) { unsigned int h = 5381; @@ -159,6 +173,9 @@ static void JNICALL classFileLoadHook( return; /* 不设置 new_class_data,类保持原样 */ } + /* BackendToken 的定义不能参与自身的 native 广播,否则会递归触发 duplicate definition。 */ + if (is_backend_protocol_name(name)) return; + if (g_callback_mid == NULL || name == NULL) return; jstring jname = (*jni)->NewStringUTF(jni, name); diff --git a/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java b/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java index d244735a8..d8f1f7253 100644 --- a/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java +++ b/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java @@ -65,8 +65,14 @@ private IncisionBridge() {} * Bridge 保留该 owner,并把 native 回调广播给所有插件后端,避免后加载插件再次 System.load。 */ private static volatile Class nativeOwner; + private static volatile Method nativeOwnerInvoke; private static final CopyOnWriteArrayList> nativeDelegates = new CopyOnWriteArrayList>(); private static final ConcurrentHashMap, Method> nativeTransformCache = new ConcurrentHashMap, Method>(); + /** + * JVMTI 回调可能在 delegate 链接辅助类时再次进入 ClassFileLoadHook;线程级闸门保证 + * 递归加载只返回原始字节,避免隔离加载器重复定义 Backend$BackendToken。 + */ + private static final ThreadLocal nativeTransformGuard = new ThreadLocal(); /** * Side-car body 的字段解析缓存。 @@ -237,7 +243,26 @@ public static int localLeaseCount() { /** 注册插件后端;返回 JVM 当前是否已有可用 native owner。 */ public static synchronized boolean registerNativeBackend(Class backendClass, boolean ownsNative) { if (backendClass == null) return nativeOwner != null; - if (ownsNative && nativeOwner == null) nativeOwner = backendClass; + try { + // getMethods 会解析 Backend 继承树上的所有公开签名。在 ClassFileLoadHook 激活期间, + // 这会递归触发 Backend$BackendToken 的定义并让隔离加载器报 duplicate definition。 + // 注册阶段只解析 Bridge 协议声明本身,并在进入 native hook 前完成缓存。 + // 同时提前初始化 delegate;否则首次 Method.invoke 仍可能在 ClassFileLoadHook 内 + // 解析 Backend 继承树,把 Backend$BackendToken 的延迟定义重新带入回调递归。 + Class.forName(backendClass.getName(), true, backendClass.getClassLoader()); + Method transform = backendClass.getDeclaredMethod( + "onSharedClassFileLoad", ClassLoader.class, String.class, byte[].class + ); + nativeTransformCache.put(backendClass, transform); + if (ownsNative && nativeOwner == null) { + Method invoke = backendClass.getDeclaredMethod("sharedNativeInvoke", String.class, Object[].class); + nativeOwner = backendClass; + nativeOwnerInvoke = invoke; + } + } catch (ReflectiveOperationException e) { + nativeTransformCache.remove(backendClass); + throw new IllegalArgumentException("Invalid Incision native backend protocol: " + backendClass.getName(), e); + } if (!nativeDelegates.contains(backendClass)) nativeDelegates.add(backendClass); return nativeOwner != null; } @@ -247,33 +272,43 @@ public static synchronized boolean registerNativeBackend(Class backendClass, * 因而两个插件对同一方法的织入会形成确定的先后链,而不是互相覆盖。 */ public static byte[] transformNative(ClassLoader loader, String name, byte[] bytes) { + // BackendToken 是 Incision 协议接口,不是业务目标。对它进行织入会在接口定义尚未完成 + // 时重新解析同一个接口,JVM 会以 duplicate interface definition 拒绝第二次定义。 + if (name != null && (name.endsWith("/Backend$BackendToken") || name.endsWith("$BackendToken"))) { + return null; + } + if (Boolean.TRUE.equals(nativeTransformGuard.get())) return null; + nativeTransformGuard.set(Boolean.TRUE); byte[] current = bytes; boolean changed = false; - for (Class backend : nativeDelegates) { - try { - Method method = nativeTransformCache.get(backend); - if (method == null) { - method = backend.getMethod("onSharedClassFileLoad", ClassLoader.class, String.class, byte[].class); - nativeTransformCache.put(backend, method); - } - byte[] output = (byte[]) method.invoke(null, loader, name, current); - if (output != null) { - current = output; - changed = true; + try { + for (Class backend : nativeDelegates) { + try { + Method method = nativeTransformCache.get(backend); + if (method == null) { + throw new IllegalStateException("native transformer delegate was not pre-resolved"); + } + byte[] output = (byte[]) method.invoke(null, loader, name, current); + if (output != null) { + current = output; + changed = true; + } + } catch (Throwable t) { + System.err.println("[Incision][Bridge] native transformer delegate failed: " + backend.getName() + " — " + t); } - } catch (Throwable t) { - System.err.println("[Incision][Bridge] native transformer delegate failed: " + backend.getName() + " — " + t); } + return changed ? current : null; + } finally { + nativeTransformGuard.remove(); } - return changed ? current : null; } /** 非 owner 插件通过这一入口复用唯一 native image。 */ public static Object invokeNative(String operation, Object[] args) { Class owner = nativeOwner; - if (owner == null) throw new IllegalStateException("Incision native owner unavailable"); + Method method = nativeOwnerInvoke; + if (owner == null || method == null) throw new IllegalStateException("Incision native owner unavailable"); try { - Method method = owner.getMethod("sharedNativeInvoke", String.class, Object[].class); return method.invoke(null, operation, args); } catch (Throwable t) { throw new IllegalStateException("Incision shared native invocation failed: " + operation, t); @@ -290,11 +325,12 @@ public static synchronized void unregisterNativeBackend(Class backendClass) { nativeTransformCache.remove(backendClass); if (!nativeDelegates.isEmpty()) return; Class owner = nativeOwner; + Method invoke = nativeOwnerInvoke; nativeOwner = null; - if (owner == null) return; + nativeOwnerInvoke = null; + if (owner == null || invoke == null) return; try { - owner.getMethod("sharedNativeInvoke", String.class, Object[].class) - .invoke(null, "dispose", new Object[0]); + invoke.invoke(null, "dispose", new Object[0]); } catch (Throwable t) { System.err.println("[Incision][Bridge] native dispose failed: " + t); } diff --git a/module/incision/src/test/kotlin/taboolib/module/incision/bridge/IncisionBridgeNativeRoutingTest.kt b/module/incision/src/test/kotlin/taboolib/module/incision/bridge/IncisionBridgeNativeRoutingTest.kt index 86142ba97..1b83fb0ca 100644 --- a/module/incision/src/test/kotlin/taboolib/module/incision/bridge/IncisionBridgeNativeRoutingTest.kt +++ b/module/incision/src/test/kotlin/taboolib/module/incision/bridge/IncisionBridgeNativeRoutingTest.kt @@ -3,7 +3,10 @@ package taboolib.module.incision.bridge import io.izzel.incision.bridge.IncisionBridge import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertSame import org.junit.jupiter.api.Test +import java.lang.reflect.Method +import java.util.concurrent.ConcurrentHashMap /** * 验证多个隔离后端共享唯一 native owner 时,字节码必须按 delegate 注册顺序串联处理。 @@ -25,6 +28,40 @@ class IncisionBridgeNativeRoutingTest { } } + @Test + fun delegateMethodsAreResolvedBeforeClassFileLoadHook() { + IncisionBridge.registerNativeBackend(FirstBackend::class.java, true) + try { + val field = IncisionBridge::class.java.getDeclaredField("nativeTransformCache").apply { isAccessible = true } + @Suppress("UNCHECKED_CAST") + val cache = field.get(null) as ConcurrentHashMap, Method> + val callback = cache[FirstBackend::class.java] + + assertSame(FirstBackend::class.java, callback?.declaringClass) + assertEquals("onSharedClassFileLoad", callback?.name) + assertEquals(3, callback?.parameterCount) + } finally { + IncisionBridge.unregisterNativeBackend(FirstBackend::class.java) + } + } + + @Test + fun nativeHookDoesNotReenterDelegateOrWeaveBackendProtocol() { + IncisionBridge.registerNativeBackend(ReentrantBackend::class.java, true) + try { + assertArrayEquals( + byteArrayOf(1, 4), + IncisionBridge.transformNative(null, "example/Target", byteArrayOf(1)), + ) + assertEquals( + null, + IncisionBridge.transformNative(null, "taboolib/module/incision/loader/Backend\$BackendToken", byteArrayOf(1)), + ) + } finally { + IncisionBridge.unregisterNativeBackend(ReentrantBackend::class.java) + } + } + object FirstBackend { @JvmStatic fun onSharedClassFileLoad(loader: ClassLoader?, name: String, bytes: ByteArray) = bytes + 2 @JvmStatic fun sharedNativeInvoke(operation: String, args: Array): Any? = @@ -34,4 +71,14 @@ class IncisionBridgeNativeRoutingTest { object SecondBackend { @JvmStatic fun onSharedClassFileLoad(loader: ClassLoader?, name: String, bytes: ByteArray) = bytes + 3 } + + object ReentrantBackend { + @JvmStatic fun onSharedClassFileLoad(loader: ClassLoader?, name: String, bytes: ByteArray): ByteArray { + // 模拟 native hook 内触发协议类加载;内层回调必须直接放行,外层仍需完成一次织入。 + IncisionBridge.transformNative(loader, "example/Backend\$BackendToken", bytes) + return bytes + 4 + } + + @JvmStatic fun sharedNativeInvoke(operation: String, args: Array): Any? = null + } }