mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 08:05:22 +08:00
548 lines
16 KiB
JavaScript
548 lines
16 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { createHash } = require('node:crypto');
|
|
|
|
const {
|
|
RESOURCE_TIERS,
|
|
createWorkloadPlans,
|
|
} = require('./ql3-linux-resource-gate.cjs');
|
|
|
|
const ARCHITECTURES = Object.freeze(['x64', 'arm64']);
|
|
const TIER_NAMES = Object.freeze([
|
|
'router-stress-ci',
|
|
'edge-release-ci',
|
|
'cluster-control-ci',
|
|
]);
|
|
const MAX_EVIDENCE_BYTES = 8 * 1024 * 1024;
|
|
const MAX_CANONICAL_DEPTH = 32;
|
|
const MAX_CANONICAL_NODES = 100_000;
|
|
const REVISION_PATTERN = /^[0-9a-f]{40}$/;
|
|
const REPOSITORY_PATTERN =
|
|
/^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99})\/[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99})$/;
|
|
const RUN_ID_PATTERN = /^[1-9][0-9]{0,31}$/;
|
|
const NODE_VERSION = 'v24.18.0';
|
|
|
|
class QingLong3LinuxResourceReleaseEvidenceError extends Error {
|
|
constructor(message) {
|
|
super(`QingLong 3.0 Linux resource release evidence failed: ${message}`);
|
|
this.name = 'QingLong3LinuxResourceReleaseEvidenceError';
|
|
}
|
|
}
|
|
|
|
function fail(message) {
|
|
throw new QingLong3LinuxResourceReleaseEvidenceError(message);
|
|
}
|
|
|
|
function isRecord(value) {
|
|
return (
|
|
value !== null &&
|
|
typeof value === 'object' &&
|
|
!Array.isArray(value) &&
|
|
Object.getPrototypeOf(value) === Object.prototype
|
|
);
|
|
}
|
|
|
|
function assertRecord(value, label) {
|
|
if (!isRecord(value)) fail(`${label} must be a plain object`);
|
|
return value;
|
|
}
|
|
|
|
function assertExactKeys(value, expected, label) {
|
|
const actual = Object.keys(assertRecord(value, label)).sort();
|
|
const wanted = [...expected].sort();
|
|
if (JSON.stringify(actual) !== JSON.stringify(wanted)) {
|
|
fail(`${label} fields are invalid`);
|
|
}
|
|
}
|
|
|
|
function assertSafeInteger(value, label, minimum = 0) {
|
|
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
fail(`${label} must be an integer >= ${minimum}`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function canonicalize(value, depth = 0, budget = { nodes: 0 }) {
|
|
budget.nodes += 1;
|
|
if (budget.nodes > MAX_CANONICAL_NODES) fail('evidence node budget exceeded');
|
|
if (depth > MAX_CANONICAL_DEPTH) fail('evidence depth budget exceeded');
|
|
if (value === null || typeof value === 'string' || typeof value === 'boolean') {
|
|
return value;
|
|
}
|
|
if (typeof value === 'number') {
|
|
if (!Number.isFinite(value)) fail('evidence contains a non-finite number');
|
|
return value;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return value.map((entry) => canonicalize(entry, depth + 1, budget));
|
|
}
|
|
if (!isRecord(value)) fail('evidence contains an unsupported value');
|
|
const result = {};
|
|
for (const key of Object.keys(value).sort()) {
|
|
if (!key || key.length > 128) fail('evidence contains an invalid key');
|
|
result[key] = canonicalize(value[key], depth + 1, budget);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function evidenceDigest(value) {
|
|
return createHash('sha256')
|
|
.update('qinglong/linux-resource-release-evidence\0')
|
|
.update(JSON.stringify(canonicalize(value)))
|
|
.digest('hex');
|
|
}
|
|
|
|
function normalizeSource(source) {
|
|
assertExactKeys(
|
|
source,
|
|
['repository', 'revision', 'workflow', 'runId', 'runAttempt'],
|
|
'source',
|
|
);
|
|
if (
|
|
typeof source.repository !== 'string' ||
|
|
!REPOSITORY_PATTERN.test(source.repository)
|
|
) {
|
|
fail('source repository is invalid');
|
|
}
|
|
if (
|
|
typeof source.revision !== 'string' ||
|
|
!REVISION_PATTERN.test(source.revision)
|
|
) {
|
|
fail('source revision must be a lowercase 40-character commit SHA');
|
|
}
|
|
if (
|
|
typeof source.workflow !== 'string' ||
|
|
source.workflow.length < 1 ||
|
|
source.workflow.length > 128 ||
|
|
/[\u0000-\u001f\u007f]/.test(source.workflow)
|
|
) {
|
|
fail('source workflow is invalid');
|
|
}
|
|
if (
|
|
typeof source.runId !== 'string' ||
|
|
!RUN_ID_PATTERN.test(source.runId)
|
|
) {
|
|
fail('source runId is invalid');
|
|
}
|
|
assertSafeInteger(source.runAttempt, 'source runAttempt', 1);
|
|
return Object.freeze({
|
|
repository: source.repository,
|
|
revision: source.revision,
|
|
workflow: source.workflow,
|
|
runId: source.runId,
|
|
runAttempt: source.runAttempt,
|
|
});
|
|
}
|
|
|
|
function validateMemoryEvents(before, after, tierName) {
|
|
assertRecord(before, `${tierName} memoryEventsBefore`);
|
|
assertRecord(after, `${tierName} memoryEventsAfter`);
|
|
for (const event of ['max', 'oom', 'oom_kill', 'oom_group_kill']) {
|
|
const previous = assertSafeInteger(
|
|
before[event],
|
|
`${tierName} memoryEventsBefore.${event}`,
|
|
);
|
|
const current = assertSafeInteger(
|
|
after[event],
|
|
`${tierName} memoryEventsAfter.${event}`,
|
|
);
|
|
if (current !== previous) fail(`${tierName} memory event ${event} changed`);
|
|
}
|
|
}
|
|
|
|
function validateTierReport(report, expectedTier, expectedArchitecture) {
|
|
assertExactKeys(
|
|
report,
|
|
[
|
|
'schemaVersion',
|
|
'tier',
|
|
'evidenceClass',
|
|
'supportedMinimum',
|
|
'identity',
|
|
'envelope',
|
|
'workloads',
|
|
'gates',
|
|
],
|
|
`${expectedTier} report`,
|
|
);
|
|
const tier = RESOURCE_TIERS[expectedTier];
|
|
if (
|
|
report.schemaVersion !== 1 ||
|
|
report.tier !== expectedTier ||
|
|
report.evidenceClass !== tier.evidenceClass ||
|
|
report.supportedMinimum !== tier.supportedMinimum
|
|
) {
|
|
fail(`${expectedTier} identity is invalid`);
|
|
}
|
|
|
|
assertExactKeys(
|
|
report.identity,
|
|
['platform', 'architecture', 'node', 'uid', 'gid'],
|
|
`${expectedTier} process identity`,
|
|
);
|
|
if (
|
|
report.identity.platform !== 'linux' ||
|
|
report.identity.architecture !== expectedArchitecture ||
|
|
report.identity.node !== NODE_VERSION ||
|
|
report.identity.uid !== 65532 ||
|
|
report.identity.gid !== 65532
|
|
) {
|
|
fail(`${expectedTier} did not run under the reviewed native identity`);
|
|
}
|
|
|
|
assertExactKeys(
|
|
report.envelope,
|
|
[
|
|
'memoryMaxBytes',
|
|
'memoryPeakBytes',
|
|
'swapMaxBytes',
|
|
'cpuQuotaCores',
|
|
'pidsMax',
|
|
'noNewPrivileges',
|
|
'seccompMode',
|
|
'rootReadOnly',
|
|
'workspaceReadOnly',
|
|
'tmpWritable',
|
|
'memoryEventsBefore',
|
|
'memoryEventsAfter',
|
|
],
|
|
`${expectedTier} envelope`,
|
|
);
|
|
if (
|
|
report.envelope.memoryMaxBytes !== tier.memoryMaxBytes ||
|
|
report.envelope.swapMaxBytes !== tier.swapMaxBytes ||
|
|
report.envelope.cpuQuotaCores !== tier.cpuQuotaCores ||
|
|
report.envelope.pidsMax !== tier.pidsMax ||
|
|
report.envelope.noNewPrivileges !== 1 ||
|
|
report.envelope.seccompMode !== 2 ||
|
|
report.envelope.rootReadOnly !== true ||
|
|
report.envelope.workspaceReadOnly !== true ||
|
|
report.envelope.tmpWritable !== true
|
|
) {
|
|
fail(`${expectedTier} envelope drifted`);
|
|
}
|
|
const peak = assertSafeInteger(
|
|
report.envelope.memoryPeakBytes,
|
|
`${expectedTier} memoryPeakBytes`,
|
|
1,
|
|
);
|
|
if (peak > tier.memoryMaxBytes) fail(`${expectedTier} memory peak exceeded`);
|
|
validateMemoryEvents(
|
|
report.envelope.memoryEventsBefore,
|
|
report.envelope.memoryEventsAfter,
|
|
expectedTier,
|
|
);
|
|
|
|
if (!Array.isArray(report.workloads)) fail(`${expectedTier} workloads are invalid`);
|
|
const expectedNames = createWorkloadPlans('/workspace', expectedTier).map(
|
|
({ name }) => name,
|
|
);
|
|
const actualNames = report.workloads.map((entry, index) => {
|
|
assertExactKeys(entry, ['name', 'report'], `${expectedTier} workload ${index}`);
|
|
if (!isRecord(entry.report)) fail(`${expectedTier} workload report is invalid`);
|
|
return entry.name;
|
|
});
|
|
if (JSON.stringify(actualNames) !== JSON.stringify(expectedNames)) {
|
|
fail(`${expectedTier} workload set drifted`);
|
|
}
|
|
|
|
assertExactKeys(report.gates, ['passed', 'violations'], `${expectedTier} gates`);
|
|
if (
|
|
report.gates.passed !== true ||
|
|
!Array.isArray(report.gates.violations) ||
|
|
report.gates.violations.length !== 0
|
|
) {
|
|
fail(`${expectedTier} gate did not pass`);
|
|
}
|
|
canonicalize(report);
|
|
return report;
|
|
}
|
|
|
|
function architecturePayload(source, architecture, reports) {
|
|
return {
|
|
schemaVersion: 1,
|
|
fixture: 'qinglong/linux-resource-architecture-evidence@v1',
|
|
source,
|
|
architecture,
|
|
reports,
|
|
gates: {
|
|
nativeArchitecture: true,
|
|
exactTierSet: true,
|
|
exactResourceEnvelopes: true,
|
|
memoryEventsStable: true,
|
|
allTierGatesPassed: true,
|
|
sourceBound: true,
|
|
passed: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
function bundleArchitectureEvidence({ source, architecture, reports }) {
|
|
const normalizedSource = normalizeSource(source);
|
|
if (!ARCHITECTURES.includes(architecture)) fail('architecture is invalid');
|
|
assertExactKeys(reports, TIER_NAMES, 'reports');
|
|
const normalizedReports = Object.freeze(
|
|
Object.fromEntries(
|
|
TIER_NAMES.map((tierName) => [
|
|
tierName,
|
|
validateTierReport(reports[tierName], tierName, architecture),
|
|
]),
|
|
),
|
|
);
|
|
const payload = architecturePayload(
|
|
normalizedSource,
|
|
architecture,
|
|
normalizedReports,
|
|
);
|
|
return Object.freeze({
|
|
...payload,
|
|
bundleDigest: evidenceDigest(payload),
|
|
});
|
|
}
|
|
|
|
function validateArchitectureEvidence(value, expectedSource, architecture) {
|
|
assertExactKeys(
|
|
value,
|
|
[
|
|
'schemaVersion',
|
|
'fixture',
|
|
'source',
|
|
'architecture',
|
|
'reports',
|
|
'gates',
|
|
'bundleDigest',
|
|
],
|
|
`${architecture} architecture evidence`,
|
|
);
|
|
const rebuilt = bundleArchitectureEvidence({
|
|
source: value.source,
|
|
architecture: value.architecture,
|
|
reports: value.reports,
|
|
});
|
|
if (
|
|
value.schemaVersion !== rebuilt.schemaVersion ||
|
|
value.fixture !== rebuilt.fixture ||
|
|
value.architecture !== architecture ||
|
|
JSON.stringify(value.gates) !== JSON.stringify(rebuilt.gates) ||
|
|
value.bundleDigest !== rebuilt.bundleDigest
|
|
) {
|
|
fail(`${architecture} architecture evidence digest or gates drifted`);
|
|
}
|
|
if (JSON.stringify(rebuilt.source) !== JSON.stringify(normalizeSource(expectedSource))) {
|
|
fail(`${architecture} architecture evidence belongs to another source`);
|
|
}
|
|
return rebuilt;
|
|
}
|
|
|
|
function tierSummary(report) {
|
|
return Object.freeze({
|
|
tier: report.tier,
|
|
evidenceClass: report.evidenceClass,
|
|
memoryMaxBytes: report.envelope.memoryMaxBytes,
|
|
memoryPeakBytes: report.envelope.memoryPeakBytes,
|
|
cpuQuotaCores: report.envelope.cpuQuotaCores,
|
|
pidsMax: report.envelope.pidsMax,
|
|
workloadCount: report.workloads.length,
|
|
});
|
|
}
|
|
|
|
function releasePayload(source, x64, arm64) {
|
|
return {
|
|
schemaVersion: 1,
|
|
fixture: 'qinglong/linux-resource-cross-architecture-evidence@v1',
|
|
source,
|
|
architectures: [x64, arm64].map((entry) => ({
|
|
architecture: entry.architecture,
|
|
node: entry.reports['router-stress-ci'].identity.node,
|
|
bundleDigest: entry.bundleDigest,
|
|
tiers: TIER_NAMES.map((tierName) => tierSummary(entry.reports[tierName])),
|
|
})),
|
|
gates: {
|
|
nativeX64Passed: true,
|
|
nativeArm64Passed: true,
|
|
sameSourceRevision: true,
|
|
sameWorkflowRun: true,
|
|
exactTierParity: true,
|
|
releaseEvidenceComplete: true,
|
|
passed: true,
|
|
},
|
|
limitations: [
|
|
'CI cgroup evidence is not a supported minimum hardware claim',
|
|
'CI evidence does not replace fixed-device power-loss, flash, thermal, or soak evidence',
|
|
'GitHub workflow identity binding is not a cryptographic hardware attestation',
|
|
],
|
|
};
|
|
}
|
|
|
|
function mergeCrossArchitectureEvidence({ source, x64, arm64 }) {
|
|
const normalizedSource = normalizeSource(source);
|
|
const validatedX64 = validateArchitectureEvidence(x64, normalizedSource, 'x64');
|
|
const validatedArm64 = validateArchitectureEvidence(
|
|
arm64,
|
|
normalizedSource,
|
|
'arm64',
|
|
);
|
|
if (validatedX64.bundleDigest === validatedArm64.bundleDigest) {
|
|
fail('architecture bundles must be independently measured');
|
|
}
|
|
const payload = releasePayload(
|
|
normalizedSource,
|
|
validatedX64,
|
|
validatedArm64,
|
|
);
|
|
return Object.freeze({
|
|
...payload,
|
|
releaseDigest: evidenceDigest(payload),
|
|
});
|
|
}
|
|
|
|
function readJsonFile(filePath, label) {
|
|
const absolute = path.resolve(filePath);
|
|
if (!Number.isInteger(fs.constants.O_NOFOLLOW)) {
|
|
fail('platform does not support no-follow evidence reads');
|
|
}
|
|
let handle;
|
|
try {
|
|
handle = fs.openSync(
|
|
absolute,
|
|
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW,
|
|
);
|
|
} catch (error) {
|
|
fail(
|
|
`${label} must be a readable non-symlink file (${error?.code ?? 'unknown'})`,
|
|
);
|
|
}
|
|
try {
|
|
const stat = fs.fstatSync(handle);
|
|
if (!stat.isFile() || stat.size < 2 || stat.size > MAX_EVIDENCE_BYTES) {
|
|
fail(`${label} size is invalid`);
|
|
}
|
|
const content = fs.readFileSync(handle);
|
|
if (content.length < 2 || content.length > MAX_EVIDENCE_BYTES) {
|
|
fail(`${label} size is invalid`);
|
|
}
|
|
let value;
|
|
try {
|
|
value = JSON.parse(content.toString('utf8'));
|
|
} catch {
|
|
fail(`${label} must contain valid JSON`);
|
|
}
|
|
canonicalize(value);
|
|
return value;
|
|
} finally {
|
|
fs.closeSync(handle);
|
|
}
|
|
}
|
|
|
|
function writeJsonFile(filePath, value) {
|
|
const absolute = path.resolve(filePath);
|
|
fs.mkdirSync(path.dirname(absolute), { recursive: true });
|
|
const handle = fs.openSync(absolute, 'wx', 0o600);
|
|
try {
|
|
fs.writeFileSync(handle, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
} finally {
|
|
fs.closeSync(handle);
|
|
}
|
|
}
|
|
|
|
function parseArguments(argv) {
|
|
const options = {};
|
|
for (const argument of argv) {
|
|
const separator = argument.indexOf('=');
|
|
if (separator < 3 || !argument.startsWith('--')) {
|
|
fail(`unsupported argument ${argument}`);
|
|
}
|
|
const name = argument.slice(2, separator);
|
|
if (Object.hasOwn(options, name)) fail(`duplicate argument --${name}`);
|
|
options[name] = argument.slice(separator + 1);
|
|
}
|
|
const mode = options.mode;
|
|
const common = [
|
|
'mode',
|
|
'repository',
|
|
'revision',
|
|
'workflow',
|
|
'run-id',
|
|
'run-attempt',
|
|
'output',
|
|
];
|
|
const modeKeys =
|
|
mode === 'bundle'
|
|
? [...common, 'architecture', ...TIER_NAMES]
|
|
: mode === 'merge'
|
|
? [...common, 'x64', 'arm64']
|
|
: fail('--mode must be bundle or merge');
|
|
if (JSON.stringify(Object.keys(options).sort()) !== JSON.stringify(modeKeys.sort())) {
|
|
fail(`${mode} arguments are incomplete or widened`);
|
|
}
|
|
const runAttempt = Number(options['run-attempt']);
|
|
const source = normalizeSource({
|
|
repository: options.repository,
|
|
revision: options.revision,
|
|
workflow: options.workflow,
|
|
runId: options['run-id'],
|
|
runAttempt,
|
|
});
|
|
return Object.freeze({ ...options, mode, source });
|
|
}
|
|
|
|
function main() {
|
|
const options = parseArguments(process.argv.slice(2));
|
|
if (options.mode === 'bundle') {
|
|
const reports = Object.fromEntries(
|
|
TIER_NAMES.map((tierName) => [
|
|
tierName,
|
|
readJsonFile(options[tierName], tierName),
|
|
]),
|
|
);
|
|
const result = bundleArchitectureEvidence({
|
|
source: options.source,
|
|
architecture: options.architecture,
|
|
reports,
|
|
});
|
|
writeJsonFile(options.output, result);
|
|
process.stdout.write(
|
|
`${JSON.stringify({ schemaVersion: 1, mode: 'bundle', architecture: result.architecture, bundleDigest: result.bundleDigest, passed: true })}\n`,
|
|
);
|
|
return;
|
|
}
|
|
const result = mergeCrossArchitectureEvidence({
|
|
source: options.source,
|
|
x64: readJsonFile(options.x64, 'x64 evidence'),
|
|
arm64: readJsonFile(options.arm64, 'arm64 evidence'),
|
|
});
|
|
writeJsonFile(options.output, result);
|
|
process.stdout.write(
|
|
`${JSON.stringify({ schemaVersion: 1, mode: 'merge', releaseDigest: result.releaseDigest, passed: true })}\n`,
|
|
);
|
|
}
|
|
|
|
module.exports = {
|
|
ARCHITECTURES,
|
|
MAX_EVIDENCE_BYTES,
|
|
NODE_VERSION,
|
|
QingLong3LinuxResourceReleaseEvidenceError,
|
|
TIER_NAMES,
|
|
bundleArchitectureEvidence,
|
|
evidenceDigest,
|
|
mergeCrossArchitectureEvidence,
|
|
normalizeSource,
|
|
parseArguments,
|
|
readJsonFile,
|
|
validateArchitectureEvidence,
|
|
validateTierReport,
|
|
};
|
|
|
|
if (require.main === module) {
|
|
try {
|
|
main();
|
|
} catch (error) {
|
|
process.stderr.write(
|
|
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|