diff --git a/.github/release-notes/versions/1.8.5.md b/.github/release-notes/versions/1.8.5.md new file mode 100644 index 0000000..1220e52 --- /dev/null +++ b/.github/release-notes/versions/1.8.5.md @@ -0,0 +1,12 @@ +### PVM2 组件方法兼容修复 + +- 修复 Activity、Application、Service 等组件中的普通方法因包含字段、调用、数组元素或同步块而被叶子白名单整批排除的问题,统一进入共用编译器逐指令校验。 +- 修复分支和异常处理区中的可空接收者被静态拒绝的问题;方法长度上限从 512 扩大到 4096 个 DEX 代码单元,同时严格检查展开后的代码长度、寄存器、跳转和异常表边界。 +- 修复字符串常量的内嵌空字符、补充平面字符和驻留身份;修复祖先类引用、派生对象及接口默认实现中的父类调用分派。 +- PVM2 镜像升级至 v9,打包器、运行时和覆盖报告同步适配。初始化器、生成桥、特殊对象契约及其他未支持方法仍明确保留;零转换及必保方法未覆盖仍失败,不把跳过项计为已保护。 + +### 验证与使用说明 + +- 自建 Java/D8 样本中 46 个方法通过转换和最终产物验证;在 Android 9、15、16 模拟器及 Android 15 真机上完成 20 次启动、10,500 项执行断言,覆盖 16 KB 内存页、无资源表和原生接口默认方法。 +- 本地实际 APK 的限定选择范围为 21 个方法,18 个普通方法转换成功,3 个初始化器保留;完成五种安装/启动场景及生命周期、通知服务检查。该结果不代表全部业务流程或全部方法分支已验证。 +- 升级后需重新加固,使用配套打包器与运行时;已有 APK 不会自动获得修复。PVM2 需要 JDK 17 或更高版本,具体覆盖范围以本次产物逐方法报告为准,分享前请手动脱敏。 diff --git a/Cargo.lock b/Cargo.lock index ee885cc..2b87896 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2201,7 +2201,7 @@ dependencies = [ [[package]] name = "mocika-shield" -version = "1.8.4" +version = "1.8.5" dependencies = [ "dunce", "reqwest 0.12.28", @@ -2229,7 +2229,7 @@ dependencies = [ [[package]] name = "mocikashield" -version = "1.8.4" +version = "1.8.5" dependencies = [ "cc", "chacha20poly1305", @@ -3635,7 +3635,7 @@ dependencies = [ [[package]] name = "shield-cli" -version = "1.8.4" +version = "1.8.5" dependencies = [ "anyhow", "clap", @@ -3653,7 +3653,7 @@ dependencies = [ [[package]] name = "shield-core" -version = "1.8.4" +version = "1.8.5" dependencies = [ "anyhow", "chacha20poly1305", diff --git a/apps/shield-cli/Cargo.toml b/apps/shield-cli/Cargo.toml index 30e9d9b..395267d 100644 --- a/apps/shield-cli/Cargo.toml +++ b/apps/shield-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shield-cli" -version = "1.8.4" +version = "1.8.5" edition = "2021" authors = ["MocikaDev"] license = "MIT OR Apache-2.0" diff --git a/apps/shield-gui/package-lock.json b/apps/shield-gui/package-lock.json index cd45ed1..41514ea 100644 --- a/apps/shield-gui/package-lock.json +++ b/apps/shield-gui/package-lock.json @@ -1,12 +1,12 @@ { "name": "mocika-shield-gui", - "version": "1.8.4", + "version": "1.8.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mocika-shield-gui", - "version": "1.8.4", + "version": "1.8.5", "license": "MIT OR Apache-2.0", "dependencies": { "@radix-ui/react-dialog": "^1.1.19", diff --git a/apps/shield-gui/package.json b/apps/shield-gui/package.json index 2027415..0cb70a8 100644 --- a/apps/shield-gui/package.json +++ b/apps/shield-gui/package.json @@ -1,6 +1,6 @@ { "name": "mocika-shield-gui", - "version": "1.8.4", + "version": "1.8.5", "license": "MIT OR Apache-2.0", "private": true, "type": "module", diff --git a/apps/shield-gui/src-tauri/Cargo.toml b/apps/shield-gui/src-tauri/Cargo.toml index 012ff08..2d9eba3 100644 --- a/apps/shield-gui/src-tauri/Cargo.toml +++ b/apps/shield-gui/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mocika-shield" -version = "1.8.4" +version = "1.8.5" edition = "2021" license = "MIT OR Apache-2.0" publish = false diff --git a/apps/shield-gui/src-tauri/tauri.conf.json b/apps/shield-gui/src-tauri/tauri.conf.json index 82d7902..b6b93ee 100644 --- a/apps/shield-gui/src-tauri/tauri.conf.json +++ b/apps/shield-gui/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Shellsmith", - "version": "1.8.4", + "version": "1.8.5", "identifier": "dev.mocika.shield-gui", "build": { "beforeDevCommand": "npm run dev -- --host 127.0.0.1", diff --git a/apps/shield-gui/src/hooks/use-about-page.ts b/apps/shield-gui/src/hooks/use-about-page.ts index 35e0beb..58c4502 100644 --- a/apps/shield-gui/src/hooks/use-about-page.ts +++ b/apps/shield-gui/src/hooks/use-about-page.ts @@ -9,7 +9,7 @@ type AppInfo = { }; const defaultAppInfo: AppInfo = { - version: "1.8.4", + version: "1.8.5", git_hash: "dev", build_date: "unknown", }; diff --git a/crates/shield-core/Cargo.toml b/crates/shield-core/Cargo.toml index cbe45ad..a37d1df 100644 --- a/crates/shield-core/Cargo.toml +++ b/crates/shield-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shield-core" -version = "1.8.4" +version = "1.8.5" edition = "2021" authors = ["MocikaDev"] license = "MIT OR Apache-2.0" diff --git a/crates/shield-core/src/protection_report.rs b/crates/shield-core/src/protection_report.rs index 4b7d437..b5766a2 100644 --- a/crates/shield-core/src/protection_report.rs +++ b/crates/shield-core/src/protection_report.rs @@ -167,7 +167,7 @@ impl Pvm2Failure { ensure!( report.schema_version == 2 && report.code_version == 4 - && matches!(report.image_version, 7 | 8), + && matches!(report.image_version, 7..=9), "方法失败报告协议无效" ); let mut classes = BTreeSet::new(); @@ -453,7 +453,7 @@ impl MethodReport { ensure!( self.schema_version == 2 && self.code_version == 4 - && matches!(self.image_version, 7 | 8), + && matches!(self.image_version, 7..=9), "方法报告协议版本不受支持" ); ensure!( @@ -795,6 +795,12 @@ mod tests { "reason": reason, "artifact_verified": false, "plaintext_policy": null })).unwrap()); } + for image in [7, 8, 9, 10] { + detail.image_version = image; + detail.write(&path).unwrap(); + assert_eq!(Pvm2Failure::zero_methods(&path).is_ok(), image <= 9); + } + detail.image_version = 9; detail.write(&path).unwrap(); let failure = Pvm2Failure::zero_methods(&path).unwrap(); assert_eq!(failure.summary.selected_classes, 1); @@ -995,14 +1001,14 @@ mod tests { ); } } - for image in [7, 8] { + for image in [7, 8, 9] { let mut candidate: MethodReport = serde_json::from_value(data.clone()).unwrap(); candidate.image_version = image; assert!(candidate .validate(dir.path(), &code, &modules, &BTreeSet::new(), 1, &inputs) .is_ok()); } - for (schema, image) in [(1, 7), (2, 6), (2, 9)] { + for (schema, image) in [(1, 7), (2, 6), (2, 10)] { let mut candidate: MethodReport = serde_json::from_value(data.clone()).unwrap(); candidate.schema_version = schema; candidate.image_version = image; diff --git a/docs/usage.md b/docs/usage.md index d2031ea..6780c56 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -543,11 +543,13 @@ shield protect --input input.apk --output protected.apk \ 启用 PVM2 的成功输出旁会生成 `protected.apk.protection.json` 或 `protected.aab.protection.json`。统计分别展示转换成功率、选中方法覆盖率、原始指令字节覆盖率和必保方法覆盖率;未声明必保方法时该指标为 `null`,不记为 100%。这些百分比仅针对选择范围,不代表整个应用。报告包含逐方法原因、最终索引与产物摘要,不包含密钥或明文镜像,也不进入安装包。 -选择范围在兼容性过滤**之前**统计。UI/导航、数据/资源辅助和媒体命名不再直接排除整类:没有对象状态访问、外部调用或同步的独立方法,可以继续进入 PVM2 指令与产物检查。支持基本类型计算,以及对象/数组参数未使用、引用透传、空值分支和引用身份比较;引用参数不会再仅因其类型被排除,但数组元素和字段访问仍不在这一兼容放行范围内。私有、静态和实例方法适用相同的方法体检查;继承父类或实现接口本身不再排除普通实例方法。跳板保留原声明、访问标志、参数与接收者,虚方法分派仍由 ART 完成;`hashCode/finalize` 等特殊对象契约仍保守保留。分支、循环和数值运算仍须通过现有编译器检查,不能只凭命名认定可转换。 +选择范围在兼容性过滤**之前**统计。UI、导航和 Android 组件中的普通方法不再因包含字段、方法调用、数组元素或同步块而被叶子方法白名单整批排除;它们与普通业务类共用编译器,逐指令校验后再决定是否转换。跳板保留原声明、访问标志、参数与接收者,虚方法分派仍由 ART 完成。父类调用保留声明类的直接父类信息,接口默认实现单独按原接口调用,不能由接收者的动态类型推断。 -独立方法中的异常处理不再仅因存在异常表而被排除。捕获、异常透传、重新抛出和不访问外部状态的清理逻辑继续经过异常表映射、控制流及指令检查;`throw null` 产生可被捕获的 `NullPointerException`。这不意味着任意 `try/catch/finally` 都受支持:异常区中的外部调用、字段、数组元素或同步仍受上述限制。 +这一路径支持经回归验证的字段读写、对象创建、静态/虚拟/接口/直接/父类调用、数组元素、空值调用、异常捕获与 finally、同步块。空值本身不再作为静态拒绝条件,运行时在原调用位置抛出并派发 `NullPointerException`。方法仍受支持指令、32 个寄存器(含编译临时寄存器)、4096 个 DEX 代码单元、编译后代码 65535 字节、分支偏移和异常表映射等边界约束;不是任意方法都能转换。标准 UTF-8 字符串保留内嵌空字符、补充平面字符及驻留身份;孤立 UTF-16 代理项明确拒绝,不能静默替换。 -构造器、静态初始化器、生成桥接/合成方法和仍涉及框架状态的候选保持原兼容边界;放行继承类的独立方法不等于放行包含 `invoke-super` 等外部调用的方法体。跳过项仍计入覆盖率分母,并保留 `ui_runtime_glue`、`compiler_generated_bridge`、`data_resource_runtime_glue` 或 `playback_compatibility` 等实际分类。精确必保清单同样受方法兼容检查约束,必保项被排除时任务失败,不会自动降级。所有候选都不可转换时仍返回 `PVM2_ZERO_METHODS`,不会把零转换记为成功。这些规则不代表已识别应用全部关键业务;完整覆盖率需用配套的新打包器和运行时重新生成。 +数据、资源和媒体框架的专用兼容规则仍仅放行不访问外部状态、不调用其他方法的独立叶子方法。构造器、静态初始化器、生成桥接/合成方法、组件中的声明同步方法及 `hashCode/finalize` 等特殊对象契约仍保留。同步块已支持不代表所有声明同步方法都已开放。跳过项始终计入覆盖率分母,并保留实际分类。 + +精确必保清单同样受方法兼容检查约束,必保项被排除时任务失败,不会自动降级。所有候选都不可转换时仍返回 `PVM2_ZERO_METHODS`。本轮镜像协议为 v9,需要使用配套打包器和运行时重新加固;旧运行时拒绝新镜像,新运行时继续读取旧协议。覆盖报告只证明选择范围内的产物转换,不代表全部关键业务或全部运行路径已验收。 兼容排除的本地原因带有首个阻断条件,例如 `ui_runtime_glue:virtual_contract`;脱敏摘要分别统计特殊对象方法或虚方法契约未验证、同步方法及叶子方法白名单之外的指令,并兼容旧打包器返回的异常区排除原因。它们表示准入限制,不能据此认定方法一定是生命周期回调。旧报告没有细分信息时仍按原分类展示,不能反推具体方法体。基础预检通过也不代表 PVM2 方法覆盖已经通过。 diff --git a/shield-stub/compat/api19-rust/Cargo.lock b/shield-stub/compat/api19-rust/Cargo.lock index 5247f87..d23f88d 100644 --- a/shield-stub/compat/api19-rust/Cargo.lock +++ b/shield-stub/compat/api19-rust/Cargo.lock @@ -242,7 +242,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "mocikashield-api19" -version = "1.8.4" +version = "1.8.5" dependencies = [ "cc", "chacha20poly1305", diff --git a/shield-stub/compat/api19-rust/Cargo.toml b/shield-stub/compat/api19-rust/Cargo.toml index bdf2769..adf04c9 100644 --- a/shield-stub/compat/api19-rust/Cargo.toml +++ b/shield-stub/compat/api19-rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mocikashield-api19" -version = "1.8.4" +version = "1.8.5" edition = "2021" license = "MIT OR Apache-2.0" build = "../../src/main/rust/build.rs" diff --git a/shield-stub/src/main/rust/Cargo.toml b/shield-stub/src/main/rust/Cargo.toml index d70838a..e6bf3c2 100644 --- a/shield-stub/src/main/rust/Cargo.toml +++ b/shield-stub/src/main/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mocikashield" -version = "1.8.4" +version = "1.8.5" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/tests/README.md b/tests/README.md index 411575d..805599b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -45,11 +45,13 @@ Windows classpath 分隔符改为 `;`。输出目录必须不存在。检查恢 ## PVM2 必保方法清理与 v8 声明参数 -`make test-pvm2` 验证 schema 2 报告及认证策略,`make test-pvm2-runtime` 用实际 Java 编译器/加密器生成三种 ISA 的普通、必保和异常镜像,再由生产 C++ 在宿主 JVM 中执行。断言覆盖普通缓存保留、必保方法正常/异常退出清理、递归与跨线程最后引用释放、8 线程竞争,以及旧 v6 解析和未知/截断策略拒绝。可用 `PVM2_SANITIZER=thread python3 tests/scripts/check-pvm2-runtime.py` 检查竞争;默认使用 ASan/UBSan。宿主结果不代替 Android 设备或性能验收。 +`make test-pvm2` 验证 schema 2 报告及认证策略,`make test-pvm2-runtime` 用实际 Java 编译器/加密器生成三种 ISA 的普通、必保和异常镜像,再由生产 C++ 在宿主 JVM 中执行。断言覆盖普通缓存保留、必保方法正常及异常返回后的清理、递归和并发、认证失败不使用明文及阻断时的清理。 -同一检查包含有限的结构变换试验:六种整数比较、条件反转加跳转、三种 ISA,在生产解释器中执行 900 次边界对照;恢复检查对 180 个随机镜像再次规范化比较关系。该规则仍可跨构建复用,因此没有将这种简单变换启用为生产抗还原策略。接收者准入另覆盖等值边、未知值、字段/数组访问的正常边、对象与结果共用寄存器、绕过访问的分支汇合、宽值双寄存器覆盖、lit8 高位目标和异常入口覆盖 `this` 的正反例。写寄存器范围复用已锁定 dexlib2 的指令元数据。 新增异常边只传播实际可抛异常指令执行前的状态,汇合所有前驱;`instanceof` 成功边可传播源对象非空,任何源/结果写入及不一致汇合都会使关系失效。生产 JNI 对照另覆盖两种语义、三种 ISA 的 900 次类型检查和 catch 路径。 +可空接收者不再因分支或异常区被静态拒绝。生产 JNI 对照覆盖两种接收者语义、三种 ISA 的 900 次正常与异常路径;结构检查仍拒绝损坏指令、非指令边界的跳转、错误载荷、越界调用寄存器及异常处理表。代码长度检查覆盖 4096/4097 个代码单元边界、数组载荷展开超过 65535 字节的拒绝,以及不丢失字符的 UTF-8 编码边界。 -v8 在寄存器映射后追加参数数量及规范化类型表,并与镜像一起认证。对象包装类按声明保留为引用,`long`/`double` 仅在声明为基本类型时占两格。检查覆盖五类包装引用、null、混合宽参数、错误数量/类型拒绝,以及 v6/v7 解析兼容。新产物必须重建标准资源和 Packer;旧资源在打包前被版本校验拒绝。 +v8 追加经认证的参数类型表,包装对象按声明保留引用。v9 的 `invoke-super` 方法引用记录 `直接父类|原引用类型->方法签名`,类调用从声明父类分派,接口调用保留原接口;旧运行时因协议版本不匹配拒绝新镜像。新运行时仍能解析 v6/v7/v8,未知未来版本必须拒绝。必须一起重建打包器和标准壳资源。 + +`ANDROID_SDK_ROOT=/path/to/sdk python3 tests/scripts/check-pvm2-art.py --output target/pvm2-art --serial 明确设备序列号` 生成自有 Java/D8 整包,实际验证未加固基线、加固首次启动、再次启动、覆盖安装及装回基线。默认 46 个方法通过最终产物验证,每次执行 525 项断言,覆盖字段/数组/对象状态、调用、空值和异常、锁竞争、较大方法、字符串空字符/补充平面字符/驻留身份,以及祖先引用和派生接收者下的父类调用。`--min-api 26` 额外保留原生接口默认方法,默认 23 验证 D8 脱糖;`--without-resource-table` 验证无资源表。省略 `--serial` 只构建检查,不能当作执行通过。同一设备重复安装必须复用输出目录内的测试证书。 加载器复制优化的检查为 `shield-stub` 的 `testReleaseUnitTest` 与 `cargo test --manifest-path shield-stub/src/main/rust/Cargo.toml bin_loader`:前者检查完整 ZIP 字节及伪造长度拒绝,后者检查借用密文原地址、v5/v6 全部截断位置和原认证往返。`MOCIKA_RUNTIME_PROFILE=1` 的诊断构建可输出冷解密和缓存命中阶段的 Java/Native 堆分配观测;发布性能对照使用未启用诊断的资源,堆分配数也不等于 PSS 或泄漏结论。 diff --git a/tests/java/com/yqsh/protector/packer/Pvm2ProtectionCheck.java b/tests/java/com/yqsh/protector/packer/Pvm2ProtectionCheck.java index c333ac7..e34f9d6 100644 --- a/tests/java/com/yqsh/protector/packer/Pvm2ProtectionCheck.java +++ b/tests/java/com/yqsh/protector/packer/Pvm2ProtectionCheck.java @@ -19,87 +19,8 @@ public final class Pvm2ProtectionCheck { private static final String KEY = "000102030405060708090a0b0c0d0e0f"; public static void main(String[] args) throws Exception { - // if-ne 的相等落空边、if-eq 的相等跳转边均可继承 this 的非空事实。 - short[] equalFallthrough = {0x1033, 5, 0x106e, 0, 1, 0x000e}; - assert Pvm2Safety.check(equalFallthrough, 2, 2, false, false) == null; - assert Pvm2Safety.check(new short[]{0x1032, 3, 0x000e, 0x106e, 0, 1, 0x000e}, 2, 2, false, false) == null; - short[] unequalFallthrough = equalFallthrough.clone(); unequalFallthrough[0] = 0x1032; - assert Pvm2Safety.check(unequalFallthrough, 2, 2, false, false) != null; - assert Pvm2Safety.check(equalFallthrough, 2, 2, true, false) != null; - assert Pvm2Safety.check(new short[]{0x1033, 6, 0x0112, 0x106e, 0, 1, 0x000e}, 2, 2, false, false) != null; - // 首次调用可能抛 NPE,但正常边之后的控制流可以复用同一接收者。 - short[] repeated = {0x106e, 0, 1, 0x0038, 5, 0x106e, 0, 1, 0x000e}; - assert Pvm2Safety.check(repeated, 2, 2, true, false) == null; - assert Pvm2Safety.check(repeated, 2, 2, true, true) != null; - // 字段/数组正常边可证明对象非空,但读取结果、写入值和异常边不能继承该事实。 - for (short[] access : List.of(new short[]{0x1021}, new short[]{0x1052, 0}, - new short[]{0x1054, 0}, new short[]{0x1059, 0}, - new short[]{0x0044, 0x0201}, new short[]{0x004b, 0x0201})) { - short[] flow = Arrays.copyOf(access, access.length + 6); - short[] tail = {0x0238, 5, 0x106e, 0, 1, 0x000e}; - System.arraycopy(tail, 0, flow, access.length, tail.length); - assert Pvm2Safety.check(flow, 3, 3, true, false) == null; - flow[access.length + 4] = 0; // v0 的字段值/数组元素仍可能为空。 - assert Pvm2Safety.check(flow, 3, 3, true, false) != null; - } - assert Pvm2Safety.check(new short[]{0x1154, 0, 0x0238, 5, 0x106e, 0, 1, 0x000e}, 3, 3, true, false) != null; - assert Pvm2Safety.check(new short[]{0x0146, 0x0201, 0x0238, 5, 0x106e, 0, 1, 0x000e}, 3, 3, true, false) != null; - // 一条路径绕过解引用时,汇合点仍按未知处理。 - assert Pvm2Safety.check(new short[]{0x0238, 4, 0x1052, 0, 0x106e, 0, 1, 0x000e}, 3, 3, true, false) != null; - // wide 写入必须清掉第二格,不能继续相信原来的非空值。 - for (short[] write : List.of(new short[]{0x0004}, new short[]{0x0005, 0}, - new short[]{0x0006, 0, 0}, new short[]{0x000b}, new short[]{0x0016, 0}, - new short[]{0x0017, 0, 0}, new short[]{0x0018, 0, 0, 0, 0}, new short[]{0x0019, 0}, - new short[]{0x0045, 0}, new short[]{0x0053, 0}, new short[]{0x0061, 0}, - new short[]{0x007d}, new short[]{0x0081}, new short[]{0x0083}, new short[]{0x0086}, - new short[]{0x009b, 0}, new short[]{0x00ab, 0}, new short[]{0x00bb}, new short[]{0x00cb})) { - short[] flow = new short[write.length + 8]; - flow[0] = 0x011a; - System.arraycopy(write, 0, flow, 2, write.length); - short[] tail = {0x0238, 5, 0x106e, 0, 1, 0x000e}; - System.arraycopy(tail, 0, flow, 2 + write.length, tail.length); - assert Pvm2Safety.check(flow, 3, 0, true, false) != null; - } - // lit8 的目标 v16 不能被错误解码为 v0;对象自移动和 check-cast 保留事实。 - assert Pvm2Safety.check(new short[]{0x101a, 0, 0x10d8, 0, 0x0038, 5, 0x0174, 0, 16, 0x000e}, 17, 0, true, false) != null; - assert Pvm2Safety.check(new short[]{0x011a, 0, 0x1107, 0x011f, 0, 0x0038, 5, 0x106e, 0, 1, 0x000e}, 2, 0, true, false) == null; - // this 寄存器在 try 前被覆盖后,catch 不能重新把它当成入口时的非空 this。 - var tryConstructor = com.android.dex.Code.Try.class.getDeclaredConstructor(int.class, int.class, int.class); - tryConstructor.setAccessible(true); - var caught = new com.android.dex.Code(2, 2, 1, 0, - new short[]{0x1007, 0x001f, 0, 0x000e, 0x010d, 0x106e, 0, 0, 0x000e}, - new com.android.dex.Code.Try[]{tryConstructor.newInstance(1, 2, 0)}, - new com.android.dex.Code.CatchHandler[]{new com.android.dex.Code.CatchHandler(new int[0], new int[0], 4, 0)}); - assert Pvm2Safety.check(caught, false) != null; - // 未改写的 this 可以沿真实抛异常边进入 catch;只有正常边建立的事实不可沿异常边传播。 - for (boolean preserved : new boolean[]{true, false}) { - short[] instructions = {preserved ? (short)0x0000 : (short)0x1007, - 0x001f, 0, 0x000e, 0x010d, 0x106e, 0, 0, 0x000e}; - var precise = new com.android.dex.Code(2, 2, 1, 0, instructions, - new com.android.dex.Code.Try[]{tryConstructor.newInstance(1, 2, 0)}, caught.getCatchHandlers()); - assert (Pvm2Safety.check(precise, false) == null) == preserved; - } - // 两条抛异常路径汇合,任一边覆盖 this 都必须拒绝;不抛异常的覆盖不污染 catch。 - for (boolean secondThrow : new boolean[]{false, true}) { - short[] instructions = {0x001f, 0, 0x1007, secondThrow ? (short)0x001f : (short)0x0000, - 0, 0x000e, 0x010d, 0x106e, 0, 0, 0x000e}; - var merged = new com.android.dex.Code(2, 2, 1, 0, instructions, - new com.android.dex.Code.Try[]{tryConstructor.newInstance(0, 5, 0)}, - new com.android.dex.Code.CatchHandler[]{new com.android.dex.Code.CatchHandler(new int[0], new int[0], 6, 0)}); - assert (Pvm2Safety.check(merged, false) == null) == !secondThrow; - } - // instanceof 的真假边、覆盖及汇合:仅为真且关系仍有效时可证明接收者。 - assert Pvm2Safety.check(new short[]{0x1020, 0, 0x0038, 5, 0x106e, 0, 1, 0x000e}, 2, 1, true, false) == null; - assert Pvm2Safety.check(new short[]{0x1020, 0, 0x0039, 3, 0x000e, 0x106e, 0, 1, 0x000e}, 2, 1, true, false) == null; - assert Pvm2Safety.check(new short[]{0x1020, 0, 0x0039, 5, 0x106e, 0, 1, 0x000e}, 2, 1, true, false) != null; - for (short write : new short[]{0x0112, 0x0012, 0x0016}) { - short[] overwritten = write == 0x0016 - ? new short[]{0x1020, 0, write, 0, 0x0038, 5, 0x106e, 0, 1, 0x000e} - : new short[]{0x1020, 0, write, 0x0038, 5, 0x106e, 0, 1, 0x000e}; - assert Pvm2Safety.check(overwritten, 2, 1, true, false) != null; - } - assert Pvm2Safety.check(new short[]{0x0238, 4, 0x1020, 0, 0x0038, 5, 0x106e, 0, 1, 0x000e}, 3, 2, true, false) != null; Path root = Path.of(args[0]); + checkInstructionBoundaries(root); checkEmptyResourceTable(root); checkZeroMethodReports(root); checkMethodCompatibility(root); @@ -358,6 +279,76 @@ private static void writeClasses(Path path, List classes) thr try { pool.writeTo(store); } finally { store.close(); } } + private static void checkInstructionBoundaries(Path root) throws Exception { + // 分支后的可空调用是合法程序;空值及 catch/finally 的结果由 ART 整包回归验证。 + for (short[] instructions : List.of(new short[]{0x1033, 5, 0x106e, 0, 1, 0x000e}, + new short[]{0x1032, 5, 0x106e, 0, 1, 0x000e}, + new short[]{0x0238, 4, 0x1052, 0, 0x106e, 0, 1, 0x000e})) { + var code = new com.android.dex.Code(3, 2, 1, 0, instructions, + new com.android.dex.Code.Try[0], new com.android.dex.Code.CatchHandler[0]); + assert Pvm2Safety.check(code) == null; + } + for (short[] malformed : List.of(new short[]{0x106e}, // 截断指令 + new short[]{0x0228, 0x106e, 0, 1, 0x000e}, // 跳入指令中间 + new short[]{0x1028}, // 跳出方法 + new short[]{0x006e, 0, 0, 0x000e}, // 实例调用缺接收者 + new short[]{0x606e, 0, 0, 0x000e}, // 35c 超过五格 + new short[]{0x106e, 0, 3, 0x000e}, // 接收者越界 + new short[]{0x2074, 0, 2, 0x000e}, // range 尾寄存器越界 + new short[]{0x002b, 4, 0, 0, 0x0100, 1, 0, 0, 1, 0})) { // case 跳入指令中间 + var code = new com.android.dex.Code(3, 2, 1, 0, malformed, + new com.android.dex.Code.Try[0], new com.android.dex.Code.CatchHandler[0]); + assert Pvm2Safety.check(code) != null : Arrays.toString(malformed); + } + var constructor = com.android.dex.Code.Try.class.getDeclaredConstructor(int.class, int.class, int.class); + constructor.setAccessible(true); + for (int start : new int[]{0, 1}) for (int handler : new int[]{1, 4}) { + var code = new com.android.dex.Code(2, 2, 1, 0, + new short[]{0x106e, 0, 1, 0x000e, 0x000d, 0x000e}, + new com.android.dex.Code.Try[]{constructor.newInstance(start, 3 - start, 0)}, + new com.android.dex.Code.CatchHandler[]{new com.android.dex.Code.CatchHandler(new int[0], new int[0], handler, 0)}); + assert (Pvm2Safety.check(code) == null) == (start == 0 && handler == 4); + } + // 字符串可包含空字符和代理对,但不能在 UTF-8 编码时静默替换孤立代理项。 + for (String text : List.of("中文\u0000\ud83d\ude00", new String(new char[]{0xd800}), "a".repeat(65536))) { + var method = new ImmutableMethod(OWNER, "text", List.of(), "Ljava/lang/String;", 9, Set.of(), Set.of(), + new ImmutableMethodImplementation(1, List.of( + new ImmutableInstruction21c(Opcode.CONST_STRING, 0, new ImmutableStringReference(text)), + new ImmutableInstruction11x(Opcode.RETURN_OBJECT, 0)), List.of(), List.of())); + Path path = root.resolve("string-boundary.dex"); + writeClasses(path, List.of(calculationOwner(OWNER, "Ljava/lang/Object;", List.of(), List.of(method)))); + var dex = new com.android.dex.Dex(Files.readAllBytes(path)); + var cls = dex.classDefs().iterator().next(); + var code = dex.readCode(dex.readClassData(cls).getDirectMethods()[0]); + var result = Pvm2Compiler.tryCompile(dex, code, "Ljava/lang/String;", true); + if (text.length() == 1) assert result.failReason.equals("unpaired UTF-16 surrogate"); + else if (text.length() > 65535) assert result.failReason.equals("string too long"); + else assert result.isOk() : result.failReason; + } + var superCall = new com.android.dex.Code(2, 2, 2, 0, + new short[]{0x206f, 0, 0x0010, 0x000a, 0x000f}, + new com.android.dex.Code.Try[0], new com.android.dex.Code.CatchHandler[0]); + assert Pvm2Compiler.tryCompile(null, superCall, "I", false, null, false, new String[]{"I"}) + .failReason.equals("invoke-super requires declaring class context"); + // 数组载荷展开不能溢出镜像的 u16 代码长度字段。 + short[] expanded = new short[4010]; + expanded[0] = 0x0026; expanded[1] = 6; expanded[3] = 0x000e; + expanded[6] = 0x0300; expanded[7] = 1; expanded[8] = 8000; + var expandedCode = new com.android.dex.Code(1, 1, 0, 0, expanded, + new com.android.dex.Code.Try[0], new com.android.dex.Code.CatchHandler[0]); + assert Pvm2Compiler.tryCompile(null, expandedCode, "V", true, null, false, new String[]{"[B"}) + .failReason.equals("compiled code too large"); + // 长度上限在编译前生效,零寄存器静态 void 合法。 + for (int size : new int[]{1, 4096, 4097}) { + short[] units = new short[size]; units[size - 1] = 0x000e; + var code = new com.android.dex.Code(0, 0, 0, 0, units, + new com.android.dex.Code.Try[0], new com.android.dex.Code.CatchHandler[0]); + var result = Pvm2Compiler.tryCompile(null, code, "V", true); + assert result.isOk() == (size <= 4096) : result.failReason; + if (size > 4096) assert result.failReason.equals("too large"); + } + } + private static void checkMethodCompatibility(Path root) throws Exception { for (String reason : List.of("ui_runtime_glue", "data_resource_runtime_glue", "compiler_generated_bridge", "playback_compatibility")) { assert Pvm2Admission.classify(reason) == Pvm2Admission.SkipReason.compatibility; @@ -407,7 +398,7 @@ private static void checkMethodCompatibility(Path root) throws Exception { var obfuscated = calculation("Lfixture/a;", "callback", 1); assert AutoVmpPolicy.exclusionReason(calculationOwner("Lfixture/a;", "Landroid/app/Activity;", List.of(), List.of()), obfuscated) == null; assert AutoVmpPolicy.exclusionReason(calculationOwner("Lfixture/a;", "Ljava/lang/Object;", List.of("Landroid/view/View$OnClickListener;"), List.of()), obfuscated) == null; - // 字段/数组元素访问、调用和锁仍不能借叶子方法规则放行。 + // 组件转交共用编译器逐指令验证;数据与媒体框架仍保留专用兼容边界。 var field = new ImmutableFieldReference(type, "state", "I"); var call = new ImmutableMethodReference(type, "external", List.of(), "I"); for (var instruction : List.of(new ImmutableInstruction21c(Opcode.SGET, 0, field), new ImmutableInstruction22c(Opcode.IGET, 0, 1, field), @@ -418,7 +409,9 @@ private static void checkMethodCompatibility(Path root) throws Exception { new ImmutableInstruction21c(Opcode.NEW_INSTANCE, 0, new ImmutableTypeReference(type)))) { var method = new ImmutableMethod(type, "stateful", List.of(), "I", 9, Set.of(), Set.of(), new ImmutableMethodImplementation(3, List.of(instruction, new ImmutableInstruction11x(Opcode.RETURN, 0)), List.of(), List.of())); - assert AutoVmpPolicy.exclusionReason(plain, method).equals("ui_runtime_glue:non_leaf_instruction") : instruction.getOpcode(); + assert AutoVmpPolicy.exclusionReason(plain, method) == null : instruction.getOpcode(); + for (String owner : List.of("Lfixture/data/Rules;", "Lfixture/audio/Rules;")) + assert AutoVmpPolicy.exclusionReason(calculationOwner(owner, "Ljava/lang/Object;", List.of(), List.of()), method) != null; } Path work = Files.createDirectory(root.resolve("method-compatibility")); diff --git a/tests/native/pvm2_runtime_check.cpp b/tests/native/pvm2_runtime_check.cpp index 01bd454..e8e68ab 100644 --- a/tests/native/pvm2_runtime_check.cpp +++ b/tests/native/pvm2_runtime_check.cpp @@ -98,7 +98,7 @@ int main(int argc, char** argv) { if (i >= 98) { std::ifstream input(std::string(argv[1]) + "/image" + std::to_string(i - 98) + ".bin", std::ios::binary); bytes.assign(std::istreambuf_iterator(input), {}); - assert(bytes.size() > 18 && bytes[4] == PVM2_VERSION_V8 && bytes[15] == (i - 98) % 3 && bytes[18] == (i >= 101 ? 1 : 0)); + assert(bytes.size() > 18 && bytes[4] == PVM2_VERSION_V9 && bytes[15] == (i - 98) % 3 && bytes[18] == (i >= 101 ? 1 : 0)); } if (i == 97) { bytes[16] = 1; } // 认证成功、字符串池截断 auto enc = record(bytes, key, i); @@ -437,6 +437,8 @@ int main(int argc, char** argv) { std::vector policy_image(std::istreambuf_iterator(policy_input), {}); assert(parse_pvm2(policy_image.data(), policy_image.size(), &parsed) && parsed.clear_after_use); auto legacy = policy_image; + legacy[4] = 8; + assert(parse_pvm2(legacy.data(), legacy.size(), &parsed) && parsed.clear_after_use); // 此样本参数为空,类型表在虚拟指令前只有一个 count 字节。 size_t args_offset = legacy.size() - (size_t(legacy[12]) | (size_t(legacy[13]) << 8)) - 1; legacy.erase(legacy.begin() + args_offset); @@ -448,7 +450,7 @@ int main(int argc, char** argv) { auto broken = policy_image; if (fault == 0) broken[18] = 2; if (fault == 1) broken.resize(18); - if (fault == 2) broken[4] = 9; + if (fault == 2) broken[4] = PVM2_VERSION_V9 + 1; assert(!parse_pvm2(broken.data(), broken.size(), &parsed)); assert(!parsed.valid && !parsed.clear_after_use && parsed.code.capacity() == 0); } diff --git a/tests/scripts/check-pvm2-art.py b/tests/scripts/check-pvm2-art.py index baf16c8..483cd06 100644 --- a/tests/scripts/check-pvm2-art.py +++ b/tests/scripts/check-pvm2-art.py @@ -27,7 +27,10 @@ int compute(int value); default int defaultValue(int value) { return value * 3; } } - public static class Base { public int compute(int value) { return -1; } } + public static class Base { + public int compute(int value) { return -1; } + public int sum(int a, int b, int c, int d, int e, int f) { return -2; } + } public static class RulesView extends Base implements Contract { @Override public int compute(int value) { return value + 7; } public long compute(long value) { return value ^ 0x123456789abcdefL; } @@ -97,10 +100,79 @@ } } public double floating(double value) { return value * 1.5d + 2.0d; } + private Object state; + private int count; + private long wide; + private double decimal; + private float fraction; + private boolean enabled; + private byte small; + private short medium; + private char character; + private static Object shared; + public Object fields(Object value) { + state = value; shared = value; count++; wide = -1234567890123L; + decimal = -3.25d; fraction = 1.5f; enabled = true; small = -7; medium = -1234; character = '\ufffe'; + if (wide != -1234567890123L || decimal != -3.25d || fraction != 1.5f || !enabled + || small != -7 || medium != -1234 || character != '\ufffe' || shared != value) + throw new AssertionError("字段损坏"); + return state; + } + public String text() { return "中文\u0000\ud83d\ude00"; } + public Object construct(Object value) { return new Holder(value).get(); } + public int calls(Contract other, int value) { + return other.compute(value) + privateValue(value) + staticValue(value) + Math.abs(value); + } + public String nullable(Object value, boolean clear) { + if (clear) value = null; + try { return value.toString(); } catch (NullPointerException error) { return "null"; } + } + public int invokeCatch(Holder value) { + try { return value.fail(); } catch (IllegalArgumentException error) { return 91; } + catch (NullPointerException error) { return 92; } finally { count++; } + } + public Object lockedBlock(Object lock, Object value, boolean fail) { + synchronized (lock) { + state = value; count++; + if (fail) throw new IllegalStateException("测试异常"); + return state; + } + } + public Object[] arrays(Object value) { + Object[] result = new Object[]{value, this, null}; + int[] numbers = new int[]{1, 2, 3}; + numbers[1] += numbers[0]; + result[2] = Integer.valueOf(numbers[1]); + return result; + } + public Object framework(android.content.Context context) { + android.widget.TextView view = new android.widget.TextView(context); + view.setText(text()); view.setTag(this); + return view; + } + public long invokeWide(Holder value, long a, double b, float c) { return value.wide(a, b, c); } + public int recursive(int value) { return value <= 0 ? 1 : value * recursive(value - 1); } + public int interfaceDefault(Contract value) { return value.defaultValue(11); } + public int defaultSuper(int value) { return Contract.super.defaultValue(value); } + @Override public int sum(int a, int b, int c, int d, int e, int f) { return a+b+c+d+e+f; } + /* LARGE_METHOD */ + + } + public static class Holder { + final Object value; + Holder(Object value) { this.value = value; } + Object get() { return value; } + int fail() { throw new IllegalArgumentException("测试异常"); } + long wide(long a, double b, float c) { return a + (long)b + (long)c; } } public static class ChildView extends RulesView { @Override public int compute(int value) { return value + 17; } @Override public Object self() { return this; } + public int parentCompute(int value) { return super.compute(value); } + public int parentMany(int a, int b, int c, int d, int e, int f) { return super.sum(a,b,c,d,e,f); } + } + public static class GrandChild extends ChildView { + @Override public int compute(int value) { return value + 100; } } public static class RejectedView extends Base { private int counter; @@ -178,9 +250,52 @@ } for (double value : new double[]{-Double.MAX_VALUE, -0.0d, 0.0d, Double.NaN, Double.POSITIVE_INFINITY, 3.25d}) check(Double.doubleToLongBits(parent.floating(value)) == Double.doubleToLongBits(value * 1.5d + 2.0d), "浮点特殊值"); + + for (Object input : new Object[]{null, first, array, parent}) { + check(parent.fields(input) == input && parent.construct(input) == input, "对象状态与构造"); + Object[] values = parent.arrays(input); + check(values[0] == input && values[1] == parent && values[2].equals(3), "对象和整数数组"); + check(parent.nullable(input, true).equals("null"), "分支空接收者捕获"); + } + check(parent.text() == "中文\u0000\ud83d\ude00", "字符串编码与驻留身份"); + check(parent.text().length() == 5 && parent.text().charAt(2) == 0, "字符串空字符和代理对"); + check(parent.calls(child, 7) == (24 + (7 ^ 0x5a) - 4 + 7), "接口私有静态嵌套调用"); + check(parent.nullable("value", false).equals("value") && parent.nullable(null, false).equals("null"), "调用异常语义"); + check(parent.invokeCatch(new Holder(first)) == 91 && parent.invokeCatch(null) == 92, "调用异常传播及清理"); + check(parent.invokeWide(new Holder(first), Long.MIN_VALUE, 12.75d, -2.5f) == Long.MIN_VALUE + 10, "宽参数调用"); + check(parent.recursive(6) == 720 && parent.interfaceDefault(child) == 33, "递归与默认接口调用"); + check(child.parentCompute(5) == 12, "父类非虚分派"); + for (int value : ints) { + int expected = value; + for (int i = 0; i < 160; i++) expected = (expected * 31) ^ i; + check(parent.large(value) == expected, "超过旧代码长度上限的方法"); + } + Object lock = new Object(); + check(parent.lockedBlock(lock, first, false) == first, "锁内对象状态"); + try { parent.lockedBlock(lock, other, true); throw new AssertionError("异常丢失"); } + catch (IllegalStateException expected) { check(parent.fields(first) == first, "锁异常传播"); } + Thread contender = new Thread(() -> { synchronized (lock) { } }); + contender.start(); contender.join(2000); check(!contender.isAlive(), "异常退出释放锁"); + try { parent.lockedBlock(null, first, false); throw new AssertionError("空锁未抛出"); } + catch (NullPointerException expected) { check(true, "空锁异常"); } + java.lang.reflect.Field counter = RulesView.class.getDeclaredField("count"); + counter.setAccessible(true); + int before = counter.getInt(parent); + Thread[] workers = new Thread[4]; + for (int i = 0; i < workers.length; i++) { + workers[i] = new Thread(() -> { for (int j = 0; j < 200; j++) parent.lockedBlock(lock, first, false); }); + workers[i].start(); + } + for (Thread worker : workers) { worker.join(5000); check(!worker.isAlive(), "竞争线程完成"); } + check(counter.getInt(parent) == before + 800, "跨线程同步块状态一致"); + android.widget.TextView widget = (android.widget.TextView)parent.framework(this); + check(widget.getTag() == parent && widget.getText().toString().equals(parent.text()), "真实框架对象调用"); RejectedView rejected = new RejectedView(); check(rejected.stateful() == 1 && rejected.stateful() == 2 && rejected.callOther(-3) == 3, "状态和调用保留"); check(rejected.superCompute(5) == -1 && rejected.locked() == 5 && rejected.hashCode() == 13, "super 同步与对象契约保留"); + check(new GrandChild().parentCompute(5) == 12, "祖先引用按声明父类分派"); + check(new GrandChild().parentMany(1,2,3,4,5,6) == 21, "range 父类调用"); + check(parent.defaultSuper(7) == 21, "接口默认父实现"); Log.e("Pvm2Regression", "PVM2_REGRESSION_OK token=" + getIntent().getStringExtra("run_token") + " checks=" + checks + " pages=" + android.system.Os.sysconf(android.system.OsConstants._SC_PAGESIZE)); } catch (Throwable error) { throw new AssertionError("PVM2 回归失败 checks=" + checks, error); } @@ -188,9 +303,12 @@ } ''' +SOURCE = SOURCE.replace('/* LARGE_METHOD */', 'public int large(int value) {\n' + '\n'.join('value = (value * 31) ^ ' + str(i) + ';' for i in range(160)) + '\nreturn value; }') + parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--serial', action='append', default=[], help='明确指定本轮有权测试的设备,可重复传入;省略只构建和检查覆盖') parser.add_argument('--output', type=Path, default=ROOT / 'target/pvm2-art-checks') +parser.add_argument('--min-api', type=int, choices=[23, 26], default=23, help='23 验证 D8 脱糖,26 验证原生接口默认方法') parser.add_argument('--without-resource-table', action='store_true', help='生成完全不带 resources.arsc 的 APK,默认保留合法空表') args = parser.parse_args() work = args.output.resolve() @@ -220,7 +338,51 @@ def run(command): run([bt / 'aapt2', 'link', '-o', work / 'base.apk', '--manifest', work / 'AndroidManifest.xml', '-I', android]) run(['javac', '--release', '8', '-cp', android, '-d', work / 'classes', work / 'CompatibilityActivity.java']) run(['jar', 'cf', work / 'classes.jar', '-C', work / 'classes', '.']) -run([bt / 'd8', '--release', '--lib', android, '--min-api', '23', '--output', work / 'dex', work / 'classes.jar']) +run([bt / 'd8', '--release', '--lib', android, '--min-api', str(args.min_api), '--output', work / 'dex', work / 'classes.jar']) +# 将合法 super 引用规范到祖先,覆盖混淆器可能产生的引用;基线也使用同一 DEX。 +(work / 'RewriteSuper.java').write_text(r'''import java.nio.file.*; +import java.util.*; +import com.android.tools.smali.dexlib2.*; +import com.android.tools.smali.dexlib2.dexbacked.*; +import com.android.tools.smali.dexlib2.iface.*; +import com.android.tools.smali.dexlib2.iface.instruction.*; +import com.android.tools.smali.dexlib2.iface.instruction.formats.*; +import com.android.tools.smali.dexlib2.immutable.instruction.*; +import com.android.tools.smali.dexlib2.immutable.reference.*; +import com.android.tools.smali.dexlib2.rewriter.*; +import com.android.tools.smali.dexlib2.writer.pool.*; +import com.android.tools.smali.dexlib2.writer.io.*; +public class RewriteSuper { + public static void main(String[] args) throws Exception { + var dex = new DexBackedDexFile(Opcodes.getDefault(), Files.readAllBytes(Path.of(args[0]))); + int[] changed = {0, 0}; + var rewriter = new DexRewriter(new RewriterModule() { + public Rewriter getInstructionRewriter(Rewriters rewriters) { + return instruction -> { + if (instruction.getOpcode() == Opcode.INVOKE_SUPER || instruction.getOpcode() == Opcode.INVOKE_SUPER_RANGE) { + var method = (com.android.tools.smali.dexlib2.iface.reference.MethodReference)((ReferenceInstruction)instruction).getReference(); + if (method.getDefiningClass().endsWith("$RulesView;")) { + var ref = new ImmutableMethodReference(method.getDefiningClass().replace("$RulesView;", "$Base;"), method.getName(), method.getParameterTypes(), method.getReturnType()); + if (instruction instanceof Instruction35c i) { + changed[0]++; + return new ImmutableInstruction35c(i.getOpcode(), i.getRegisterCount(), i.getRegisterC(), i.getRegisterD(), i.getRegisterE(), i.getRegisterF(), i.getRegisterG(), ref); + } + var i = (Instruction3rc)instruction; changed[1]++; + return new ImmutableInstruction3rc(i.getOpcode(), i.getStartRegister(), i.getRegisterCount(), ref); + } + } + return instruction; + }; + } + }); + var pool = new DexPool(Opcodes.getDefault()); + for (var cls : rewriter.getDexFileRewriter().rewrite(dex).getClasses()) pool.internClass(cls); + assert changed[0] > 0 && changed[1] > 0 : Arrays.toString(changed); + var output = new FileDataStore(Path.of(args[0]).toFile()); try { pool.writeTo(output); } finally { output.close(); } + } +}''') +run(['javac', '--release', '17', '-cp', ROOT / 'tools/xop-pvm2-packer.jar', '-d', work, work / 'RewriteSuper.java']) +run(['java', '-ea', '-cp', str(work) + os.pathsep + str(ROOT / 'tools/xop-pvm2-packer.jar'), 'RewriteSuper', work / 'dex/classes.dex']) with zipfile.ZipFile(work / 'base.apk') as base, zipfile.ZipFile(work / 'input-unsigned.apk', 'w') as archive: for entry in base.infolist(): if not args.without_resource_table or entry.filename != 'resources.arsc': @@ -243,6 +405,7 @@ def sign(source, destination): sign('protected.apk', 'protected-signed.apk') report = json.loads((work / 'protected-signed.apk.protection.json').read_text()) assert report['output_sha256'] == hashlib.sha256((work / 'protected-signed.apk').read_bytes()).hexdigest() +assert any(row['descriptor'].endswith('->large(I)I') and row['input_instruction_bytes'] > 1024 and row['artifact_verified'] for row in report['methods']) counts = Counter() for row in report['methods']: if '-><' in row['descriptor']: @@ -250,20 +413,19 @@ def sign(source, destination): elif row['descriptor'].endswith('$RulesView;->defaultValue(I)I'): # D8 为默认接口方法生成转发桥;它应留在 ART,运行时仍须验证接口调用结果。 assert not row['artifact_verified'] and row['reason'] == 'compiler_generated_bridge', row - elif '$RejectedView;' in row['descriptor']: + elif '$RejectedView;' in row['descriptor'] and row['descriptor'].split('->')[1].split('(')[0] not in ['stateful', 'callOther', 'superCompute']: assert not row['artifact_verified'], row - expected = {'stateful': 'ui_runtime_glue:non_leaf_instruction', 'callOther': 'ui_runtime_glue:non_leaf_instruction', - 'superCompute': 'ui_runtime_glue:non_leaf_instruction', 'locked': 'ui_runtime_glue:synchronization', + expected = {'locked': 'ui_runtime_glue:synchronization', 'hashCode': 'ui_runtime_glue:virtual_contract', 'nativeCall': 'no_code'} name = row['descriptor'].split('->')[1].split('(')[0] assert row['reason'] == expected[name], row else: assert row['artifact_verified'], row counts['verified' if row['artifact_verified'] else row['reason']] += 1 -assert counts['verified'] == 26 and len(report['methods']) == 36, counts -print('Java/D8 整包:26 个方法验证通过,3 个初始化方法、1 个 D8 桥接方法和 6 个明确不支持的方法保留。', flush=True) +assert counts['verified'] == 46 and len(report['methods']) == (53 if args.min_api == 23 else 52), counts +print('Java/D8 整包:46 个方法验证通过,初始化、桥接、同步声明和特殊对象契约保留。', flush=True) results = {'coverage': dict(counts), 'apk_sha256': report['output_sha256'], - 'resource_table': 'absent' if args.without_resource_table else 'empty', + 'resource_table': 'absent' if args.without_resource_table else 'empty', 'min_api': args.min_api, 'source_sha256': hashlib.sha256(SOURCE.encode()).hexdigest(), 'devices': []} (work / 'result.json').write_text(json.dumps(results, ensure_ascii=False, indent=2) + '\n') for serial in args.serial: @@ -291,7 +453,7 @@ def sign(source, destination): match = re.search(re.escape(marker) + r'(\d+) pages=(\d+)', logs) assert match and 'FATAL EXCEPTION' not in logs, str(path) checks = int(match.group(1)) - assert checks == 482, checks + assert checks == 525, checks device['page_size'] = int(match.group(2)) assert run(adb + ['shell', 'pidof', package]).strip(), str(path) device['scenarios'].append({'name': scenario, 'checks': checks}) diff --git a/third_party/xopprotector/native/src/main/cpp/vm/pvm2_format.cpp b/third_party/xopprotector/native/src/main/cpp/vm/pvm2_format.cpp index c534959..b0a4f74 100644 --- a/third_party/xopprotector/native/src/main/cpp/vm/pvm2_format.cpp +++ b/third_party/xopprotector/native/src/main/cpp/vm/pvm2_format.cpp @@ -118,7 +118,7 @@ bool parse_pvm2(const uint8_t* data, size_t size, Pvm2Image* out) { if (out->version != PVM2_VERSION_V1 && out->version != PVM2_VERSION_V2 && out->version != PVM2_VERSION_V3 && out->version != PVM2_VERSION_V4 && out->version != PVM2_VERSION_V5 && out->version != PVM2_VERSION_V6 - && out->version != PVM2_VERSION_V7 && out->version != PVM2_VERSION_V8) { + && out->version != PVM2_VERSION_V7 && out->version != PVM2_VERSION_V8 && out->version != PVM2_VERSION_V9) { PLOGE("PVM2 unsupported version %u", out->version); return false; } diff --git a/third_party/xopprotector/native/src/main/cpp/vm/pvm2_format.h b/third_party/xopprotector/native/src/main/cpp/vm/pvm2_format.h index 6b5cf6d..1f90a15 100644 --- a/third_party/xopprotector/native/src/main/cpp/vm/pvm2_format.h +++ b/third_party/xopprotector/native/src/main/cpp/vm/pvm2_format.h @@ -20,6 +20,7 @@ constexpr uint16_t PVM2_VERSION_V5 = 5; constexpr uint16_t PVM2_VERSION_V6 = 6; constexpr uint16_t PVM2_VERSION_V7 = 7; constexpr uint16_t PVM2_VERSION_V8 = 8; +constexpr uint16_t PVM2_VERSION_V9 = 9; /** Morph table size for v3 images. */ constexpr uint8_t PVM2_OP_COUNT_V3 = 40; diff --git a/third_party/xopprotector/native/src/main/cpp/vm/pvm2_interp.cpp b/third_party/xopprotector/native/src/main/cpp/vm/pvm2_interp.cpp index f90bb1c..34fe47d 100644 --- a/third_party/xopprotector/native/src/main/cpp/vm/pvm2_interp.cpp +++ b/third_party/xopprotector/native/src/main/cpp/vm/pvm2_interp.cpp @@ -831,6 +831,31 @@ static bool ensure_well_known_classes(JNIEnv* env) { return true; } +static jstring string_from_utf8(JNIEnv* env, const std::string& value) { + // 镜像使用标准 UTF-8,NewStringUTF 接受的却是 JNI 修改版 UTF-8。 + // 复用 Java 解码器保留内嵌空字符和代理对;const-string 必须维持 ART 的驻留身份。 + JniLocalFrame frame(env, 8); + if (!frame.active) return nullptr; + jclass cls = cache_global_class(env, "java/lang/String"); + if (cls == nullptr) return nullptr; + jmethodID ctor = env->GetMethodID(cls, "", "([BLjava/lang/String;)V"); + if (ctor == nullptr) return nullptr; + jmethodID intern = env->GetMethodID(cls, "intern", "()Ljava/lang/String;"); + if (intern == nullptr) return nullptr; + jbyteArray bytes = env->NewByteArray(static_cast(value.size())); + if (bytes == nullptr) return nullptr; + env->SetByteArrayRegion(bytes, 0, static_cast(value.size()), + reinterpret_cast(value.data())); + if (env->ExceptionCheck()) return nullptr; + jstring encoding = env->NewStringUTF("UTF-8"); + if (encoding == nullptr) return nullptr; + jobject text = env->NewObject(cls, ctor, bytes, encoding); + if (env->ExceptionCheck() || text == nullptr) return nullptr; + jobject result = env->CallObjectMethod(text, intern); + if (env->ExceptionCheck()) return nullptr; + return reinterpret_cast(frame.pop(result)); +} + /** * Resolve payload classes through the loader installed by the single-stub runtime. * @@ -878,7 +903,7 @@ static jclass find_class_desc(JNIEnv* env, const std::string& desc) { "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;") : nullptr; const std::string binary_name = descriptor_to_binary_name(desc); - jstring name = env->NewStringUTF(binary_name.c_str()); + jstring name = string_from_utf8(env, binary_name); if (for_name != nullptr && name != nullptr) { local = reinterpret_cast(env->CallStaticObjectMethod( class_cls, for_name, name, JNI_FALSE, loader)); @@ -1170,7 +1195,7 @@ static bool fill_jargs(const char* sig, const uint8_t* arg_regs, uint8_t argc, static bool invoke_method(JNIEnv* env, uint8_t op, const std::string& desc, const uint8_t* arg_regs, uint8_t argc, - const std::vector& regs, PendingResult& pending) { + const std::vector& regs, PendingResult& pending, uint16_t version) { // Outer locals (pending/regs) must be DeleteLocalRef'd ONLY while no inner // PushLocalFrame is active — otherwise ART reports // "Attempt to remove index outside index area" / DeleteLocalRef failed. @@ -1186,9 +1211,26 @@ static bool invoke_method(JNIEnv* env, uint8_t op, const std::string& desc, if (!parse_member_ref(desc, &owner, &name, &sig)) { return false; } + std::string direct_super; + if (op == OP_INVOKE_SUPER && version >= PVM2_VERSION_V9) { + size_t separator = owner.find('|'); + if (separator == std::string::npos || separator == 0 || separator + 1 == owner.size()) return false; + direct_super = owner.substr(0, separator); + owner.erase(0, separator + 1); + } cls = find_class_desc(env, owner); - if (cls == nullptr) { - return false; + if (cls == nullptr) return false; + if (!direct_super.empty()) { + // 类 super 从声明者的直接父类分派;接口 super 必须保持原接口的默认实现。 + // 不能从 thiz 的动态类型取父类,否则派生实例会调用错误的实现或递归。 + jclass class_class = cache_global_class(env, "java/lang/Class"); + if (class_class == nullptr) return false; + jmethodID is_interface = env->GetMethodID(class_class, "isInterface", "()Z"); + if (is_interface == nullptr) return false; + jboolean interface_owner = env->CallBooleanMethod(cls, is_interface); + if (env->ExceptionCheck()) return false; + if (!interface_owner) cls = find_class_desc(env, direct_super); + if (cls == nullptr) return false; } mid = (op == OP_INVOKE_STATIC) ? env->GetStaticMethodID(cls, name.c_str(), sig.c_str()) @@ -2450,7 +2492,11 @@ PROTECTOR_ENCRYPT static jobject interpret_body(JNIEnv* env, int dex_index, uint uint16_t idx = read_u16(code + pc); pc += 2; if (!reg_bounds(dst, regs.size()) || idx >= img.strings.size()) goto fail; - reg_take_o(env, ®s[dst], env->NewStringUTF(img.strings[idx].c_str())); + reg_take_o(env, ®s[dst], string_from_utf8(env, img.strings[idx])); + if (env->ExceptionCheck()) { + if (dispatch_exception(env, img, fault_pc, &pc, &stashed_exception)) break; + goto fail; + } break; } case OP_MOVE: { @@ -2851,7 +2897,7 @@ PROTECTOR_ENCRYPT static jobject interpret_body(JNIEnv* env, int dex_index, uint if (mid >= img.methods.size()) goto fail; const uint8_t* arg_regs = code + pc; pc += argc; - if (!invoke_method(env, op, img.methods[mid], arg_regs, argc, regs, pending)) { + if (!invoke_method(env, op, img.methods[mid], arg_regs, argc, regs, pending, img.version)) { if (env->ExceptionCheck()) { if (dispatch_exception(env, img, fault_pc, &pc, &stashed_exception)) { break; diff --git a/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/AutoVmpPolicy.java b/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/AutoVmpPolicy.java index 72067e7..e1a3464 100644 --- a/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/AutoVmpPolicy.java +++ b/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/AutoVmpPolicy.java @@ -134,18 +134,21 @@ static String exclusionReason(ClassDef owner, Method method) { reason = "playback_compatibility"; if (reason == null) return null; if (method.getImplementation() == null || method.getName().startsWith("<")) return reason; - String restriction = independentMethodRestriction(method); + String restriction = independentMethodRestriction(method, "ui_runtime_glue".equals(reason)); return restriction == null ? null : reason + ":" + restriction; } - private static String independentMethodRestriction(Method method) { + private static String independentMethodRestriction(Method method, boolean component) { int flags = method.getAccessFlags(); var code = method.getImplementation(); if ((flags & (0x20 | 0x20000)) != 0) return "synchronization"; - // 跳板保留原声明、访问标志和 this,虚分派仍由 ART 完成;继承/接口本身不改变叶子方法语义。 + // 跳板保留原声明、访问标志和 this,虚分派仍由 ART 完成;继承/接口本身不构成拒绝理由。 // ponytail: hashCode/finalize 仍留在 ART,避免运行时重入;扩展需单独验证 GC 与内部对象操作。 if ((flags & (0x2 | 0x8)) == 0 && (method.getName().equals("hashCode") || method.getName().equals("finalize"))) return "virtual_contract"; + // 组件的字段、调用、数组与异常统一交给共用编译器校验;跳板保留 ART 入口与 this。 + // ponytail: 媒体及数据框架的专用兼容规则仍只放行叶子方法,扩展须有对应框架回归。 + if (component) return null; // 异常表交给 Pvm2Safety 和编译器校验;move-exception/throw 支持捕获、finally 和重新抛出。 // ponytail: 仅放行无状态访问、外部调用的叶子方法;引用和异常复用已有类型化运行时。 // 字段、数组元素、调用等仍留在 ART;扩展需验证对象状态与框架语义,不能直接移除命名规则。 @@ -176,8 +179,7 @@ static String exclusionReason(String typeDescriptor) { if (isDataAndResourceRuntimeGlue(typeDescriptor)) { return "data_resource_runtime_glue"; } - // UI and navigation glue is callback/lifecycle driven. It contributes little - // protection value and must keep Android/AndroidX receiver semantics on ART. + // 组件分类用于保留特殊契约;普通方法继续进入共用编译器,不按名称限制为叶子方法。 if (isUiRuntimeGlue(typeDescriptor)) { return "ui_runtime_glue"; } diff --git a/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/PackerMain.java b/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/PackerMain.java index a8ccc77..537cfc4 100644 --- a/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/PackerMain.java +++ b/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/PackerMain.java @@ -1778,6 +1778,7 @@ private int[] walkDexMethods(com.android.dex.Dex dex, RandomAccessFile raf, byte for (com.android.dex.ClassDef classDef : dex.classDefs()) { if (classDef.getClassDataOffset() == 0) continue; String type = dex.typeNames().get(classDef.getTypeIndex()); + String directSuperclass = classDef.getSupertypeIndex() < 0 ? null : dex.typeNames().get(classDef.getSupertypeIndex()); if (!shouldProcessType(type)) continue; boolean autoPay = paymentAutoVmpEffective && PaymentVmpRules.matches(type); boolean autoIndustry = !autoPay && industryAutoVmpEffective @@ -1794,7 +1795,7 @@ private int[] walkDexMethods(com.android.dex.Dex dex, RandomAccessFile raf, byte com.android.dex.ClassData classData = dex.readClassData(classDef); for (com.android.dex.ClassData.Method method : classData.getDirectMethods()) { notePvm2MethodScanned(phase); - InsnRecord rec = extractOne(dex, raf, method, aesKey, type, phase, skipMethodKeys, pvm2Exclusions); + InsnRecord rec = extractOne(dex, raf, method, aesKey, type, directSuperclass, phase, skipMethodKeys, pvm2Exclusions); if (rec == null) { continue; } @@ -1820,7 +1821,7 @@ private int[] walkDexMethods(com.android.dex.Dex dex, RandomAccessFile raf, byte } for (com.android.dex.ClassData.Method method : classData.getVirtualMethods()) { notePvm2MethodScanned(phase); - InsnRecord rec = extractOne(dex, raf, method, aesKey, type, phase, skipMethodKeys, pvm2Exclusions); + InsnRecord rec = extractOne(dex, raf, method, aesKey, type, directSuperclass, phase, skipMethodKeys, pvm2Exclusions); if (rec == null) { continue; } @@ -2072,7 +2073,7 @@ private boolean shouldTrueVmp(String typeDescriptor) { private InsnRecord extractOne(com.android.dex.Dex dex, RandomAccessFile raf, com.android.dex.ClassData.Method method, byte[] aesKey, - String typeDescriptor, ExtractPhase phase, + String typeDescriptor, String directSuperclass, ExtractPhase phase, Set skipMethodKeys, Map pvm2Exclusions) throws Exception { if (method.getCodeOffset() == 0) return null; // Constructors/clinit must keep invokespecial/super; returning stubs fail ART verify. @@ -2160,7 +2161,7 @@ private InsnRecord extractOne(com.android.dex.Dex dex, RandomAccessFile raf, pvm2Admission.noteCandidate(); Pvm2Compiler.Result compiled = Pvm2Compiler.tryCompile(dex, code, returnType, isStatic, pvm2Morph, - methodReport != null && methodReport.requires(key), paramTypes); + methodReport != null && methodReport.requires(key), paramTypes, directSuperclass); if (!compiled.isOk()) { pvm2Admission.noteFail(compiled.failReason); if (methodReport != null) methodReport.skipped(key, compiled.failReason); diff --git a/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/Pvm2Compiler.java b/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/Pvm2Compiler.java index 37cd36b..74c44bb 100644 --- a/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/Pvm2Compiler.java +++ b/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/Pvm2Compiler.java @@ -20,7 +20,7 @@ */ public final class Pvm2Compiler { private static final int MAX_REGS = 32; - private static final int MAX_CODE_UNITS = 512; + private static final int MAX_CODE_UNITS = 4096; private static final int PACKED_SWITCH_IDENT = 0x0100; private static final int SPARSE_SWITCH_IDENT = 0x0200; private static final int FILL_ARRAY_DATA_IDENT = 0x0300; @@ -66,6 +66,12 @@ public static Result tryCompile(Dex dex, Code code, String returnType, boolean i public static Result tryCompile(Dex dex, Code code, String returnType, boolean isStatic, Pvm2Morph morph, boolean clearAfterUse, String[] parameterTypes) { + return tryCompile(dex, code, returnType, isStatic, morph, clearAfterUse, parameterTypes, null); + } + + public static Result tryCompile(Dex dex, Code code, String returnType, boolean isStatic, + Pvm2Morph morph, boolean clearAfterUse, String[] parameterTypes, + String directSuperclass) { if (code == null) return Result.fail("no code"); if (code.getRegistersSize() > MAX_REGS) return Result.fail("too many regs"); short[] units = code.getInstructions(); @@ -90,7 +96,7 @@ public static Result tryCompile(Dex dex, Code code, String returnType, boolean i } if (arguments.length > 255 || argumentSlots != code.getInsSize()) return Result.fail("argument types do not match register frame"); - String safetyFailure = Pvm2Safety.check(code, isStatic); + String safetyFailure = Pvm2Safety.check(code); if (safetyFailure != null) return Result.fail(safetyFailure); // Extra scratch: lit* / packed-switch use +1; fill-array-data needs +2 (index+value), @@ -118,7 +124,7 @@ public static Result tryCompile(Dex dex, Code code, String returnType, boolean i dalvikPcToEmitIndex.put(pc, emits.size()); int op = units[pc] & 0xff; try { - int advance = translateOne(dex, units, pc, op, scratchReg, totalRegs, strings, + int advance = translateOne(dex, units, pc, op, directSuperclass, scratchReg, totalRegs, strings, methodPool, fieldPool, typePool, emits, fixups); if (advance <= 0) { return Result.fail("bad advance at pc=" + pc + " op=0x" + Integer.toHexString(op)); @@ -142,6 +148,7 @@ public static Result tryCompile(Dex dex, Code code, String returnType, boolean i return Result.fail(e.getMessage()); } byte[] codeBytes = codeBuf.toByteArray(); + if (codeBytes.length > 0xffff) return Result.fail("compiled code too large"); for (Fixup f : fixups) { Integer targetEmit = dalvikPcToEmitIndex.get(f.targetDalvikPc); @@ -303,7 +310,7 @@ private static int kindOfType(String type) { } } - private static int translateOne(Dex dex, short[] units, int pc, int op, + private static int translateOne(Dex dex, short[] units, int pc, int op, String directSuperclass, int scratchReg, int totalRegs, List strings, List methodPool, @@ -596,10 +603,10 @@ private static int translateOne(Dex dex, short[] units, int pc, int op, return 2; } case 0x6e: case 0x6f: case 0x70: case 0x71: case 0x72: { - return translateInvoke35(dex, units, pc, op, strings, methodPool, emits); + return translateInvoke35(dex, units, pc, op, directSuperclass, strings, methodPool, emits); } case 0x74: case 0x75: case 0x76: case 0x77: case 0x78: { - return translateInvoke3r(dex, units, pc, op, strings, methodPool, emits); + return translateInvoke3r(dex, units, pc, op, directSuperclass, strings, methodPool, emits); } case 0x7b: { // neg-int emits.add(Emit.of(Pvm2Opcodes.OP_NEG, (u0 >> 8) & 0x0f, (u0 >> 12) & 0x0f)); @@ -1013,7 +1020,7 @@ private static int readI32Units(short[] units, int index) { return (units[index] & 0xffff) | ((units[index + 1] & 0xffff) << 16); } - private static int translateInvoke35(Dex dex, short[] units, int pc, int op, + private static int translateInvoke35(Dex dex, short[] units, int pc, int op, String directSuperclass, List strings, List methodPool, List emits) { int pvmOp = invokeOp(op); @@ -1025,7 +1032,10 @@ private static int translateInvoke35(Dex dex, short[] units, int pc, int op, int e = (c >> 8) & 0x0f; int d = (c >> 4) & 0x0f; int cc = c & 0x0f; - int mid = internMethod(dex, methodPool, strings, methodIdx); + if (pvmOp == Pvm2Opcodes.OP_INVOKE_SUPER && directSuperclass == null) + throw new UnsupportedOperationException("invoke-super requires declaring class context"); + int mid = internMethod(dex, methodPool, strings, methodIdx, + pvmOp == Pvm2Opcodes.OP_INVOKE_SUPER ? directSuperclass : null); int[] regs = new int[argCount]; if (argCount > 0) regs[0] = cc; if (argCount > 1) regs[1] = d; @@ -1036,7 +1046,7 @@ private static int translateInvoke35(Dex dex, short[] units, int pc, int op, return 3; } - private static int translateInvoke3r(Dex dex, short[] units, int pc, int op, + private static int translateInvoke3r(Dex dex, short[] units, int pc, int op, String directSuperclass, List strings, List methodPool, List emits) { int pvmOp = invokeOpRange(op); @@ -1044,7 +1054,10 @@ private static int translateInvoke3r(Dex dex, short[] units, int pc, int op, int argCount = (u0 >> 8) & 0xff; int methodIdx = units[pc + 1] & 0xffff; int first = units[pc + 2] & 0xffff; - int mid = internMethod(dex, methodPool, strings, methodIdx); + if (pvmOp == Pvm2Opcodes.OP_INVOKE_SUPER && directSuperclass == null) + throw new UnsupportedOperationException("invoke-super requires declaring class context"); + int mid = internMethod(dex, methodPool, strings, methodIdx, + pvmOp == Pvm2Opcodes.OP_INVOKE_SUPER ? directSuperclass : null); int[] regs = new int[argCount]; for (int i = 0; i < argCount; i++) regs[i] = first + i; emits.add(Emit.invoke(pvmOp, mid, regs)); @@ -1362,13 +1375,15 @@ private static int internType(List typePool, List strings, Stri return typePool.size() - 1; } - private static int internMethod(Dex dex, List methodPool, List strings, int methodIdx) { + private static int internMethod(Dex dex, List methodPool, List strings, int methodIdx, String directSuperclass) { MethodId mid = dex.methodIds().get(methodIdx); String owner = dex.typeNames().get(mid.getDeclaringClassIndex()); String name = dex.strings().get(mid.getNameIndex()); com.android.dex.ProtoId proto = dex.protoIds().get(mid.getProtoIndex()); String ret = dex.typeNames().get(proto.getReturnTypeIndex()); StringBuilder sb = new StringBuilder(); + // v9 super 引用同时保留调用者的直接父类与原引用类型;运行时区分类与接口。 + if (directSuperclass != null) sb.append(directSuperclass).append('|'); sb.append(owner).append("->").append(name).append('('); if (proto.getParametersOffset() != 0) { short[] types = dex.readTypeList(proto.getParametersOffset()).getTypes(); @@ -1431,6 +1446,7 @@ private static byte[] buildImageV3(int regCount, int extraScratch, int insSize, // v7:清理策略随镜像一起认证,不能由外部报告或未认证标志覆盖。 out.writeByte(clearAfterUse ? 1 : 0); for (String s : strings) { + if (!StandardCharsets.UTF_8.newEncoder().canEncode(s)) throw new IOException("unpaired UTF-16 surrogate"); byte[] utf = s.getBytes(StandardCharsets.UTF_8); if (utf.length > 0xffff) throw new IOException("string too long"); writeU16(out, utf.length); diff --git a/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/Pvm2Opcodes.java b/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/Pvm2Opcodes.java index 1e0e37b..17efae0 100644 --- a/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/Pvm2Opcodes.java +++ b/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/Pvm2Opcodes.java @@ -6,7 +6,7 @@ private Pvm2Opcodes() { } /** Per-build opcode/immediate/register/handler morph (v6). */ - public static final int VERSION = 8; + public static final int VERSION = 9; public static final int RET_V = 0; public static final int RET_I = 1; diff --git a/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/Pvm2Safety.java b/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/Pvm2Safety.java index cf43a00..a08adf8 100644 --- a/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/Pvm2Safety.java +++ b/third_party/xopprotector/packer/src/main/java/com/yqsh/protector/packer/Pvm2Safety.java @@ -1,501 +1,115 @@ package com.yqsh.protector.packer; import com.android.dex.Code; -import com.android.tools.smali.dexlib2.Opcode; import com.android.tools.smali.dexlib2.Opcodes; +import java.util.BitSet; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Conservative, package-agnostic admission checks for PVM2 methods. - * - *

