From adeaa2bafdf5cd4db9bd27c21e913be4f376e6a7 Mon Sep 17 00:00:00 2001 From: Lu Yifei <118034848+LuYifei2011@users.noreply.github.com> Date: Sun, 8 Nov 2026 14:07:19 +0800 Subject: [PATCH] feat(protection): add optional aggressive VM property renaming Expose aggressiveRenaming in the website and Node API with a default-off setting. Extend the seeded mapping to VM methods, internal fields and core block packages. Scope thread status separately from external response status and reserve native platform properties. Add differential and browser coverage for the new mode. --- .github/workflows/build.yml | 4 + .github/workflows/deploy.yml | 4 + docs/specialized-builds.md | 9 ++ node-api-docs/README.md | 3 + package.json | 3 +- scripts/prepare-runtime-sources.mjs | 12 +- src/locales/en.json | 6 + src/locales/zh-cn.json | 2 + src/locales/zh-tw.json | 2 + src/p4/PackagerOptions.svelte | 7 +- src/packager/compile-project.js | 9 +- src/packager/packager.js | 4 +- src/specialized/aggressive-properties.mjs | 69 +++++++++++ src/specialized/link.mjs | 53 ++++++++- test/p4/persistent-store.test.js | 6 +- test/packager/compile-project.test.js | 7 +- test/specialized-build.mjs | 16 ++- test/specialized-differential.mjs | 15 ++- test/specialized-properties.mjs | 135 ++++++++++++++++++++-- test/specialized-worker-input.mjs | 3 +- 20 files changed, 331 insertions(+), 38 deletions(-) create mode 100644 src/specialized/aggressive-properties.mjs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 92294d5..8d620cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,10 +28,14 @@ jobs: npm test -- --runInBand npm run test:specialized node test/specialized-differential.mjs --linked + npm run test:specialized-aggressive npm run test:specialized-build node test/specialized-worker-input.mjs npx playwright install --with-deps chromium firefox webkit npm run test:specialized-browser + node test/specialized-build.mjs --aggressive + node test/specialized-worker-input.mjs --aggressive + npm run test:specialized-browser npm run test:specialized-performance - name: Build standalone run: npm run build-standalone-prod diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ed3d9e2..ea717f3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -36,10 +36,14 @@ jobs: npm run build-node-prod npm run test:specialized node test/specialized-differential.mjs --linked + npm run test:specialized-aggressive npm run test:specialized-build node test/specialized-worker-input.mjs npx playwright install --with-deps chromium firefox webkit npm run test:specialized-browser + node test/specialized-build.mjs --aggressive + node test/specialized-worker-input.mjs --aggressive + npm run test:specialized-browser npm run test:specialized-performance - name: Build website run: | diff --git a/docs/specialized-builds.md b/docs/specialized-builds.md index 50ee386..15d10ae 100644 --- a/docs/specialized-builds.md +++ b/docs/specialized-builds.md @@ -16,6 +16,12 @@ This is a bounded scheduling audit, not whole-program ownership inference. Ambig Capabilities are selected from emitted operations, assets, hats, monitors and extensions. Unused sound, video and pen modules are removed. Builtin block classes retain only referenced methods, transitive helpers and initialization. Builtin extensions with static registration metadata are pruned by required opcodes and direct IR calls. Dynamic registration metadata conservatively retains its extension as a unit. `diagnostics` lists retained and removed modules/members, capability reasons, shared dispatch groups, fixed-extension dependencies and surviving third-party CommonJS wrappers. +The optional `aggressiveRenaming` build option (or `packager.options.compiler.aggressiveRenaming`) defaults to `false`. It extends the shared seeded property map to methods and internal fields collected from the prepared Runtime, Thread, Sequencer, Target, RenderedTarget, Sprite, Variable and core block classes. This includes core module entry fields (`ext_scratch3_*`), private helpers, public sprite methods (`setXY`, `setSize`, `setCostume`, `getCostumes`, `isTouchingObject`), runtime methods (`startHats`, `getTargetForStage`) and thread fields such as `isKilled`. Definitions, initialization, compiled calls and monitor code use the same mapping. + +Thread `status` is normalized to an internal field before linking in an explicit source-module/receiver manifest, including the pause addon's saved state. Unexpected status receivers fail the build for review. HTTP `Response.status` and other external status fields retain their names. `diagnostics.scopedProperties['Thread.status']` reports the internal field's final name. + +The source pack includes the pinned Terser platform-property inventory, covered by its fingerprint. Native JS/browser APIs, the frozen player API, extension registration/RPC shapes and SB3 state keys remain stable. In particular, `variables`, `currentCostume` and `value` remain unchanged. String literals (including literal property access and reflection keys) also reserve names. This mode targets standard Scratch projects and may break third-party extension compatibility. `diagnostics.aggressiveRenaming` records the mode; `diagnostics.aggressiveProperties` lists candidate mappings and reservation reasons. Regenerate old source packs before using this expanded mode. + The wrapper allowlist contains only JSZip's `utils.js`/`base64.js` cycle, whose `require` bindings capture partially initialized mutable export objects. Their source hashes, dependency cycle and initialization reason are recorded in the build report; source changes require a fresh review. Player-only ZIP preparation excludes Node stream adapters and obsolete Promise polyfills. Text utility module closures are converted to ESM. TWGL 4.4.0's pinned 15-module acyclic webpack bundle is unpacked into ESM at build-tool preparation time, removing its factory table and loader. Both transformations check their source shape, and the TWGL transform checks its exact source hash. CommonJS uses the explicit allowlist, rather than automatic conditional-require wrapping. Any surviving unaudited wrapper fails the build. ## Playback API @@ -53,6 +59,7 @@ npm test -- --runInBand npm run test:specialized node --test test/specialized-structure.mjs node --test test/specialized-properties.mjs +npm run test:specialized-aggressive node test/specialized-build.mjs node test/specialized-worker-input.mjs node test/specialized-browser.mjs @@ -63,6 +70,8 @@ node test/specialized-standalone.mjs Playwright's Chromium, Firefox and WebKit binaries must be installed for browser tests. Build/performance artifacts go to the sibling `work/acceptance` directory and are not published. Use Node 22.15 or newer. The lockfile pins the Node and browser Rollup versions; the WASM bootstrap patch deliberately fails if that pinned entry changes. +To exercise aggressive mode through the public Node API, HTML/ZIP packaging, real browser playback and browser Worker parity, run `node test/specialized-build.mjs --aggressive` and `node test/specialized-worker-input.mjs --aggressive` before `node test/specialized-browser.mjs`. These regenerate the local acceptance artifacts for the selected mode. + CI checks out both repositories as siblings. Set the `SPECIALIZED_VM_REF` repository variable to the full commit SHA containing the matching VM changes; workflows reject an absent or floating revision. No commit is invented or automatically pushed by local builds. `node test/specialized-differential.mjs --linked` runs the same fixture comparison against the complete linked/pruned/renamed production VM, in addition to the faster continuation-only harness. The execution harness covers all 112 existing VM fixtures and compares variables, lists and SAY event order with JS AOT. The linked variant also exercises serialization, pruning and joint transformation; it caught headless target-layer loss and inline legacy primitive registration, both corrected in the implementation. Separate lifecycle tests cover stale Promise completions after stop/restart and completion during pause. Artifact tests reject direct/aliased/computed dynamic evaluation, runtime imports and generators, and inspect source removal, capability removal, seed determinism and dispatch structure. Browser tests prohibit JavaScript dynamic evaluation and exercise playback, API isolation, async extensions, actual WebGL pen pixels and Node/Worker byte equality. diff --git a/node-api-docs/README.md b/node-api-docs/README.md index 27e9153..3daeac2 100644 --- a/node-api-docs/README.md +++ b/node-api-docs/README.md @@ -119,10 +119,13 @@ Every export jointly links the optimized project and its specialized VM. Node 22 ```js packager.options.compiler.seed = 'reproducible-build'; packager.options.compiler.warpTimer = false; +packager.options.compiler.aggressiveRenaming = false; ``` The seed is optional; an empty seed selects fresh cryptographic random build material. A seed is not a secret or an encryption key. +Enable `packager.options.compiler.aggressiveRenaming = true` (or pass `aggressiveRenaming: true` to `compileProject`) to also rename VM internal fields and runtime, sprite and core block methods. Coverage includes `isKilled`, thread execution status, `startHats`, `getTargetForStage`, `setXY`, `setSize`, `getCostumes`, and calls such as `runtime.ext_scratch3_motion._moveSteps(10, target)`. The default is `false`. This mode is intended for standard Scratch projects and may break third-party extension compatibility. Native JS/browser APIs, public player/extension APIs, SB3 state keys (`variables`, `currentCostume`) and names referenced as string literals remain stable. HTTP response `status` is preserved separately from thread status. `diagnostics.aggressiveProperties` records renamed and reserved candidates; `diagnostics.scopedProperties['Thread.status']` records the thread status mapping. + To obtain the linked JavaScript and resource archive without a page wrapper: ```js diff --git a/package.json b/package.json index 730b528..0f13707 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,12 @@ "build-node": "cross-env BUILD_MODE=node npm run build", "build-node-prod": "cross-env NODE_ENV=production npm run build-node", "test": "jest", - "prepublishOnly": "npm run test && npm run build-node-prod && npm run test:specialized && npm run test:specialized-linked && npm run test:specialized-build && npm run test:specialized-performance && node src/build/nodejs/update-readme.js node", + "prepublishOnly": "npm run test && npm run build-node-prod && npm run test:specialized && npm run test:specialized-linked && npm run test:specialized-aggressive && npm run test:specialized-build && npm run test:specialized-performance && node src/build/nodejs/update-readme.js node", "postpublish": "node src/build/nodejs/update-readme.js restore", "prepare:specialized": "node scripts/prepare-runtime-sources.mjs && node scripts/build-linker.mjs", "test:specialized": "node test/specialized-differential.mjs && node --test test/specialized-lifecycle.mjs test/specialized-structure.mjs test/specialized-rendering.mjs test/specialized-properties.mjs", "test:specialized-linked": "node test/specialized-differential.mjs --linked", + "test:specialized-aggressive": "node test/specialized-differential.mjs --linked --aggressive", "test:specialized-build": "node test/specialized-build.mjs", "test:specialized-browser": "node test/specialized-browser.mjs", "test:specialized-performance": "node test/specialized-performance.mjs", diff --git a/scripts/prepare-runtime-sources.mjs b/scripts/prepare-runtime-sources.mjs index ade33e9..0644b07 100644 --- a/scripts/prepare-runtime-sources.mjs +++ b/scripts/prepare-runtime-sources.mjs @@ -413,8 +413,16 @@ helperAST.program.body = helperAST.program.body.filter(n => !t.isVariableDeclara !n.declarations.some(d => ['stuckCounter', 'isStuck', 'colorToList'].includes(d.id.name))); helperAST.program.body = helperAST.program.body.map(n => t.isVariableDeclaration(n) ? t.exportNamedDeclaration(n) : n); const helpers = generate(helperAST, {comments: false}).code; -const fingerprint = createHash('sha256').update(JSON.stringify({sources, helpers})).digest('hex'); +// Use the pinned minifier's platform property inventory in both Node and the +// browser linker. Native browser/JS properties must never be global candidates. +const platformTree = ast(await fs.readFile(path.join(path.dirname(require.resolve('terser/package.json')), 'tools/domprops.js'), 'utf8')); +const platformArray = platformTree.program.body[0]?.declaration?.declarations[0]?.init; +if (!t.isArrayExpression(platformArray) || !platformArray.elements.every(t.isStringLiteral)) { + throw new Error('Terser platform property inventory changed; review aggressive renaming'); +} +const platformProperties = platformArray.elements.map(node => node.value); +const fingerprint = createHash('sha256').update(JSON.stringify({sources, helpers, platformProperties})).digest('hex'); await fs.mkdir(path.dirname(output), {recursive: true}); -await fs.writeFile(output, JSON.stringify({version: 1, fingerprint, entry, extensionApi, sources, provenance, helpers, wrapperAudit})); +await fs.writeFile(output, JSON.stringify({version: 1, fingerprint, entry, extensionApi, sources, provenance, helpers, wrapperAudit, platformProperties})); await bundle.close(); console.log(`Prepared ${Object.keys(sources).length} runtime modules (${fingerprint.slice(0, 12)})`); diff --git a/src/locales/en.json b/src/locales/en.json index 6029c19..b7b0c93 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -647,6 +647,12 @@ "buildSeed": { "string": "Build seed" }, + "aggressiveRenaming": { + "string": "Aggressive renaming" + }, + "aggressiveRenamingDescription": { + "string": "Also rename internal runtime fields, sprite methods and core block methods. Suitable for standard Scratch projects; may break third-party extensions." + }, "randomBuildSeed": { "string": "Random for each build when empty" } diff --git a/src/locales/zh-cn.json b/src/locales/zh-cn.json index 826cac4..bfb8bc1 100644 --- a/src/locales/zh-cn.json +++ b/src/locales/zh-cn.json @@ -149,6 +149,8 @@ "zip-one-asset": "Zip 压缩包,把所有素材打包在一个文件中(不推荐)", "specializedProtection": "程序专用化保护", "specializedProtectionDescription": "作品脚本与运行时联合编译,生成专用于此作品的离线播放器。", + "aggressiveRenaming": "激进重命名", + "aggressiveRenamingDescription": "同时重命名运行时内部字段、角色方法和原版积木方法。适用于原版 Scratch 作品,可能破坏第三方扩展兼容性。", "buildSeed": "构建 seed", "randomBuildSeed": "留空时每次构建随机生成" }, diff --git a/src/locales/zh-tw.json b/src/locales/zh-tw.json index 5e515b4..0ff4d45 100644 --- a/src/locales/zh-tw.json +++ b/src/locales/zh-tw.json @@ -86,6 +86,8 @@ "zip-one-asset": "Zip,封裝所有素材(不推薦)", "specializedProtection": "程式專用化保護", "specializedProtectionDescription": "作品腳本與執行環境聯合編譯,產生此作品專用的離線播放器。", + "aggressiveRenaming": "激進重新命名", + "aggressiveRenamingDescription": "同時重新命名執行環境內部欄位、角色方法和原版積木方法。適用於原版 Scratch 作品,可能影響第三方擴充功能的相容性。", "buildSeed": "建置 seed", "randomBuildSeed": "每次建置隨機產生", "customExtensionsSecurity": "擴充功能及程式碼相依套件會在建置時固定,並在獨立 Worker 中執行。不支援需要存取完整 VM 的擴充功能。" diff --git a/src/p4/PackagerOptions.svelte b/src/p4/PackagerOptions.svelte index 2034533..b938eb2 100644 --- a/src/p4/PackagerOptions.svelte +++ b/src/p4/PackagerOptions.svelte @@ -457,10 +457,15 @@ -
resetOptions(['compiler.seed'])}> +
resetOptions(['compiler.seed', 'compiler.aggressiveRenaming'])}>

{$_('options.specializedProtection')}

{$_('options.specializedProtectionDescription')}

+ +

{$_('options.aggressiveRenamingDescription')}