Skip to content

[thread] Cross-heap unsoundness in perry/thread: global queue drains, timer queues, module globals — design decision needed #6185

Description

@proggeramlug

Problem

perry/thread's cross-thread machinery has several unsound edges (all code-verified in the 2026-07-09 GC audit, threading dimension):

  1. Await loop drains global queues on whatever thread runs it (perry-codegen/src/expr/fs_await.rs:194-207promise/microtasks.rs:110-122thread.rs:1283-1307): a worker's await can steal another thread's completion, deserialize it into the worker's arena, and resolve a main-heap promise with a foreign pointer; the settle task then runs main-heap closures on the worker. Worker exit unmaps the arena → main thread reads freed memory.
  2. Global timer queues carry raw closure/promise pointers across heaps (timer.rs:25-40, 323, 367-380): any thread's await loop fires foreign-heap timer callbacks (the "only accessed from the pump thread" SAFETY comment is not enforced).
  3. Module-level globals are process-wide but rooted/rewritten only by the thread that ran module init (gc/roots.rs:63,425-434): worker closures calling top-level functions alias main-heap objects with no synchronization and no rewrite when a main-thread moving cycle relocates them.
  4. Serializer silently converts Map/Set/Promise/Error/TypedArray/ArrayBuffer captures to undefined (thread.rs:349-374) — contradicts the documented "views deep-copy" claim; should fail loudly.
  5. Worker death leaks every malloc-tracked object — no Drop for MallocState (gc/malloc.rs:112-128), so per-request spawn workers accrete promises/maps/closures forever.

Proposed fix (decision needed)

Either enforce the single-JS-thread discipline mechanically — owner-ThreadId tags on every queued completion/timer with drains skipping foreign entries, compiler rejection (or serialize-snapshot) of module-global access in thread closures, loud serializer errors, Drop for MallocState — or commit to real multi-heap isolation. The current halfway point is the least safe option.

Quick wins already shipping separately (Wave-1 PR3): ensure_gc_initialized() at worker entry, rooting the rebuilt worker closure, a scanner/pinned-gate for PENDING_THREAD_RESULTS, and the ws.rs deferred-resolution fix.

Context

2026-07-09 GC audit. Items 1-3 are P0-severity UAF classes when perry/thread is combined with await/timers/module state.

Metadata

Metadata

Assignees

No one assigned

    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