You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[P0] Hook sandbox crashes on nested cross-object writes — forces denormalized workarounds across templates #1867
Hook handlers run in a single-suspended-async QuickJS (asyncify) sandbox. A hook that issues a nested engine write (e.g. ctx.api.object('parent').update(...) from a child's hook) re-enters the sandbox while the first call is suspended and crashes the process with memory access out of bounds.
Impact (platform-wide)
This single limitation forces a reaction-free, denormalized workaround across five shipped templates. Each CHARTER explicitly documents it as the reason header/rollup fields are hand-maintained:
Authors cannot write the most natural automation ("when a child changes, update the parent").
Repro
Add an afterInsert/afterUpdate hook on a child object that calls ctx.api.object(parent).update(...); trigger it → process crash (memory access out of bounds). ctx.services.data is also undefined inside the sandbox (silent no-op), so the "obvious" alternative also fails.
Expected
Nested engine writes from a hook should be safe — e.g. queued and flushed after the current op commits, or hooks executed in a model that allows more than one suspended async call.
Affected
@objectstack/runtime (hook execution sandbox).
Found while runtime-testing objectstack-ai/templates (all 9 templates via objectstack dev all) on @objectstack 9.5.1. Build + typecheck pass; these only surface at runtime.
Closing as completed — fixed via #3232 (merged 2026-07-18).
Summary of the resolution:
The crash itself no longer reproduces on main. The sandbox was reworked to a deferred-promise + pump host-call model (each host call runs in its own newAsyncContext() VM), so asyncify's single-suspension limitation no longer applies. The memory access out of bounds crash was specific to the pre-rework sandbox in 9.5.1.
fix(runtime): honor hook body timeoutMs so nested cross-object writes aren't clamped to 250ms (#1867) #3232 fixed the remaining reliability gap: timeout clamping.resolveTimeout previously fed the engine default (250ms for hooks) into Math.min(...), silently clamping any author-declared ScriptBody.timeoutMs back to 250ms and killing nested rollup writes mid-flight. Explicit timeouts are now honored, with the engine default acting only as a fallback.
Coverage:nested-write.integration.test.ts, nested-write-real-sqlite.integration.test.ts, and nested-write.canary.test.ts exercise the exact repro from this issue — a child object's afterInsert/afterUpdate hook rolling up totals to a parent that itself has hooks (true nested VM re-entry) — with no crash and correct results (a 4-level nested write chain completes in ~65ms).
ctx.services.data remains intentionally unavailable in the sandbox: the supported contract for data access from hooks is ctx.api (capability-gated). Accessing ctx.services inside the sandbox now throws explicitly rather than silently no-oping. With nested writes via ctx.api working, that alternative is no longer needed.
The denormalized rollup workarounds in the templates (expense_report.total_amount, procurement_order.received_amount, pm_project.progress_percent/actual_cost, content_publication.total_*, compliance rollups) can now be replaced with natural child→parent hooks; that follow-up belongs in objectstack-ai/templates.
Activity
docs(automation): document nested cross-object writes from hook bodies (
test(runtime): real-SQLite regression for nested cross-object write r…
fix(runtime): honor hook body timeoutMs so nested cross-object writes…
perf(runtime): drop asyncify — sandbox on the sync QuickJS variant (A…
os-zhuang commentedon Jul 22, 2026
Closing as completed — fixed via #3232 (merged 2026-07-18).
Summary of the resolution:
main. The sandbox was reworked to a deferred-promise + pump host-call model (each host call runs in its ownnewAsyncContext()VM), so asyncify's single-suspension limitation no longer applies. Thememory access out of boundscrash was specific to the pre-rework sandbox in 9.5.1.resolveTimeoutpreviously fed the engine default (250ms for hooks) intoMath.min(...), silently clamping any author-declaredScriptBody.timeoutMsback to 250ms and killing nested rollup writes mid-flight. Explicit timeouts are now honored, with the engine default acting only as a fallback.nested-write.integration.test.ts,nested-write-real-sqlite.integration.test.ts, andnested-write.canary.test.tsexercise the exact repro from this issue — a child object'safterInsert/afterUpdatehook rolling up totals to a parent that itself has hooks (true nested VM re-entry) — with no crash and correct results (a 4-level nested write chain completes in ~65ms).ctx.services.dataremains intentionally unavailable in the sandbox: the supported contract for data access from hooks isctx.api(capability-gated). Accessingctx.servicesinside the sandbox now throws explicitly rather than silently no-oping. With nested writes viactx.apiworking, that alternative is no longer needed.The denormalized rollup workarounds in the templates (
expense_report.total_amount,procurement_order.received_amount,pm_project.progress_percent/actual_cost,content_publication.total_*, compliance rollups) can now be replaced with natural child→parent hooks; that follow-up belongs inobjectstack-ai/templates.Generated by Claude Code