Skip to content

[P0] Hook sandbox crashes on nested cross-object writes — forces denormalized workarounds across templates #1867

Closed

Description

@xuyushun441-sys

Problem

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:

  • expense_report.total_amount, procurement_order.received_amount, pm_project.progress_percent/actual_cost, content_publication.total_*, compliance rollups.

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.

Activity

  1. self-assigned this
    on Jul 18, 2026
  2. os-zhuang commented on Jul 22, 2026

    @os-zhuang
    Contributor

    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.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions