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
@@ -1,8 +1,13 @@
package io.izzel.incision.bridge;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

Expand Down Expand Up @@ -55,6 +60,16 @@ private IncisionBridge() {}
private static final ConcurrentHashMap<String, Boolean> routeConflictWarnings =
new ConcurrentHashMap<String, Boolean>();

/**
* Side-car body 的字段解析缓存。
*
* key 与 value 都必须是弱引用语义,避免系统级 Bridge 通过 Field 反向强持有插件 ClassLoader;
* 此处不能使用匿名 ClassValue 子类,因为 bootstrap 注入协议只复制 IncisionBridge.class,
* 任何 IncisionBridge$1.class 都会让 canonical 类初始化失败。
*/
private static final Map<Class<?>, WeakReference<ConcurrentHashMap<String, Field>>> accessFields =
Collections.synchronizedMap(new WeakHashMap<Class<?>, WeakReference<ConcurrentHashMap<String, Field>>>());

/**
* 供 weaver 注入的 INVOKESTATIC 目标。
*
Expand Down Expand Up @@ -133,6 +148,64 @@ public static boolean isBypassMiss(Object value) {
return value == BYPASS_MISS;
}

/**
* Side-car body 读取宿主私有字段的稳定入口。
* 普通字段访问不应依赖某个插件 ClassLoader 独占的 JVMTI native image。
*/
public static Object accessFieldGet(Object receiver, Class<?> ownerClass, String fieldName, String fieldDesc) {
try {
return resolveAccessField(ownerClass, fieldName, fieldDesc).get(receiver);
} catch (Throwable t) {
throw new IllegalStateException("Incision field read failed: " + ownerClass.getName() + "." + fieldName, t);
}
}

/** Side-car body 写入宿主私有字段;访问规则与 {@link #accessFieldGet} 相同。 */
public static void accessFieldSet(Object receiver, Class<?> ownerClass, String fieldName, String fieldDesc, Object value) {
try {
resolveAccessField(ownerClass, fieldName, fieldDesc).set(receiver, value);
} catch (Throwable t) {
throw new IllegalStateException("Incision field write failed: " + ownerClass.getName() + "." + fieldName, t);
}
}

/** Side-car body 读取宿主私有静态字段。 */
public static Object accessStaticFieldGet(Class<?> ownerClass, String fieldName, String fieldDesc) {
return accessFieldGet(null, ownerClass, fieldName, fieldDesc);
}

/** Side-car body 写入宿主私有静态字段。 */
public static void accessStaticFieldSet(Class<?> ownerClass, String fieldName, String fieldDesc, Object value) {
accessFieldSet(null, ownerClass, fieldName, fieldDesc, value);
}

private static Field resolveAccessField(Class<?> ownerClass, String fieldName, String fieldDesc) throws NoSuchFieldException {
String key = fieldName + ':' + fieldDesc;
ConcurrentHashMap<String, Field> fields;
synchronized (accessFields) {
WeakReference<ConcurrentHashMap<String, Field>> reference = accessFields.get(ownerClass);
fields = reference == null ? null : reference.get();
if (fields == null) {
fields = new ConcurrentHashMap<String, Field>();
accessFields.put(ownerClass, new WeakReference<ConcurrentHashMap<String, Field>>(fields));
}
}
Field cached = fields.get(key);
if (cached != null) return cached;
Class<?> cursor = ownerClass;
while (cursor != null) {
try {
Field field = cursor.getDeclaredField(fieldName);
field.setAccessible(true);
Field previous = fields.putIfAbsent(key, field);
return previous == null ? field : previous;
} catch (NoSuchFieldException ignored) {
cursor = cursor.getSuperclass();
}
}
throw new NoSuchFieldException(ownerClass.getName() + '.' + fieldName + ':' + fieldDesc);
}

