@hew-lang/sandbox-vm 0.6.1 → 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 +48 -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/strings.d.ts +21 -0
- package/dist/interpreter/strings.js +191 -0
- package/dist/interpreter/types.d.ts +6 -0
- package/dist/interpreter/unicode-data.d.ts +4 -0
- package/dist/interpreter/unicode-data.js +1673 -0
- package/dist/interpreter/unicode.d.ts +3 -0
- package/dist/interpreter/unicode.js +35 -0
- package/dist/interpreter/v1/actor-identity.d.ts +3 -0
- package/dist/interpreter/v1/actor-identity.js +7 -0
- package/dist/interpreter/v1/actor-waits.d.ts +6 -0
- package/dist/interpreter/v1/actor-waits.js +33 -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 +8 -11
- package/dist/interpreter/v1/exec.d.ts +2 -2
- package/dist/interpreter/v1/exec.js +1761 -489
- 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 -645
- package/dist/interpreter/v1/package.js +0 -12
- package/dist/interpreter/v1/pipes.d.ts +1 -0
- package/dist/interpreter/v1/pipes.js +9 -6
- package/dist/interpreter/v1/scheduler.d.ts +8 -1
- package/dist/interpreter/v1/scheduler.js +50 -11
- package/dist/interpreter/v1/shims.d.ts +260 -7
- package/dist/interpreter/v1/shims.js +647 -175
- 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 +284 -200
- package/dist/interpreter/v1/value-operations.d.ts +11 -3
- package/dist/interpreter/v1/value-operations.js +70 -18
- package/dist/interpreter/values.d.ts +30 -8
- package/dist/interpreter/values.js +31 -5
- 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,111 +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.race",
|
|
68
|
-
]);
|
|
69
|
-
const TERMS = new Set([
|
|
70
|
-
"goto",
|
|
71
|
-
"branch",
|
|
72
|
-
"switch.variant",
|
|
73
|
-
"return",
|
|
74
|
-
"checked.binary",
|
|
75
|
-
"dyn.call",
|
|
76
|
-
"call",
|
|
77
|
-
"indirect.call",
|
|
78
|
-
"actor.call",
|
|
79
|
-
"runtime.call",
|
|
80
|
-
"extern.call",
|
|
81
|
-
"value.call",
|
|
82
|
-
"wire.codec",
|
|
83
|
-
"panic",
|
|
84
|
-
"checked_raise",
|
|
85
|
-
"trap",
|
|
86
|
-
"cleanup.dispatch",
|
|
87
|
-
"enter_defer",
|
|
88
|
-
"finish_defer",
|
|
89
|
-
"recover_fault",
|
|
90
|
-
"resume_unwind",
|
|
91
|
-
"suspend",
|
|
92
|
-
"unreachable",
|
|
93
|
-
]);
|
|
94
|
-
const ACTOR_OPS = new Set([
|
|
95
|
-
"spawn",
|
|
96
|
-
"self_handle",
|
|
97
|
-
"stop",
|
|
98
|
-
"terminate",
|
|
99
|
-
"await_stopped",
|
|
100
|
-
"await_restarted",
|
|
101
|
-
"call_start",
|
|
102
|
-
"call_take",
|
|
103
|
-
"submit",
|
|
104
|
-
"stream_start",
|
|
105
|
-
"supervisor_spawn",
|
|
106
|
-
"supervisor_child",
|
|
107
|
-
"supervisor_pool_view",
|
|
108
|
-
]);
|
|
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
|
+
}
|
|
109
15
|
function capabilityMessage(capability, native) {
|
|
110
16
|
const nativeFeatures = {
|
|
111
17
|
FileRead: "Filesystem access",
|
|
@@ -114,13 +20,11 @@ function capabilityMessage(capability, native) {
|
|
|
114
20
|
NativeIo: "Host input and output",
|
|
115
21
|
Read: "Host input and output",
|
|
116
22
|
Accept: "Accepting network connections",
|
|
117
|
-
RemoteAsk: "Remote actor calls",
|
|
118
23
|
};
|
|
119
24
|
const features = {
|
|
120
25
|
Vector: "This vector operation",
|
|
121
26
|
Map: "This map operation",
|
|
122
27
|
Set: "This set operation",
|
|
123
|
-
"actor.periodic": "Periodic actor handlers",
|
|
124
28
|
"actor.local_observation": "Actor links and monitors",
|
|
125
29
|
"actor.heap_limit": "Per-actor heap limits",
|
|
126
30
|
"actor.ingress_adapter": "Actor ingress adapters",
|
|
@@ -139,8 +43,8 @@ function unavailable(capability) {
|
|
|
139
43
|
span: null,
|
|
140
44
|
};
|
|
141
45
|
}
|
|
142
|
-
/// The first refusal, or `null` when
|
|
143
|
-
export function admitPackage(pkg) {
|
|
46
|
+
/// The first refusal, or `null` when all operations and bridges are available.
|
|
47
|
+
export function admitPackage(pkg, bridges = {}) {
|
|
144
48
|
if (!pkg.entry) {
|
|
145
49
|
return {
|
|
146
50
|
category: "invalid_package",
|
|
@@ -150,6 +54,7 @@ export function admitPackage(pkg) {
|
|
|
150
54
|
span: null,
|
|
151
55
|
};
|
|
152
56
|
}
|
|
57
|
+
const opaqueResources = new Set();
|
|
153
58
|
for (const function_ of pkg.functions) {
|
|
154
59
|
for (const block of function_.blocks) {
|
|
155
60
|
if (block.ops.some((op) => op.op === "actor.ingress_adapter"))
|
|
@@ -168,120 +73,299 @@ export function admitPackage(pkg) {
|
|
|
168
73
|
span: null,
|
|
169
74
|
};
|
|
170
75
|
if (block.term.op === "actor.call" &&
|
|
171
|
-
|
|
76
|
+
block.term.operation.op === "local_observation")
|
|
172
77
|
return unavailable(`actor.${block.term.operation.op}`);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
}
|
|
141
|
+
if (term.op === "runtime.call" &&
|
|
142
|
+
pkg.runtime_families[term.family]?.family === "BytesDecodeUtf8") {
|
|
143
|
+
const shapes = term.variant_result_shapes;
|
|
144
|
+
const error = shapes && pkg.aggregates[shapes.error];
|
|
145
|
+
const length = shapes && pkg.variants[shapes.error_len];
|
|
146
|
+
const result = term.result_shape === null
|
|
147
|
+
? undefined
|
|
148
|
+
: pkg.variants[term.result_shape];
|
|
149
|
+
if (!error ||
|
|
150
|
+
error.fields.length !== 2 ||
|
|
151
|
+
!length ||
|
|
152
|
+
!result ||
|
|
153
|
+
length.runtime_tags?.OptionSome === undefined ||
|
|
154
|
+
length.runtime_tags?.OptionNone === undefined ||
|
|
155
|
+
result.runtime_tags?.ResultOk === undefined ||
|
|
156
|
+
result.runtime_tags?.ResultErr === undefined)
|
|
157
|
+
return {
|
|
158
|
+
category: "invalid_package",
|
|
159
|
+
code: "sandbox.package.variant_result_shapes",
|
|
160
|
+
capability: null,
|
|
161
|
+
message: "UTF-8 decode lacks its checked result carrier",
|
|
162
|
+
span: null,
|
|
163
|
+
};
|
|
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
|
+
}
|
|
185
|
+
if (term.op === "actor.call" && term.operation.op === "timer_schedule") {
|
|
186
|
+
const operation = term.operation;
|
|
187
|
+
const handler = pkg.actors
|
|
188
|
+
?.find((actor) => actor.id === operation.actor)
|
|
189
|
+
?.handlers.find((handler) => handler.message_id === operation.message);
|
|
190
|
+
const resource = pkg.resources?.[operation.resource];
|
|
191
|
+
if (!handler ||
|
|
192
|
+
handler.streams ||
|
|
193
|
+
term.args.length !== 5 ||
|
|
194
|
+
!["reject", "drop_newest"].includes(operation.policy) ||
|
|
195
|
+
resource?.kind !== "opaque" ||
|
|
196
|
+
resource.close === undefined ||
|
|
197
|
+
!pkg.functions.some((fn) => fn.id === resource.close))
|
|
198
|
+
return {
|
|
199
|
+
category: "invalid_package",
|
|
200
|
+
code: "sandbox.package.invalid_timer",
|
|
201
|
+
capability: "actor.timer_schedule",
|
|
202
|
+
message: "timer schedule lacks its checked handler, timing or non-waiting admission",
|
|
203
|
+
span: null,
|
|
204
|
+
};
|
|
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);
|
|
231
|
+
}
|
|
232
|
+
if (term.op === "suspend") {
|
|
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);
|
|
237
|
+
if (term.kind === "Safepoint"
|
|
238
|
+
? (term.cancel == null) !== mandatoryClose
|
|
239
|
+
: term.cancel == null)
|
|
240
|
+
return {
|
|
241
|
+
category: "invalid_package",
|
|
242
|
+
code: "sandbox.package.cancellation_ancestry",
|
|
243
|
+
capability: null,
|
|
244
|
+
message: "safepoint cancellation ancestry must match its authored close lifecycle",
|
|
245
|
+
span: null,
|
|
246
|
+
};
|
|
184
247
|
}
|
|
185
|
-
const unknown = block.ops.find((op) => !OPS.has(op.op))?.op ??
|
|
186
|
-
(!TERMS.has(block.term.op) ? block.term.op : null);
|
|
187
|
-
if (unknown)
|
|
188
|
-
return {
|
|
189
|
-
category: "invalid_package",
|
|
190
|
-
code: "sandbox.package.unknown_opcode",
|
|
191
|
-
capability: unknown,
|
|
192
|
-
message: `unknown bytecode opcode ${unknown}`,
|
|
193
|
-
span: null,
|
|
194
|
-
};
|
|
195
248
|
}
|
|
196
249
|
}
|
|
197
|
-
for (const resource of pkg.resources ?? []) {
|
|
198
|
-
if (resource.kind === "
|
|
199
|
-
(resource.shape === undefined ||
|
|
200
|
-
!pkg.aggregates.some((shape) => shape.id === resource.shape)))
|
|
201
|
-
return {
|
|
202
|
-
category: "invalid_package",
|
|
203
|
-
code: "sandbox.package.resource_shape_missing",
|
|
204
|
-
capability: null,
|
|
205
|
-
message: "record resource has no exact aggregate shape",
|
|
206
|
-
span: null,
|
|
207
|
-
};
|
|
208
|
-
if (resource.kind === "opaque")
|
|
250
|
+
for (const [index, resource] of (pkg.resources ?? []).entries()) {
|
|
251
|
+
if (resource.kind === "opaque" && !opaqueResources.has(index))
|
|
209
252
|
return unavailable("resource.opaque_close");
|
|
210
253
|
if (resource.kind === "nominal" &&
|
|
211
|
-
|
|
254
|
+
registrationRefusal(resolveExtern({ symbol: resource.release ?? "" }), bridges))
|
|
212
255
|
return unavailable("resource.external_close");
|
|
213
256
|
}
|
|
214
257
|
for (const actor of pkg.actors ?? []) {
|
|
215
|
-
if (actor.
|
|
216
|
-
return
|
|
258
|
+
if (typeof actor.name !== "string" || actor.name.length === 0)
|
|
259
|
+
return {
|
|
260
|
+
category: "invalid_package",
|
|
261
|
+
code: "sandbox.package.actor_name",
|
|
262
|
+
capability: null,
|
|
263
|
+
message: "actor lacks its declared diagnostic name",
|
|
264
|
+
span: null,
|
|
265
|
+
};
|
|
266
|
+
if (actor.crash != null &&
|
|
267
|
+
(actor.crash_info == null ||
|
|
268
|
+
!pkg.aggregates[actor.crash_info] ||
|
|
269
|
+
actor.crash_kind == null ||
|
|
270
|
+
!pkg.variants[actor.crash_kind] ||
|
|
271
|
+
actor.crash_action == null ||
|
|
272
|
+
!pkg.variants[actor.crash_action]))
|
|
273
|
+
return {
|
|
274
|
+
category: "invalid_package",
|
|
275
|
+
code: "sandbox.package.crash_hook_shapes",
|
|
276
|
+
capability: null,
|
|
277
|
+
message: "crash hook lacks its exact info, kind or action shape",
|
|
278
|
+
span: null,
|
|
279
|
+
};
|
|
280
|
+
if (actor.handlers.some((handler) => handler.every_ns != null &&
|
|
281
|
+
(!Number.isInteger(handler.every_ns) ||
|
|
282
|
+
handler.every_ns < 1_000_000 ||
|
|
283
|
+
handler.params !== 0 ||
|
|
284
|
+
handler.streams ||
|
|
285
|
+
handler.fallible ||
|
|
286
|
+
!pkg.functions.some((fn) => fn.id === handler.callable && fn.params.length === 1))))
|
|
287
|
+
return {
|
|
288
|
+
category: "invalid_package",
|
|
289
|
+
code: "sandbox.package.invalid_periodic",
|
|
290
|
+
capability: "actor.periodic",
|
|
291
|
+
message: "periodic handler lacks its checked interval or zero-argument body",
|
|
292
|
+
span: null,
|
|
293
|
+
};
|
|
217
294
|
if (actor.exit != null || actor.down != null)
|
|
218
295
|
return unavailable("actor.local_observation");
|
|
219
296
|
if (actor.max_heap_bytes != null)
|
|
220
297
|
return unavailable("actor.heap_limit");
|
|
221
298
|
}
|
|
222
|
-
for (const
|
|
223
|
-
|
|
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)))
|
|
224
307
|
return {
|
|
225
|
-
category:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
capability: familyIdentity(entry.family, entry.detail),
|
|
230
|
-
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",
|
|
231
312
|
span: null,
|
|
232
313
|
};
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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))))
|
|
238
324
|
return {
|
|
239
|
-
category:
|
|
240
|
-
code:
|
|
241
|
-
capability:
|
|
242
|
-
message:
|
|
243
|
-
? "This external-library operation is not available in the browser runtime yet."
|
|
244
|
-
: `${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",
|
|
245
329
|
span: null,
|
|
246
330
|
};
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
for (const kind of pkg.suspend_kinds) {
|
|
250
|
-
const refused = kind === "NativeIo"
|
|
251
|
-
? refusedNativeIo(pkg)
|
|
252
|
-
: SUPPORTED_SUSPEND_KINDS.has(kind)
|
|
253
|
-
? null
|
|
254
|
-
: kind;
|
|
255
|
-
if (refused !== null) {
|
|
331
|
+
if (supervisor.children.some((child) => !pkg.functions.some((fn) => fn.id === child.spawn)))
|
|
256
332
|
return {
|
|
257
|
-
category:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
capability: refused,
|
|
262
|
-
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",
|
|
263
337
|
span: null,
|
|
264
338
|
};
|
|
265
|
-
}
|
|
266
339
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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);
|
|
280
354
|
}
|
|
281
355
|
return null;
|
|
282
356
|
}
|
|
283
|
-
function
|
|
284
|
-
return
|
|
285
|
-
|
|
286
|
-
:
|
|
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
|
+
};
|
|
287
371
|
}
|