@markii/lua 0.3.1 → 0.4.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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # @markii/lua
2
2
 
3
- A sandboxed Lua 5.4 (via `wasmoon`) execution runtime for [Mark](https://github.com/sadigaxund/markii)'s document scripting: an empty-environment global whitelist, capability-gated net/cache/bundle access, instruction-count and wall-clock/memory limits, and depth/size-capped Lua↔JS value marshaling. No React, no markdown parsing.
3
+ A sandboxed Lua 5.4 (via `wasmoon`) execution runtime for [Markii](https://github.com/sadigaxund/markii)'s document scripting: an empty-environment global whitelist, capability-gated net/cache/bundle access, instruction-count and wall-clock/memory limits, and depth/size-capped Lua↔JS value marshaling. No React, no markdown parsing.
4
4
 
5
5
  See the [repository](https://github.com/sadigaxund/markii) for the format spec and the reference library as a whole.
@@ -390,28 +390,58 @@ net.patch = function(url, body) return __smd_net_patch_blocked(url, body):await(
390
390
  // below — so without this check, a 300k-element cached array reached
391
391
  // the script completely uncapped even though the FETCH path was
392
392
  // already capped.
393
- const budgetCheck = checkJsonWithinLimits(entry.value, marshalLimits);
394
- if (!budgetCheck.ok) {
395
- const message = `cached value for "${key}" ${budgetCheck.message}`;
396
- recordDenial('denied', message);
397
- throw capabilityError(message);
393
+ //
394
+ // An entry that fails the budget check, isn't a plain object at all,
395
+ // or carries a missing/non-finite `storedAtMs` (adversarial finding:
396
+ // a poisoned host snapshot like `{value: 'x'}` with no `storedAtMs`,
397
+ // or `storedAtMs: 'abc'`, would otherwise reach the Lua-side
398
+ // `now - storedAtMs` arithmetic in `cache.get` below and throw a
399
+ // script-error, wedging the note permanently instead of self-healing)
400
+ // — or that cannot be `JSON.stringify`'d at all (cyclic, or
401
+ // BigInt-bearing — something other than this sandbox's own WRITE
402
+ // side must have written it, since that side already rejects both) —
403
+ // is SELF-HEALED rather than denied (orchestrator decision, #6
404
+ // verification notes): it is treated as a cache MISS, exactly as if
405
+ // `key` had never been stored. `cache.get`'s Lua body (below) then
406
+ // calls `fn()` and writes the fresh, already-capped result back
407
+ // through `__smd_cache_set_raw`, which quietly repairs the stored
408
+ // entry for next time. No denial is recorded for this path — a
409
+ // capability denial is reserved for the fresh recompute itself
410
+ // failing the WRITE side's own caps, which is unchanged.
411
+ //
412
+ // Finiteness alone isn't enough (pentest finding N-1): a poisoned
413
+ // snapshot with a huge FUTURE `storedAtMs` (e.g.
414
+ // `Number.MAX_SAFE_INTEGER * 1000`) is finite, so it used to pass
415
+ // straight through to the Lua-side `(now - storedAtMs) < ttl*1000`
416
+ // freshness check below, which then went hugely negative and
417
+ // reported the poisoned value as fresh forever — served without
418
+ // `fn()` ever running. `storedAtMs` must additionally be an integer
419
+ // and plausible: not before the epoch, and not after "now" as this
420
+ // same call sees it. `nowMs` is read once, right before the check,
421
+ // and reused for the equality bound so a genuine entry written and
422
+ // read in the same millisecond (`storedAtMs === nowMs`) is still a
423
+ // HIT, not a false self-heal.
424
+ const nowMs = Date.now();
425
+ const storedAtMs = entry.storedAtMs;
426
+ if (typeof entry !== 'object' ||
427
+ entry === null ||
428
+ Array.isArray(entry) ||
429
+ typeof storedAtMs !== 'number' ||
430
+ !Number.isInteger(storedAtMs) ||
431
+ storedAtMs < 0 ||
432
+ storedAtMs > nowMs) {
433
+ return undefined;
398
434
  }
435
+ const budgetCheck = checkJsonWithinLimits(entry.value, marshalLimits);
436
+ if (!budgetCheck.ok)
437
+ return undefined;
399
438
  let text;
400
439
  try {
401
- // A cyclic or BigInt-bearing stored value throws here. This
402
- // module's own WRITE side (`__smd_cache_set_raw` below) already
403
- // rejects both before ever storing anything, but a `CacheProvider`
404
- // is host-controlled storage that something other than this
405
- // sandbox could have written — turn that into the same clean,
406
- // catchable denial instead of an unclassified throw out of the
407
- // capability layer.
408
440
  const encoded = JSON.stringify(entry.value);
409
441
  text = encoded === undefined ? 'null' : encoded;
410
442
  }
411
- catch (err) {
412
- const message = `cached value for "${key}" could not be encoded: ${describeThrown(err)}`;
413
- recordDenial('denied', message);
414
- throw capabilityError(message);
443
+ catch {
444
+ return undefined;
415
445
  }
416
446
  return LuaMultiReturn.of(text, entry.storedAtMs);
417
447
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markii/lua",
3
- "version": "0.3.1",
3
+ "version": "0.4.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.3.1",
48
- "@markii/runtime": "0.3.1",
47
+ "@markii/bundle": "0.4.0",
48
+ "@markii/runtime": "0.4.0",
49
49
  "wasmoon": "^1.16.0"
50
50
  }
51
51
  }