/** 宿主绑定入口 — GateBootstrapper 创建 host 后调用此方法完成注册 */
public static synchronized void bindSystemHost(Object host) {
systemHost = host;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ object IncisionBootstrap {
/** 当前 incision API 版本,用于网关版本协商 */
const val API_VERSION = 2

/** 系统属性名必须运行时拼接,避免插件 Shadow 把协议键误当作 taboolib 包名重定位。 */
private val backendProperty =
String(charArrayOf('t', 'a', 'b', 'o', 'o', 'l', 'i', 'b')) + ".incision.backend"

init {
prepareConst()
}
Expand Down Expand Up @@ -167,8 +171,10 @@ object IncisionBootstrap {
Forensics.warn("IncisionBridge.class 资源未找到: $resourcePath")
return
}
// 显式选择 Instrumentation 时不得探测 native;System.load 的进程级副作用无法在插件 CL 间回滚。
val forcedBackend = System.getProperty(backendProperty, "auto").lowercase()
// 路径 1: JVMTI native — defineClass 直接注入 bootstrap CL
if (JvmtiBackend.available()) {
if (forcedBackend != "instrumentation" && JvmtiBackend.available()) {
val cls = JvmtiBackend.defineClassInClassLoader(null, bridgeClassName, bytes)
if (cls != null) {
Forensics.info("IncisionBridge 已注入 bootstrap ClassLoader (JVMTI)")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package taboolib.module.incision.diagnostic

import taboolib.common.PrimitiveSettings
import taboolib.common.platform.function.warning
import taboolib.common.platform.function.debug as platformDebug
import taboolib.common.platform.function.info as platformInfo
import taboolib.common.platform.function.warning as platformWarning

/**
* 结构化诊断日志器。
Expand All @@ -18,22 +20,34 @@ object Forensics {
get() = PrimitiveSettings.IS_DEBUG_MODE

fun info(message: String) {
if (DEBUG) taboolib.common.platform.function.info("[Incision] $message")
if (DEBUG) emitSafely("[Incision] $message", false) { platformInfo(it) }
}

fun debug(message: String) {
if (DEBUG) taboolib.common.platform.function.debug("[Incision][DEBUG] $message")
if (DEBUG) emitSafely("[Incision][DEBUG] $message", false) { platformDebug(it) }
}

fun warn(message: String) {
if (DEBUG) warning("[Incision][WARN] $message")
if (DEBUG) emitSafely("[Incision][WARN] $message", true) { platformWarning(it) }
}

fun error(message: String, cause: Throwable? = null) {
System.err.println("[Incision][ERROR] $message")
cause?.printStackTrace(System.err)
}

/**
* CONST 阶段可能早于 BukkitPlugin 实例完成构造,平台日志实现此时会访问尚未就绪的插件实例。
* 诊断路径绝不能反向中断 Incision 初始化,因此平台输出失败时只退回 JDK 标准流。
*/
private fun emitSafely(message: String, stderr: Boolean, platformLog: (String) -> Unit) {
try {
platformLog(message)
} catch (_: Throwable) {
if (stderr) System.err.println(message) else System.out.println(message)
}
}

/**
* 上报 Trauma — 输出结构化字段 + 触发栈。
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,15 +192,21 @@ object Scalpel {
group.forEach { ownerEntries[it.id] = it }
activeTokens.remove(resolvedOwner)?.remove()
val targets = buildRuntimeTargets(resolvedOwner, ownerEntries.values.toList())
val weaver = ScalpelWeaver(targetsByOwner = mapOf(resolvedOwner to targets))
val weaver = ScalpelWeaver(
targetsByOwner = mapOf(resolvedOwner to targets),
useJvmtiBaseline = backend === JvmtiBackend,
)
val installation = backend.install(resolvedOwner) { bytes -> weaver.weave(bytes) }
val token = installation.token
if (installation.status !in setOf(Backend.InstallStatus.INSTALLED, Backend.InstallStatus.PENDING_LOAD) || token == null) {
ownerEntries.clear()
ownerEntries.putAll(previousEntries)
if (previousEntries.isNotEmpty()) {
val previousTargets = buildRuntimeTargets(resolvedOwner, previousEntries.values.toList())
val previousWeaver = ScalpelWeaver(targetsByOwner = mapOf(resolvedOwner to previousTargets))
val previousWeaver = ScalpelWeaver(
targetsByOwner = mapOf(resolvedOwner to previousTargets),
useJvmtiBaseline = backend === JvmtiBackend,
)
val restored = backend.install(resolvedOwner) { bytes -> previousWeaver.weave(bytes) }
restored.token?.takeIf {
restored.status == Backend.InstallStatus.INSTALLED || restored.status == Backend.InstallStatus.PENDING_LOAD
Expand Down Expand Up @@ -252,7 +258,10 @@ object Scalpel {
return backend.isClassLoaded(owner) == false || backend.retransform(owner.replace('/', '.'))
}
val targets = buildRuntimeTargets(owner, entries)
val weaver = ScalpelWeaver(targetsByOwner = mapOf(owner to targets))
val weaver = ScalpelWeaver(
targetsByOwner = mapOf(owner to targets),
useJvmtiBaseline = backend === JvmtiBackend,
)
val installation = backend.install(owner) { bytes -> weaver.weave(bytes) }
val token = installation.token ?: return false
if (installation.status !in setOf(Backend.InstallStatus.INSTALLED, Backend.InstallStatus.PENDING_LOAD)) return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ package taboolib.module.incision.pred
* 谓词编译上下文。由 advice 注册方提供,传给 [PredCompiler.compile]。
*
* @property adviceId advice id,仅用于错误诊断(出现在 [taboolib.module.incision.diagnostic.Trauma.Predicate.RuntimeFailure] 中)。
* @property classLoader 生成的谓词类的装载目标 ClassLoader。通常是声明 advice 的插件主 CL;
* 后续 script / external 场景可指向脚本沙箱 CL。
* @property classLoader 生成谓词专用 ClassLoader 的 parent。通常是声明 advice 的插件主 CL;
* 后续 script / external 场景可指向脚本沙箱 CL,以限制可见类型边界
* @property extraVars 除默认 `args/this/result/env/site/caller` 外允许出现的顶层变量名。
* 未列入白名单的变量会在编译期抛 [taboolib.module.incision.diagnostic.Trauma.Predicate.UndefinedVariable]。
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import org.objectweb.asm.Label
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
import taboolib.module.incision.diagnostic.Trauma
import taboolib.module.incision.loader.JvmtiBackend
import java.lang.reflect.Method
import java.lang.ref.WeakReference
import java.util.WeakHashMap
import java.util.concurrent.atomic.AtomicInteger

/**
Expand Down Expand Up @@ -448,49 +448,44 @@ object PredCompiler {
}
}

// ---------- 类装载器:反射优先;JDK 9+ 模块边界失败则退到 JNI DefineClass ----------
// ---------- 类装载器 -------------------------------------------------------

private object LoaderHelper {
private val defineMethod: Method? by lazy {
try {
val m = ClassLoader::class.java.getDeclaredMethod(
"defineClass",
String::class.java,
ByteArray::class.java,
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType,
)
m.isAccessible = true
m
} catch (_: Throwable) {
null
}
}

/**
* 每个 advice defining loader 对应一个弱引用生成类加载器。
*
* 生成谓词只需要通过 parent 看见插件中的 Predicate/PredOps,并不需要强行定义进插件
* ClassLoader。使用子加载器可以同时避开 JDK 9+ 对 ClassLoader#defineClass 的模块封装,
* 也避免为普通类定义误触 JVMTI native;弱键确保插件卸载后不会被全局缓存阻止回收。
*/
private val generatedLoaders = WeakHashMap<ClassLoader, WeakReference<GeneratedPredicateClassLoader>>()

fun define(cl: ClassLoader, name: String, bytes: ByteArray): Class<*> {
val binaryName = name.replace('/', '.')
try {
return Class.forName(binaryName, false, cl)
} catch (_: Throwable) {
}
val reflectError: Throwable? = defineMethod?.let { m ->
try {
return m.invoke(cl, binaryName, bytes, 0, bytes.size) as Class<*>
} catch (t: Throwable) {
t
val generatedLoader = synchronized(generatedLoaders) {
generatedLoaders[cl]?.get() ?: GeneratedPredicateClassLoader(cl).also {
// value 也必须是弱引用;GeneratedPredicateClassLoader.parent 会反向强持有 key,
// 若直接把 loader 作为 value,WeakHashMap 的弱键将永远无法回收。
generatedLoaders[cl] = WeakReference(it)
}
}
// JDK 9+ 或反射被禁:退到 native JNI DefineClass(不受模块系统限制)
try {
val cls = JvmtiBackend.defineClassInClassLoader(cl, name, bytes)
if (cls != null) return cls
} catch (_: Throwable) {
// native 不可用时继续抛原反射异常
return try {
generatedLoader.define(binaryName, bytes)
} catch (t: Throwable) {
throw Trauma.Predicate.RuntimeFailure("<gen $name>", null, t)
}
throw Trauma.Predicate.RuntimeFailure(
"<gen $name>", null,
reflectError ?: IllegalStateException("defineClass unavailable (reflection + JVMTI both failed)")
)
}

/**
* defineClass 只能由 ClassLoader 子类合法调用;同步保证同一生成名称不会被并发重复定义。
* 类名由全局递增序列生成,findLoadedClass 仍作为防御性检查保留。
*/
private class GeneratedPredicateClassLoader(parent: ClassLoader) : ClassLoader(parent) {

@Synchronized
fun define(binaryName: String, bytes: ByteArray): Class<*> =
findLoadedClass(binaryName) ?: defineClass(binaryName, bytes, 0, bytes.size)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package taboolib.module.incision.pred
/**
* 编译后的谓词。
*
* 由 [PredCompiler] 从 [PredAst] 生成 ASM 字节码并装载到目标 ClassLoader 后实例化。
* 由 [PredCompiler] 从 [PredAst] 生成 ASM 字节码,并装载到以 advice ClassLoader 为 parent 的
* 专用生成类加载器后实例化。专用 loader 避免依赖受模块封装限制的反射 defineClass 或 JVMTI。
*
* 实现类要求:
* - 无状态、线程安全(dispatcher 多线程并发 `test`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,19 +197,11 @@ object BodiesClassGenerator {
}

/**
* JvmtiBackend 的 JVM 内部类名(全斜杠形式)。
*
* 注意:绝不能写成 `const val "taboolib/module/incision/loader/JvmtiBackend"`。
* 因为 const 字符串会被内联,TabooLib 的 Shadow relocation 会把其中的 `taboolib`
* token 按「包名(点号)」规则替换为重定位前缀,得到点斜混合的非法名,例如
* `group.taboolib/module/incision/loader/JvmtiBackend`,导致生成的 Bodies 类在
* defineClass 时抛 ClassFormatError: Illegal class name。
*
* 改为运行期从已重定位的实际类对象推导:`Class.name` 在重定位后是正确的全限定名,
* 仅需把 `.` 换成 `/` 即可得到合法内部名,且对任意重定位前缀都成立。
* canonical Bridge 固定存在于 bootstrap/system ClassLoader,服务端类与第三方插件类均可解析。
* Side-car body 不能调用插件私有副本中的 JVMTI native:多插件各有独立 ClassLoader,而同一
* native image 在 JVM 中只能由一个 loader 绑定,后续插件会得到 UnsatisfiedLinkError。
*/
private val JVMTI_BACKEND: String =
taboolib.module.incision.loader.JvmtiBackend::class.java.name.replace('.', '/')
private const val ACCESS_BRIDGE = "io/izzel/incision/bridge/IncisionBridge"

/**
* 克隆原方法指令流到 [out],做 slot 偏移、return 替换、private 字段访问替换。
Expand All @@ -219,7 +211,7 @@ object BodiesClassGenerator {
* - xRETURN(基本类型):装箱 + ARETURN
* - RETURN(void):ACONST_NULL + ARETURN
* - ARETURN:保持
* - GETFIELD/PUTFIELD 访问 private 字段:替换为 JNI 层 nFieldGet/nFieldSet
* - GETFIELD/PUTFIELD 访问 private 字段:替换为 canonical Bridge 的反射访问入口
*/
private fun cloneInstructionsInto(
out: InsnList,
Expand All @@ -237,7 +229,7 @@ object BodiesClassGenerator {
is IincInsnNode -> cloned.`var` += 2
}

// private 字段访问 → 通过 C 层 JNI 绕过访问控制
// private 字段访问 → 通过系统级 Bridge 解析,避免 side-car 不具备宿主 nestmate 权限。
if (cloned is FieldInsnNode && cloned.owner == ownerInternal && cloned.name in privateFields) {
when (cloned.opcode) {
GETFIELD -> {
Expand All @@ -247,7 +239,7 @@ object BodiesClassGenerator {
out.add(LdcInsnNode(cloned.name))
out.add(LdcInsnNode(cloned.desc))
out.add(MethodInsnNode(
INVOKESTATIC, JVMTI_BACKEND, "nFieldGet",
INVOKESTATIC, ACCESS_BRIDGE, "accessFieldGet",
"(Ljava/lang/Object;Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;",
false
))
Expand All @@ -271,7 +263,7 @@ object BodiesClassGenerator {
out.add(LdcInsnNode(cloned.desc))
out.add(VarInsnNode(ALOAD, 0)) // 取回 boxedValue
out.add(MethodInsnNode(
INVOKESTATIC, JVMTI_BACKEND, "nFieldSet",
INVOKESTATIC, ACCESS_BRIDGE, "accessFieldSet",
"(Ljava/lang/Object;Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V",
false
))
Expand All @@ -283,7 +275,7 @@ object BodiesClassGenerator {
out.add(LdcInsnNode(cloned.name))
out.add(LdcInsnNode(cloned.desc))
out.add(MethodInsnNode(
INVOKESTATIC, JVMTI_BACKEND, "nStaticFieldGet",
INVOKESTATIC, ACCESS_BRIDGE, "accessStaticFieldGet",
"(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;",
false
))
Expand All @@ -301,7 +293,7 @@ object BodiesClassGenerator {
out.add(LdcInsnNode(cloned.desc))
out.add(VarInsnNode(ALOAD, 0))
out.add(MethodInsnNode(
INVOKESTATIC, JVMTI_BACKEND, "nStaticFieldSet",
INVOKESTATIC, ACCESS_BRIDGE, "accessStaticFieldSet",
"(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V",
false
))
Expand Down
Loading
Loading