Skip to content

Object.freeze/seal/preventExtensions write flag bits into (value-8) for values with no GcHeader: segfault on the namespace stub, silent wild write on registered symbols #10933

Description

@proggeramlug

Summary

Object.freeze / Object.seal / Object.preventExtensions write flag bits into (value - 8) + 2 — where a real object's GcHeader._reserved sits — for any pointer-tagged value above the handle band, with no check that the value actually has a header. Several values perry hands to JS have no GcHeader, so the write lands in memory that belongs to something else. On one of them it writes into .rodata and segfaults.

Every earlier finding in this class (#10917, #10925, #10926) was a wild read. This is the write side of the same hole.

Reproduce (v0.5.1633, Linux x86_64)

Segfault, 3/3 deterministic — the unresolved-namespace stub is a .rodata static, so the write faults:

import * as crypto from "node:crypto";
const stub: any = crypto.createHash("sha256").constructor;
Object.freeze(stub);        // node: returns the object   perry: SIGSEGV

Silent wild write — a registered symbol is a Box::into_raw'd SymbolHeader with no header. Measured directly in a runtime probe: read the 8 bytes at sym - 8 before and after, over 32 registered symbols.

PROBE pre[0] 0x8000000000000000 -> 0x8000000000070000
PROBE pre[1] 0x0000000000000000 -> 0x0000000000070000
PROBE pre[2] 0x0000000000000004 -> 0x0000000000070004
PROBE pre_header_words_changed=30 of 32

0x7 is OBJ_FLAG_FROZEN | OBJ_FLAG_SEALED | OBJ_FLAG_NO_EXTEND landing in _reserved (offset 2), i.e. six bytes in front of each symbol. Reached from plain JS as Object.freeze(Symbol.for("x")).

The write sites

crates/perry-runtime/src/object/object_ops_frozen.rs, all reached through extract_obj_ptr, which admits any POINTER_TAG value above the handle band (object_ops.rs:57):

op line guard before the write
Object.freeze 146 is_above_handle_band(obj)
Object.seal 256, 265 (obj as usize) > 0x10000 — a bare magnitude floor
Object.preventExtensions 368 (obj as usize) > 0x10000

None of them establishes that the value has a GcHeader. The band check keeps small registry ids out; it says nothing about whether value - 8 is a header.

Affected populations (values with no GcHeader)

I did not find a user-visible symptom for the symbol and async cases: in the probe run the affected blocks were not adjacent to other symbols, so nothing symbol-shaped was hit, and where the bits do land on a symbol's id the OR preserves distinctness for small ids. That is luck about the allocator's layout, not a property of the code — the write goes into memory the runtime did not intend to modify, and a run where a live object sits there is silent corruption.

Fix direction

Two independent parts:

  1. Gate the write on the value actually having a header. The band check is the wrong predicate; the question is ownership. try_read_tracked_gc_header answers it. A value that has no tracked header should take the no-op path that Object.freeze(handle) already takes (test_gap_handle_band_object_ops covers that shape for band ids).
  2. Remove the header-less populations, which is the honest-tag work: fix(runtime): the unresolved-namespace stub is an ordinary object, not a header-less static (#10821 row 4, fixes #10917) #10924 (stub), fix(runtime): a SharedArrayBuffer carries a real GcHeader — user bytes decided another SAB's type, and a brand check segfaulted (fixes #10925) #10932 (SAB), AsyncHook / AsyncResource handles have no GcHeader: JSON.stringify gives []/"" instead of {}, String() is build-dependent #10926/row 13 (async), and symbols (which need a header-bearing representation while keeping typeof "symbol").

Either alone narrows it; both together close it. (1) is small and does not wait on the migration.

Activity

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

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