Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ private IncisionBridge() {}
private static final ConcurrentHashMap<String, Boolean> routeConflictWarnings =
new ConcurrentHashMap<String, Boolean>();

/**
* JVM 进程只能由一个隔离 ClassLoader 直接拥有已加载的 JVMTI DLL。
* Bridge 保留该 owner,并把 native 回调广播给所有插件后端,避免后加载插件再次 System.load。
*/
private static volatile Class<?> nativeOwner;
private static final CopyOnWriteArrayList<Class<?>> nativeDelegates = new CopyOnWriteArrayList<Class<?>>();
private static final ConcurrentHashMap<Class<?>, Method> nativeTransformCache = new ConcurrentHashMap<Class<?>, Method>();

/**
* Side-car body 的字段解析缓存。
*
Expand Down Expand Up @@ -226,6 +234,72 @@ public static int localLeaseCount() {
return localCache.size();
}

/** 注册插件后端;返回 JVM 当前是否已有可用 native owner。 */
public static synchronized boolean registerNativeBackend(Class<?> backendClass, boolean ownsNative) {
if (backendClass == null) return nativeOwner != null;
if (ownsNative && nativeOwner == null) nativeOwner = backendClass;
if (!nativeDelegates.contains(backendClass)) nativeDelegates.add(backendClass);
return nativeOwner != null;
}

/**
* native ClassFileLoadHook 的 JVM 级聚合入口。每个 delegate 接收前一个插件产生的字节码,
* 因而两个插件对同一方法的织入会形成确定的先后链,而不是互相覆盖。
*/
public static byte[] transformNative(ClassLoader loader, String name, byte[] bytes) {
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;
}
} catch (Throwable t) {
System.err.println("[Incision][Bridge] native transformer delegate failed: " + backend.getName() + " — " + t);
}
}
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");
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);
}
}

/**
* 插件卸载只移除自己的 delegate。最后一个 lease 才关闭 JVMTI;若 owner 先卸载,
* 其 Class 对象必须暂留到最后一个 lease 结束,否则其他插件无法继续调用 native image。
*/
public static synchronized void unregisterNativeBackend(Class<?> backendClass) {
if (backendClass == null) return;
nativeDelegates.remove(backendClass);
nativeTransformCache.remove(backendClass);
if (!nativeDelegates.isEmpty()) return;
Class<?> owner = nativeOwner;
nativeOwner = null;
if (owner == null) return;
try {
owner.getMethod("sharedNativeInvoke", String.class, Object[].class)
.invoke(null, "dispose", new Object[0]);
} catch (Throwable t) {
System.err.println("[Incision][Bridge] native dispose failed: " + t);
}
}

// -----------------------------------------------------------------

private static Object findSystemHost() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ object IncisionBootstrap {
try {
// 后续 target 注册、host 绑定与卸载必须复用同一个类句柄,不能再直接链接插件内同名 Bridge。
CanonicalBridge.bind(bridgeClass)
JvmtiBackend.bindCanonicalBridge()
CanonicalBridge.registerDispatcher(TheatreDispatcher::class.java)
Forensics.info("IncisionBridge dispatcher 已注册 (CL=${bridgeClass.classLoader ?: "bootstrap"})")
} catch (t: Throwable) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package taboolib.module.incision.loader

import taboolib.module.incision.diagnostic.Forensics
import taboolib.module.incision.runtime.CanonicalBridge
import java.io.File
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
Expand All @@ -27,32 +28,50 @@ object JvmtiBackend : Backend {

@Volatile private var loaded = false
@Volatile private var available = false
@Volatile private var localNativeOwner = false
@Volatile private var sharedBound = false

override val name: String = "JVMTI"

override fun available(): Boolean {
if (sharedBound) return available
if (!loaded) tryLoad()
return available
}

/** Bridge 注入完成后加入 JVM 级 native lease;后加载插件不得再次调用 System.load。 */
fun bindCanonicalBridge() {
sharedBound = true
available = CanonicalBridge.registerNativeBackend(JvmtiBackend::class.java, localNativeOwner)
loaded = true
if (available && !localNativeOwner) {
Forensics.info("JvmtiBackend: 复用 JVM canonical native owner")
}
}

override fun addTransformer(className: String, transformer: (ByteArray) -> ByteArray?): Backend.BackendToken {
val key = className.replace('.', '/')
transformers.computeIfAbsent(key) { CopyOnWriteArrayList() }.add(transformer)
return object : Backend.BackendToken {
override fun remove() { transformers[key]?.remove(transformer) }
override fun remove() {
transformers[key]?.remove(transformer)
if (transformers[key].isNullOrEmpty()) {
transformers.remove(key)
}
}
}
}

override fun retransform(className: String): Boolean {
if (!available()) return false
val internalName = className.replace('.', '/')
val loadedCount = runCatching { nLoadedClassCount(internalName) }.getOrDefault(0)
val loadedCount = runCatching { nativeLoadedClassCount(internalName) }.getOrDefault(0)
if (loadedCount <= 0) {
Forensics.debug("JvmtiBackend.retransform: $className not loaded yet, will transform on load")
return false
}
return try {
val transformed = nRetransformByName(internalName)
val transformed = nativeRetransformByName(internalName)
val ok = transformed == loadedCount
Forensics.debug("JvmtiBackend.retransform $className → $ok (loaded=$loadedCount transformed=$transformed)")
ok
Expand All @@ -65,6 +84,13 @@ object JvmtiBackend : Backend {
/** Called from native ClassFileLoadHook for every (re)loaded class. */
@JvmStatic
fun onClassFileLoad(loader: ClassLoader?, name: String, bytes: ByteArray): ByteArray? {
if (sharedBound) return CanonicalBridge.transformNative(loader, name, bytes)
return onSharedClassFileLoad(loader, name, bytes)
}

/** 由 bootstrap Bridge 广播到当前插件的本地 transformer 链。 */
@JvmStatic
fun onSharedClassFileLoad(loader: ClassLoader?, name: String, bytes: ByteArray): ByteArray? {
// 防重入:如果当前线程正在 weave 中(触发了新类加载),跳过 transformer 避免无限递归
if (reentrantGuard.get()) return null
val list = transformers[name]
Expand Down Expand Up @@ -114,10 +140,43 @@ object JvmtiBackend : Backend {
@JvmStatic external fun nStaticFieldSet(ownerClass: Class<*>, fieldName: String, fieldDesc: String, value: Any?)
@JvmStatic external fun nInvokeMethod(obj: Any?, ownerClass: Class<*>, methodName: String, methodDesc: String, args: Array<Any?>?): Any?

private fun nativeLoadedClassCount(internalName: String): Int =
(nativeInvoke("loadedClassCount", internalName) as? Number)?.toInt() ?: 0

private fun nativeRetransformByName(internalName: String): Int =
(nativeInvoke("retransformByName", internalName) as? Number)?.toInt() ?: -1

@Suppress("UNCHECKED_CAST")
private fun nativeInvoke(operation: String, vararg args: Any?): Any? =
if (sharedBound) CanonicalBridge.invokeNative(operation, *args)
else sharedNativeInvoke(operation, args.copyOf() as Array<Any?>)

/**
* canonical Bridge 通过这个固定反射入口调用唯一 native owner。
* switch 明确限制可调用协议,避免把任意反射能力暴露给其他插件 ClassLoader。
*/
@JvmStatic
fun sharedNativeInvoke(operation: String, args: Array<Any?>): Any? = when (operation) {
"loadedClassCount" -> nLoadedClassCount(args[0] as String)
"retransformByName" -> nRetransformByName(args[0] as String)
"defineClass" -> nDefineClass(args[0] as ClassLoader?, args[1] as String, args[2] as ByteArray)
"cacheOriginal" -> nCacheOriginal(args[0] as String, args[1] as ByteArray)
"getCachedOriginal" -> nGetCachedOriginal(args[0] as String)
"extractClassBytes" -> nExtractClassBytes(args[0] as Class<*>)
"purgeCache" -> nPurgeCache(args[0] as String)
"fieldGet" -> nFieldGet(args[0]!!, args[1] as Class<*>, args[2] as String, args[3] as String)
"fieldSet" -> nFieldSet(args[0]!!, args[1] as Class<*>, args[2] as String, args[3] as String, args[4])
"staticFieldGet" -> nStaticFieldGet(args[0] as Class<*>, args[1] as String, args[2] as String)
"staticFieldSet" -> nStaticFieldSet(args[0] as Class<*>, args[1] as String, args[2] as String, args[3])
"invokeMethod" -> nInvokeMethod(args[0], args[1] as Class<*>, args[2] as String, args[3] as String, args[4] as Array<Any?>?)
"dispose" -> nDispose()
else -> throw IllegalArgumentException("Unsupported shared native operation: $operation")
}

fun defineClassInClassLoader(loader: ClassLoader?, name: String, bytes: ByteArray): Class<*>? {
if (!available()) return null
return try {
val cls = nDefineClass(loader, name.replace('.', '/'), bytes)
val cls = nativeInvoke("defineClass", loader, name.replace('.', '/'), bytes) as? Class<*>
if (cls != null) Forensics.debug("JvmtiBackend.defineClass: $name in ${loader ?: "bootstrap"}")
cls
} catch (t: Throwable) {
Expand All @@ -126,22 +185,32 @@ object JvmtiBackend : Backend {
}
}

override fun isClassLoaded(className: String): Boolean =
available() && runCatching { nLoadedClassCount(className.replace('.', '/')) > 0 }.getOrDefault(false)
override fun isClassLoaded(className: String): Boolean {
if (!available()) return false
val internalName = className.replace('.', '/')
val count = runCatching { nativeLoadedClassCount(internalName) }.getOrDefault(0)
Forensics.debug("JvmtiBackend.isClassLoaded: $internalName count=$count")
return count > 0
}

/** 当前插件 lease 释放时停止 native 回调并清空所有插件类引用,避免 reload 持有已关闭 loader。 */
@Synchronized
fun dispose() {
transformers.clear()
if (!available) return
runCatching { nDispose() }.onFailure { Forensics.warn("JvmtiBackend.dispose 失败: ${it.message}") }
if (sharedBound) {
CanonicalBridge.unregisterNativeBackend(JvmtiBackend::class.java)
} else {
runCatching { nDispose() }.onFailure { Forensics.warn("JvmtiBackend.dispose 失败: ${it.message}") }
}
available = false
sharedBound = false
}

fun cacheOriginal(owner: String, bytes: ByteArray): Boolean {
if (!available()) return false
return try {
nCacheOriginal(cacheKey(owner), bytes)
nativeInvoke("cacheOriginal", cacheKey(owner), bytes) as? Boolean ?: false
} catch (t: Throwable) {
Forensics.warn("nCacheOriginal 失败: ${t.message}")
false
Expand All @@ -151,7 +220,7 @@ object JvmtiBackend : Backend {
fun getCachedOriginal(owner: String): ByteArray? {
if (!available()) return null
return try {
nGetCachedOriginal(cacheKey(owner))
nativeInvoke("getCachedOriginal", cacheKey(owner)) as? ByteArray
} catch (t: Throwable) {
Forensics.warn("nGetCachedOriginal 失败: ${t.message}")
null
Expand All @@ -161,7 +230,7 @@ object JvmtiBackend : Backend {
fun extractClassBytes(target: Class<*>): ByteArray? {
if (!available()) return null
return try {
nExtractClassBytes(target)
nativeInvoke("extractClassBytes", target) as? ByteArray
} catch (t: Throwable) {
Forensics.warn("nExtractClassBytes 失败: ${t.message}")
null
Expand All @@ -171,7 +240,7 @@ object JvmtiBackend : Backend {
fun purgeCache(owner: String) {
if (!available()) return
try {
nPurgeCache(cacheKey(owner))
nativeInvoke("purgeCache", cacheKey(owner))
} catch (t: Throwable) {
Forensics.warn("nPurgeCache 失败: ${t.message}")
}
Expand All @@ -190,7 +259,7 @@ object JvmtiBackend : Backend {
fun fieldGet(obj: Any, ownerClass: Class<*>, fieldName: String, fieldDesc: String): Any? {
if (!available()) return null
return try {
nFieldGet(obj, ownerClass, fieldName, fieldDesc)
nativeInvoke("fieldGet", obj, ownerClass, fieldName, fieldDesc)
} catch (t: Throwable) {
Forensics.warn("nFieldGet 失败: ${ownerClass.name}.$fieldName — ${t.message}")
null
Expand All @@ -200,7 +269,7 @@ object JvmtiBackend : Backend {
fun fieldSet(obj: Any, ownerClass: Class<*>, fieldName: String, fieldDesc: String, value: Any?) {
if (!available()) return
try {
nFieldSet(obj, ownerClass, fieldName, fieldDesc, value)
nativeInvoke("fieldSet", obj, ownerClass, fieldName, fieldDesc, value)
} catch (t: Throwable) {
Forensics.warn("nFieldSet 失败: ${ownerClass.name}.$fieldName — ${t.message}")
}
Expand All @@ -209,7 +278,7 @@ object JvmtiBackend : Backend {
fun staticFieldGet(ownerClass: Class<*>, fieldName: String, fieldDesc: String): Any? {
if (!available()) return null
return try {
nStaticFieldGet(ownerClass, fieldName, fieldDesc)
nativeInvoke("staticFieldGet", ownerClass, fieldName, fieldDesc)
} catch (t: Throwable) {
Forensics.warn("nStaticFieldGet 失败: ${ownerClass.name}.$fieldName — ${t.message}")
null
Expand All @@ -219,7 +288,7 @@ object JvmtiBackend : Backend {
fun staticFieldSet(ownerClass: Class<*>, fieldName: String, fieldDesc: String, value: Any?) {
if (!available()) return
try {
nStaticFieldSet(ownerClass, fieldName, fieldDesc, value)
nativeInvoke("staticFieldSet", ownerClass, fieldName, fieldDesc, value)
} catch (t: Throwable) {
Forensics.warn("nStaticFieldSet 失败: ${ownerClass.name}.$fieldName — ${t.message}")
}
Expand All @@ -228,7 +297,7 @@ object JvmtiBackend : Backend {
fun invokeMethod(obj: Any?, ownerClass: Class<*>, methodName: String, methodDesc: String, args: Array<Any?>?): Any? {
if (!available()) return null
return try {
nInvokeMethod(obj, ownerClass, methodName, methodDesc, args)
nativeInvoke("invokeMethod", obj, ownerClass, methodName, methodDesc, args)
} catch (t: Throwable) {
Forensics.warn("nInvokeMethod 失败: ${ownerClass.name}.$methodName — ${t.message}")
null
Expand All @@ -250,6 +319,7 @@ object JvmtiBackend : Backend {
System.setProperty("incision.jvmti.class", JvmtiBackend::class.java.name)
System.load(lib.absolutePath)
available = nInit(JvmtiBackend::class.java)
localNativeOwner = available
if (available) Forensics.info("JvmtiBackend: JVMTI native loaded from $lib")
else Forensics.warn("JvmtiBackend: nInit returned false")
available
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,35 @@ internal object CanonicalBridge {
invoke("registerLocalDispatcher", arrayOf<Class<*>>(Class::class.java), dispatcherClass)
}

fun registerNativeBackend(backendClass: Class<*>, ownsNative: Boolean): Boolean =
invokeResult(
"registerNativeBackend",
arrayOf<Class<*>>(Class::class.java, Boolean::class.javaPrimitiveType!!),
backendClass,
ownsNative,
) as? Boolean ?: false

fun transformNative(loader: ClassLoader?, name: String, bytes: ByteArray): ByteArray? =
invokeResult(
"transformNative",
arrayOf<Class<*>>(ClassLoader::class.java, String::class.java, ByteArray::class.java),
loader,
name,
bytes,
) as? ByteArray

fun invokeNative(operation: String, vararg args: Any?): Any? =
invokeResult(
"invokeNative",
arrayOf<Class<*>>(String::class.java, Array<Any?>::class.java),
operation,
args,
)

fun unregisterNativeBackend(backendClass: Class<*>) {
invoke("unregisterNativeBackend", arrayOf<Class<*>>(Class::class.java), backendClass)
}

fun unregisterDispatcher(classLoader: ClassLoader) {
invoke("unregisterLocalDispatcher", arrayOf<Class<*>>(ClassLoader::class.java), classLoader)
}
Expand Down
Loading
Loading