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
12 changes: 12 additions & 0 deletions .github/release-notes/versions/1.8.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
### PVM2 引用与短方法兼容修复

- 修复独立方法因对象、数组参数被过度排除的问题。符合条件的引用透传、空值判断和引用身份比较可继续进入 PVM2 转换与产物核验。
- 修复单条 `return` 方法被旧式原地回填长度检查拦截的问题,覆盖对象、数组和基本类型/宽值参数透传;PVM2 仍须通过完整跳板及加密镜像验证。
- 细分继承/接口虚方法契约、同步、异常处理区及指令限制,避免把所有兼容排除都显示为生命周期问题。零转换、必保方法未覆盖仍明确失败,跳过项仍计入完整覆盖率。
- 修复 APK、AAB 和签名任务失败后仍显示“执行中”的进度标题,补齐结束计时和失败步骤标记;明确基础预检通过不代表方法覆盖已通过。

### 验证与使用说明

- 已完成自建 APK 的加固、签名和产物复核,以及生产 Native 解释器三种 ISA 下 666 次引用语义检查。旧版为零转换的引用样本在修复后验证了 18/21 个方法,3 个构造器明确保留;继承契约负例仍正确失败。
- 本次未新增 Android 真机回归,宿主 JVM/JNI 测试不能代替 ART 真机验收。没有客户 APK 和方法体时,不能据此保证其具体方法全部兼容。
- 升级后需要重新加固,已有 APK 不会自动获得本次修复。逐方法报告包含业务标识,分享前请手动脱敏。
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/shield-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "shield-cli"
version = "1.8.2"
version = "1.8.3"
edition = "2021"
authors = ["MocikaDev"]
license = "MIT OR Apache-2.0"
Expand Down
4 changes: 2 additions & 2 deletions apps/shield-gui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/shield-gui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mocika-shield-gui",
"version": "1.8.2",
"version": "1.8.3",
"license": "MIT OR Apache-2.0",
"private": true,
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion apps/shield-gui/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mocika-shield"
version = "1.8.2"
version = "1.8.3"
edition = "2021"
license = "MIT OR Apache-2.0"
publish = false
Expand Down
2 changes: 1 addition & 1 deletion apps/shield-gui/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Shellsmith",
"version": "1.8.2",
"version": "1.8.3",
"identifier": "dev.mocika.shield-gui",
"build": {
"beforeDevCommand": "npm run dev -- --host 127.0.0.1",
Expand Down
12 changes: 7 additions & 5 deletions apps/shield-gui/src/components/app/protect-progress-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { Check, Loader2 } from "lucide-react";
import { Check, CircleX, Loader2 } from "lucide-react";
import { stepLabels } from "@/components/app/branding";
import { t, type Locale } from "@/lib/i18n";

Expand All @@ -24,10 +24,10 @@ export function ProtectProgressPanel({
}) {
const [tick, refresh] = useState(0);
useEffect(() => {
if (!startedAt || finishedAt) return;
if (state !== "running" || !startedAt || finishedAt) return;
const timer = window.setInterval(() => refresh((value) => value + 1), 1000);
return () => window.clearInterval(timer);
}, [finishedAt, startedAt]);
}, [state, finishedAt, startedAt]);
const currentIndex = steps.indexOf(currentStep);
const percent = state === "done" ? 100 : currentIndex < 0 ? 0 : Math.min(99, Math.round(((currentIndex + 0.5) / steps.length) * 100));
const elapsed = (() => {
Expand All @@ -39,7 +39,7 @@ export function ProtectProgressPanel({
return (
<aside className="rounded-[14px] border bg-card p-4">
<div className="mb-3 flex items-center justify-between gap-3">
<h2 className="text-sm font-semibold">{showProgress ? t(locale, "running") : t(locale, "ready")}</h2>
<h2 className="text-sm font-semibold" aria-live="polite">{t(locale, state === "failed" || state === "done" || state === "running" ? state : "ready")}</h2>
{showProgress && <span className="text-xs tabular-nums text-muted-foreground">{percent}%{elapsed ? ` · ${elapsed}` : ""}</span>}
</div>
<div className="mb-4 h-2 overflow-hidden rounded-full bg-muted"><div className="h-full rounded-full bg-primary transition-[width]" style={{ width: `${percent}%` }} /></div>
Expand All @@ -51,7 +51,9 @@ export function ProtectProgressPanel({
<div className="space-y-2">
{steps.map((step) => (
<div key={step} className="flex items-center gap-2 rounded-md px-2 py-1.5">
{currentStep === step && state === "running" ? (
{currentStep === step && state === "failed" ? (
<CircleX className="h-4 w-4 text-destructive" aria-label={t(locale, "failed")} />
) : currentStep === step && state === "running" ? (
<Loader2 className="h-4 w-4 animate-spin text-primary" />
) : steps.indexOf(step) < steps.indexOf(currentStep) || state === "done" ? (
<Check className="h-4 w-4 text-success" />
Expand Down
2 changes: 1 addition & 1 deletion apps/shield-gui/src/hooks/use-about-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type AppInfo = {
};

const defaultAppInfo: AppInfo = {
version: "1.8.2",
version: "1.8.3",
git_hash: "dev",
build_date: "unknown",
};
Expand Down
1 change: 1 addition & 0 deletions apps/shield-gui/src/hooks/use-protect-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ export function useProtectWorkflow({
notifySuccess(t(locale, "protectCompleted"));
} catch (err) {
const message = String(err);
setFinishedAt((previous) => previous ?? Date.now());
setError(message);
notifyError(message);
setState("failed");
Expand Down
2 changes: 2 additions & 0 deletions apps/shield-gui/src/hooks/use-sign-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,12 @@ export function useSignWorkflow({
sharing: sharingChoice,
});
await api.deleteFile(`${outputPath}.idsig`).catch(() => undefined);
setFinishedAt((previous) => previous ?? Date.now());
setState("done");
notifySuccess(t(locale, "signDone"));
} catch (err) {
const message = String(err);
setFinishedAt((previous) => previous ?? Date.now());
setError(message);
notifyError(message);
setState("failed");
Expand Down
4 changes: 2 additions & 2 deletions apps/shield-gui/src/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ const dict = {
preflightReady: "预检通过",
preflightWarning: "存在兼容风险",
preflightBlocked: "当前不能加固",
preflightReadyHint: "当前设置满足已验证的加固条件。",
preflightReadyHint: "基础环境与输入检查已通过;方法可转换性和实际覆盖范围将在加固时验证。",
preflightWarningHint: "可以继续,但请确认下列风险符合目标设备范围。",
preflightBlockedHint: "请先处理阻断项或调整本次加固设置。",
preflightViewDetails: "查看详情({count} 项)",
Expand Down Expand Up @@ -651,7 +651,7 @@ const dict = {
preflightReady: "Preflight passed",
preflightWarning: "Compatibility risks found",
preflightBlocked: "Protection is blocked",
preflightReadyHint: "The current settings meet the verified protection requirements.",
preflightReadyHint: "Environment and input checks passed; method eligibility and coverage will be verified during protection.",
preflightWarningHint: "You may continue after confirming the risks match your target devices.",
preflightBlockedHint: "Resolve the blocking items or adjust the protection settings first.",
preflightViewDetails: "View details ({count})",
Expand Down
6 changes: 5 additions & 1 deletion apps/shield-gui/src/lib/protect-diagnostic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,14 @@ export function buildPvm2FailureDiagnostic(summary: Pvm2FailureSummary, locale:
const labels: Record<keyof Pvm2FailureSummary["reasons"], [string, string]> = {
no_code: ["无 DEX 方法体(Native/抽象方法)", "No DEX body (native/abstract)"],
initializer: ["构造器/静态初始化器", "Initializer"],
ui_runtime_glue: ["界面/生命周期兼容规则", "UI/lifecycle compatibility"],
ui_runtime_glue: ["界面范围兼容规则(未细分)", "UI scope compatibility (unspecified)"],
data_resource_runtime_glue: ["数据/资源框架兼容规则", "Data/resource compatibility"],
compiler_generated_bridge: ["编译器生成桥接/合成方法", "Compiler-generated bridge/synthetic method"],
playback_compatibility: ["媒体/辅助类兼容规则", "Media/helper compatibility"],
compatibility_virtual_contract: ["兼容范围:继承/接口虚方法契约未验证", "Compatibility: unverified inherited/interface contract"],
compatibility_synchronization: ["兼容范围:同步方法", "Compatibility: synchronized method"],
compatibility_exception_region: ["兼容范围:含异常处理区", "Compatibility: exception region"],
compatibility_non_leaf_instruction: ["兼容范围:含叶子方法白名单之外的指令", "Compatibility: instruction outside the leaf allowlist"],
nullable_receiver: ["接收者非空语义无法证明", "Nullable receiver"],
unsupported_opcode: ["不支持的指令", "Unsupported opcode"],
register_limit: ["寄存器超限", "Register limit"],
Expand Down
2 changes: 1 addition & 1 deletion apps/shield-gui/src/lib/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export type TaskSnapshot = {
export type Pvm2FailureSummary = {
selected_classes: number;
selected_methods: number;
reasons: Partial<Record<"no_code" | "initializer" | "ui_runtime_glue" | "data_resource_runtime_glue" | "compiler_generated_bridge" | "playback_compatibility" | "nullable_receiver" | "unsupported_opcode" | "register_limit" | "code_size_limit" | "exception_mapping" | "control_flow" | "type" | "not_attempted" | "other", number>>;
reasons: Partial<Record<"no_code" | "initializer" | "ui_runtime_glue" | "data_resource_runtime_glue" | "compiler_generated_bridge" | "playback_compatibility" | "compatibility_virtual_contract" | "compatibility_synchronization" | "compatibility_exception_region" | "compatibility_non_leaf_instruction" | "nullable_receiver" | "unsupported_opcode" | "register_limit" | "code_size_limit" | "exception_mapping" | "control_flow" | "type" | "not_attempted" | "other", number>>;
};

export type DragDropPayload = {
Expand Down
1 change: 1 addition & 0 deletions apps/shield-gui/src/pages/aab-protect-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export function AabProtectPage({
notifySuccess(t(locale, "aabProtectCompleted"));
} catch (cause) {
const message = String(cause);
setFinishedAt((previous) => previous ?? Date.now());
setError(message);
setState("failed");
notifyError(message);
Expand Down
12 changes: 12 additions & 0 deletions apps/shield-gui/tests/protect-diagnostic.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,15 @@ test("普通预检不混入上一任务的失败统计", () => {
xopPvm2Configured: false, currentStep: "", failed: false, pvm2Failure: failure });
assert.doesNotMatch(text, /PVM2_ZERO_METHODS/);
});

test("兼容限制按方法实际阻断条件统计,不再全部显示为生命周期", () => {
const text = buildPvm2FailureDiagnostic({ selected_classes: 3, selected_methods: 21,
reasons: { initializer: 3, compatibility_virtual_contract: 6, compatibility_synchronization: 2,
compatibility_exception_region: 3, compatibility_non_leaf_instruction: 7 } }, "zh");
assert.match(text, /所选方法数: 21/);
assert.match(text, /继承\/接口虚方法契约未验证\): 6/);
assert.match(text, /同步方法\): 2/);
assert.match(text, /含异常处理区\): 3/);
assert.match(text, /白名单之外的指令\): 7/);
assert.doesNotMatch(text, /生命周期|统计无效/);
});
27 changes: 27 additions & 0 deletions apps/shield-gui/tests/protect-progress.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import assert from "node:assert/strict";
import { after, test } from "node:test";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { createServer } from "vite";

const server = await createServer({ server: { middlewareMode: true }, appType: "custom" });
after(() => server.close());
const { ProtectProgressPanel } = await server.ssrLoadModule("/src/components/app/protect-progress-panel.tsx");

test("APK、AAB、签名共享进度组件显示真实终态及失败步骤", () => {
for (const currentStep of ["ProcessDex", "SignApk"]) {
for (const [state, label] of [["idle", "就绪"], ["running", "执行中"], ["failed", "失败"], ["done", "完成"]]) {
const html = renderToStaticMarkup(createElement(ProtectProgressPanel, {
locale: "zh", state, currentStep, steps: ["CheckTools", currentStep, "Cleanup"],
showProgress: state !== "idle", startedAt: 1000, finishedAt: 86000,
}));
assert.match(html, new RegExp(`<h2[^>]*>${label}</h2>`));
if (state === "failed") {
assert.match(html, /aria-label="失败"/);
assert.match(html, /1m 25s/);
assert.doesNotMatch(html, /animate-spin|执行中/);
}
if (state === "done") assert.match(html, /100%/);
}
}
});
2 changes: 1 addition & 1 deletion crates/shield-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "shield-core"
version = "1.8.2"
version = "1.8.3"
edition = "2021"
authors = ["MocikaDev"]
license = "MIT OR Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion crates/shield-core/src/protect/xop_pvm2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1151,7 +1151,7 @@ mod tests {
);
for (kind, expected, count) in [
("native", Pvm2SkipReason::NoCode, 2),
("ui", Pvm2SkipReason::UiRuntimeGlue, 3),
("ui", Pvm2SkipReason::CompatibilityVirtualContract, 3),
("registers", Pvm2SkipReason::RegisterLimit, 3),
] {
let dex = work.path().join(format!("zero-{kind}/dex"));
Expand Down
49 changes: 47 additions & 2 deletions crates/shield-core/src/protection_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ pub enum Pvm2SkipReason {
DataResourceRuntimeGlue,
CompilerGeneratedBridge,
PlaybackCompatibility,
CompatibilityVirtualContract,
CompatibilitySynchronization,
CompatibilityExceptionRegion,
CompatibilityNonLeafInstruction,
NullableReceiver,
UnsupportedOpcode,
RegisterLimit,
Expand All @@ -72,6 +76,23 @@ pub enum Pvm2SkipReason {

impl Pvm2SkipReason {
fn classify(reason: &str) -> Self {
if let Some((scope, restriction)) = reason.split_once(':') {
if matches!(
scope,
"ui_runtime_glue"
| "data_resource_runtime_glue"
| "playback_compatibility"
| "compiler_generated_bridge"
) {
return match restriction {
"virtual_contract" => Self::CompatibilityVirtualContract,
"synchronization" => Self::CompatibilitySynchronization,
"exception_region" => Self::CompatibilityExceptionRegion,
"non_leaf_instruction" => Self::CompatibilityNonLeafInstruction,
_ => Self::Other,
};
}
}
match reason {
"no_code" => Self::NoCode,
"initializer" => Self::Initializer,
Expand Down Expand Up @@ -99,10 +120,14 @@ impl Pvm2SkipReason {
match self {
Self::NoCode => "无 DEX 方法体(Native/抽象方法)",
Self::Initializer => "构造器/静态初始化器",
Self::UiRuntimeGlue => "界面/生命周期兼容规则",
Self::UiRuntimeGlue => "界面范围兼容规则(未细分)",
Self::DataResourceRuntimeGlue => "数据/资源框架兼容规则",
Self::CompilerGeneratedBridge => "编译器生成桥接/合成方法",
Self::PlaybackCompatibility => "媒体/辅助类兼容规则",
Self::CompatibilityVirtualContract => "兼容范围:继承/接口虚方法契约未验证",
Self::CompatibilitySynchronization => "兼容范围:同步方法",
Self::CompatibilityExceptionRegion => "兼容范围:含异常处理区",
Self::CompatibilityNonLeafInstruction => "兼容范围:含叶子方法白名单之外的指令",
Self::NullableReceiver => "接收者非空语义无法证明",
Self::UnsupportedOpcode => "不支持的指令",
Self::RegisterLimit => "寄存器超限",
Expand Down Expand Up @@ -260,9 +285,13 @@ impl std::fmt::Display for Pvm2Failure {
| Pvm2SkipReason::DataResourceRuntimeGlue
| Pvm2SkipReason::CompilerGeneratedBridge
| Pvm2SkipReason::PlaybackCompatibility
| Pvm2SkipReason::CompatibilityVirtualContract
| Pvm2SkipReason::CompatibilitySynchronization
| Pvm2SkipReason::CompatibilityExceptionRegion
| Pvm2SkipReason::CompatibilityNonLeafInstruction
)
}) {
write!(f, "\n部分方法被兼容规则排除,请根据本地报告确认要保护的业务范围;不应强行绕过兼容检查。")?;
write!(f, "\n兼容分类表示当前准入限制,不代表这些方法都是生命周期回调。仅修改类前缀或降低保护级别不能证明这些方法已获 PVM2 保护;可将独立业务逻辑提取到普通类,并重新核验逐方法覆盖。")?;
}
if self.summary.reasons.keys().any(|r| {
matches!(
Expand Down Expand Up @@ -714,6 +743,22 @@ mod tests {
let cases = [
("no_code", Pvm2SkipReason::NoCode),
("initializer", Pvm2SkipReason::Initializer),
(
"ui_runtime_glue:virtual_contract",
Pvm2SkipReason::CompatibilityVirtualContract,
),
(
"data_resource_runtime_glue:synchronization",
Pvm2SkipReason::CompatibilitySynchronization,
),
(
"playback_compatibility:exception_region",
Pvm2SkipReason::CompatibilityExceptionRegion,
),
(
"ui_runtime_glue:non_leaf_instruction",
Pvm2SkipReason::CompatibilityNonLeafInstruction,
),
("ui_runtime_glue", Pvm2SkipReason::UiRuntimeGlue),
(
"data_resource_runtime_glue",
Expand Down
Loading
Loading