- VERSION file as authoritative version source - export.mjs reads VERSION with package.json fallback - bw-ota.ps1 DryRun mode for safe testing - auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
139 lines
4.7 KiB
JavaScript
139 lines
4.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* PoC H2: manifest 并发写入安全性
|
|
*
|
|
* 红队 red-team-logic A3 [HIGH]: 多 hook 并发 JSON.stringify 整文件写→撕裂
|
|
* 方案对比:
|
|
* M1. JSON 整写 (当前多 hook 做法) 期望: 撕裂
|
|
* M2. JSONL append-only + proper-lockfile 期望: 无撕裂
|
|
* M3. fs.appendFileSync 原子(小于 PIPE_BUF=4096) 期望: Linux 原子, Windows 部分保证
|
|
*
|
|
* 测试: 10 个子进程并发写 100 条记录, 读回检查总条数和完整性
|
|
* 产出: h2-report.json
|
|
*/
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { spawn } = require('child_process');
|
|
|
|
const SANDBOX = path.join(__dirname, '..', '..', 'ai-delivery-pipeline', '_poc-sandbox');
|
|
const WORKER = path.join(__dirname, 'poc-h2-worker.js');
|
|
const REPORT = path.join(SANDBOX, 'h2-report.json');
|
|
|
|
function makeWorkerIfMissing() {
|
|
if (fs.existsSync(WORKER)) return;
|
|
const src = `
|
|
'use strict';
|
|
const fs = require('fs');
|
|
const mode = process.argv[2]; // 'jsonl' | 'json-full'
|
|
const target = process.argv[3];
|
|
const workerId = process.argv[4];
|
|
const count = Number(process.argv[5]) || 100;
|
|
|
|
function writeJsonlRecord() {
|
|
for (let i = 0; i < count; i++) {
|
|
const rec = JSON.stringify({ worker: workerId, i, ts: Date.now() }) + '\\n';
|
|
fs.appendFileSync(target, rec);
|
|
}
|
|
}
|
|
|
|
function writeJsonFullRewrite() {
|
|
for (let i = 0; i < count; i++) {
|
|
let arr = [];
|
|
try { arr = JSON.parse(fs.readFileSync(target, 'utf8') || '[]'); } catch { arr = []; }
|
|
arr.push({ worker: workerId, i, ts: Date.now() });
|
|
fs.writeFileSync(target, JSON.stringify(arr), 'utf8');
|
|
}
|
|
}
|
|
|
|
if (mode === 'jsonl') writeJsonlRecord();
|
|
else writeJsonFullRewrite();
|
|
`;
|
|
fs.writeFileSync(WORKER, src, 'utf8');
|
|
}
|
|
|
|
function runConcurrent(mode, target, workers = 10, perWorker = 100) {
|
|
if (fs.existsSync(target)) fs.unlinkSync(target);
|
|
if (mode === 'jsonl') fs.writeFileSync(target, '', 'utf8');
|
|
|
|
return new Promise((resolve) => {
|
|
let done = 0;
|
|
const started = Date.now();
|
|
for (let w = 0; w < workers; w++) {
|
|
const proc = spawn(process.execPath, [WORKER, mode, target, 'w' + w, perWorker], {
|
|
stdio: 'ignore', windowsHide: true,
|
|
});
|
|
proc.on('exit', () => {
|
|
if (++done === workers) resolve(Date.now() - started);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function verifyJsonl(target, expected) {
|
|
const raw = fs.readFileSync(target, 'utf8');
|
|
const lines = raw.split('\n').filter(Boolean);
|
|
let parsed = 0, corrupt = 0;
|
|
for (const L of lines) {
|
|
try { JSON.parse(L); parsed++; } catch { corrupt++; }
|
|
}
|
|
return { total: lines.length, parsed, corrupt, expected };
|
|
}
|
|
|
|
function verifyJsonFull(target, expected) {
|
|
let parsed = 0, corrupt = 0, final = null;
|
|
try {
|
|
final = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
parsed = Array.isArray(final) ? final.length : 0;
|
|
} catch (e) {
|
|
corrupt = 1;
|
|
}
|
|
return { finalCount: parsed, corrupt, expected, lost: expected - parsed };
|
|
}
|
|
|
|
async function main() {
|
|
fs.mkdirSync(SANDBOX, { recursive: true });
|
|
makeWorkerIfMissing();
|
|
|
|
const W = 10, P = 100, EXPECTED = W * P;
|
|
|
|
const jsonlTarget = path.join(SANDBOX, 'h2-jsonl.log');
|
|
const jsonFullTarget = path.join(SANDBOX, 'h2-jsonfull.log');
|
|
|
|
console.log('[H2] running jsonl append-only with', W, 'workers x', P, 'records...');
|
|
const t1 = await runConcurrent('jsonl', jsonlTarget, W, P);
|
|
const jsonlResult = verifyJsonl(jsonlTarget, EXPECTED);
|
|
console.log('[H2] jsonl elapsed', t1, 'ms', jsonlResult);
|
|
|
|
console.log('[H2] running json full-rewrite (vulnerable pattern)...');
|
|
const t2 = await runConcurrent('json-full', jsonFullTarget, W, P);
|
|
const jsonResult = verifyJsonFull(jsonFullTarget, EXPECTED);
|
|
console.log('[H2] json-full elapsed', t2, 'ms', jsonResult);
|
|
|
|
const report = {
|
|
hypothesis: 'H2 · manifest 并发写入安全性',
|
|
platform: os.platform() + ' ' + os.release(),
|
|
nodeVersion: process.version,
|
|
timestamp: new Date().toISOString(),
|
|
workers: W, perWorker: P, expected: EXPECTED,
|
|
jsonl_append_only: { elapsedMs: t1, ...jsonlResult },
|
|
json_full_rewrite: { elapsedMs: t2, ...jsonResult },
|
|
verdict: {
|
|
jsonlSafe: jsonlResult.corrupt === 0 && jsonlResult.parsed === EXPECTED,
|
|
jsonFullSafe: jsonResult.corrupt === 0 && jsonResult.lost === 0,
|
|
},
|
|
recommendation: '',
|
|
};
|
|
report.recommendation = report.verdict.jsonlSafe && !report.verdict.jsonFullSafe
|
|
? '✅ manifest 必须用 JSONL append-only, 禁用 JSON 整文件写'
|
|
: report.verdict.jsonlSafe
|
|
? '✅ JSONL 安全; 整写偶然未暴露但仍有风险, 强制 JSONL'
|
|
: '❌ JSONL 也撕裂, 必须加 proper-lockfile';
|
|
fs.writeFileSync(REPORT, JSON.stringify(report, null, 2), 'utf8');
|
|
console.log('\n' + JSON.stringify(report, null, 2));
|
|
}
|
|
|
|
main();
|