ReviewOS

zig-utils/zig-js

Security: authenticate structured-clone SharedArrayBuffer references

#38
Closed chris opened this 9 days ago ·
9 days ago

Parent: #1

Problem

The structured-clone wire format currently serializes a retained SharedBufferStorage address as a raw u64. deserialize() and releaseSerialized() convert that field back to a pointer and dereference it without proving that the stream actually minted and owns a retained reference.

The normal Worker path only consumes engine-produced streams, but these helpers are public and malformed or replayed bytes must fail closed. A forged address can become an invalid pointer dereference; a replayed legitimate address can consume a reference more than once. Deserialization also accepts trailing bytes, which can hide unconsumed retained references.

Goal

Replace raw SAB addresses in structured-clone streams with opaque, unguessable, single-use transfer tokens backed by a thread-safe registry. Consuming or releasing a token must atomically remove it before returning the retained storage reference.

Acceptance criteria

  • Serialized SAB payloads contain no process pointer.
  • Arbitrary, unknown, and replayed tokens fail closed without dereferencing attacker-controlled addresses.
  • Successful deserialization consumes exactly one retained reference and transfers it to the wrapper.
  • releaseSerialized() consumes and releases each valid token at most once.
  • Wrapper-construction failure releases a token that was already consumed.
  • Deserialization rejects trailing bytes and does not silently strand valid trailing SAB tokens.
  • Focused tests cover valid transfer, forged token, replay, release, trailing bytes, and concurrent token consumption.
  • The structured-clone and thread security docs describe the opaque-token contract.
9 days ago

Implemented and pushed the #38 runtime slice in 285b534c (security: authenticate cloned SAB references).

What changed:

  • Structured-clone SAB payloads now carry random opaque 128-bit tokens, never process pointers.
  • A process-wide locked registry owns exactly one checked retained SharedBufferStorage reference per token.
  • Deserialization and dropped-message cleanup atomically remove tokens before transfer/release.
  • Unknown and replayed tokens fail closed without pointer conversion or dereference.
  • Cleanup is idempotent and continues past already-consumed tokens so later live references in a partially consumed stream are released.
  • Deserialization structurally preflights the complete root, rejects trailing bytes, and releases valid tokens in the rejected stream.
  • Standalone wrapper-construction failure now releases the transferred reference as its ownership contract requires.
  • Reader bounds checks use subtraction form so length arithmetic cannot wrap around the payload boundary.

Focused validation:

  • five structured-clone token/forgery/replay/trailing tests
  • exact wrapper-allocation-failure ownership test
  • concurrent token consumption under normal and suppression-free TSan
  • existing structuredClone: identity, cycles, types, SAB sharing, transfer
  • existing four-worker round-trip/shared-SAB/termination test
  • docs build: 21 pages
  • git diff --check

Combined CI gate: https://github.com/zig-utils/zig-js/actions/runs/29219914395

I am keeping #38 open until that gate is green.

9 days ago

Accepted and complete in 285b534. Structured-clone SharedArrayBuffer references use opaque random single-use tokens with atomic consumption and failure-safe cleanup; focused coverage includes forgery, replay, concurrent consumption, trailing payload cleanup, wrapper allocation failure, Context round trips, and Worker sharing. The combined production gate is green: 31 jobs succeeded, 1 nightly-only job was skipped, and 0 failed: https://github.com/zig-utils/zig-js/actions/runs/29219914395

Sign in to comment on this issue.