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
11 changes: 11 additions & 0 deletions module/incision/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,15 @@ dependencies {
compileOnly("org.ow2.asm:asm-util:9.8")
// self-attach 默认路径
compileOnly("net.bytebuddy:byte-buddy-agent:1.14.18")
// 测试
testImplementation(project(":common"))
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
// BodiesClassGenerator 在运行期直接使用 ASM,测试需要 ASM 运行时
testImplementation("org.ow2.asm:asm:9.8")
testImplementation("org.ow2.asm:asm-commons:9.8")
testImplementation("org.ow2.asm:asm-util:9.8")
}

tasks.test {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ object IncisionBootstrap {
}.getOrNull() ?: "?"
Forensics.debug("asm-tree probe: loader=${cl.javaClass.name} core=$asmCoreLoc tree=$asmTreeLoc")
try {
val bytes = cl.getResourceAsStream("taboolib/module/incision/IncisionBootstrap.class")?.use { it.readBytes() }
// 资源路径从实际类对象推导,避免 const/字面量被 relocation 改写成点斜混合的非法路径
val resourcePath = IncisionBootstrap::class.java.name.replace('.', '/') + ".class"
val bytes = cl.getResourceAsStream(resourcePath)?.use { it.readBytes() }
if (bytes != null) {
val node = org.objectweb.asm.tree.ClassNode()
org.objectweb.asm.ClassReader(bytes).accept(node, 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ object JvmtiBackend : Backend {

private val transformers = ConcurrentHashMap<String, MutableList<(ByteArray) -> ByteArray?>>()

/** 防重入保护:weave 过程中触发的类加载不应再次进入 transformer */
private val reentrantGuard = ThreadLocal.withInitial { false }

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

Expand Down Expand Up @@ -59,19 +62,26 @@ object JvmtiBackend : Backend {
/** Called from native ClassFileLoadHook for every (re)loaded class. */
@JvmStatic
fun onClassFileLoad(loader: ClassLoader?, name: String, bytes: ByteArray): ByteArray? {
// 防重入:如果当前线程正在 weave 中(触发了新类加载),跳过 transformer 避免无限递归
if (reentrantGuard.get()) return null
val list = transformers[name]
if (list == null) return null
Forensics.debug("JvmtiBackend.onClassFileLoad: $name (${list.size} transformers, ${bytes.size} bytes)")
var cur = bytes
var changed = false
for (t in list) {
val out = try { t(cur) } catch (e: Throwable) {
Forensics.error("JvmtiBackend transformer error: $name", e); null
reentrantGuard.set(true)
try {
var cur = bytes
var changed = false
for (t in list) {
val out = try { t(cur) } catch (e: Throwable) {
Forensics.error("JvmtiBackend transformer error: $name", e); null
}
if (out != null) { cur = out; changed = true }
}
if (out != null) { cur = out; changed = true }
Forensics.debug("JvmtiBackend.onClassFileLoad: $name → changed=$changed (${cur.size} bytes)")
return if (changed) cur else null
} finally {
reentrantGuard.set(false)
}
Forensics.debug("JvmtiBackend.onClassFileLoad: $name → changed=$changed (${cur.size} bytes)")
return if (changed) cur else null
}

// ----------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,19 @@ object BodiesClassGenerator {
private fun generateBodyMethod(original: MethodNode, ownerInternal: String, privateFields: Map<String, String>): MethodNode? {
if (original.access and ACC_STATIC != 0) return null

// 跳过构造函数 / 静态初始化块:
// 1. JVM 方法名规范禁止普通方法名包含 '<' '>',"<init>$body" / "<clinit>$body" 属非法方法名,
// 直接生成会触发 ClassFormatError: Illegal method name。
// 2. 构造函数体通常包含对 super()/this() 的 INVOKESPECIAL,无法在 static body 上下文中安全复制。
// 这类目标由 Incision 运行期回退到 IncisionBridge.dispatch 反射分发处理,无需 side-car body。
if (original.name == "<init>" || original.name == "<clinit>") {
Forensics.debug(
"BodiesClassGenerator: 跳过 ${ownerInternal}.${original.name}${original.desc} " +
"(构造/静态初始化方法不生成 side-car body)"
)
return null
}

val argTypes = Type.getArgumentTypes(original.desc)
val returnType = Type.getReturnType(original.desc)

Expand Down Expand Up @@ -186,8 +199,20 @@ object BodiesClassGenerator {
return false
}

/** JvmtiBackend 在 JNI 注册后的内部类名(可能被 Shadow 重定位) */
private const val JVMTI_BACKEND = "taboolib/module/incision/loader/JvmtiBackend"
/**
* 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` 在重定位后是正确的全限定名,
* 仅需把 `.` 换成 `/` 即可得到合法内部名,且对任意重定位前缀都成立。
*/
private val JVMTI_BACKEND: String =
taboolib.module.incision.loader.JvmtiBackend::class.java.name.replace('.', '/')

/**
* 克隆原方法指令流到 [out],做 slot 偏移、return 替换、private 字段访问替换。
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package taboolib.module.incision.loader

import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicInteger

/**
* JvmtiBackend 防重入保护测试
*
* 复现场景:
* Scalpel.weave(AdvancementDataPlayer)
* → RemapperBridge.mapFieldName
* → RemapTranslationLegacy.findParents
* → ClassHelper.getClass → Class.forName
* → ClassLoader.defineClass1 (触发 JVMTI ClassFileLoadHook)
* → JvmtiBackend.onClassFileLoad (再次进入 transformer)
* → Scalpel.weave → ... StackOverflowError
*/
@DisplayName("JvmtiBackend 防重入保护")
class JvmtiBackendReentrantTest {

@Suppress("UNCHECKED_CAST")
private fun getTransformers(): ConcurrentHashMap<String, MutableList<(ByteArray) -> ByteArray?>> {
val field = JvmtiBackend::class.java.getDeclaredField("transformers")
field.isAccessible = true
return field.get(JvmtiBackend) as ConcurrentHashMap<String, MutableList<(ByteArray) -> ByteArray?>>
}

private fun getReentrantGuard(): ThreadLocal<Boolean> {
val field = JvmtiBackend::class.java.getDeclaredField("reentrantGuard")
field.isAccessible = true
@Suppress("UNCHECKED_CAST")
return field.get(JvmtiBackend) as ThreadLocal<Boolean>
}

@BeforeEach
fun reset() {
getReentrantGuard().remove()
getTransformers().clear()
}

// ===== 基础功能 =====

@Test
@DisplayName("正常情况下 transformer 被调用并返回修改后的字节")
fun normalTransformerInvocation() {
val callCount = AtomicInteger(0)
val transformers = getTransformers()
transformers.computeIfAbsent("com/example/TestClass") { mutableListOf() }
.add { bytes -> callCount.incrementAndGet(); bytes }

val result = JvmtiBackend.onClassFileLoad(null, "com/example/TestClass", byteArrayOf(1, 2, 3))

assertEquals(1, callCount.get())
assertNotNull(result)
}

@Test
@DisplayName("无注册 transformer 时返回 null 不做任何处理")
fun noTransformerReturnsNull() {
val result = JvmtiBackend.onClassFileLoad(null, "com/example/Unknown", byteArrayOf(1))
assertNull(result)
}

// ===== 核心:递归地狱复现 =====

@Test
@DisplayName("互相触发类加载的两个 transformer 形成 A→B→A 递归,防重入保护打断递归链")
fun recursiveTransformerHellIsPrevented() {
val callCountA = AtomicInteger(0)
val callCountB = AtomicInteger(0)
val transformers = getTransformers()

// transformer B:处理 InnerClass 时反向触发 OuterClass 加载
transformers.computeIfAbsent("com/example/InnerClass") { mutableListOf() }
.add { bytes ->
callCountB.incrementAndGet()
JvmtiBackend.onClassFileLoad(null, "com/example/OuterClass", byteArrayOf(0xCA.toByte()))
bytes
}

// transformer A:处理 OuterClass 时触发 InnerClass 加载
transformers.computeIfAbsent("com/example/OuterClass") { mutableListOf() }
.add { bytes ->
callCountA.incrementAndGet()
JvmtiBackend.onClassFileLoad(null, "com/example/InnerClass", byteArrayOf(0xFE.toByte()))
bytes
}

// 入口:加载 OuterClass
val result = JvmtiBackend.onClassFileLoad(null, "com/example/OuterClass", byteArrayOf(1, 2, 3))

// A 执行 1 次,B 被防重入拦截不执行
assertEquals(1, callCountA.get())
assertEquals(0, callCountB.get())
assertNotNull(result)
}

@Test
@DisplayName("单类自引用递归(transformer 内部再次触发同一类的 onClassFileLoad)被拦截")
fun selfRecursiveTransformerIsPrevented() {
val callCount = AtomicInteger(0)
val transformers = getTransformers()

transformers.computeIfAbsent("com/example/SelfRef") { mutableListOf() }
.add { bytes ->
val depth = callCount.incrementAndGet()
if (depth > 100) {
fail<Unit>("递归深度超过 100,防重入保护失效")
}
JvmtiBackend.onClassFileLoad(null, "com/example/SelfRef", bytes)
bytes
}

val result = JvmtiBackend.onClassFileLoad(null, "com/example/SelfRef", byteArrayOf(1))

assertEquals(1, callCount.get())
assertNotNull(result)
}

@Test
@DisplayName("三层循环依赖 A→B→C→A 被防重入保护打断")
fun deepRecursiveChainIsBroken() {
val counts = ConcurrentHashMap<String, AtomicInteger>()
val transformers = getTransformers()

for (cls in listOf("com/example/A", "com/example/B", "com/example/C")) {
counts[cls] = AtomicInteger(0)
val nextCls = when (cls) {
"com/example/A" -> "com/example/B"
"com/example/B" -> "com/example/C"
else -> "com/example/A"
}
transformers.computeIfAbsent(cls) { mutableListOf() }
.add { bytes ->
counts[cls]!!.incrementAndGet()
JvmtiBackend.onClassFileLoad(null, nextCls, byteArrayOf(0))
bytes
}
}

JvmtiBackend.onClassFileLoad(null, "com/example/A", byteArrayOf(1))

assertEquals(1, counts["com/example/A"]!!.get())
assertEquals(0, counts["com/example/B"]!!.get())
assertEquals(0, counts["com/example/C"]!!.get())
}

// ===== 线程隔离 =====

@Test
@DisplayName("防重入标记是 ThreadLocal 的,不阻塞其他线程的正常 transformer 执行")
fun reentrantGuardIsPerThread() {
val threadBCount = AtomicInteger(0)
val latch = CountDownLatch(1)
val transformers = getTransformers()

transformers.computeIfAbsent("com/example/SharedClass") { mutableListOf() }
.add { bytes -> threadBCount.incrementAndGet(); bytes }

// 线程 A:手动设置重入标记模拟正在 weave
val threadA = Thread({
getReentrantGuard().set(true)
val result = JvmtiBackend.onClassFileLoad(null, "com/example/SharedClass", byteArrayOf(1))
assertNull(result)
latch.countDown()
}, "thread-A")

// 线程 B:正常调用
val threadB = Thread({
latch.await()
val result = JvmtiBackend.onClassFileLoad(null, "com/example/SharedClass", byteArrayOf(2))
assertNotNull(result)
}, "thread-B")

threadA.start()
threadB.start()
threadA.join(5000)
threadB.join(5000)

assertEquals(1, threadBCount.get())
}

// ===== 异常安全 =====

@Test
@DisplayName("transformer 抛异常后重入标记正确清除,后续调用不受影响")
fun reentrantGuardClearedAfterException() {
val transformers = getTransformers()
transformers.computeIfAbsent("com/example/ErrorClass") { mutableListOf() }
.add { throw RuntimeException("模拟异常") }

// 第一次:异常
JvmtiBackend.onClassFileLoad(null, "com/example/ErrorClass", byteArrayOf(1))

// 第二次:标记应已清除
val secondCount = AtomicInteger(0)
transformers["com/example/ErrorClass"]!!.clear()
transformers["com/example/ErrorClass"]!!.add { bytes -> secondCount.incrementAndGet(); bytes }

val result = JvmtiBackend.onClassFileLoad(null, "com/example/ErrorClass", byteArrayOf(2))
assertEquals(1, secondCount.get())
assertNotNull(result)
}
}
Loading
Loading