The interpreter intentionally throws the same NPE as ART when an invoke - * receiver is null. That is correct VM behaviour, but a static transform must - * not move a method into PVM2 when it cannot prove the receiver's nullability. - * This checker keeps the non-null instance receiver and proven non-null branch - * paths compatible with ART. At control-flow joins it intersects facts from - * all incoming paths; unknown receivers crossing a branch or exception edge - * stay on ART, where verifier and handler semantics remain authoritative.

- */ +/** 校验编译所需的结构边界;可空接收者交给运行时按原指令位置抛出并派发异常。 */ final class Pvm2Safety { private static final Opcodes OPCODES = Opcodes.getDefault(); - private Pvm2Safety() { - } + private Pvm2Safety() {} - static String check(Code code, boolean isStatic) { + static String check(Code code) { if (code == null) return "semantic safety: no code"; short[] units = code.getInstructions(); - ExceptionalInfo exceptional = exceptionalBoundaries(code, units); - return checkInternal(units, code.getRegistersSize(), code.getInsSize(), isStatic, - exceptional, false); - } - - static String check(short[] units, int registersSize, int insSize, - boolean isStatic, boolean hasExceptionalFlow) { - return checkInternal(units, registersSize, insSize, isStatic, null, - hasExceptionalFlow); - } - - private static String checkInternal(short[] units, int registersSize, int insSize, - boolean isStatic, ExceptionalInfo exceptional, - boolean hasExceptionalFlow) { - if (exceptional != null && exceptional.invalid) return "semantic safety: malformed exception table"; - if (units == null || units.length == 0) return null; - if (registersSize <= 0 || registersSize > 255) { + int registers = code.getRegistersSize(), inputs = code.getInsSize(); + if (registers < 0 || registers > 255 || inputs < 0 || inputs > registers) return "semantic safety: invalid register frame"; - } - Map widths = new HashMap<>(); - int pc = 0; - while (pc < units.length) { - int op = units[pc] & 0xff; - int width; - try { - width = widthOf(units, pc, op); - } catch (RuntimeException ex) { - return "semantic safety: malformed instruction"; - } - if (width <= 0 || pc + width > units.length) { - return "semantic safety: malformed instruction"; - } - widths.put(pc, width); - pc += width; - } - if (pc != units.length || widths.isEmpty()) return "semantic safety: malformed instruction"; - - FlowState initial = new FlowState(registersSize); - int thisReg = registersSize - Math.max(insSize, 0); - if (!isStatic && insSize > 0 && thisReg >= 0 && thisReg < registersSize) { - initial.knownNonNull[thisReg] = true; - } - - Map inStates = new HashMap<>(); - ArrayDeque pending = new ArrayDeque<>(); - inStates.put(0, initial); - pending.add(0); - - while (!pending.isEmpty()) { - pc = pending.removeFirst(); - Integer widthValue = widths.get(pc); - if (widthValue == null) return "semantic safety: malformed branch target"; - int width = widthValue; - int u0 = units[pc] & 0xffff; - int op = u0 & 0xff; - FlowState state = inStates.get(pc).copy(); - if (exceptional != null && pc < exceptional.inTry.length - && (exceptional.inTry[pc] || exceptional.handler[pc])) { - state.controlFlow = true; - } else if (exceptional == null && hasExceptionalFlow) { - state.controlFlow = true; - } - - // 异常边读取执行前的事实,不能继承解引用正常完成后的非空结论。 - Opcode metadata = OPCODES.getOpcodeByValue(op); - if (exceptional != null && metadata != null && metadata.canThrow()) { - for (int handlerPc : exceptional.targets.getOrDefault(pc, List.of())) { - if (!widths.containsKey(handlerPc)) return "semantic safety: malformed exception target"; - FlowState thrown = state.copy(); - thrown.controlFlow = true; - // ponytail: 写目标在异常边保守作废,避免依赖 JNI 失败前是否已写入;精确放行需逐指令证明。 - if (metadata.setsRegister() && op != 0x1f) { - int destination = destination(metadata, u0, units, pc); - clearKnown(destination, thrown.knownNonNull); - thrown.forgetTypeTest(destination); - if (metadata.setsWideRegister()) { - clearKnown(destination + 1, thrown.knownNonNull); - thrown.forgetTypeTest(destination + 1); - } + if (units == null || units.length == 0) return null; + BitSet boundaries = new BitSet(units.length + 1); + BitSet executable = new BitSet(units.length); + try { + for (int pc = 0; pc < units.length;) { + int width = widthOf(units, pc); + if (width <= 0 || width > units.length - pc) + return "semantic safety: malformed instruction"; + boundaries.set(pc); + if ((units[pc] & 0xff) != 0 || units[pc] == 0) executable.set(pc); + pc += width; + } + boundaries.set(units.length); + for (int pc = executable.nextSetBit(0); pc >= 0; pc = executable.nextSetBit(pc + 1)) { + int u0 = units[pc] & 0xffff, op = u0 & 0xff; + long target = -1; + if (op == 0x28) target = (long)pc + (byte)(u0 >>> 8); + else if (op == 0x29 || (op >= 0x32 && op <= 0x3d)) target = (long)pc + units[pc + 1]; + else if (op == 0x2a) target = (long)pc + signed32(units, pc + 1); + if ((op >= 0x28 && op <= 0x2a || op >= 0x32 && op <= 0x3d) + && !contains(executable, target)) return "semantic safety: malformed branch target"; + if (op == 0x2b || op == 0x2c || op == 0x26) { + long address = (long)pc + signed32(units, pc + 1); + if (!contains(boundaries, address) || address >= units.length || (address & 1) != 0) + return "semantic safety: malformed payload target"; + int payload = (int)address; + int ident = op == 0x2b ? 0x0100 : op == 0x2c ? 0x0200 : 0x0300; + if ((units[payload] & 0xffff) != ident) return "semantic safety: malformed payload type"; + if (op != 0x26) { + int size = units[payload + 1] & 0xffff; + int base = payload + (op == 0x2b ? 4 : 2 + size * 2); + for (int i = 0; i < size; i++) + if (!contains(executable, (long)pc + signed32(units, base + i * 2))) + return "semantic safety: malformed switch target"; } - if (mergeState(inStates, handlerPc, thrown)) pending.addLast(handlerPc); - } - } - - if (requiresReceiver(op)) { - int argc = isInvoke35(op) ? (u0 >>> 12) & 0x0f : (u0 >>> 8) & 0xff; - if (argc == 0) { - return isInvoke35(op) - ? "semantic safety: invoke missing receiver" - : "semantic safety: invoke/range missing receiver"; - } - int receiver = isInvoke35(op) - ? (units[pc + 2] & 0xffff) & 0x0f - : units[pc + 2] & 0xffff; - String reason = receiverReason(receiver, state.knownNonNull, state.controlFlow); - if (reason != null) return reason; - // 调用正常返回后接收者必非空;异常边已在执行前传播。 - state.knownNonNull[receiver] = true; - } - - // 字段/数组访问正常完成后,其对象必非空;先记录,再处理可能覆盖对象的目标寄存器。 - if (op == 0x21 || (op >= 0x52 && op <= 0x5f)) { - setKnown((u0 >>> 12) & 0x0f, state.knownNonNull, true); - } else if (op >= 0x44 && op <= 0x51) { - setKnown(units[pc + 1] & 0xff, state.knownNonNull, true); - } - if (metadata != null && metadata.setsRegister() && op != 0x1f) { - int destination = destination(metadata, u0, units, pc); - state.forgetTypeTest(destination); - if (metadata.setsWideRegister()) state.forgetTypeTest(destination + 1); - int source = (u0 >>> 12) & 0x0f; - if (op == 0x20 && source != destination && source < registersSize && destination < registersSize) - state.typeTestSource[destination] = source; - } - updateKnownNonNull(op, u0, units, pc, state.knownNonNull); - for (Successor successor : successors(units, pc, op, width, state, widths)) { - if (!widths.containsKey(successor.pc)) { - return "semantic safety: malformed branch target"; } - if (mergeState(inStates, successor.pc, successor.state)) { - pending.addLast(successor.pc); + if (op >= 0x6e && op <= 0x72 || op >= 0x74 && op <= 0x78) { + boolean range = op >= 0x74; + int count = range ? u0 >>> 8 : u0 >>> 12; + if (!range && count > 5) return "semantic safety: invalid invoke argument count"; + if (count == 0 && op != 0x71 && op != 0x77) + return "semantic safety: invoke missing receiver"; + int packed = units[pc + 2] & 0xffff; + for (int i = 0; i < count; i++) { + int register = range ? packed + i : i == 4 ? (u0 >>> 8) & 15 : (packed >>> (i * 4)) & 15; + if (register >= registers) return "semantic safety: invoke register out of range"; + } } } + if (code.getTries() != null) for (Code.Try block : code.getTries()) { + int start = block.getStartAddress(), count = block.getInstructionCount(); + int index = block.getCatchHandlerIndex(); + Code.CatchHandler[] handlers = code.getCatchHandlers(); + if (!contains(executable, start) || count <= 0 || count > units.length - start + || !boundaries.get(start + count) || handlers == null || index < 0 || index >= handlers.length) + return "semantic safety: malformed exception table"; + Code.CatchHandler handler = handlers[index]; + int[] addresses = handler.getAddresses(), types = handler.getTypeIndexes(); + if (addresses == null || types == null || addresses.length != types.length + || addresses.length == 0 && handler.getCatchAllAddress() < 0) + return "semantic safety: malformed exception table"; + for (int address : addresses) if (!contains(executable, address)) + return "semantic safety: malformed exception target"; + if (handler.getCatchAllAddress() >= 0 && !contains(executable, handler.getCatchAllAddress())) + return "semantic safety: malformed exception target"; + } + } catch (RuntimeException malformed) { + return "semantic safety: malformed instruction"; } return null; } - private static String receiverReason(int receiver, boolean[] knownNonNull, - boolean hasControlFlow) { - if (receiver < 0 || receiver >= knownNonNull.length) { - return "semantic safety: receiver register out of range"; - } - if (knownNonNull[receiver]) return null; - // ART and the interpreter both throw NPE for a null receiver in - // straight-line code, so this does not change observable semantics. - // The risky case is a nullable value crossing a branch/handler, where - // PVM2 must not approximate ART's verifier and exception edges. - if (!hasControlFlow) return null; - return "nullable receiver with control flow"; - } - - private static boolean requiresReceiver(int op) { - // invoke-static and invoke-static/range have no receiver. Treating - // their first argument as one unnecessarily rejected safe methods. - return (op >= 0x6e && op <= 0x72 && op != 0x71) - || (op >= 0x74 && op <= 0x78 && op != 0x77); - } - - private static boolean mergeState(Map states, int pc, FlowState incoming) { - FlowState existing = states.get(pc); - if (existing == null) { - states.put(pc, incoming.copy()); - return true; - } - boolean changed = existing.controlFlow != (existing.controlFlow || incoming.controlFlow); - existing.controlFlow |= incoming.controlFlow; - for (int i = 0; i < existing.knownNonNull.length; i++) { - boolean merged = existing.knownNonNull[i] && incoming.knownNonNull[i]; - changed |= existing.knownNonNull[i] != merged; - existing.knownNonNull[i] = merged; - if (existing.typeTestSource[i] != incoming.typeTestSource[i] && existing.typeTestSource[i] != -1) { - existing.typeTestSource[i] = -1; - changed = true; - } - } - return changed; - } - - private static List successors(short[] units, int pc, int op, int width, - FlowState state, Map widths) { - List out = new ArrayList<>(2); - if (isReturnOrThrow(op)) return out; - - if (isIf(op)) { - int target = pc + signed16(units[pc + 1]); - FlowState targetState = state.copy(); - FlowState fallthroughState = state.copy(); - targetState.controlFlow = true; - fallthroughState.controlFlow = true; - int register = (units[pc] >>> 8) & 0xff; - // instanceof 为真保证源对象非空;只保留尚未被改写的直接关系。 - if ((op == 0x38 || op == 0x39) && register < state.typeTestSource.length) { - int source = state.typeTestSource[register]; - if (source >= 0) setKnown(source, - (op == 0x38 ? fallthroughState : targetState).knownNonNull, true); - } - // ponytail: 仅传播当前等值边的非空事实;跨指令关系需另建别名分析。 - if (op == 0x38) { // if-eqz: fallthrough is the non-null path. - setKnown(register, fallthroughState.knownNonNull, true); - } else if (op == 0x39) { // if-nez: target is the non-null path. - setKnown(register, targetState.knownNonNull, true); - } else if (op == 0x32 || op == 0x33) { - int left = register & 0x0f, right = register >>> 4; - FlowState equal = op == 0x32 ? targetState : fallthroughState; - if (left < equal.knownNonNull.length && right < equal.knownNonNull.length - && (equal.knownNonNull[left] || equal.knownNonNull[right])) { - equal.knownNonNull[left] = equal.knownNonNull[right] = true; - } - } - out.add(new Successor(target, targetState)); - if (widths.containsKey(pc + width)) { - out.add(new Successor(pc + width, fallthroughState)); - } - return out; - } - - if (isGoto(op)) { - int target = pc + gotoOffset(units, pc, op); - state.controlFlow = true; - out.add(new Successor(target, state)); - return out; - } - - if (op == 0x2b || op == 0x2c) { - // Switch targets are all control-flow edges. Decode them here so - // a receiver used only from a case is not missed by the analysis. - state.controlFlow = true; - int payload = pc + signed32(units[pc + 1], units[pc + 2]); - addSwitchTargets(units, pc, payload, state, out); - if (widths.containsKey(pc + width)) { - out.add(new Successor(pc + width, state.copy())); - } - return out; - } - - if (isControlFlow(op)) state.controlFlow = true; - if (widths.containsKey(pc + width)) out.add(new Successor(pc + width, state)); - return out; - } - - private static void addSwitchTargets(short[] units, int switchPc, int payload, FlowState state, - List out) { - if (payload < 0 || payload + 1 >= units.length) { - out.add(new Successor(-1, state.copy())); - return; - } - int ident = units[payload] & 0xffff; - int size = units[payload + 1] & 0xffff; - if (ident == 0x0100) { - int base = payload + 4; - if (base < 0 || base > units.length || size > (units.length - base) / 2) { - out.add(new Successor(-1, state.copy())); - return; - } - for (int i = 0; i < size; i++) { - int off = signed32(units[base + i * 2], units[base + i * 2 + 1]); - out.add(new Successor(switchPc + off, state.copy())); - } - } else if (ident == 0x0200) { - int base = payload + 2 + size * 2; - if (base < 0 || base > units.length || size > (units.length - base) / 2 - || payload + 2 > units.length || size > (units.length - payload - 2) / 4) { - out.add(new Successor(-1, state.copy())); - return; - } - for (int i = 0; i < size; i++) { - int off = signed32(units[base + i * 2], units[base + i * 2 + 1]); - out.add(new Successor(switchPc + off, state.copy())); - } - } else { - out.add(new Successor(-1, state.copy())); - } + private static boolean contains(BitSet boundaries, long pc) { + return pc >= 0 && pc < Integer.MAX_VALUE && boundaries.get((int)pc); } - private static boolean isIf(int op) { - return op >= 0x32 && op <= 0x3d; + private static int signed32(short[] units, int pc) { + return (units[pc] & 0xffff) | (units[pc + 1] << 16); } - private static boolean isGoto(int op) { - return op == 0x28 || op == 0x29 || op == 0x2a; - } - - private static int gotoOffset(short[] units, int pc, int op) { - if (op == 0x28) return (byte) ((units[pc] >>> 8) & 0xff); - if (op == 0x29) return signed16(units[pc + 1]); - return signed32(units[pc + 1], units[pc + 2]); - } - - private static int signed16(short value) { - return (short) (value & 0xffff); - } - - private static int signed32(short low, short high) { - return (low & 0xffff) | ((high & 0xffff) << 16); - } - - private static boolean isReturnOrThrow(int op) { - return op >= 0x0e && op <= 0x11 || op == 0x27; - } - - private static ExceptionalInfo exceptionalBoundaries(Code code, short[] units) { - ExceptionalInfo result = new ExceptionalInfo(units == null ? 0 : units.length); - if (units == null) return result; - Code.Try[] tries = code.getTries(); - if (tries == null || tries.length == 0) return result; - Code.CatchHandler[] handlers = code.getCatchHandlers(); - if (handlers == null) { result.invalid = true; return result; } - for (Code.Try t : tries) { - int start = t.getStartAddress(), count = t.getInstructionCount(); - if (start < 0 || start >= units.length || count <= 0 || count > units.length - start - || t.getCatchHandlerIndex() < 0 || t.getCatchHandlerIndex() >= handlers.length) { - result.invalid = true; return result; - } - int end = start + count; - Code.CatchHandler handler = handlers[t.getCatchHandlerIndex()]; - List targets = new ArrayList<>(); - if (handler.getAddresses() != null) for (int address : handler.getAddresses()) targets.add(address); - if (handler.getCatchAllAddress() >= 0) targets.add(handler.getCatchAllAddress()); - if (targets.isEmpty()) { result.invalid = true; return result; } - for (int address : targets) { - if (address < 0 || address >= units.length) { result.invalid = true; return result; } - result.handler[address] = true; - } - for (int pc = start; pc < end;) { - result.inTry[pc] = true; - result.targets.computeIfAbsent(pc, ignored -> new ArrayList<>()).addAll(targets); - try { - int width = widthOf(units, pc, units[pc] & 0xff); - if (width <= 0 || width > end - pc) { result.invalid = true; return result; } - pc += width; - } catch (RuntimeException ex) { result.invalid = true; return result; } - } - } - return result; - } - - private static final class ExceptionalInfo { - final boolean[] inTry; - final boolean[] handler; - final Map> targets = new HashMap<>(); - boolean invalid; - - ExceptionalInfo(int size) { - inTry = new boolean[size]; - handler = new boolean[size]; - } - } - - private static final class FlowState { - final boolean[] knownNonNull; - final int[] typeTestSource; - boolean controlFlow; - - FlowState(int registersSize) { - knownNonNull = new boolean[registersSize]; - typeTestSource = new int[registersSize]; - Arrays.fill(typeTestSource, -1); - } - - void forgetTypeTest(int register) { - for (int i = 0; i < typeTestSource.length; i++) - if (i == register || typeTestSource[i] == register) typeTestSource[i] = -1; - } - - FlowState copy() { - FlowState copy = new FlowState(knownNonNull.length); - System.arraycopy(knownNonNull, 0, copy.knownNonNull, 0, knownNonNull.length); - System.arraycopy(typeTestSource, 0, copy.typeTestSource, 0, typeTestSource.length); - copy.controlFlow = controlFlow; - return copy; - } - } - - private static final class Successor { - final int pc; - final FlowState state; - - Successor(int pc, FlowState state) { - this.pc = pc; - this.state = state; - } - } - - private static void updateKnownNonNull(int op, int u0, short[] units, int pc, - boolean[] known) { - // 对象移动先读取源事实,避免同寄存器移动被误清空;check-cast 不改变值。 - if (op == 0x07) { copyKnown((u0 >>> 8) & 0x0f, (u0 >>> 12) & 0x0f, known); return; } - if (op == 0x08) { copyKnown((u0 >>> 8) & 0xff, units[pc + 1] & 0xffff, known); return; } - if (op == 0x09) { copyKnown(units[pc + 1] & 0xffff, units[pc + 2] & 0xffff, known); return; } - if (op == 0x1f) return; - - // 复用 dexlib2 的写寄存器标记,统一清除 wide 的两格及 lit8 的完整 8 位目标。 - Opcode opcode = OPCODES.getOpcodeByValue(op); - if (opcode == null || !opcode.setsRegister()) return; - int destination = destination(opcode, u0, units, pc); - clearKnown(destination, known); - if (opcode.setsWideRegister()) clearKnown(destination + 1, known); - if (op == 0x0d || op == 0x1a || op == 0x1b || op == 0x1c || op == 0x22 || op == 0x23) { - setKnown(destination, known, true); - } - } - - private static int destination(Opcode opcode, int u0, short[] units, int pc) { - return switch (opcode.format) { - case Format11n, Format12x, Format22c, Format22cs, Format22s -> (u0 >>> 8) & 0x0f; - case Format32x -> units[pc + 1] & 0xffff; - default -> (u0 >>> 8) & 0xff; - }; - } - - private static void copyKnown(int dst, int src, boolean[] known) { - if (dst >= 0 && dst < known.length) { - known[dst] = src >= 0 && src < known.length && known[src]; - } - } - - private static void setKnown(int reg, boolean[] known, boolean value) { - if (reg >= 0 && reg < known.length) known[reg] = value; - } - - private static void clearKnown(int reg, boolean[] known) { - setKnown(reg, known, false); - } - - private static boolean isInvoke35(int op) { - return op == 0x6e || op == 0x6f || op == 0x70 || op == 0x72; - } - - private static boolean isInvoke3r(int op) { - return op == 0x74 || op == 0x75 || op == 0x76 || op == 0x78; - } - - private static boolean isControlFlow(int op) { - return (op >= 0x28 && op <= 0x3d) || op == 0x2b || op == 0x2c; - } - - private static int widthOf(short[] units, int pc, int op) { + private static int widthOf(short[] units, int pc) { int ident = units[pc] & 0xffff; - if (op == 0 && (ident == 0x0100 || ident == 0x0200 || ident == 0x0300)) { - if (ident == 0x0100) { - int size = units[pc + 1] & 0xffff; - return 4 + size * 2; - } - if (ident == 0x0200) { - int size = units[pc + 1] & 0xffff; - return 2 + size * 4; - } - int elemWidth = units[pc + 1] & 0xffff; - long size = (units[pc + 2] & 0xffffL) | ((units[pc + 3] & 0xffffL) << 16); - long dataUnits = (size * elemWidth + 1L) / 2L; - if (elemWidth <= 0 || dataUnits > Integer.MAX_VALUE - 4L) { - throw new IllegalArgumentException("bad payload"); - } - return 4 + (int) dataUnits; - } - switch (op) { - case 0x03: case 0x06: case 0x09: case 0x14: case 0x17: case 0x1b: - case 0x24: case 0x25: case 0x26: case 0x2a: case 0x2b: case 0x2c: - case 0x6e: case 0x6f: case 0x70: case 0x71: case 0x72: - case 0x74: case 0x75: case 0x76: case 0x77: case 0x78: - return 3; - case 0x18: return 5; - case 0xfa: case 0xfb: return 4; - case 0x01: case 0x04: case 0x07: case 0x0a: case 0x0b: case 0x0c: - case 0x0d: case 0x0e: case 0x0f: case 0x10: case 0x11: case 0x12: - case 0x1d: case 0x1e: case 0x21: case 0x27: case 0x28: - case 0x7b: case 0x7c: case 0x7d: case 0x7e: case 0x7f: case 0x80: - case 0x81: case 0x82: case 0x83: case 0x84: case 0x85: case 0x86: - case 0x87: case 0x88: case 0x89: case 0x8a: case 0x8b: case 0x8c: - case 0x8d: case 0x8e: case 0x8f: - return 1; - default: - if (op >= 0xb0 && op <= 0xcf) return 1; - if (op <= 0x00) return 1; - return 2; - } + if (ident == 0x0100) return 4 + (units[pc + 1] & 0xffff) * 2; + if (ident == 0x0200) return 2 + (units[pc + 1] & 0xffff) * 4; + if (ident == 0x0300) { + int elementWidth = units[pc + 1] & 0xffff; + long size = Integer.toUnsignedLong(signed32(units, pc + 2)); + long width = 4 + (size * elementWidth + 1) / 2; + if (elementWidth != 1 && elementWidth != 2 && elementWidth != 4 && elementWidth != 8 + || width > Integer.MAX_VALUE) throw new IllegalArgumentException("bad payload"); + return (int)width; + } + var opcode = OPCODES.getOpcodeByValue(ident & 0xff); + if (opcode == null || opcode.format.size <= 0) throw new IllegalArgumentException("unknown opcode"); + return opcode.format.size / 2; } } diff --git a/tools/xop-pvm2-packer.jar b/tools/xop-pvm2-packer.jar index 17f7718..fc1dfb6 100644 Binary files a/tools/xop-pvm2-packer.jar and b/tools/xop-pvm2-packer.jar differ