@hew-lang/sandbox-vm 0.6.2 → 0.6.3
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 +43 -4
- package/cli/test-runner.mjs +4 -1
- package/dist/interpreter/interpreter.d.ts +1 -1
- package/dist/interpreter/interpreter.js +1 -1
- package/dist/interpreter/parity-runner.js +1 -1
- package/dist/interpreter/run-program.d.ts +3 -2
- package/dist/interpreter/run-program.js +5 -3
- package/dist/interpreter/schema-validator.d.ts +9 -4
- package/dist/interpreter/schema-validator.js +15 -49
- package/dist/interpreter/types.d.ts +4 -0
- package/dist/interpreter/v1/browser-directory.d.ts +107 -0
- package/dist/interpreter/v1/browser-directory.js +288 -0
- package/dist/interpreter/v1/calendar.d.ts +4 -0
- package/dist/interpreter/v1/calendar.js +64 -0
- package/dist/interpreter/v1/codecs.js +2 -2
- package/dist/interpreter/v1/exec.d.ts +2 -2
- package/dist/interpreter/v1/exec.js +439 -183
- package/dist/interpreter/v1/instructions.generated.d.ts +1135 -0
- package/dist/interpreter/v1/instructions.generated.js +106 -0
- package/dist/interpreter/v1/package-graph.d.ts +2 -0
- package/dist/interpreter/v1/package-graph.js +729 -0
- package/dist/interpreter/v1/package-validator.generated.d.ts +3 -0
- package/dist/interpreter/v1/package-validator.generated.js +75902 -0
- package/dist/interpreter/v1/package.d.ts +5 -680
- package/dist/interpreter/v1/package.js +0 -12
- package/dist/interpreter/v1/scheduler.d.ts +5 -1
- package/dist/interpreter/v1/scheduler.js +32 -3
- package/dist/interpreter/v1/shims.d.ts +250 -7
- package/dist/interpreter/v1/shims.js +430 -150
- package/dist/interpreter/v1/structural.d.ts +2 -28
- package/dist/interpreter/v1/timestamp.d.ts +7 -0
- package/dist/interpreter/v1/timestamp.js +54 -0
- package/dist/interpreter/v1/validate.d.ts +8 -2
- package/dist/interpreter/v1/validate.js +190 -202
- package/dist/interpreter/v1/value-operations.d.ts +7 -2
- package/dist/interpreter/v1/value-operations.js +13 -6
- package/dist/interpreter/values.d.ts +4 -0
- package/dist/interpreter/values.js +6 -0
- package/dist/interpreter/worker.js +4 -3
- package/package.json +4 -4
|
@@ -1,31 +1,6 @@
|
|
|
1
1
|
import { type VmValue } from "../values.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
recipe: number;
|
|
5
|
-
}
|
|
6
|
-
export type StructuralRecipe = {
|
|
7
|
-
kind: "scalar" | "unit";
|
|
8
|
-
} | {
|
|
9
|
-
kind: "display";
|
|
10
|
-
callee: number;
|
|
11
|
-
} | {
|
|
12
|
-
kind: "identity";
|
|
13
|
-
name: string;
|
|
14
|
-
} | {
|
|
15
|
-
kind: "tuple" | "vector" | "map";
|
|
16
|
-
members: number[];
|
|
17
|
-
} | {
|
|
18
|
-
kind: "record";
|
|
19
|
-
name: string;
|
|
20
|
-
fields: Field[];
|
|
21
|
-
} | {
|
|
22
|
-
kind: "enum";
|
|
23
|
-
cases: Array<{
|
|
24
|
-
name: string;
|
|
25
|
-
kind: "unit" | "tuple" | "struct";
|
|
26
|
-
fields: Field[];
|
|
27
|
-
}>;
|
|
28
|
-
};
|
|
2
|
+
import type { StructuralRecipe } from "./package.js";
|
|
3
|
+
export type { StructuralRecipe } from "./package.js";
|
|
29
4
|
/** A borrowed traversal. Display callbacks can suspend, so their callers retain
|
|
30
5
|
* this stack and resume it with the returned string. No later sibling is read
|
|
31
6
|
* after a callback fails. */
|
|
@@ -41,4 +16,3 @@ export declare class StructuralRenderer {
|
|
|
41
16
|
};
|
|
42
17
|
private sequence;
|
|
43
18
|
}
|
|
44
|
-
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { VmValue } from "../values.js";
|
|
2
|
+
import type { PackageV1, TermV1 } from "./package.js";
|
|
3
|
+
type TimestampBinary = Extract<TermV1, {
|
|
4
|
+
op: "timestamp.binary";
|
|
5
|
+
}>;
|
|
6
|
+
export declare function timestampBinary(pkg: PackageV1, term: TimestampBinary, lhs: VmValue, rhs: VmValue): VmValue | null;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const BILLION = 1000000000n;
|
|
2
|
+
const MIN_I64 = -(1n << 63n);
|
|
3
|
+
const MAX_I64 = (1n << 63n) - 1n;
|
|
4
|
+
function exactNanos(pkg, value, timestamp) {
|
|
5
|
+
if (!timestamp) {
|
|
6
|
+
if (value.kind !== "i64" || value.value < MIN_I64 || value.value > MAX_I64)
|
|
7
|
+
throw new TypeError("timestamp offset is not a duration");
|
|
8
|
+
return value.value;
|
|
9
|
+
}
|
|
10
|
+
if (value.kind !== "record" ||
|
|
11
|
+
value.fields.length !== 2 ||
|
|
12
|
+
value.shape === undefined ||
|
|
13
|
+
pkg.aggregates[value.shape]?.builtin !== "Timestamp")
|
|
14
|
+
throw new TypeError("timestamp lacks its normalized pair");
|
|
15
|
+
const [seconds, nanos] = value.fields;
|
|
16
|
+
if (seconds?.kind !== "i64" ||
|
|
17
|
+
nanos?.kind !== "i64" ||
|
|
18
|
+
seconds.value < MIN_I64 ||
|
|
19
|
+
seconds.value > MAX_I64 ||
|
|
20
|
+
nanos.value < 0n ||
|
|
21
|
+
nanos.value >= BILLION)
|
|
22
|
+
throw new TypeError("invalid normalized timestamp");
|
|
23
|
+
return seconds.value * BILLION + nanos.value;
|
|
24
|
+
}
|
|
25
|
+
export function timestampBinary(pkg, term, lhs, rhs) {
|
|
26
|
+
const left = exactNanos(pkg, lhs, term.lhs_timestamp);
|
|
27
|
+
const right = exactNanos(pkg, rhs, term.rhs_timestamp);
|
|
28
|
+
const exact = term.binary_op === "Add" ? left + right : left - right;
|
|
29
|
+
if (term.shape === null) {
|
|
30
|
+
return exact < MIN_I64 || exact > MAX_I64
|
|
31
|
+
? null
|
|
32
|
+
: { kind: "i64", value: exact };
|
|
33
|
+
}
|
|
34
|
+
if (exact < MIN_I64 * BILLION || exact > MAX_I64 * BILLION + BILLION - 1n)
|
|
35
|
+
return null;
|
|
36
|
+
let seconds = exact / BILLION;
|
|
37
|
+
let nanos = exact % BILLION;
|
|
38
|
+
if (nanos < 0n) {
|
|
39
|
+
seconds -= 1n;
|
|
40
|
+
nanos += BILLION;
|
|
41
|
+
}
|
|
42
|
+
const shape = pkg.aggregates[term.shape];
|
|
43
|
+
if (!shape)
|
|
44
|
+
throw new TypeError("timestamp result shape is absent");
|
|
45
|
+
return {
|
|
46
|
+
kind: "record",
|
|
47
|
+
typeId: shape.name,
|
|
48
|
+
shape: term.shape,
|
|
49
|
+
fields: [
|
|
50
|
+
{ kind: "i64", value: seconds },
|
|
51
|
+
{ kind: "i64", value: nanos },
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import type { SandboxRejection } from "../types.js";
|
|
2
|
-
import type {
|
|
3
|
-
|
|
2
|
+
import type { CheckedPackage } from "../schema-validator.js";
|
|
3
|
+
import { type HostBridges } from "./shims.js";
|
|
4
|
+
export declare class AdmittedPackage {
|
|
5
|
+
readonly bytecode: CheckedPackage;
|
|
6
|
+
private constructor();
|
|
7
|
+
static load(pkg: CheckedPackage, bridges: HostBridges): AdmittedPackage | SandboxRejection;
|
|
8
|
+
}
|
|
9
|
+
export declare function admitPackage(pkg: CheckedPackage, bridges?: HostBridges): SandboxRejection | null;
|
|
@@ -1,114 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/// The package declares what it needs: `runtime_families` names every runtime
|
|
4
|
-
/// call family the instruction stream reaches, and `externs` names every
|
|
5
|
-
/// declared C-ABI symbol. This validator matches each against the shim table
|
|
6
|
-
/// the VM implements. A family or symbol with no shim is refused before a
|
|
7
|
-
/// single instruction runs. Unknown is rejected, never allowed.
|
|
8
|
-
///
|
|
9
|
-
/// The check keys on the declared identity: it never matches a symbol prefix
|
|
10
|
-
/// and never reads an instruction stream to guess a capability.
|
|
11
|
-
import { SUPPORTED_NATIVE_IO, SUPPORTED_SUSPEND_KINDS, resolveExternShim, resolveRuntimeShim, } from "./shims.js";
|
|
1
|
+
import { calendarResultFields } from "./calendar.js";
|
|
2
|
+
import { CODEC_REGISTRATION, registrationRefusal, resolveExtern, resolveRuntime, resolveSuspension, suspensionRefusal, } from "./shims.js";
|
|
12
3
|
const UNSUPPORTED = "sandbox.capability.unsupported";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"const.bytes",
|
|
25
|
-
"copy_value",
|
|
26
|
-
"fork",
|
|
27
|
-
"move",
|
|
28
|
-
"destroy_value",
|
|
29
|
-
"begin_borrow",
|
|
30
|
-
"end_borrow",
|
|
31
|
-
"finish_linear_receiver",
|
|
32
|
-
"alloc_place",
|
|
33
|
-
"store.init",
|
|
34
|
-
"store.assign",
|
|
35
|
-
"load.copy",
|
|
36
|
-
"load.take",
|
|
37
|
-
"load.borrow",
|
|
38
|
-
"end_lifetime",
|
|
39
|
-
"tuple.make",
|
|
40
|
-
"tuple.get",
|
|
41
|
-
"aggregate.make",
|
|
42
|
-
"aggregate.project_copy",
|
|
43
|
-
"aggregate.project_borrow",
|
|
44
|
-
"destructure",
|
|
45
|
-
"array.make",
|
|
46
|
-
"array.repeat",
|
|
47
|
-
"variant.make",
|
|
48
|
-
"variant.is",
|
|
49
|
-
"variant.project_copy",
|
|
50
|
-
"variant.project_borrow",
|
|
51
|
-
"variant.destructure",
|
|
52
|
-
"unary",
|
|
53
|
-
"binary",
|
|
54
|
-
"cast",
|
|
55
|
-
"str.eq",
|
|
56
|
-
"bytes.eq",
|
|
57
|
-
"function.make",
|
|
58
|
-
"closure.make",
|
|
59
|
-
"generator.make",
|
|
60
|
-
"callable.coerce",
|
|
61
|
-
"dyn.make",
|
|
62
|
-
"register_defer",
|
|
63
|
-
"stream.pipe",
|
|
64
|
-
"task_scope.enter",
|
|
65
|
-
"task_scope.close",
|
|
66
|
-
"task.spawn",
|
|
67
|
-
"task.invoke",
|
|
68
|
-
"task.race",
|
|
69
|
-
]);
|
|
70
|
-
const TERMS = new Set([
|
|
71
|
-
"goto",
|
|
72
|
-
"branch",
|
|
73
|
-
"switch.variant",
|
|
74
|
-
"return",
|
|
75
|
-
"checked.binary",
|
|
76
|
-
"dyn.call",
|
|
77
|
-
"call",
|
|
78
|
-
"indirect.call",
|
|
79
|
-
"actor.call",
|
|
80
|
-
"runtime.call",
|
|
81
|
-
"extern.call",
|
|
82
|
-
"value.call",
|
|
83
|
-
"wire.codec",
|
|
84
|
-
"panic",
|
|
85
|
-
"checked_raise",
|
|
86
|
-
"trap",
|
|
87
|
-
"cleanup.dispatch",
|
|
88
|
-
"enter_defer",
|
|
89
|
-
"finish_defer",
|
|
90
|
-
"recover_fault",
|
|
91
|
-
"resume_unwind",
|
|
92
|
-
"suspend",
|
|
93
|
-
"unreachable",
|
|
94
|
-
]);
|
|
95
|
-
const ACTOR_OPS = new Set([
|
|
96
|
-
"identity",
|
|
97
|
-
"spawn",
|
|
98
|
-
"self_handle",
|
|
99
|
-
"stop",
|
|
100
|
-
"terminate",
|
|
101
|
-
"await_stopped",
|
|
102
|
-
"await_restarted",
|
|
103
|
-
"call_start",
|
|
104
|
-
"call_take",
|
|
105
|
-
"submit",
|
|
106
|
-
"stream_start",
|
|
107
|
-
"timer_schedule",
|
|
108
|
-
"supervisor_spawn",
|
|
109
|
-
"supervisor_child",
|
|
110
|
-
"supervisor_pool_view",
|
|
111
|
-
]);
|
|
4
|
+
/// The executor can only be constructed after structural/reference checks and
|
|
5
|
+
/// the canonical host registration have both accepted the same package.
|
|
6
|
+
export class AdmittedPackage {
|
|
7
|
+
bytecode;
|
|
8
|
+
constructor(bytecode) {
|
|
9
|
+
this.bytecode = bytecode;
|
|
10
|
+
}
|
|
11
|
+
static load(pkg, bridges) {
|
|
12
|
+
return admitPackage(pkg, bridges) ?? new AdmittedPackage(pkg);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
112
15
|
function capabilityMessage(capability, native) {
|
|
113
16
|
const nativeFeatures = {
|
|
114
17
|
FileRead: "Filesystem access",
|
|
@@ -140,8 +43,8 @@ function unavailable(capability) {
|
|
|
140
43
|
span: null,
|
|
141
44
|
};
|
|
142
45
|
}
|
|
143
|
-
/// The first refusal, or `null` when
|
|
144
|
-
export function admitPackage(pkg) {
|
|
46
|
+
/// The first refusal, or `null` when all operations and bridges are available.
|
|
47
|
+
export function admitPackage(pkg, bridges = {}) {
|
|
145
48
|
if (!pkg.entry) {
|
|
146
49
|
return {
|
|
147
50
|
category: "invalid_package",
|
|
@@ -151,7 +54,7 @@ export function admitPackage(pkg) {
|
|
|
151
54
|
span: null,
|
|
152
55
|
};
|
|
153
56
|
}
|
|
154
|
-
const
|
|
57
|
+
const opaqueResources = new Set();
|
|
155
58
|
for (const function_ of pkg.functions) {
|
|
156
59
|
for (const block of function_.blocks) {
|
|
157
60
|
if (block.ops.some((op) => op.op === "actor.ingress_adapter"))
|
|
@@ -170,21 +73,71 @@ export function admitPackage(pkg) {
|
|
|
170
73
|
span: null,
|
|
171
74
|
};
|
|
172
75
|
if (block.term.op === "actor.call" &&
|
|
173
|
-
|
|
76
|
+
block.term.operation.op === "local_observation")
|
|
174
77
|
return unavailable(`actor.${block.term.operation.op}`);
|
|
175
|
-
if (block.ops.some((op) => op.op === "coverage.hit" &&
|
|
176
|
-
(!Number.isSafeInteger(op.counter) ||
|
|
177
|
-
op.counter < 0 ||
|
|
178
|
-
op.counter > 0xffff_ffff))) {
|
|
179
|
-
return {
|
|
180
|
-
category: "invalid_package",
|
|
181
|
-
code: "sandbox.package.invalid_counter",
|
|
182
|
-
capability: "coverage.hit",
|
|
183
|
-
message: "coverage counter must be a u32",
|
|
184
|
-
span: null,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
78
|
const term = block.term;
|
|
79
|
+
if (term.op === "runtime.call" &&
|
|
80
|
+
pkg.runtime_families[term.family]?.family === "TimestampNow") {
|
|
81
|
+
const shape = term.aggregate_result_shape == null
|
|
82
|
+
? undefined
|
|
83
|
+
: pkg.aggregates[term.aggregate_result_shape];
|
|
84
|
+
if (!shape ||
|
|
85
|
+
shape.builtin !== "Timestamp" ||
|
|
86
|
+
shape.fields.length !== 2) {
|
|
87
|
+
return {
|
|
88
|
+
category: "invalid_package",
|
|
89
|
+
code: "sandbox.package.timestamp_shape",
|
|
90
|
+
capability: "TimestampNow",
|
|
91
|
+
message: "timestamp clock lacks its checked result shape",
|
|
92
|
+
span: null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (term.op === "extern.call") {
|
|
97
|
+
const symbol = pkg.externs[term.extern]?.symbol ?? "";
|
|
98
|
+
const fields = calendarResultFields(symbol);
|
|
99
|
+
if (fields) {
|
|
100
|
+
const shape = term.aggregate_result_shape == null
|
|
101
|
+
? undefined
|
|
102
|
+
: pkg.aggregates[term.aggregate_result_shape];
|
|
103
|
+
if (!shape ||
|
|
104
|
+
shape.fields.length !== fields.length ||
|
|
105
|
+
shape.fields.some((field, index) => field !== fields[index])) {
|
|
106
|
+
return {
|
|
107
|
+
category: "invalid_package",
|
|
108
|
+
code: "sandbox.package.calendar_shape",
|
|
109
|
+
capability: symbol,
|
|
110
|
+
message: "calendar operation lacks its checked result shape",
|
|
111
|
+
span: null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (term.op === "timestamp.binary") {
|
|
117
|
+
const both = term.lhs_timestamp && term.rhs_timestamp;
|
|
118
|
+
const validOperation = (term.binary_op === "Add" &&
|
|
119
|
+
term.lhs_timestamp !== term.rhs_timestamp) ||
|
|
120
|
+
(term.binary_op === "Subtract" && term.lhs_timestamp);
|
|
121
|
+
const shape = term.shape === null ? undefined : pkg.aggregates[term.shape];
|
|
122
|
+
if (typeof term.lhs_timestamp !== "boolean" ||
|
|
123
|
+
typeof term.rhs_timestamp !== "boolean" ||
|
|
124
|
+
!validOperation ||
|
|
125
|
+
(both
|
|
126
|
+
? term.shape !== null
|
|
127
|
+
: !shape ||
|
|
128
|
+
shape.fields.length !== 2 ||
|
|
129
|
+
shape.builtin !== "Timestamp") ||
|
|
130
|
+
term.failures.length !== 1 ||
|
|
131
|
+
term.failures[0]?.trap !== "integer_overflow") {
|
|
132
|
+
return {
|
|
133
|
+
category: "invalid_package",
|
|
134
|
+
code: "sandbox.package.timestamp_shape",
|
|
135
|
+
capability: "timestamp.binary",
|
|
136
|
+
message: "timestamp arithmetic lacks its checked operands, result shape or overflow edge",
|
|
137
|
+
span: null,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
188
141
|
if (term.op === "runtime.call" &&
|
|
189
142
|
pkg.runtime_families[term.family]?.family === "BytesDecodeUtf8") {
|
|
190
143
|
const shapes = term.variant_result_shapes;
|
|
@@ -209,6 +162,26 @@ export function admitPackage(pkg) {
|
|
|
209
162
|
span: null,
|
|
210
163
|
};
|
|
211
164
|
}
|
|
165
|
+
if (term.op === "extern.call") {
|
|
166
|
+
const registration = resolveExtern(pkg.externs[term.extern]);
|
|
167
|
+
if (registration.kind === "directory" &&
|
|
168
|
+
"release" in registration.operation) {
|
|
169
|
+
const resource = term.result_resource == null
|
|
170
|
+
? undefined
|
|
171
|
+
: pkg.resources?.[term.result_resource];
|
|
172
|
+
if (resource?.kind !== "nominal" ||
|
|
173
|
+
resource.release !== registration.operation.release ||
|
|
174
|
+
term.result == null ||
|
|
175
|
+
term.result === "never")
|
|
176
|
+
return {
|
|
177
|
+
category: "invalid_package",
|
|
178
|
+
code: "sandbox.package.owner_result",
|
|
179
|
+
capability: null,
|
|
180
|
+
message: "filesystem acquisition lacks its compiler-selected owner release slot",
|
|
181
|
+
span: null,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
212
185
|
if (term.op === "actor.call" && term.operation.op === "timer_schedule") {
|
|
213
186
|
const operation = term.operation;
|
|
214
187
|
const handler = pkg.actors
|
|
@@ -229,10 +202,38 @@ export function admitPackage(pkg) {
|
|
|
229
202
|
message: "timer schedule lacks its checked handler, timing or non-waiting admission",
|
|
230
203
|
span: null,
|
|
231
204
|
};
|
|
232
|
-
|
|
205
|
+
opaqueResources.add(operation.resource);
|
|
206
|
+
}
|
|
207
|
+
if (term.op === "actor.call" &&
|
|
208
|
+
term.operation.op === "supervisor_spawn") {
|
|
209
|
+
const id = term.operation.supervisor;
|
|
210
|
+
const supervisor = pkg.supervisors?.find((layout) => layout.id === id);
|
|
211
|
+
const config = term.args.length -
|
|
212
|
+
(supervisor?.children.some((child) => child.is_pool) ? 1 : 0);
|
|
213
|
+
if (!supervisor ||
|
|
214
|
+
config < 0 ||
|
|
215
|
+
supervisor.children.some((child) => {
|
|
216
|
+
const params = pkg.functions.find((fn) => fn.id === child.spawn)?.params;
|
|
217
|
+
return !params || ![config, config + 1].includes(params.length);
|
|
218
|
+
}))
|
|
219
|
+
return {
|
|
220
|
+
category: "invalid_package",
|
|
221
|
+
code: "sandbox.package.supervisor_constructor",
|
|
222
|
+
capability: null,
|
|
223
|
+
message: "supervisor constructor signature does not match its config",
|
|
224
|
+
span: null,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (term.op === "wire.codec") {
|
|
228
|
+
const refusal = registrationRefusal(CODEC_REGISTRATION, bridges);
|
|
229
|
+
if (refusal)
|
|
230
|
+
return rejectRegistration(refusal);
|
|
233
231
|
}
|
|
234
232
|
if (term.op === "suspend") {
|
|
235
|
-
const
|
|
233
|
+
const registration = resolveSuspension(term);
|
|
234
|
+
if (registration.kind === "refused")
|
|
235
|
+
return rejectRegistration(registration);
|
|
236
|
+
const mandatoryClose = (pkg.resources ?? []).some((resource) => "close" in resource && resource.close === function_.id);
|
|
236
237
|
if (term.kind === "Safepoint"
|
|
237
238
|
? (term.cancel == null) !== mandatoryClose
|
|
238
239
|
: term.cancel == null)
|
|
@@ -244,33 +245,13 @@ export function admitPackage(pkg) {
|
|
|
244
245
|
span: null,
|
|
245
246
|
};
|
|
246
247
|
}
|
|
247
|
-
const unknown = block.ops.find((op) => !OPS.has(op.op))?.op ??
|
|
248
|
-
(!TERMS.has(block.term.op) ? block.term.op : null);
|
|
249
|
-
if (unknown)
|
|
250
|
-
return {
|
|
251
|
-
category: "invalid_package",
|
|
252
|
-
code: "sandbox.package.unknown_opcode",
|
|
253
|
-
capability: unknown,
|
|
254
|
-
message: `unknown bytecode opcode ${unknown}`,
|
|
255
|
-
span: null,
|
|
256
|
-
};
|
|
257
248
|
}
|
|
258
249
|
}
|
|
259
250
|
for (const [index, resource] of (pkg.resources ?? []).entries()) {
|
|
260
|
-
if (resource.kind === "
|
|
261
|
-
(resource.shape === undefined ||
|
|
262
|
-
!pkg.aggregates.some((shape) => shape.id === resource.shape)))
|
|
263
|
-
return {
|
|
264
|
-
category: "invalid_package",
|
|
265
|
-
code: "sandbox.package.resource_shape_missing",
|
|
266
|
-
capability: null,
|
|
267
|
-
message: "record resource has no exact aggregate shape",
|
|
268
|
-
span: null,
|
|
269
|
-
};
|
|
270
|
-
if (resource.kind === "opaque" && !timerResources.has(index))
|
|
251
|
+
if (resource.kind === "opaque" && !opaqueResources.has(index))
|
|
271
252
|
return unavailable("resource.opaque_close");
|
|
272
253
|
if (resource.kind === "nominal" &&
|
|
273
|
-
|
|
254
|
+
registrationRefusal(resolveExtern({ symbol: resource.release ?? "" }), bridges))
|
|
274
255
|
return unavailable("resource.external_close");
|
|
275
256
|
}
|
|
276
257
|
for (const actor of pkg.actors ?? []) {
|
|
@@ -315,69 +296,76 @@ export function admitPackage(pkg) {
|
|
|
315
296
|
if (actor.max_heap_bytes != null)
|
|
316
297
|
return unavailable("actor.heap_limit");
|
|
317
298
|
}
|
|
318
|
-
for (const
|
|
319
|
-
|
|
299
|
+
for (const supervisor of pkg.supervisors ?? []) {
|
|
300
|
+
const order = supervisor.startup_order === undefined
|
|
301
|
+
? supervisor.children.map((_child, index) => index)
|
|
302
|
+
: supervisor.startup_order;
|
|
303
|
+
if (!Array.isArray(order) ||
|
|
304
|
+
supervisor.children.some((child) => child.dependencies !== undefined &&
|
|
305
|
+
(!Array.isArray(child.dependencies) ||
|
|
306
|
+
new Set(child.dependencies).size !== child.dependencies.length)))
|
|
320
307
|
return {
|
|
321
|
-
category:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
capability: familyIdentity(entry.family, entry.detail),
|
|
326
|
-
message: capabilityMessage(entry.family, NATIVE_FAMILIES.has(entry.family)),
|
|
308
|
+
category: "invalid_package",
|
|
309
|
+
code: "sandbox.package.supervisor_startup_order",
|
|
310
|
+
capability: null,
|
|
311
|
+
message: "supervisor startup order and dependencies must be arrays of unique children",
|
|
327
312
|
span: null,
|
|
328
313
|
};
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
314
|
+
const ranks = new Map(order.map((child, rank) => [child, rank]));
|
|
315
|
+
if (order.length !== supervisor.children.length ||
|
|
316
|
+
ranks.size !== order.length ||
|
|
317
|
+
order.some((child) => !Number.isInteger(child) ||
|
|
318
|
+
child < 0 ||
|
|
319
|
+
child >= supervisor.children.length) ||
|
|
320
|
+
supervisor.children.some((child, index) => (child.dependencies ?? []).some((dependency) => !Number.isInteger(dependency) ||
|
|
321
|
+
dependency < 0 ||
|
|
322
|
+
!ranks.has(dependency) ||
|
|
323
|
+
ranks.get(dependency) >= ranks.get(index))))
|
|
334
324
|
return {
|
|
335
|
-
category:
|
|
336
|
-
code:
|
|
337
|
-
capability:
|
|
338
|
-
message:
|
|
339
|
-
? "This external-library operation is not available in the browser runtime yet."
|
|
340
|
-
: `${native} require native execution.`,
|
|
325
|
+
category: "invalid_package",
|
|
326
|
+
code: "sandbox.package.supervisor_startup_order",
|
|
327
|
+
capability: null,
|
|
328
|
+
message: "supervisor startup order must precede each declared dependency",
|
|
341
329
|
span: null,
|
|
342
330
|
};
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
for (const kind of pkg.suspend_kinds) {
|
|
346
|
-
const refused = kind === "NativeIo"
|
|
347
|
-
? refusedNativeIo(pkg)
|
|
348
|
-
: SUPPORTED_SUSPEND_KINDS.has(kind)
|
|
349
|
-
? null
|
|
350
|
-
: kind;
|
|
351
|
-
if (refused !== null) {
|
|
331
|
+
if (supervisor.children.some((child) => !pkg.functions.some((fn) => fn.id === child.spawn)))
|
|
352
332
|
return {
|
|
353
|
-
category:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
capability: refused,
|
|
358
|
-
message: capabilityMessage(kind, NATIVE_SUSPENSIONS.has(kind)),
|
|
333
|
+
category: "invalid_package",
|
|
334
|
+
code: "sandbox.package.supervisor_constructor",
|
|
335
|
+
capability: null,
|
|
336
|
+
message: "supervisor child has no constructor callable",
|
|
359
337
|
span: null,
|
|
360
338
|
};
|
|
361
|
-
}
|
|
362
339
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
340
|
+
for (const entry of pkg.runtime_families) {
|
|
341
|
+
const refusal = registrationRefusal(resolveRuntime(entry), bridges);
|
|
342
|
+
if (refusal)
|
|
343
|
+
return rejectRegistration(refusal);
|
|
344
|
+
}
|
|
345
|
+
for (const entry of pkg.externs) {
|
|
346
|
+
const refusal = registrationRefusal(resolveExtern(entry), bridges);
|
|
347
|
+
if (refusal)
|
|
348
|
+
return rejectRegistration(refusal);
|
|
349
|
+
}
|
|
350
|
+
for (const kind of pkg.suspend_kinds) {
|
|
351
|
+
const refusal = suspensionRefusal(kind);
|
|
352
|
+
if (refusal)
|
|
353
|
+
return rejectRegistration(refusal);
|
|
376
354
|
}
|
|
377
355
|
return null;
|
|
378
356
|
}
|
|
379
|
-
function
|
|
380
|
-
return
|
|
381
|
-
|
|
382
|
-
:
|
|
357
|
+
function rejectRegistration(refusal) {
|
|
358
|
+
return {
|
|
359
|
+
category: refusal.category,
|
|
360
|
+
code: UNSUPPORTED,
|
|
361
|
+
capability: refusal.capability,
|
|
362
|
+
message: refusal.message ??
|
|
363
|
+
(refusal.capability === "codecBridge" ||
|
|
364
|
+
refusal.capability === "calendarBridge"
|
|
365
|
+
? `This operation requires the ${refusal.capability === "codecBridge" ? "codec" : "calendar"} Wasm bridge.`
|
|
366
|
+
: refusal.description !== undefined
|
|
367
|
+
? `${refusal.description} require native execution.`
|
|
368
|
+
: capabilityMessage(refusal.capability.split("::")[0], refusal.category === "native_only")),
|
|
369
|
+
span: null,
|
|
370
|
+
};
|
|
383
371
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RuntimeContinuation } from "./shims.js";
|
|
1
2
|
import { type VmValue, type RcOwner } from "../values.js";
|
|
2
3
|
import { type PackageV1 } from "./package.js";
|
|
3
4
|
export type ValueRequest = {
|
|
@@ -16,8 +17,12 @@ export type ValueProgram = Generator<ValueRequest, VmValue, VmValue>;
|
|
|
16
17
|
/** Execute the exact checked plan, including every derived component selection.
|
|
17
18
|
* A user method yields to an ordinary VM frame and can suspend or fail. */
|
|
18
19
|
export declare function selectedValue(pkg: PackageV1, id: number, args: VmValue[]): ValueProgram;
|
|
19
|
-
export declare function sharedOperation(operation:
|
|
20
|
+
export declare function sharedOperation(operation: Extract<RuntimeContinuation, {
|
|
21
|
+
handler: "shared";
|
|
22
|
+
}>["operation"], args: VmValue[], owner: RcOwner | null, typeName: string, option: (value: VmValue | null) => VmValue): ValueProgram;
|
|
20
23
|
/** Open-addressed collection operations preserve native callback ordering:
|
|
21
24
|
* growth hashes occupied slots before probing the incoming key. Staged slots
|
|
22
25
|
* borrow their owners until every fallible callback has completed. */
|
|
23
|
-
export declare function collectionOperation(pkg: PackageV1,
|
|
26
|
+
export declare function collectionOperation(pkg: PackageV1, request: Extract<RuntimeContinuation, {
|
|
27
|
+
handler: "collection";
|
|
28
|
+
}>, args: VmValue[], callbacks: number[], shape: number | null | undefined): ValueProgram;
|
|
@@ -82,7 +82,7 @@ export function* selectedValue(pkg, id, args) {
|
|
|
82
82
|
const plan = pkg.value_capabilities[id];
|
|
83
83
|
if (!plan)
|
|
84
84
|
throw new Error(`selected value plan ${id} is missing`);
|
|
85
|
-
if (plan.callable
|
|
85
|
+
if (plan.callable != null)
|
|
86
86
|
return yield { kind: "call", callee: plan.callable, args };
|
|
87
87
|
const [left, right] = args;
|
|
88
88
|
if (!left)
|
|
@@ -315,14 +315,17 @@ export function* sharedOperation(operation, args, owner, typeName, option) {
|
|
|
315
315
|
yield { kind: "release", value: old };
|
|
316
316
|
return UNIT;
|
|
317
317
|
}
|
|
318
|
-
default:
|
|
319
|
-
|
|
318
|
+
default: {
|
|
319
|
+
const exhaustive = operation;
|
|
320
|
+
throw new Error(`shared operation ${String(exhaustive)} has no executor`);
|
|
321
|
+
}
|
|
320
322
|
}
|
|
321
323
|
}
|
|
322
324
|
/** Open-addressed collection operations preserve native callback ordering:
|
|
323
325
|
* growth hashes occupied slots before probing the incoming key. Staged slots
|
|
324
326
|
* borrow their owners until every fallible callback has completed. */
|
|
325
|
-
export function* collectionOperation(pkg,
|
|
327
|
+
export function* collectionOperation(pkg, request, args, callbacks, shape) {
|
|
328
|
+
const { family, operation } = request;
|
|
326
329
|
if (family === "Vector") {
|
|
327
330
|
const receiver = args[0];
|
|
328
331
|
if (receiver?.kind !== "vector")
|
|
@@ -382,7 +385,10 @@ export function* collectionOperation(pkg, family, operation, args, callbacks, sh
|
|
|
382
385
|
};
|
|
383
386
|
return receiver;
|
|
384
387
|
}
|
|
385
|
-
if (
|
|
388
|
+
if (operation === "Keys" ||
|
|
389
|
+
operation === "Values" ||
|
|
390
|
+
operation === "Elements" ||
|
|
391
|
+
operation === "Entries") {
|
|
386
392
|
return {
|
|
387
393
|
kind: "vector",
|
|
388
394
|
elementType: "",
|
|
@@ -462,5 +468,6 @@ export function* collectionOperation(pkg, family, operation, args, callbacks, sh
|
|
|
462
468
|
? boolean(result.found)
|
|
463
469
|
: option(pkg, shape, entry?.value));
|
|
464
470
|
}
|
|
465
|
-
|
|
471
|
+
const exhaustive = operation;
|
|
472
|
+
throw new Error(`collection operation ${String(exhaustive)} has no executor`);
|
|
466
473
|
}
|