Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions module/incision/src/main/c/incision_jvmti.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Class<?>> nativeDelegates = new CopyOnWriteArrayList<Class<?>>();
private static final ConcurrentHashMap<Class<?>, Method> nativeTransformCache = new ConcurrentHashMap<Class<?>, Method>();
/**
* JVMTI 回调可能在 delegate 链接辅助类时再次进入 ClassFileLoadHook;线程级闸门保证
* 递归加载只返回原始字节,避免隔离加载器重复定义 Backend$BackendToken。
*/
private static final ThreadLocal<Boolean> nativeTransformGuard = new ThreadLocal<Boolean>();

/**
* Side-car body 的字段解析缓存。
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 注册顺序串联处理。
Expand All @@ -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<Class<*>, 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?>): Any? =
Expand All @@ -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?>): Any? = null
}
}
Loading