@pulse-compute/wasm-guest-link 0.0.0 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -2
- package/package.json +29 -7
- package/src/constants.js +159 -0
- package/src/contracts.js +1143 -0
- package/src/errors.js +76 -0
- package/src/files.js +149 -0
- package/src/index.js +33 -0
- package/src/inspect.js +510 -0
- package/src/materialize.js +424 -0
- package/src/memory-owner.js +29 -0
- package/src/pipeline.js +729 -0
- package/src/stage.js +578 -0
- package/src/toolchain.js +125 -0
package/src/pipeline.js
ADDED
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const {
|
|
6
|
+
versions,
|
|
7
|
+
diagnosticCodes,
|
|
8
|
+
memoryAbi,
|
|
9
|
+
memoryAbiV2
|
|
10
|
+
} = require('./constants.js');
|
|
11
|
+
const { fail, wrap } = require('./errors.js');
|
|
12
|
+
const { normalizeGuestUnitPlan, optimizationArguments } = require('./contracts.js');
|
|
13
|
+
const { materializePackagePrebuilt } = require('./materialize.js');
|
|
14
|
+
const { inspectWasmFile, reportInspection, surfaceImport, surfaceExport } = require('./inspect.js');
|
|
15
|
+
const { runTool, binaryenIdentity } = require('./toolchain.js');
|
|
16
|
+
const { fileRecord, stableJson, writeAtomic } = require('./files.js');
|
|
17
|
+
|
|
18
|
+
function sortedSurface(values) {
|
|
19
|
+
return values.slice().sort((first, second) => (
|
|
20
|
+
compareText(String(first.module || ''), String(second.module || ''))
|
|
21
|
+
|| compareText(first.name, second.name)
|
|
22
|
+
|| compareText(first.kind, second.kind)
|
|
23
|
+
));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function compareText(first, second) {
|
|
27
|
+
return first < second ? -1 : first > second ? 1 : 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function targetImport(entry) {
|
|
31
|
+
return Object.freeze({ module: entry.module, name: entry.name, kind: entry.kind });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function equal(value, expected) {
|
|
35
|
+
return JSON.stringify(value) === JSON.stringify(expected);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function segmentFacts(values) {
|
|
39
|
+
return values
|
|
40
|
+
.map((entry) => ({
|
|
41
|
+
memoryIndex: entry.memoryIndex,
|
|
42
|
+
offset: entry.offset,
|
|
43
|
+
endExclusive: entry.endExclusive,
|
|
44
|
+
bytes: entry.bytes,
|
|
45
|
+
sha256: entry.sha256
|
|
46
|
+
}))
|
|
47
|
+
.sort((first, second) => (
|
|
48
|
+
first.memoryIndex - second.memoryIndex
|
|
49
|
+
|| first.offset - second.offset
|
|
50
|
+
|| compareText(first.sha256, second.sha256)
|
|
51
|
+
));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function rangeContains(range, segment) {
|
|
55
|
+
return segment.offset >= range.start && segment.endExclusive <= range.endExclusive;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function memoryContractForPlan(plan) {
|
|
59
|
+
return plan.version === versions.guestUnitPlanV2 ? memoryAbiV2 : memoryAbi;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function memoryContractForManifest(manifest) {
|
|
63
|
+
return manifest.version === versions.guestUnitV2 ? memoryAbiV2 : memoryAbi;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function pairwiseDisjoint(ranges) {
|
|
67
|
+
const sorted = ranges.slice().sort((left, right) => left.start - right.start);
|
|
68
|
+
return sorted.every((range, index) => (
|
|
69
|
+
range.start >= 0
|
|
70
|
+
&& range.endExclusive > range.start
|
|
71
|
+
&& (index === 0 || sorted[index - 1].endExclusive <= range.start)
|
|
72
|
+
));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function memoryLayoutProof(plan, before, inspection, stage) {
|
|
76
|
+
const contract = memoryContractForPlan(plan);
|
|
77
|
+
const expectedSegments = segmentFacts([
|
|
78
|
+
...before.owner.dataSegments,
|
|
79
|
+
...before.primary.dataSegments,
|
|
80
|
+
...before.guest.dataSegments
|
|
81
|
+
]);
|
|
82
|
+
const observedSegments = segmentFacts(inspection.dataSegments);
|
|
83
|
+
const ranges = [
|
|
84
|
+
Object.freeze({
|
|
85
|
+
id: 'rust-stack',
|
|
86
|
+
...contract.layout.rustStack,
|
|
87
|
+
disposition: plan.version === versions.guestUnitPlanV2
|
|
88
|
+
? 'stack-and-transient-fixed-scratch'
|
|
89
|
+
: 'reserved-stack'
|
|
90
|
+
}),
|
|
91
|
+
Object.freeze({
|
|
92
|
+
id: 'rust-static-data',
|
|
93
|
+
...contract.layout.rustStatic,
|
|
94
|
+
disposition: 'reserved-static-region'
|
|
95
|
+
}),
|
|
96
|
+
Object.freeze({
|
|
97
|
+
id: 'primary-static-data',
|
|
98
|
+
...contract.layout.primaryStatic,
|
|
99
|
+
disposition: 'reserved-static-region'
|
|
100
|
+
})
|
|
101
|
+
];
|
|
102
|
+
if (plan.version === versions.guestUnitPlanV2) {
|
|
103
|
+
ranges.push(Object.freeze({
|
|
104
|
+
id: 'private-invocation-frame-v2',
|
|
105
|
+
...contract.layout.invocationFrame,
|
|
106
|
+
disposition: 'caller-owned-cleared-after-scalar-capture'
|
|
107
|
+
}));
|
|
108
|
+
} else {
|
|
109
|
+
ranges.push(Object.freeze({
|
|
110
|
+
id: 'borrowed-input',
|
|
111
|
+
...contract.layout.borrowedInput,
|
|
112
|
+
disposition: 'synchronous-read-only'
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
if (!pairwiseDisjoint(ranges)) {
|
|
116
|
+
fail(diagnosticCodes.finalAuditFailed, `${stage} memory reservations overlap.`);
|
|
117
|
+
}
|
|
118
|
+
for (const segment of observedSegments) {
|
|
119
|
+
if (
|
|
120
|
+
!rangeContains(contract.layout.rustStatic, segment)
|
|
121
|
+
&& !rangeContains(contract.layout.primaryStatic, segment)
|
|
122
|
+
) {
|
|
123
|
+
fail(diagnosticCodes.finalAuditFailed, `${stage} contains static data outside its reserved regions.`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return Object.freeze({
|
|
127
|
+
stage,
|
|
128
|
+
memoryIdentity: contract.identity,
|
|
129
|
+
fixedMemory: Object.freeze({
|
|
130
|
+
pages: contract.minimumPages,
|
|
131
|
+
bytes: contract.bytes,
|
|
132
|
+
growable: false
|
|
133
|
+
}),
|
|
134
|
+
ranges: Object.freeze(ranges.map((range) => Object.freeze({
|
|
135
|
+
...range,
|
|
136
|
+
bytes: range.endExclusive - range.start,
|
|
137
|
+
observedDataSegments: Object.freeze(observedSegments.filter((segment) => rangeContains(range, segment)))
|
|
138
|
+
}))),
|
|
139
|
+
fixedScratch: Object.freeze({
|
|
140
|
+
separateRange: false,
|
|
141
|
+
location: plan.version === versions.guestUnitPlanV2
|
|
142
|
+
? 'rust-stack'
|
|
143
|
+
: 'none-observed'
|
|
144
|
+
}),
|
|
145
|
+
outputStatus: Object.freeze({
|
|
146
|
+
representation: 'scalar-i32-function-result',
|
|
147
|
+
linearMemoryRange: null
|
|
148
|
+
}),
|
|
149
|
+
expectedStaticSegments: Object.freeze(expectedSegments),
|
|
150
|
+
observedStaticSegments: Object.freeze(observedSegments),
|
|
151
|
+
staticSegmentsMatchValidatedInputs: equal(expectedSegments, observedSegments),
|
|
152
|
+
occupiedAndReservedRangesPairwiseDisjoint: true
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function assertMemoryType(inspection, ownership, label, contract) {
|
|
157
|
+
const expectedType = [{
|
|
158
|
+
minimumPages: contract.minimumPages,
|
|
159
|
+
maximumPages: contract.maximumPages,
|
|
160
|
+
shared: false
|
|
161
|
+
}];
|
|
162
|
+
if (
|
|
163
|
+
inspection.memories !== 1
|
|
164
|
+
|| inspection.importedMemories !== ownership.imported
|
|
165
|
+
|| inspection.definedMemories !== ownership.defined
|
|
166
|
+
|| !equal(inspection.memoryTypes, expectedType)
|
|
167
|
+
) {
|
|
168
|
+
fail(diagnosticCodes.memoryMismatch, `${label} does not use the accepted fixed 32-page memory.`);
|
|
169
|
+
}
|
|
170
|
+
if (inspection.hasStart) fail(diagnosticCodes.startMismatch, `${label} has a forbidden start section.`);
|
|
171
|
+
if (inspection.features.length) fail(diagnosticCodes.featureMismatch, `${label} uses a feature outside the MVP baseline.`);
|
|
172
|
+
if (
|
|
173
|
+
inspection.instructions.memoryGrow
|
|
174
|
+
|| inspection.instructions.memoryCopy
|
|
175
|
+
|| inspection.instructions.memoryFill
|
|
176
|
+
|| inspection.instructions.memoryInit
|
|
177
|
+
|| inspection.instructions.callIndirect
|
|
178
|
+
) {
|
|
179
|
+
fail(diagnosticCodes.memoryMismatch, `${label} violates the accepted fixed-memory instruction policy.`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function assertPrimaryAndOwner(plan, manifest, primary, owner) {
|
|
184
|
+
const contract = memoryContractForManifest(manifest);
|
|
185
|
+
assertMemoryType(primary, { imported: 1, defined: 0 }, 'Primary module', contract);
|
|
186
|
+
assertMemoryType(owner, { imported: 0, defined: 1 }, 'Memory-owner module', contract);
|
|
187
|
+
if (owner.imports.length !== 0 || owner.exports.length !== 1) {
|
|
188
|
+
fail(diagnosticCodes.memoryMismatch, 'Memory-owner module must contain only one exported memory.');
|
|
189
|
+
}
|
|
190
|
+
const ownerExport = owner.exports[0];
|
|
191
|
+
if (ownerExport.name !== contract.importName || ownerExport.kind !== 'memory') {
|
|
192
|
+
fail(diagnosticCodes.memoryMismatch, 'Memory-owner export must be env.memory.');
|
|
193
|
+
}
|
|
194
|
+
if (
|
|
195
|
+
owner.dataSegments.length
|
|
196
|
+
|| owner.mutableGlobals.length
|
|
197
|
+
|| Object.values(owner.instructions).some((count) => count !== 0)
|
|
198
|
+
|| owner.tables !== 0
|
|
199
|
+
) {
|
|
200
|
+
fail(diagnosticCodes.memoryMismatch, 'Memory-owner module must be inert.');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const abiExports = manifest.exports.filter((entry) => entry.role === 'abi');
|
|
204
|
+
const guestImports = primary.imports.filter((entry) => entry.module === manifest.module);
|
|
205
|
+
const expectedGuestImports = abiExports.map((entry) => ({
|
|
206
|
+
module: manifest.module,
|
|
207
|
+
name: entry.name,
|
|
208
|
+
kind: entry.kind,
|
|
209
|
+
type: { parameters: entry.parameters, results: entry.results }
|
|
210
|
+
}));
|
|
211
|
+
if (!equal(guestImports.map(surfaceImport), expectedGuestImports)) {
|
|
212
|
+
fail(diagnosticCodes.abiMismatch, 'Primary guest imports do not exactly match the selected guest ABI.');
|
|
213
|
+
}
|
|
214
|
+
const memoryImports = primary.imports.filter((entry) => entry.kind === 'memory');
|
|
215
|
+
if (
|
|
216
|
+
memoryImports.length !== 1
|
|
217
|
+
|| memoryImports[0].module !== plan.composition.memoryOwnerModule
|
|
218
|
+
|| memoryImports[0].name !== contract.importName
|
|
219
|
+
) {
|
|
220
|
+
fail(diagnosticCodes.memoryMismatch, 'Primary memory import does not bind to the accepted memory owner.');
|
|
221
|
+
}
|
|
222
|
+
const unresolved = primary.imports.filter((entry) => (
|
|
223
|
+
entry.module !== manifest.module
|
|
224
|
+
&& !(entry.kind === 'memory' && entry.module === plan.composition.memoryOwnerModule && entry.name === contract.importName)
|
|
225
|
+
)).map(targetImport);
|
|
226
|
+
if (!equal(sortedSurface(unresolved), sortedSurface(plan.targetPolicy.allowedImports))) {
|
|
227
|
+
fail(diagnosticCodes.importMismatch, 'Primary host imports do not match the selected target policy.');
|
|
228
|
+
}
|
|
229
|
+
for (const segment of primary.dataSegments) {
|
|
230
|
+
if (!rangeContains(contract.layout.primaryStatic, segment)) {
|
|
231
|
+
fail(diagnosticCodes.memoryMismatch, 'Primary static data escapes its reserved region.');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function expectedFinalExports(primary, guest, owner) {
|
|
237
|
+
const values = [
|
|
238
|
+
...owner.exports.map(surfaceExport),
|
|
239
|
+
...primary.exports.map(surfaceExport),
|
|
240
|
+
...guest.exports.map(surfaceExport)
|
|
241
|
+
];
|
|
242
|
+
const names = values.map((entry) => entry.name);
|
|
243
|
+
if (new Set(names).size !== names.length) {
|
|
244
|
+
fail(diagnosticCodes.exportMismatch, 'Guest composition inputs contain conflicting export names.');
|
|
245
|
+
}
|
|
246
|
+
return sortedSurface(values);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function assertFinal(plan, before, final) {
|
|
250
|
+
const contract = memoryContractForPlan(plan);
|
|
251
|
+
const allowedImports = sortedSurface(plan.targetPolicy.allowedImports);
|
|
252
|
+
const actualImports = sortedSurface(final.imports.map(targetImport));
|
|
253
|
+
if (!equal(actualImports, allowedImports)) {
|
|
254
|
+
fail(diagnosticCodes.finalAuditFailed, 'Final Wasm imports do not match the target policy.', {
|
|
255
|
+
expected: allowedImports,
|
|
256
|
+
actual: actualImports
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const expectedExports = expectedFinalExports(before.primary, before.guest, before.owner);
|
|
260
|
+
const actualExports = sortedSurface(final.exports.map(surfaceExport));
|
|
261
|
+
if (!equal(actualExports, expectedExports)) {
|
|
262
|
+
fail(diagnosticCodes.finalAuditFailed, 'Final Wasm exports do not exactly preserve the validated input surface.');
|
|
263
|
+
}
|
|
264
|
+
for (const required of plan.targetPolicy.requiredExports) {
|
|
265
|
+
if (!actualExports.some((entry) => entry.name === required.name && entry.kind === required.kind)) {
|
|
266
|
+
fail(diagnosticCodes.finalAuditFailed, `Final Wasm is missing target-required export ${required.name}.`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
assertMemoryType(final, { imported: 0, defined: 1 }, 'Final Wasm', contract);
|
|
270
|
+
const inputMutableGlobals = before.primary.mutableGlobals.length
|
|
271
|
+
+ before.guest.mutableGlobals.length
|
|
272
|
+
+ before.owner.mutableGlobals.length;
|
|
273
|
+
const inputTables = before.primary.tables + before.guest.tables + before.owner.tables;
|
|
274
|
+
const inputGlobalSets = before.primary.instructions.globalSet
|
|
275
|
+
+ before.guest.instructions.globalSet
|
|
276
|
+
+ before.owner.instructions.globalSet;
|
|
277
|
+
const inputStores = before.primary.instructions.store
|
|
278
|
+
+ before.guest.instructions.store
|
|
279
|
+
+ before.owner.instructions.store;
|
|
280
|
+
if (
|
|
281
|
+
final.mutableGlobals.length > inputMutableGlobals
|
|
282
|
+
|| final.tables > inputTables
|
|
283
|
+
|| final.instructions.globalSet > inputGlobalSets
|
|
284
|
+
|| (
|
|
285
|
+
plan.version === versions.guestUnitPlanV2
|
|
286
|
+
&& final.instructions.store > inputStores
|
|
287
|
+
)
|
|
288
|
+
) {
|
|
289
|
+
fail(
|
|
290
|
+
diagnosticCodes.finalAuditFailed,
|
|
291
|
+
plan.version === versions.guestUnitPlanV2
|
|
292
|
+
? 'Final Wasm introduces mutable state or writes absent from the validated inputs.'
|
|
293
|
+
: 'Final Wasm introduces mutable global or indirect state absent from the validated inputs.'
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const expectedSegments = segmentFacts([
|
|
297
|
+
...before.owner.dataSegments,
|
|
298
|
+
...before.primary.dataSegments,
|
|
299
|
+
...before.guest.dataSegments
|
|
300
|
+
]);
|
|
301
|
+
const actualSegments = segmentFacts(final.dataSegments);
|
|
302
|
+
if (!equal(actualSegments, expectedSegments)) {
|
|
303
|
+
fail(diagnosticCodes.finalAuditFailed, 'Final Wasm did not preserve validated static data segments exactly.');
|
|
304
|
+
}
|
|
305
|
+
for (const segment of final.dataSegments) {
|
|
306
|
+
if (
|
|
307
|
+
!rangeContains(contract.layout.rustStatic, segment)
|
|
308
|
+
&& !rangeContains(contract.layout.primaryStatic, segment)
|
|
309
|
+
) {
|
|
310
|
+
fail(diagnosticCodes.finalAuditFailed, 'Final Wasm static data escapes accepted reserved regions.');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function redactionRecord() {
|
|
316
|
+
return Object.freeze({
|
|
317
|
+
environmentSecrets: 'omitted',
|
|
318
|
+
credentials: 'omitted',
|
|
319
|
+
privateMaterial: 'omitted',
|
|
320
|
+
rawBuffers: 'omitted',
|
|
321
|
+
diagnosticStrategy: 'key-classification-pattern-and-environment-value-redaction'
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function evidenceClassification() {
|
|
326
|
+
return Object.freeze({
|
|
327
|
+
observedFacts: Object.freeze([
|
|
328
|
+
'packaged-hashes-verified',
|
|
329
|
+
'input-bytes-independently-inspected',
|
|
330
|
+
'validated-inputs-composed',
|
|
331
|
+
'post-link-bytes-optimized',
|
|
332
|
+
'final-bytes-independently-audited'
|
|
333
|
+
]),
|
|
334
|
+
toolDocumentedBehavior: Object.freeze([]),
|
|
335
|
+
inferences: Object.freeze([]),
|
|
336
|
+
unresolvedAssumptions: Object.freeze([])
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function finalAudit(plan, before, afterLink, final, artifactRecord) {
|
|
341
|
+
const contract = memoryContractForPlan(plan);
|
|
342
|
+
const beforeOptimization = memoryLayoutProof(plan, before, afterLink, 'before-optimization');
|
|
343
|
+
const afterOptimization = memoryLayoutProof(plan, before, final, 'after-optimization');
|
|
344
|
+
if (
|
|
345
|
+
!beforeOptimization.staticSegmentsMatchValidatedInputs
|
|
346
|
+
|| !afterOptimization.staticSegmentsMatchValidatedInputs
|
|
347
|
+
) {
|
|
348
|
+
fail(diagnosticCodes.finalAuditFailed, 'Static layout changed across guest composition or optimization.');
|
|
349
|
+
}
|
|
350
|
+
const validatedInputWrites = Object.freeze({
|
|
351
|
+
globalSet: before.primary.instructions.globalSet
|
|
352
|
+
+ before.guest.instructions.globalSet
|
|
353
|
+
+ before.owner.instructions.globalSet,
|
|
354
|
+
store: before.primary.instructions.store
|
|
355
|
+
+ before.guest.instructions.store
|
|
356
|
+
+ before.owner.instructions.store
|
|
357
|
+
});
|
|
358
|
+
return Object.freeze({
|
|
359
|
+
version: plan.version === versions.guestUnitPlanV2
|
|
360
|
+
? versions.finalWasmAuditV2
|
|
361
|
+
: versions.finalWasmAudit,
|
|
362
|
+
status: 'passed',
|
|
363
|
+
artifact: Object.freeze({
|
|
364
|
+
file: 'canonical-native.wasm',
|
|
365
|
+
bytes: artifactRecord.bytes,
|
|
366
|
+
sha256: artifactRecord.sha256,
|
|
367
|
+
valid: final.valid
|
|
368
|
+
}),
|
|
369
|
+
targetPolicy: Object.freeze({
|
|
370
|
+
descriptorIdentity: plan.targetPolicy.descriptorIdentity,
|
|
371
|
+
descriptorSha256: plan.targetPolicy.descriptorSha256
|
|
372
|
+
}),
|
|
373
|
+
structure: Object.freeze({
|
|
374
|
+
imports: Object.freeze(final.imports.map(surfaceImport)),
|
|
375
|
+
exports: Object.freeze(final.exports.map(surfaceExport)),
|
|
376
|
+
definedMemories: final.definedMemories,
|
|
377
|
+
importedMemories: final.importedMemories,
|
|
378
|
+
memoryTypes: final.memoryTypes,
|
|
379
|
+
hasStart: final.hasStart,
|
|
380
|
+
features: final.features,
|
|
381
|
+
dataSegments: Object.freeze(segmentFacts(final.dataSegments)),
|
|
382
|
+
mutableGlobals: final.mutableGlobals,
|
|
383
|
+
tables: final.tables
|
|
384
|
+
}),
|
|
385
|
+
instructions: Object.freeze({
|
|
386
|
+
memoryGrow: final.instructions.memoryGrow,
|
|
387
|
+
memoryCopy: final.instructions.memoryCopy,
|
|
388
|
+
memoryFill: final.instructions.memoryFill,
|
|
389
|
+
memoryInit: final.instructions.memoryInit,
|
|
390
|
+
callIndirect: final.instructions.callIndirect,
|
|
391
|
+
...(plan.version === versions.guestUnitPlanV2 ? {
|
|
392
|
+
globalSet: final.instructions.globalSet,
|
|
393
|
+
store: final.instructions.store
|
|
394
|
+
} : {})
|
|
395
|
+
}),
|
|
396
|
+
...(plan.version === versions.guestUnitPlanV2 ? {
|
|
397
|
+
mutableState: Object.freeze({
|
|
398
|
+
globals: final.mutableGlobals,
|
|
399
|
+
authorization: 'reviewed-es256-stack-pointer-and-stack-transient-writes',
|
|
400
|
+
validatedInputWrites,
|
|
401
|
+
finalWrites: Object.freeze({
|
|
402
|
+
globalSet: final.instructions.globalSet,
|
|
403
|
+
store: final.instructions.store
|
|
404
|
+
}),
|
|
405
|
+
noAdditionalWritesIntroduced: true
|
|
406
|
+
})
|
|
407
|
+
} : {}),
|
|
408
|
+
layout: Object.freeze({
|
|
409
|
+
memoryContract: contract.identity,
|
|
410
|
+
occupiedAndReservedRangesPairwiseDisjoint: true,
|
|
411
|
+
staticSegmentsPreserved: true,
|
|
412
|
+
...(plan.version === versions.guestUnitPlanV2 ? {
|
|
413
|
+
beforeOptimization,
|
|
414
|
+
afterOptimization
|
|
415
|
+
} : {})
|
|
416
|
+
}),
|
|
417
|
+
checks: Object.freeze([
|
|
418
|
+
Object.freeze({ id: 'artifact-hash', status: 'passed' }),
|
|
419
|
+
Object.freeze({ id: 'target-policy', status: 'passed' }),
|
|
420
|
+
Object.freeze({ id: 'guest-contract', status: 'passed' }),
|
|
421
|
+
Object.freeze({ id: 'fixed-memory', status: 'passed' }),
|
|
422
|
+
Object.freeze({ id: 'static-layout', status: 'passed' }),
|
|
423
|
+
Object.freeze({ id: 'forbidden-instructions', status: 'passed' })
|
|
424
|
+
]),
|
|
425
|
+
providerPackaging: Object.freeze({
|
|
426
|
+
authorized: true,
|
|
427
|
+
auditArtifactSha256MatchesInput: true
|
|
428
|
+
}),
|
|
429
|
+
evidenceClassification: evidenceClassification(),
|
|
430
|
+
redaction: redactionRecord()
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function guestLinkReport(plan, materialized, before, afterLink, final, records, toolchain, posture) {
|
|
435
|
+
const linker = toolchain.tools.find((entry) => entry.name === 'wasm-merge');
|
|
436
|
+
const optimizer = toolchain.tools.find((entry) => entry.name === 'wasm-opt');
|
|
437
|
+
return Object.freeze({
|
|
438
|
+
version: plan.version === versions.guestUnitPlanV2
|
|
439
|
+
? versions.guestLinkReportV2
|
|
440
|
+
: versions.guestLinkReport,
|
|
441
|
+
status: 'passed',
|
|
442
|
+
profile: plan.profile,
|
|
443
|
+
target: plan.target,
|
|
444
|
+
units: Object.freeze([
|
|
445
|
+
Object.freeze({
|
|
446
|
+
id: materialized.manifest.id,
|
|
447
|
+
module: materialized.manifest.module,
|
|
448
|
+
owner: materialized.manifest.owner,
|
|
449
|
+
packageVersion: materialized.manifest.packageVersion,
|
|
450
|
+
origin: materialized.manifest.origin,
|
|
451
|
+
abi: materialized.manifest.abi,
|
|
452
|
+
artifactSha256: materialized.artifactRecord.sha256,
|
|
453
|
+
sourceIncluded: materialized.sourceRecord.included,
|
|
454
|
+
sourceSha256: materialized.sourceRecord.treeSha256,
|
|
455
|
+
...(materialized.manifest.version === versions.guestUnitV2 ? {
|
|
456
|
+
manifestVersion: materialized.manifest.version,
|
|
457
|
+
memoryIdentity: materialized.manifest.memory.identity,
|
|
458
|
+
frame: plan.unit.frame,
|
|
459
|
+
toolchain: plan.unit.toolchain
|
|
460
|
+
} : {}),
|
|
461
|
+
provenance: Object.freeze({
|
|
462
|
+
packageManifest: materialized.provenance.packageManifest,
|
|
463
|
+
lockfile: materialized.provenance.lockfile,
|
|
464
|
+
...(materialized.manifest.version === versions.guestUnitV2
|
|
465
|
+
? { buildScript: materialized.provenance.buildScript }
|
|
466
|
+
: {}),
|
|
467
|
+
source: materialized.provenance.source
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
]),
|
|
471
|
+
inputs: Object.freeze({
|
|
472
|
+
primary: records.primary,
|
|
473
|
+
memoryOwner: records.owner,
|
|
474
|
+
guest: records.guest
|
|
475
|
+
}),
|
|
476
|
+
materialization: Object.freeze({
|
|
477
|
+
directory: materialized.relativeDirectory,
|
|
478
|
+
cache: 'content-addressed',
|
|
479
|
+
hashVerifiedBeforeUse: true
|
|
480
|
+
}),
|
|
481
|
+
inspection: Object.freeze({
|
|
482
|
+
before: Object.freeze({
|
|
483
|
+
primary: reportInspection(before.primary),
|
|
484
|
+
memoryOwner: reportInspection(before.owner),
|
|
485
|
+
guest: reportInspection(before.guest)
|
|
486
|
+
}),
|
|
487
|
+
composed: reportInspection(afterLink),
|
|
488
|
+
after: reportInspection(final)
|
|
489
|
+
}),
|
|
490
|
+
composition: Object.freeze({
|
|
491
|
+
kind: plan.composition.kind,
|
|
492
|
+
implementation: Object.freeze({
|
|
493
|
+
name: toolchain.name,
|
|
494
|
+
version: toolchain.version,
|
|
495
|
+
tool: linker.name,
|
|
496
|
+
sha256: linker.sha256
|
|
497
|
+
}),
|
|
498
|
+
deterministic: true,
|
|
499
|
+
outputSha256: records.composed.sha256
|
|
500
|
+
}),
|
|
501
|
+
optimization: Object.freeze({
|
|
502
|
+
postLink: true,
|
|
503
|
+
implementation: Object.freeze({
|
|
504
|
+
name: toolchain.name,
|
|
505
|
+
version: toolchain.version,
|
|
506
|
+
tool: optimizer.name,
|
|
507
|
+
sha256: optimizer.sha256
|
|
508
|
+
}),
|
|
509
|
+
posture,
|
|
510
|
+
deterministic: true,
|
|
511
|
+
inputSha256: records.composed.sha256,
|
|
512
|
+
outputSha256: records.final.sha256
|
|
513
|
+
}),
|
|
514
|
+
...(plan.version === versions.guestUnitPlanV2 ? {
|
|
515
|
+
memoryLayout: Object.freeze({
|
|
516
|
+
beforeOptimization: memoryLayoutProof(plan, before, afterLink, 'before-optimization'),
|
|
517
|
+
afterOptimization: memoryLayoutProof(plan, before, final, 'after-optimization')
|
|
518
|
+
})
|
|
519
|
+
} : {}),
|
|
520
|
+
finalArtifact: Object.freeze({
|
|
521
|
+
file: 'canonical-native.wasm',
|
|
522
|
+
bytes: records.final.bytes,
|
|
523
|
+
sha256: records.final.sha256
|
|
524
|
+
}),
|
|
525
|
+
fallback: Object.freeze({ enabled: false, used: false }),
|
|
526
|
+
evidenceClassification: evidenceClassification(),
|
|
527
|
+
redaction: redactionRecord()
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function ensureOutputDirectory(directory) {
|
|
532
|
+
try {
|
|
533
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
534
|
+
return fs.realpathSync(directory);
|
|
535
|
+
} catch {
|
|
536
|
+
fail(diagnosticCodes.invalid, 'Guest-link output directory could not be created.');
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function assertFinalArtifactIdentity(file, audit) {
|
|
541
|
+
if (
|
|
542
|
+
!audit
|
|
543
|
+
|| (audit.version !== versions.finalWasmAudit && audit.version !== versions.finalWasmAuditV2)
|
|
544
|
+
|| audit.status !== 'passed'
|
|
545
|
+
|| !audit.artifact
|
|
546
|
+
) {
|
|
547
|
+
fail(diagnosticCodes.finalAuditFailed, 'Final Wasm audit identity is missing or unsupported.');
|
|
548
|
+
}
|
|
549
|
+
let observed;
|
|
550
|
+
try {
|
|
551
|
+
observed = fileRecord(fs.realpathSync(file));
|
|
552
|
+
} catch (error) {
|
|
553
|
+
wrap(error, diagnosticCodes.finalAuditFailed, 'Final Wasm artifact could not be read for identity verification.');
|
|
554
|
+
}
|
|
555
|
+
if (
|
|
556
|
+
observed.bytes !== audit.artifact.bytes
|
|
557
|
+
|| observed.sha256 !== audit.artifact.sha256
|
|
558
|
+
) {
|
|
559
|
+
fail(diagnosticCodes.finalAuditFailed, 'Final Wasm bytes do not match their completed audit.', {
|
|
560
|
+
expected: {
|
|
561
|
+
bytes: audit.artifact.bytes,
|
|
562
|
+
sha256: audit.artifact.sha256
|
|
563
|
+
},
|
|
564
|
+
observed
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
return Object.freeze(observed);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function assertSelectedToolchain(plan, manifest, toolchain) {
|
|
571
|
+
if (plan.version !== versions.guestUnitPlanV2) return;
|
|
572
|
+
const optimizer = toolchain.tools.find((entry) => entry.name === 'wasm-opt');
|
|
573
|
+
if (
|
|
574
|
+
!optimizer
|
|
575
|
+
|| toolchain.version !== manifest.toolchain.versions.binaryen
|
|
576
|
+
|| toolchain.version !== plan.unit.toolchain.binaryenVersion
|
|
577
|
+
|| optimizer.sha256 !== manifest.provenance.binaryenWasmOptSha256
|
|
578
|
+
|| optimizer.sha256 !== plan.unit.toolchain.wasmOptSha256
|
|
579
|
+
) {
|
|
580
|
+
fail(
|
|
581
|
+
diagnosticCodes.optimizationFailed,
|
|
582
|
+
'Pinned Binaryen identity does not match the selected ES256 guest plan.'
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function realizeGuestLinkPlan(options) {
|
|
588
|
+
const plan = normalizeGuestUnitPlan(options.plan);
|
|
589
|
+
const outputDirectory = ensureOutputDirectory(options.outputDirectory);
|
|
590
|
+
const workDirectory = ensureOutputDirectory(path.join(outputDirectory, '.guest-link-work'));
|
|
591
|
+
const posture = plan.version === versions.guestUnitPlanV2
|
|
592
|
+
? plan.optimization.posture
|
|
593
|
+
: options.optimizationPosture || 'native-size';
|
|
594
|
+
if (
|
|
595
|
+
plan.version === versions.guestUnitPlanV2
|
|
596
|
+
&& options.optimizationPosture !== undefined
|
|
597
|
+
&& options.optimizationPosture !== posture
|
|
598
|
+
) {
|
|
599
|
+
fail(
|
|
600
|
+
diagnosticCodes.optimizationFailed,
|
|
601
|
+
'Guest-link optimization option does not match the exact v2 realization plan.'
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
const optimizationArgs = optimizationArguments(posture);
|
|
605
|
+
const toolchain = binaryenIdentity();
|
|
606
|
+
|
|
607
|
+
const materialized = materializePackagePrebuilt({
|
|
608
|
+
plan,
|
|
609
|
+
packageRoot: options.packageRoot,
|
|
610
|
+
manifestFile: options.manifestFile,
|
|
611
|
+
projectRoot: options.projectRoot,
|
|
612
|
+
synchronizedPackages: options.synchronizedPackages,
|
|
613
|
+
workDirectory
|
|
614
|
+
});
|
|
615
|
+
assertSelectedToolchain(plan, materialized.manifest, toolchain);
|
|
616
|
+
|
|
617
|
+
let primaryFile;
|
|
618
|
+
let ownerFile;
|
|
619
|
+
try {
|
|
620
|
+
primaryFile = fs.realpathSync(options.primaryFile);
|
|
621
|
+
ownerFile = fs.realpathSync(options.memoryOwnerFile);
|
|
622
|
+
if (!fs.statSync(primaryFile).isFile() || !fs.statSync(ownerFile).isFile()) throw new Error('not files');
|
|
623
|
+
} catch {
|
|
624
|
+
fail(diagnosticCodes.invalid, 'Guest-link primary and memory-owner inputs must be existing files.');
|
|
625
|
+
}
|
|
626
|
+
const before = Object.freeze({
|
|
627
|
+
primary: inspectWasmFile(primaryFile, { label: 'primary-input', workDirectory }),
|
|
628
|
+
owner: inspectWasmFile(ownerFile, { label: 'memory-owner-input', workDirectory }),
|
|
629
|
+
guest: materialized.inspection
|
|
630
|
+
});
|
|
631
|
+
assertPrimaryAndOwner(plan, materialized.manifest, before.primary, before.owner);
|
|
632
|
+
|
|
633
|
+
const composedFile = path.join(outputDirectory, 'guest-link-composed.wasm');
|
|
634
|
+
try {
|
|
635
|
+
runTool('wasm-merge', [
|
|
636
|
+
ownerFile,
|
|
637
|
+
plan.composition.memoryOwnerModule,
|
|
638
|
+
primaryFile,
|
|
639
|
+
plan.composition.primaryModule,
|
|
640
|
+
materialized.artifactFile,
|
|
641
|
+
materialized.manifest.module,
|
|
642
|
+
'--mvp-features',
|
|
643
|
+
'-o',
|
|
644
|
+
composedFile
|
|
645
|
+
], {
|
|
646
|
+
cwd: workDirectory,
|
|
647
|
+
failureCode: diagnosticCodes.linkFailed,
|
|
648
|
+
failureMessage: `Guest unit ${materialized.manifest.id} could not be composed.`
|
|
649
|
+
});
|
|
650
|
+
} catch (error) {
|
|
651
|
+
wrap(error, diagnosticCodes.linkFailed, `Guest unit ${materialized.manifest.id} could not be composed.`);
|
|
652
|
+
}
|
|
653
|
+
const afterLink = inspectWasmFile(composedFile, {
|
|
654
|
+
label: 'composed-input',
|
|
655
|
+
workDirectory,
|
|
656
|
+
failureCode: diagnosticCodes.linkFailed
|
|
657
|
+
});
|
|
658
|
+
try {
|
|
659
|
+
assertFinal(plan, before, afterLink);
|
|
660
|
+
} catch (error) {
|
|
661
|
+
if (error && error.code === diagnosticCodes.finalAuditFailed) {
|
|
662
|
+
fail(diagnosticCodes.linkFailed, 'Composed Wasm violates the validated input contract.', { cause: error.message });
|
|
663
|
+
}
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const finalFile = path.join(outputDirectory, 'canonical-native.wasm');
|
|
668
|
+
runTool('wasm-opt', [
|
|
669
|
+
composedFile,
|
|
670
|
+
...optimizationArgs,
|
|
671
|
+
'-o',
|
|
672
|
+
finalFile
|
|
673
|
+
], {
|
|
674
|
+
cwd: workDirectory,
|
|
675
|
+
failureCode: diagnosticCodes.optimizationFailed,
|
|
676
|
+
failureMessage: 'Post-link whole-module optimization failed.'
|
|
677
|
+
});
|
|
678
|
+
const final = inspectWasmFile(finalFile, {
|
|
679
|
+
label: 'optimized-final',
|
|
680
|
+
workDirectory,
|
|
681
|
+
failureCode: diagnosticCodes.finalAuditFailed,
|
|
682
|
+
featureFailureCode: diagnosticCodes.finalAuditFailed
|
|
683
|
+
});
|
|
684
|
+
assertFinal(plan, before, final);
|
|
685
|
+
const finalWatFile = path.join(workDirectory, 'optimized-final.wat');
|
|
686
|
+
|
|
687
|
+
const records = Object.freeze({
|
|
688
|
+
primary: fileRecord(primaryFile),
|
|
689
|
+
owner: fileRecord(ownerFile),
|
|
690
|
+
guest: fileRecord(materialized.artifactFile),
|
|
691
|
+
composed: fileRecord(composedFile),
|
|
692
|
+
final: fileRecord(finalFile)
|
|
693
|
+
});
|
|
694
|
+
const audit = finalAudit(plan, before, afterLink, final, records.final);
|
|
695
|
+
const report = guestLinkReport(plan, materialized, before, afterLink, final, records, toolchain, posture);
|
|
696
|
+
const reportFile = path.join(outputDirectory, 'guest-link-report.json');
|
|
697
|
+
const auditFile = path.join(outputDirectory, 'final-wasm-audit.json');
|
|
698
|
+
writeAtomic(reportFile, stableJson(report));
|
|
699
|
+
writeAtomic(auditFile, stableJson(audit));
|
|
700
|
+
|
|
701
|
+
return Object.freeze({
|
|
702
|
+
plan,
|
|
703
|
+
materialization: Object.freeze({
|
|
704
|
+
directory: materialized.relativeDirectory,
|
|
705
|
+
reused: materialized.reused,
|
|
706
|
+
artifactFile: materialized.artifactFile,
|
|
707
|
+
manifestFile: materialized.manifestFile
|
|
708
|
+
}),
|
|
709
|
+
files: Object.freeze({
|
|
710
|
+
composed: composedFile,
|
|
711
|
+
final: finalFile,
|
|
712
|
+
finalWat: finalWatFile,
|
|
713
|
+
guestLinkReport: reportFile,
|
|
714
|
+
finalWasmAudit: auditFile
|
|
715
|
+
}),
|
|
716
|
+
report,
|
|
717
|
+
audit,
|
|
718
|
+
toolchain
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
module.exports = Object.freeze({
|
|
723
|
+
realizeGuestLinkPlan,
|
|
724
|
+
assertPrimaryAndOwner,
|
|
725
|
+
assertFinal,
|
|
726
|
+
assertFinalArtifactIdentity,
|
|
727
|
+
memoryLayoutProof,
|
|
728
|
+
segmentFacts
|
|
729
|
+
});
|