@markii/lua 0.1.0 → 0.2.0
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/dist/capabilities.d.ts +20 -0
- package/dist/capabilities.js +113 -24
- package/dist/errors.d.ts +54 -12
- package/dist/errors.js +32 -8
- package/dist/executor.d.ts +5 -4
- package/dist/executor.js +37 -5
- package/dist/index.d.ts +1 -1
- package/dist/marshal.js +13 -1
- package/dist/sandbox.d.ts +5 -3
- package/dist/sandbox.js +86 -20
- package/package.json +3 -3
package/dist/capabilities.d.ts
CHANGED
|
@@ -53,6 +53,25 @@ export interface CapabilityConfig {
|
|
|
53
53
|
maxFetchBytes?: number;
|
|
54
54
|
}
|
|
55
55
|
export declare const DEFAULT_MAX_FETCH_BYTES = 2000000;
|
|
56
|
+
/** One genuine capability denial, as recorded by `buildCapabilities`' `denials` handle — see its doc comment. */
|
|
57
|
+
export interface CapabilityDenial {
|
|
58
|
+
reason: 'denied' | 'tier-blocked';
|
|
59
|
+
message: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Non-spoofable, out-of-band record of the LAST genuine capability denial
|
|
63
|
+
* that happened during one `buildCapabilities` call's lifetime (i.e. one
|
|
64
|
+
* `runScript` call — see `./sandbox`). This is a plain JS closure: no Lua
|
|
65
|
+
* value, no metatable, nothing a script running in the sandbox can ever
|
|
66
|
+
* read or write, mirroring the discipline `./limits`' breach flag already
|
|
67
|
+
* uses for resource-limit kills. `sandbox.ts`'s `classifyRuntimeError`
|
|
68
|
+
* consults `last()` — never any error message string that crossed the Lua
|
|
69
|
+
* boundary — to decide whether a failed run was genuinely a `'capability'`
|
|
70
|
+
* kind, and if so which `capability` flavor (`'denied'` vs `'tier-blocked'`).
|
|
71
|
+
*/
|
|
72
|
+
export interface CapabilityDenials {
|
|
73
|
+
last(): CapabilityDenial | undefined;
|
|
74
|
+
}
|
|
56
75
|
/**
|
|
57
76
|
* `Uint8Array` <-> Lua string, byte-for-byte via one JS UTF-16 code unit
|
|
58
77
|
* per byte (Latin-1-style). Lua strings are themselves 8-bit-clean byte
|
|
@@ -121,4 +140,5 @@ export declare function luaStringToBytes(s: string): Uint8Array;
|
|
|
121
140
|
export declare function buildCapabilities(config: CapabilityConfig): {
|
|
122
141
|
rawGlobals: Record<string, (...args: never[]) => Promise<unknown>>;
|
|
123
142
|
preludeLua: string;
|
|
143
|
+
denials: CapabilityDenials;
|
|
124
144
|
};
|
package/dist/capabilities.js
CHANGED
|
@@ -96,6 +96,15 @@ export function buildCapabilities(config) {
|
|
|
96
96
|
const maxFetchBytes = config.maxFetchBytes ?? DEFAULT_MAX_FETCH_BYTES;
|
|
97
97
|
const rawGlobals = {};
|
|
98
98
|
const preludeParts = [];
|
|
99
|
+
// Out-of-band denial record — see `CapabilityDenials`'s doc comment. Every
|
|
100
|
+
// site below that throws a `capabilityError` records here FIRST, so
|
|
101
|
+
// `sandbox.ts` can classify the failure by this JS-only signal instead of
|
|
102
|
+
// by re-reading the (script-forgeable) error message.
|
|
103
|
+
let lastDenial;
|
|
104
|
+
function recordDenial(reason, message) {
|
|
105
|
+
lastDenial = { reason, message };
|
|
106
|
+
}
|
|
107
|
+
const denials = { last: () => lastDenial };
|
|
99
108
|
// --- net --------------------------------------------------------------
|
|
100
109
|
// `fetch_json` and `post`/`patch` are gated INDEPENDENTLY of each other
|
|
101
110
|
// (a manifest can grant POST to a host without granting it GET, or vice
|
|
@@ -104,9 +113,12 @@ export function buildCapabilities(config) {
|
|
|
104
113
|
// this function nested POST/PATCH wiring inside "if GET is granted",
|
|
105
114
|
// which silently produced no `net.post` at all for a POST-only grant.
|
|
106
115
|
const netGrants = config.netGrants ?? { get: [], post: [] };
|
|
116
|
+
// NOTE: no longer conditioned on `config.tier === 'manual'` for the POST
|
|
117
|
+
// half — under 'auto' with POST hosts granted, `net.post`/`net.patch` are
|
|
118
|
+
// now wired to TIER-BLOCKED STUBS below (not left undefined), so the
|
|
119
|
+
// `net` table itself must exist for those stubs to attach to.
|
|
107
120
|
const netTableNeeded = config.net !== undefined &&
|
|
108
|
-
(netGrants.get.length > 0 ||
|
|
109
|
-
(config.tier === 'manual' && netGrants.post.length > 0));
|
|
121
|
+
(netGrants.get.length > 0 || netGrants.post.length > 0);
|
|
110
122
|
if (netTableNeeded) {
|
|
111
123
|
preludeParts.push('net = net or {}\n');
|
|
112
124
|
}
|
|
@@ -114,18 +126,24 @@ export function buildCapabilities(config) {
|
|
|
114
126
|
rawGlobals.__smd_net_get_raw = (async (url) => {
|
|
115
127
|
const host = hostnameOf(url);
|
|
116
128
|
if (!host || !netGrants.get.includes(host)) {
|
|
117
|
-
|
|
129
|
+
const message = `net access to host "${host ?? url}" not granted for GET`;
|
|
130
|
+
recordDenial('denied', message);
|
|
131
|
+
throw capabilityError(message);
|
|
118
132
|
}
|
|
119
133
|
const res = await config.net.get(url);
|
|
120
134
|
if (res.body.length > maxFetchBytes) {
|
|
121
|
-
|
|
135
|
+
const message = `fetch response for "${url}" exceeds the ${maxFetchBytes}-byte cap`;
|
|
136
|
+
recordDenial('denied', message);
|
|
137
|
+
throw capabilityError(message);
|
|
122
138
|
}
|
|
123
139
|
let parsed;
|
|
124
140
|
try {
|
|
125
141
|
parsed = JSON.parse(res.body);
|
|
126
142
|
}
|
|
127
143
|
catch {
|
|
128
|
-
|
|
144
|
+
const message = `fetch response for "${url}" was not valid JSON`;
|
|
145
|
+
recordDenial('denied', message);
|
|
146
|
+
throw capabilityError(message);
|
|
129
147
|
}
|
|
130
148
|
return parsed;
|
|
131
149
|
});
|
|
@@ -135,10 +153,14 @@ __smd_net_get_raw = nil
|
|
|
135
153
|
net.fetch_json = function(url) return __smd_net_get(url):await() end
|
|
136
154
|
`);
|
|
137
155
|
}
|
|
138
|
-
// POST/PATCH are effectful
|
|
139
|
-
//
|
|
140
|
-
// 'auto'
|
|
141
|
-
//
|
|
156
|
+
// POST/PATCH are effectful. Under the 'manual' tier, wired to the real
|
|
157
|
+
// provider for hosts the effective grant set allows for POST. Under
|
|
158
|
+
// 'auto', even when POST hosts ARE granted, they are wired to STUBS
|
|
159
|
+
// that record a 'tier-blocked' denial and throw WITHOUT EVER reaching
|
|
160
|
+
// `config.net.post`/`.patch` — this grants nothing new (the provider is
|
|
161
|
+
// never called), it only makes "granted but tier-forbidden" a
|
|
162
|
+
// classifiable, non-spoofable outcome instead of collapsing into an
|
|
163
|
+
// ordinary "attempt to call a nil value" runtime error (spec §8: "An
|
|
142
164
|
// effectful call under an auto trigger fails cleanly").
|
|
143
165
|
if (config.tier === 'manual' &&
|
|
144
166
|
config.net?.post &&
|
|
@@ -146,7 +168,9 @@ net.fetch_json = function(url) return __smd_net_get(url):await() end
|
|
|
146
168
|
rawGlobals.__smd_net_post_raw = (async (url, body) => {
|
|
147
169
|
const host = hostnameOf(url);
|
|
148
170
|
if (!host || !netGrants.post.includes(host)) {
|
|
149
|
-
|
|
171
|
+
const message = `net access to host "${host ?? url}" not granted for POST`;
|
|
172
|
+
recordDenial('denied', message);
|
|
173
|
+
throw capabilityError(message);
|
|
150
174
|
}
|
|
151
175
|
return config.net.post(url, body);
|
|
152
176
|
});
|
|
@@ -154,6 +178,29 @@ net.fetch_json = function(url) return __smd_net_get(url):await() end
|
|
|
154
178
|
local __smd_net_post = __smd_net_post_raw
|
|
155
179
|
__smd_net_post_raw = nil
|
|
156
180
|
net.post = function(url, body) return __smd_net_post(url, body):await() end
|
|
181
|
+
`);
|
|
182
|
+
}
|
|
183
|
+
else if (
|
|
184
|
+
// Mirrors the 'manual' condition above EXACTLY except for the tier, so
|
|
185
|
+
// the read-only tier never exposes a wider method surface than the
|
|
186
|
+
// full-grant tier would: a stub appears only where a real `net.post`
|
|
187
|
+
// would have appeared under 'manual'. Without the `config.net?.post`
|
|
188
|
+
// half, a host whose provider implements no POST at all would still
|
|
189
|
+
// show `net.post` under 'auto' (as a tier-block stub) while showing
|
|
190
|
+
// nothing under 'manual' — an inconsistency a feature-detecting script
|
|
191
|
+
// (`if net.post then`) would read exactly backwards.
|
|
192
|
+
config.tier === 'auto' &&
|
|
193
|
+
config.net?.post &&
|
|
194
|
+
netGrants.post.length > 0) {
|
|
195
|
+
rawGlobals.__smd_net_post_tier_blocked_raw = (async () => {
|
|
196
|
+
const message = 'net.post is granted but not permitted under the read-only auto tier (requires a manual run)';
|
|
197
|
+
recordDenial('tier-blocked', message);
|
|
198
|
+
throw capabilityError(message);
|
|
199
|
+
});
|
|
200
|
+
preludeParts.push(`
|
|
201
|
+
local __smd_net_post_blocked = __smd_net_post_tier_blocked_raw
|
|
202
|
+
__smd_net_post_tier_blocked_raw = nil
|
|
203
|
+
net.post = function(url, body) return __smd_net_post_blocked(url, body):await() end
|
|
157
204
|
`);
|
|
158
205
|
}
|
|
159
206
|
if (config.tier === 'manual' &&
|
|
@@ -162,7 +209,9 @@ net.post = function(url, body) return __smd_net_post(url, body):await() end
|
|
|
162
209
|
rawGlobals.__smd_net_patch_raw = (async (url, body) => {
|
|
163
210
|
const host = hostnameOf(url);
|
|
164
211
|
if (!host || !netGrants.post.includes(host)) {
|
|
165
|
-
|
|
212
|
+
const message = `net access to host "${host ?? url}" not granted for PATCH`;
|
|
213
|
+
recordDenial('denied', message);
|
|
214
|
+
throw capabilityError(message);
|
|
166
215
|
}
|
|
167
216
|
return config.net.patch(url, body);
|
|
168
217
|
});
|
|
@@ -170,6 +219,22 @@ net.post = function(url, body) return __smd_net_post(url, body):await() end
|
|
|
170
219
|
local __smd_net_patch = __smd_net_patch_raw
|
|
171
220
|
__smd_net_patch_raw = nil
|
|
172
221
|
net.patch = function(url, body) return __smd_net_patch(url, body):await() end
|
|
222
|
+
`);
|
|
223
|
+
}
|
|
224
|
+
else if (
|
|
225
|
+
// Same mirroring as the POST stub above — see its comment.
|
|
226
|
+
config.tier === 'auto' &&
|
|
227
|
+
config.net?.patch &&
|
|
228
|
+
netGrants.post.length > 0) {
|
|
229
|
+
rawGlobals.__smd_net_patch_tier_blocked_raw = (async () => {
|
|
230
|
+
const message = 'net.patch is granted but not permitted under the read-only auto tier (requires a manual run)';
|
|
231
|
+
recordDenial('tier-blocked', message);
|
|
232
|
+
throw capabilityError(message);
|
|
233
|
+
});
|
|
234
|
+
preludeParts.push(`
|
|
235
|
+
local __smd_net_patch_blocked = __smd_net_patch_tier_blocked_raw
|
|
236
|
+
__smd_net_patch_tier_blocked_raw = nil
|
|
237
|
+
net.patch = function(url, body) return __smd_net_patch_blocked(url, body):await() end
|
|
173
238
|
`);
|
|
174
239
|
}
|
|
175
240
|
// --- cache --------------------------------------------------------------
|
|
@@ -220,24 +285,27 @@ end
|
|
|
220
285
|
// Delegates entirely to the injected `ScriptView` (`@markii/bundle`), which
|
|
221
286
|
// already enforces the path-jail and the read/write:cache/ split (spec
|
|
222
287
|
// §11). This module adds nothing on top except the tier gate for
|
|
223
|
-
// `bundle.write` (
|
|
224
|
-
// byte<->Lua-string conversion.
|
|
288
|
+
// `bundle.write` (a tier-blocked stub under 'auto' — read-only tier) and
|
|
289
|
+
// the byte<->Lua-string conversion.
|
|
225
290
|
if (config.bundle) {
|
|
226
291
|
const view = config.bundle;
|
|
227
292
|
// `ScriptView` (@markii/bundle) throws its own `ScriptCapabilityError` /
|
|
228
293
|
// `BundlePathError` for a denied or path-jail-violating call — those
|
|
229
|
-
// are re-tagged here with `CAPABILITY_ERROR_TAG`
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
// through to the
|
|
294
|
+
// are re-tagged here with `CAPABILITY_ERROR_TAG` (a cosmetic prefix
|
|
295
|
+
// only, see `./errors`'s doc comment) AND recorded on the `denials`
|
|
296
|
+
// handle as reason `'denied'`, so `sandbox.ts` reports them as
|
|
297
|
+
// `kind: 'capability', capability: 'denied'` uniformly, the same as a
|
|
298
|
+
// net host-allowlist denial, rather than falling through to the
|
|
299
|
+
// generic `'runtime'` bucket.
|
|
234
300
|
rawGlobals.__smd_bundle_read_raw = (async (path) => {
|
|
235
301
|
let data;
|
|
236
302
|
try {
|
|
237
303
|
data = await view.read(path);
|
|
238
304
|
}
|
|
239
305
|
catch (err) {
|
|
240
|
-
|
|
306
|
+
const message = describeThrown(err);
|
|
307
|
+
recordDenial('denied', message);
|
|
308
|
+
throw capabilityError(message);
|
|
241
309
|
}
|
|
242
310
|
return data === undefined ? null : bytesToLuaString(data);
|
|
243
311
|
});
|
|
@@ -246,7 +314,9 @@ end
|
|
|
246
314
|
return await view.exists(path);
|
|
247
315
|
}
|
|
248
316
|
catch (err) {
|
|
249
|
-
|
|
317
|
+
const message = describeThrown(err);
|
|
318
|
+
recordDenial('denied', message);
|
|
319
|
+
throw capabilityError(message);
|
|
250
320
|
}
|
|
251
321
|
});
|
|
252
322
|
preludeParts.push(`
|
|
@@ -264,7 +334,9 @@ bundle.exists = function(path) return __smd_bundle_exists(path):await() end
|
|
|
264
334
|
await view.write(path, luaStringToBytes(data));
|
|
265
335
|
}
|
|
266
336
|
catch (err) {
|
|
267
|
-
|
|
337
|
+
const message = describeThrown(err);
|
|
338
|
+
recordDenial('denied', message);
|
|
339
|
+
throw capabilityError(message);
|
|
268
340
|
}
|
|
269
341
|
return true;
|
|
270
342
|
});
|
|
@@ -274,8 +346,25 @@ __smd_bundle_write_raw = nil
|
|
|
274
346
|
bundle.write = function(path, data) return __smd_bundle_write(path, data):await() end
|
|
275
347
|
`);
|
|
276
348
|
}
|
|
277
|
-
|
|
278
|
-
|
|
349
|
+
else {
|
|
350
|
+
// Under 'auto': `bundle.write` is wired to a TIER-BLOCKED STUB that
|
|
351
|
+
// records a 'tier-blocked' denial and throws WITHOUT EVER reaching
|
|
352
|
+
// `view.write` — the bundle view's own write path is never touched,
|
|
353
|
+
// so this grants nothing new; it only makes "write is available but
|
|
354
|
+
// this tier forbids it" classifiable instead of collapsing into an
|
|
355
|
+
// ordinary "attempt to call a nil value" runtime error (spec §8:
|
|
356
|
+
// "bundle/cache reads, cache writes only" under the read-only tier).
|
|
357
|
+
rawGlobals.__smd_bundle_write_tier_blocked_raw = (async () => {
|
|
358
|
+
const message = 'bundle.write is not permitted under the read-only auto tier (requires a manual run)';
|
|
359
|
+
recordDenial('tier-blocked', message);
|
|
360
|
+
throw capabilityError(message);
|
|
361
|
+
});
|
|
362
|
+
preludeParts.push(`
|
|
363
|
+
local __smd_bundle_write_blocked = __smd_bundle_write_tier_blocked_raw
|
|
364
|
+
__smd_bundle_write_tier_blocked_raw = nil
|
|
365
|
+
bundle.write = function(path, data) return __smd_bundle_write_blocked(path, data):await() end
|
|
366
|
+
`);
|
|
367
|
+
}
|
|
279
368
|
}
|
|
280
|
-
return { rawGlobals, preludeLua: preludeParts.join('\n') };
|
|
369
|
+
return { rawGlobals, preludeLua: preludeParts.join('\n'), denials };
|
|
281
370
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -13,33 +13,61 @@
|
|
|
13
13
|
* checks on the far side are useless (verified empirically against
|
|
14
14
|
* wasmoon 1.16.0: `new MyError('x')` thrown inside an injected async
|
|
15
15
|
* function round-trips as `Error: MyError: x`, not `MyError`). So
|
|
16
|
-
* classification of
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
16
|
+
* classification of marshal failures raised *from inside Lua execution* is
|
|
17
|
+
* done by tagging the error message with `MARSHAL_ERROR_TAG` and
|
|
18
|
+
* pattern-matching on it in `sandbox.ts` after the run fails (a forged
|
|
19
|
+
* marshal tag only relabels one script-error-class failure as another —
|
|
20
|
+
* see `classifyRuntimeError`'s doc comment).
|
|
21
|
+
*
|
|
22
|
+
* Capability failures are classified DIFFERENTLY, and NOT by message
|
|
23
|
+
* matching (this used to be message-tag-based too — `error("MARK_CAPABILITY:
|
|
24
|
+
* ...")` from a script would forge a `kind: 'capability'` result, which is
|
|
25
|
+
* exactly the bug this taxonomy closes). See `./capabilities`'s
|
|
26
|
+
* `CapabilityDenials` handle: every genuine denial is recorded on a plain JS
|
|
27
|
+
* closure BEFORE the corresponding throw, entirely out of Lua's reach, and
|
|
28
|
+
* `sandbox.ts` consults that handle — never the message that came back out
|
|
29
|
+
* of the VM — to decide `kind: 'capability'`. This mirrors the discipline
|
|
30
|
+
* already used for resource-limit breaches: they're tracked out-of-band via
|
|
31
|
+
* a plain JS closure flag set inside the instruction hook (see `./limits`)
|
|
32
|
+
* and the raw `LuaReturn.ErrorMem` C-API status code (see
|
|
33
|
+
* `captureAssertOkStatus` in `./sandbox`), neither of which Lua code can see
|
|
34
|
+
* or touch, so classification is exact regardless of what any error message
|
|
35
|
+
* says.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Prefix tag applied to the `Error` message thrown into Lua for a capability
|
|
39
|
+
* (permission) denial. PURELY COSMETIC as of the capability-denial JS-closure
|
|
40
|
+
* rework (see the module doc comment above): it makes a message readable to
|
|
41
|
+
* a human/log, and nothing else. It is NOT, and must never again become, a
|
|
42
|
+
* classification signal — `sandbox.ts`'s `classifyRuntimeError` does not
|
|
43
|
+
* (and must not) inspect it; a script forging `error("MARK_CAPABILITY:
|
|
44
|
+
* ...")` produces a message containing this tag but classifies as an
|
|
45
|
+
* ordinary `'runtime'`/`'script-error'` failure, exactly like any other
|
|
46
|
+
* `error()` call, because no genuine denial was ever recorded on the
|
|
47
|
+
* `CapabilityDenials` handle for that run.
|
|
23
48
|
*/
|
|
24
|
-
/** Prefix tag for a capability (permission) denial raised into Lua from a host-provided function. */
|
|
25
49
|
export declare const CAPABILITY_ERROR_TAG = "MARK_CAPABILITY";
|
|
26
50
|
/** Prefix tag for a marshal-time rejection raised from the in-Lua marshal walk (see `./marshal`). */
|
|
27
51
|
export declare const MARSHAL_ERROR_TAG = "MARK_MARSHAL";
|
|
28
52
|
/** The limits a run can breach; see `./limits`. */
|
|
29
53
|
export type ScriptLimitKind = 'instructions' | 'timeout' | 'memory';
|
|
30
54
|
/** Why a return value was rejected by the marshaller; see `./marshal`. */
|
|
31
|
-
export type ScriptMarshalReason = 'depth' | 'nodes' | 'cycle' | 'type' | 'key-type' | 'non-finite-number';
|
|
55
|
+
export type ScriptMarshalReason = 'depth' | 'nodes' | 'cycle' | 'type' | 'key-type' | 'non-finite-number' | 'nul-byte';
|
|
32
56
|
/**
|
|
33
57
|
* The full discriminated failure shape `runScript` returns. `kind`:
|
|
34
58
|
* - `'limit'` — a resource limit was breached (instruction count, wall
|
|
35
59
|
* clock, or memory). `limit` says which.
|
|
36
60
|
* - `'capability'` — the script attempted something its granted
|
|
37
61
|
* capabilities don't allow (ungranted host, effectful op under an
|
|
38
|
-
* auto-run tier, disallowed bundle path/write).
|
|
62
|
+
* auto-run tier, disallowed bundle path/write). `capability` says which
|
|
63
|
+
* flavor — see below.
|
|
39
64
|
* - `'marshal'` — the script's return value could not be safely converted
|
|
40
65
|
* to a JSON-serializable JS value (function/userdata/thread, a cycle,
|
|
41
|
-
* too deep, too many nodes, a non-string table key,
|
|
42
|
-
* number
|
|
66
|
+
* too deep, too many nodes, a non-string table key, a non-finite
|
|
67
|
+
* number, or a string containing an embedded NUL byte — wasmoon silently
|
|
68
|
+
* truncates a Lua string at its first NUL when converting to JS, so this
|
|
69
|
+
* is rejected on the Lua side before that truncation can happen).
|
|
70
|
+
* `reason` says which.
|
|
43
71
|
* - `'runtime'` — an ordinary Lua error (syntax error, `error()` call,
|
|
44
72
|
* type error, stack overflow, etc.) not covered by the above.
|
|
45
73
|
*/
|
|
@@ -48,6 +76,20 @@ export interface ScriptFailure {
|
|
|
48
76
|
message: string;
|
|
49
77
|
limit?: ScriptLimitKind;
|
|
50
78
|
reason?: ScriptMarshalReason;
|
|
79
|
+
/**
|
|
80
|
+
* Set only when `kind === 'capability'`, discriminating WHICH flavor of
|
|
81
|
+
* capability failure this was — derived exclusively from `./capabilities`'
|
|
82
|
+
* `CapabilityDenials` handle (a plain JS closure, never from any message
|
|
83
|
+
* text; see the module doc comment):
|
|
84
|
+
* - `'denied'` — the grant was absent, or the host actively refused
|
|
85
|
+
* (an ungranted net host, a bundle path-jail rejection, a fetch-size
|
|
86
|
+
* cap). A manual run with the SAME grants would fail identically.
|
|
87
|
+
* - `'tier-blocked'` — the capability genuinely exists in the granted set,
|
|
88
|
+
* but the current execution tier (`'auto'`) forbids exercising it. A
|
|
89
|
+
* manual run of the exact same script, with the exact same grants,
|
|
90
|
+
* would succeed.
|
|
91
|
+
*/
|
|
92
|
+
capability?: 'denied' | 'tier-blocked';
|
|
51
93
|
}
|
|
52
94
|
/**
|
|
53
95
|
* Thrown by the instruction-count/wall-clock hook installed in `./limits`
|
package/dist/errors.js
CHANGED
|
@@ -13,15 +13,39 @@
|
|
|
13
13
|
* checks on the far side are useless (verified empirically against
|
|
14
14
|
* wasmoon 1.16.0: `new MyError('x')` thrown inside an injected async
|
|
15
15
|
* function round-trips as `Error: MyError: x`, not `MyError`). So
|
|
16
|
-
* classification of
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
16
|
+
* classification of marshal failures raised *from inside Lua execution* is
|
|
17
|
+
* done by tagging the error message with `MARSHAL_ERROR_TAG` and
|
|
18
|
+
* pattern-matching on it in `sandbox.ts` after the run fails (a forged
|
|
19
|
+
* marshal tag only relabels one script-error-class failure as another —
|
|
20
|
+
* see `classifyRuntimeError`'s doc comment).
|
|
21
|
+
*
|
|
22
|
+
* Capability failures are classified DIFFERENTLY, and NOT by message
|
|
23
|
+
* matching (this used to be message-tag-based too — `error("MARK_CAPABILITY:
|
|
24
|
+
* ...")` from a script would forge a `kind: 'capability'` result, which is
|
|
25
|
+
* exactly the bug this taxonomy closes). See `./capabilities`'s
|
|
26
|
+
* `CapabilityDenials` handle: every genuine denial is recorded on a plain JS
|
|
27
|
+
* closure BEFORE the corresponding throw, entirely out of Lua's reach, and
|
|
28
|
+
* `sandbox.ts` consults that handle — never the message that came back out
|
|
29
|
+
* of the VM — to decide `kind: 'capability'`. This mirrors the discipline
|
|
30
|
+
* already used for resource-limit breaches: they're tracked out-of-band via
|
|
31
|
+
* a plain JS closure flag set inside the instruction hook (see `./limits`)
|
|
32
|
+
* and the raw `LuaReturn.ErrorMem` C-API status code (see
|
|
33
|
+
* `captureAssertOkStatus` in `./sandbox`), neither of which Lua code can see
|
|
34
|
+
* or touch, so classification is exact regardless of what any error message
|
|
35
|
+
* says.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Prefix tag applied to the `Error` message thrown into Lua for a capability
|
|
39
|
+
* (permission) denial. PURELY COSMETIC as of the capability-denial JS-closure
|
|
40
|
+
* rework (see the module doc comment above): it makes a message readable to
|
|
41
|
+
* a human/log, and nothing else. It is NOT, and must never again become, a
|
|
42
|
+
* classification signal — `sandbox.ts`'s `classifyRuntimeError` does not
|
|
43
|
+
* (and must not) inspect it; a script forging `error("MARK_CAPABILITY:
|
|
44
|
+
* ...")` produces a message containing this tag but classifies as an
|
|
45
|
+
* ordinary `'runtime'`/`'script-error'` failure, exactly like any other
|
|
46
|
+
* `error()` call, because no genuine denial was ever recorded on the
|
|
47
|
+
* `CapabilityDenials` handle for that run.
|
|
23
48
|
*/
|
|
24
|
-
/** Prefix tag for a capability (permission) denial raised into Lua from a host-provided function. */
|
|
25
49
|
export const CAPABILITY_ERROR_TAG = 'MARK_CAPABILITY';
|
|
26
50
|
/** Prefix tag for a marshal-time rejection raised from the in-Lua marshal walk (see `./marshal`). */
|
|
27
51
|
export const MARSHAL_ERROR_TAG = 'MARK_MARSHAL';
|
package/dist/executor.d.ts
CHANGED
|
@@ -31,9 +31,10 @@ export type LuaExecutorConfig = Omit<RunScriptOptions, 'code' | 'tier'>;
|
|
|
31
31
|
* signature. `runScript` never throws (see `./sandbox`'s doc comment), so
|
|
32
32
|
* this adapter doesn't need its own try/catch — it only reshapes the
|
|
33
33
|
* result: `ok: true` passes `value` through untouched; `ok: false` maps
|
|
34
|
-
* `runScript`'s `ScriptFailure` (`{ kind, message, ... }`) down to
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* fields (still visible in `message` for anyone reading
|
|
34
|
+
* `runScript`'s `ScriptFailure` (`{ kind, message, ... }`) down to
|
|
35
|
+
* `@markii/runtime`'s closed `ExecuteFailure` shape via
|
|
36
|
+
* `toRuntimeFailureKind`, dropping the Lua-specific `limit`/`reason`/
|
|
37
|
+
* `capability` sub-fields (still visible in `message` for anyone reading
|
|
38
|
+
* the stored error).
|
|
38
39
|
*/
|
|
39
40
|
export declare function createLuaExecutor(config?: LuaExecutorConfig): ScriptExecutor;
|
package/dist/executor.js
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
import { runScript } from './sandbox.js';
|
|
2
|
+
/**
|
|
3
|
+
* Maps this package's own `ScriptFailure.kind` (`'limit' | 'capability' |
|
|
4
|
+
* 'marshal' | 'runtime'`, further discriminated by `ScriptFailure.
|
|
5
|
+
* capability` for the `'capability'` case) down to `@markii/runtime`'s
|
|
6
|
+
* closed, shared `FailureKind` union (`'script-error' | 'capability-denied'
|
|
7
|
+
* | 'tier-blocked' | 'limit'`):
|
|
8
|
+
* - `'limit'` -> `'limit'`
|
|
9
|
+
* - `'capability'` with `capability === 'tier-blocked'` -> `'tier-blocked'`
|
|
10
|
+
* - `'capability'` otherwise (i.e. `'denied'`, or — defensively — absent)
|
|
11
|
+
* -> `'capability-denied'`
|
|
12
|
+
* - `'marshal'` / `'runtime'` -> `'script-error'`
|
|
13
|
+
*
|
|
14
|
+
* This is the one place the Lua-specific taxonomy and the runtime-shared
|
|
15
|
+
* one meet; every other module in this package only knows `ScriptFailure`.
|
|
16
|
+
*/
|
|
17
|
+
function toRuntimeFailureKind(failure) {
|
|
18
|
+
switch (failure.kind) {
|
|
19
|
+
case 'limit':
|
|
20
|
+
return 'limit';
|
|
21
|
+
case 'capability':
|
|
22
|
+
return failure.capability === 'tier-blocked'
|
|
23
|
+
? 'tier-blocked'
|
|
24
|
+
: 'capability-denied';
|
|
25
|
+
case 'marshal':
|
|
26
|
+
case 'runtime':
|
|
27
|
+
return 'script-error';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
2
30
|
/**
|
|
3
31
|
* Builds a `ScriptExecutor` (`@markii/runtime`) backed by this package's
|
|
4
32
|
* `runScript`. `config` is captured once and reused for every script the
|
|
@@ -7,10 +35,11 @@ import { runScript } from './sandbox.js';
|
|
|
7
35
|
* signature. `runScript` never throws (see `./sandbox`'s doc comment), so
|
|
8
36
|
* this adapter doesn't need its own try/catch — it only reshapes the
|
|
9
37
|
* result: `ok: true` passes `value` through untouched; `ok: false` maps
|
|
10
|
-
* `runScript`'s `ScriptFailure` (`{ kind, message, ... }`) down to
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* fields (still visible in `message` for anyone reading
|
|
38
|
+
* `runScript`'s `ScriptFailure` (`{ kind, message, ... }`) down to
|
|
39
|
+
* `@markii/runtime`'s closed `ExecuteFailure` shape via
|
|
40
|
+
* `toRuntimeFailureKind`, dropping the Lua-specific `limit`/`reason`/
|
|
41
|
+
* `capability` sub-fields (still visible in `message` for anyone reading
|
|
42
|
+
* the stored error).
|
|
14
43
|
*/
|
|
15
44
|
export function createLuaExecutor(config = {}) {
|
|
16
45
|
return async ({ code, tier }) => {
|
|
@@ -20,7 +49,10 @@ export function createLuaExecutor(config = {}) {
|
|
|
20
49
|
}
|
|
21
50
|
return {
|
|
22
51
|
ok: false,
|
|
23
|
-
error: {
|
|
52
|
+
error: {
|
|
53
|
+
kind: toRuntimeFailureKind(result.error),
|
|
54
|
+
message: result.error.message,
|
|
55
|
+
},
|
|
24
56
|
};
|
|
25
57
|
};
|
|
26
58
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type { CreateEmptyLuaEngineOptions } from './globals.js';
|
|
|
4
4
|
export { ALLOWED_GLOBALS, DENIED_GLOBALS, createEmptyLuaEngine, } from './globals.js';
|
|
5
5
|
export type { LimitHandle, ScriptLimits } from './limits.js';
|
|
6
6
|
export { DEFAULT_LIMITS, installLimits } from './limits.js';
|
|
7
|
-
export type { CacheEntry, CacheProvider, CapabilityConfig, CapabilityTier, NetGrants, NetProvider, NetResponse, } from './capabilities.js';
|
|
7
|
+
export type { CacheEntry, CacheProvider, CapabilityConfig, CapabilityDenial, CapabilityDenials, CapabilityTier, NetGrants, NetProvider, NetResponse, } from './capabilities.js';
|
|
8
8
|
export { DEFAULT_MAX_FETCH_BYTES, bytesToLuaString, buildCapabilities, luaStringToBytes, } from './capabilities.js';
|
|
9
9
|
export type { MarshalLimits } from './marshal.js';
|
|
10
10
|
export { DEFAULT_MARSHAL_LIMITS, buildMarshalPrelude, finalizeMarshaledValue, wrapUserCode, } from './marshal.js';
|
package/dist/marshal.js
CHANGED
|
@@ -74,9 +74,21 @@ export const DEFAULT_MARSHAL_LIMITS = {
|
|
|
74
74
|
*/
|
|
75
75
|
export function buildMarshalPrelude(limits) {
|
|
76
76
|
return `
|
|
77
|
+
-- Captured into locals AT PRELUDE-DEFINITION TIME (this chunk is doString'd
|
|
78
|
+
-- once per engine, before any untrusted user code runs), so the walk below
|
|
79
|
+
-- closes over these as upvalues fixed to the genuine primitives -- immune to
|
|
80
|
+
-- a later script doing e.g. \`error = function() end\` in the shared globals
|
|
81
|
+
-- table before this walk runs. See finding F-1 in the sandbox audit: the
|
|
82
|
+
-- walk previously resolved error/type/pairs/math.floor as DYNAMIC GLOBAL
|
|
83
|
+
-- lookups, which a script could rebind to neuter its own caps.
|
|
84
|
+
local error, type, pairs, floor, sfind = error, type, pairs, math.floor, string.find
|
|
85
|
+
|
|
77
86
|
local function __smd_marshal(value, seen, depth, budget)
|
|
78
87
|
local t = type(value)
|
|
79
88
|
if t == "nil" or t == "boolean" or t == "string" or t == "number" then
|
|
89
|
+
if t == "string" and sfind(value, "\\0", 1, true) then
|
|
90
|
+
error("${MARSHAL_ERROR_TAG}:nul-byte")
|
|
91
|
+
end
|
|
80
92
|
budget.n = budget.n + 1
|
|
81
93
|
if budget.n > budget.maxNodes then error("${MARSHAL_ERROR_TAG}:nodes") end
|
|
82
94
|
return value
|
|
@@ -90,7 +102,7 @@ local function __smd_marshal(value, seen, depth, budget)
|
|
|
90
102
|
local isArray = true
|
|
91
103
|
for k, _ in pairs(value) do
|
|
92
104
|
count = count + 1
|
|
93
|
-
if type(k) ~= "number" or k < 1 or
|
|
105
|
+
if type(k) ~= "number" or k < 1 or floor(k) ~= k then
|
|
94
106
|
isArray = false
|
|
95
107
|
end
|
|
96
108
|
end
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -58,11 +58,13 @@ export type RunScriptResult = {
|
|
|
58
58
|
* UNCONDITIONALLY and, if set, wins over whatever the run otherwise
|
|
59
59
|
* reported — see `./limits`'s doc comment for why this is the actual
|
|
60
60
|
* enforcement point for "not swallowed by the script's own `pcall`".
|
|
61
|
-
* 7. Otherwise, a thrown error is classified by
|
|
62
|
-
*
|
|
61
|
+
* 7. Otherwise, a thrown error is classified by non-spoofable out-of-band
|
|
62
|
+
* signals, in order, before ever falling back to
|
|
63
63
|
* `classifyRuntimeError`'s message-based path: the wall-clock guard's
|
|
64
64
|
* own `ScriptLimitError` sentinel (`instanceof`, Defect 3), then the
|
|
65
|
-
* raw `LuaReturn.ErrorMem` status code (Defect 2)
|
|
65
|
+
* raw `LuaReturn.ErrorMem` status code (Defect 2), then step 3's
|
|
66
|
+
* `CapabilityDenials` handle (`denials.last()` — was ANY genuine
|
|
67
|
+
* denial/tier-block recorded during this run?). A successful return
|
|
66
68
|
* goes through `finalizeMarshaledValue` for the final NaN/Infinity
|
|
67
69
|
* check and marker cleanup.
|
|
68
70
|
* 8. `finally`: hook removed, thread popped, engine closed — every path,
|
package/dist/sandbox.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LuaReturn } from 'wasmoon';
|
|
2
2
|
import { buildCapabilities, } from './capabilities.js';
|
|
3
|
-
import {
|
|
3
|
+
import { MARSHAL_ERROR_TAG, ScriptLimitError } from './errors.js';
|
|
4
4
|
import { createEmptyLuaEngine } from './globals.js';
|
|
5
5
|
import { DEFAULT_LIMITS, installLimits } from './limits.js';
|
|
6
6
|
import { buildMarshalPrelude, DEFAULT_MARSHAL_LIMITS, finalizeMarshaledValue, wrapUserCode, } from './marshal.js';
|
|
@@ -105,6 +105,8 @@ function extractMarshalReason(message) {
|
|
|
105
105
|
return 'cycle';
|
|
106
106
|
case 'key-type':
|
|
107
107
|
return 'key-type';
|
|
108
|
+
case 'nul-byte':
|
|
109
|
+
return 'nul-byte';
|
|
108
110
|
case 'type':
|
|
109
111
|
return 'type';
|
|
110
112
|
default:
|
|
@@ -113,15 +115,27 @@ function extractMarshalReason(message) {
|
|
|
113
115
|
}
|
|
114
116
|
/**
|
|
115
117
|
* Classifies an error thrown out of `thread.run()` into the discriminated
|
|
116
|
-
* `ScriptFailure` shape
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* `
|
|
118
|
+
* `ScriptFailure` shape, EXCLUDING capability failures — those are decided
|
|
119
|
+
* separately, BEFORE this function is ever consulted (see `runScript`
|
|
120
|
+
* below), by the `CapabilityDenials` out-of-band handle (`./capabilities`).
|
|
121
|
+
* This function only ever returns `'marshal'` or `'runtime'`.
|
|
122
|
+
*
|
|
123
|
+
* Message-prefix matching (`MARSHAL_ERROR_TAG`) is used here rather than
|
|
124
|
+
* `instanceof` because wasmoon does not preserve JS `Error` subclass
|
|
125
|
+
* identity across the Lua round trip — see the doc comment on that tag in
|
|
126
|
+
* `./errors` for the empirical evidence. This IS spoofable in principle (a
|
|
127
|
+
* script could `error("MARK_MARSHAL:type ...")`), but the consequence of
|
|
128
|
+
* that forgery is bounded and accepted: it only relabels one
|
|
129
|
+
* `'script-error'`-class failure (`'runtime'`) as another (`'marshal'`) —
|
|
130
|
+
* neither claims a capability was ever exercised, so there is no security
|
|
131
|
+
* property being defended here the way there is for `'capability'`/
|
|
132
|
+
* `'limit'`.
|
|
133
|
+
*
|
|
134
|
+
* Resource-limit breaches are ALL classified separately, BEFORE this
|
|
135
|
+
* function is ever called, via non-spoofable out-of-band signals — never
|
|
136
|
+
* through a message-based path, since a script can trivially forge any
|
|
137
|
+
* message string (e.g. `error("MARK_LIMIT: ...")` or `error("not enough
|
|
138
|
+
* memory")`) but cannot forge these:
|
|
125
139
|
* - instruction/wall-clock breaches: the JS closure flag from
|
|
126
140
|
* `./limits`' hook (see `runScript` below);
|
|
127
141
|
* - the async-hang backstop: `instanceof ScriptLimitError` on the
|
|
@@ -129,15 +143,14 @@ function extractMarshalReason(message) {
|
|
|
129
143
|
* boundary (see the guard's construction in `runScript`);
|
|
130
144
|
* - memory-cap breaches: the raw `LuaReturn.ErrorMem` C-API status code
|
|
131
145
|
* captured by `captureAssertOkStatus` (see its doc comment).
|
|
146
|
+
* Capability failures are classified the same non-spoofable way, via the
|
|
147
|
+
* `CapabilityDenials` handle — a script forging `error("MARK_CAPABILITY:
|
|
148
|
+
* ...")` no longer produces `kind: 'capability'`; with no genuine denial
|
|
149
|
+
* recorded, it falls through to THIS function and comes back `'runtime'`,
|
|
150
|
+
* exactly like any other `error()` call with no special meaning.
|
|
132
151
|
*/
|
|
133
152
|
function classifyRuntimeError(err) {
|
|
134
153
|
const message = describeError(err);
|
|
135
|
-
if (message.includes(CAPABILITY_ERROR_TAG)) {
|
|
136
|
-
return {
|
|
137
|
-
kind: 'capability',
|
|
138
|
-
message: message.replace(`${CAPABILITY_ERROR_TAG}: `, ''),
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
154
|
if (message.includes(MARSHAL_ERROR_TAG)) {
|
|
142
155
|
return {
|
|
143
156
|
kind: 'marshal',
|
|
@@ -171,11 +184,13 @@ function classifyRuntimeError(err) {
|
|
|
171
184
|
* UNCONDITIONALLY and, if set, wins over whatever the run otherwise
|
|
172
185
|
* reported — see `./limits`'s doc comment for why this is the actual
|
|
173
186
|
* enforcement point for "not swallowed by the script's own `pcall`".
|
|
174
|
-
* 7. Otherwise, a thrown error is classified by
|
|
175
|
-
*
|
|
187
|
+
* 7. Otherwise, a thrown error is classified by non-spoofable out-of-band
|
|
188
|
+
* signals, in order, before ever falling back to
|
|
176
189
|
* `classifyRuntimeError`'s message-based path: the wall-clock guard's
|
|
177
190
|
* own `ScriptLimitError` sentinel (`instanceof`, Defect 3), then the
|
|
178
|
-
* raw `LuaReturn.ErrorMem` status code (Defect 2)
|
|
191
|
+
* raw `LuaReturn.ErrorMem` status code (Defect 2), then step 3's
|
|
192
|
+
* `CapabilityDenials` handle (`denials.last()` — was ANY genuine
|
|
193
|
+
* denial/tier-block recorded during this run?). A successful return
|
|
179
194
|
* goes through `finalizeMarshaledValue` for the final NaN/Infinity
|
|
180
195
|
* check and marker cleanup.
|
|
181
196
|
* 8. `finally`: hook removed, thread popped, engine closed — every path,
|
|
@@ -194,7 +209,7 @@ export async function runScript(options) {
|
|
|
194
209
|
let limitHandle;
|
|
195
210
|
let guardTimer;
|
|
196
211
|
try {
|
|
197
|
-
const { rawGlobals, preludeLua } = buildCapabilities({
|
|
212
|
+
const { rawGlobals, preludeLua, denials } = buildCapabilities({
|
|
198
213
|
tier: options.tier,
|
|
199
214
|
net: options.net,
|
|
200
215
|
netGrants: options.netGrants,
|
|
@@ -302,6 +317,39 @@ export async function runScript(options) {
|
|
|
302
317
|
},
|
|
303
318
|
};
|
|
304
319
|
}
|
|
320
|
+
// Capability failures, identified the same non-spoofable way as the
|
|
321
|
+
// limit breaches above: `denials.last()` is a plain JS closure
|
|
322
|
+
// (`./capabilities`'s `CapabilityDenials`) that Lua can never see or
|
|
323
|
+
// touch, recorded BEFORE the corresponding throw at every genuine
|
|
324
|
+
// denial/tier-block site. If ANY denial was recorded during this run,
|
|
325
|
+
// it wins over whatever `classifyRuntimeError`'s message-based path
|
|
326
|
+
// would otherwise conclude — using the RECORDED message, never the
|
|
327
|
+
// message that came back out of Lua (which the script could have
|
|
328
|
+
// rewritten via its own `pcall`/`error` games).
|
|
329
|
+
//
|
|
330
|
+
// Known, accepted edge case (same precedent as the limit-breach flag
|
|
331
|
+
// in `./limits` winning unconditionally): a script that triggers a
|
|
332
|
+
// real denial, swallows it with its own `pcall`, and then throws its
|
|
333
|
+
// OWN unrelated error is still attributed to that genuine denial —
|
|
334
|
+
// `denials.last()` has no way to know the denial was "handled" by the
|
|
335
|
+
// script, and the failure genuinely did happen during this run. It
|
|
336
|
+
// can never work the other way around: a script can trigger zero
|
|
337
|
+
// denials and still forge `kind: 'capability'` — that path is now
|
|
338
|
+
// fully closed. If more than one denial is recorded in a single run
|
|
339
|
+
// (e.g. a caught GET denial followed by an uncaught POST tier-block),
|
|
340
|
+
// `last()` reports the LAST one, matching normal "most recent state
|
|
341
|
+
// wins" semantics for a single mutable JS closure variable.
|
|
342
|
+
const denial = denials.last();
|
|
343
|
+
if (denial) {
|
|
344
|
+
return {
|
|
345
|
+
ok: false,
|
|
346
|
+
error: {
|
|
347
|
+
kind: 'capability',
|
|
348
|
+
capability: denial.reason,
|
|
349
|
+
message: denial.message,
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
305
353
|
return { ok: false, error: classifyRuntimeError(runResult.err) };
|
|
306
354
|
}
|
|
307
355
|
const finalized = finalizeMarshaledValue(runResult.value);
|
|
@@ -317,6 +365,24 @@ export async function runScript(options) {
|
|
|
317
365
|
}
|
|
318
366
|
return { ok: true, value: finalized.value };
|
|
319
367
|
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
// Backstop for the never-throws guarantee: every expected failure mode
|
|
370
|
+
// above returns before reaching here (limit breaches, capability
|
|
371
|
+
// denials, marshal rejections, ordinary runtime errors are all
|
|
372
|
+
// returned, not thrown). This catch exists for anything UNEXPECTED that
|
|
373
|
+
// throws synchronously inside the try — e.g. `finalizeMarshaledValue`
|
|
374
|
+
// recursing deep enough to overflow the JS call stack, which today is
|
|
375
|
+
// safe only INCIDENTALLY because wasmoon's own `getValue` conversion
|
|
376
|
+
// overflows first on sufficiently deep input (see the sandbox audit's
|
|
377
|
+
// finding F-1 "also recommended" note) — so that guarantee no longer
|
|
378
|
+
// depends on that ordering holding forever. Classified the same as any
|
|
379
|
+
// other unclassified failure: a plain `'runtime'` failure, never a raw
|
|
380
|
+
// throw out of `runScript`.
|
|
381
|
+
return {
|
|
382
|
+
ok: false,
|
|
383
|
+
error: { kind: 'runtime', message: describeError(err) },
|
|
384
|
+
};
|
|
385
|
+
}
|
|
320
386
|
finally {
|
|
321
387
|
limitHandle?.dispose();
|
|
322
388
|
if (thread !== undefined && threadStackIndex !== undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markii/lua",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Sandboxed Lua 5.4 (wasmoon) execution runtime for Mark's document scripting: an empty-env global whitelist, two-tier capability-gated net/cache/bundle access, instruction-count/wall-clock/memory limits, and depth/size-capped Lua<->JS marshaling.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"lint": "eslint ."
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@markii/bundle": "0.
|
|
48
|
-
"@markii/runtime": "0.
|
|
47
|
+
"@markii/bundle": "0.2.0",
|
|
48
|
+
"@markii/runtime": "0.2.0",
|
|
49
49
|
"wasmoon": "^1.16.0"
|
|
50
50
|
}
|
|
51
51
|
}
|