Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 258 additions & 0 deletions docs/ordinary-property-plan.md

Large diffs are not rendered by default.

90 changes: 90 additions & 0 deletions scripts/benchmark/ordinary_workloads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Generate fixed ordinary-property diagnostics and authenticate outputs with Node.

Timing belongs to fixed.py. These workloads include process startup, compilation
and setup; depth setup-only controls are separate whole-process measurements.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import subprocess


def programs():
for depth in (0, 1, 8, 64, 256):
for operation, count in (("write", 200000), ("setup", 0)):
yield f"own-{operation}-depth-{depth}", depth, (
f"let p=null; for(let i=0;i<{depth};i++) p=Object.create(p);\n"
"let o=Object.create(p);o.x=0;\n"
f"for(let i=0;i<{count};i++) o.x=i;\nconsole.log(o.x);\n"
)

def loop(case, setup, body, result, size=0, count=100000):
return case, size, (
f"{setup}\nfor(let i=0;i<{count};i++){{{body}}}\n"
f"console.log({result});\n"
)

for width in (4, 32, 256, 2048):
for mode in ("shape", "dictionary"):
setup = f'let o={{}}; for(let k=0;k<{width};k++)o["p"+k]=0;'
if mode == "dictionary":
setup += "delete o.p1;"
yield loop(f"write-width-{width}-{mode}", setup, "o.p0=i", "o.p0", width)
values = (
("int", "[1,2]", "1"),
("float", "[1.5,2.5]", "1.5"),
("string", '["alpha","beta"]', '"alpha"'),
("symbol", '[Symbol("a"),Symbol("b")]', 'Symbol("a")'),
("object", "[{},{}]", "{}"),
)
for kind, alternating, _ in values:
yield loop("write-value-" + kind, "let v=" + alternating + ";let o={x:v[0]};",
"o.x=v[i&1]", "o.x===v[1]")
for args in (
("get-data", "let o={x:3};let s=0;", "s+=o.x", "s"),
("get-inherited", "let o=Object.create({x:3});let s=0;", "s+=o.x", "s"),
("get-accessor", "let o={get x(){return 3}};let s=0;", "s+=o.x", "s"),
("set-accessor", "let s=0;let o={set x(v){s=v}};", "o.x=i", "s"),
("set-receiver", "let p={x:0},o={x:0};", 'Reflect.set(p,"x",i,o)', "o.x"),
("define-value", "let o={x:0};let d={value:0};", 'd.value=i;Object.defineProperty(o,"x",d)', "o.x"),
("has-own", "let o={x:1};let s=0;", 'if(Object.hasOwn(o,"x"))s++', "s"),
("enumerable", "let o={x:1};let s=0;", 'if(o.propertyIsEnumerable("x"))s++', "s"),
("proxy-set", "let target={x:0};let p=new Proxy(target,{});", "p.x=i", "target.x", 0, 20000),
("missing-get", "let o=Object.create({});let s=0;", "if(o.x===undefined)s++", "s"),
):
yield loop(*args)
for kind, _, value in values:
yield "write-same-value-" + kind, 100000, (
f"let v={value};let o={{x:v}};for(let i=0;i<100000;i++){{o.x=v}} console.log(o.x===v);\n"
)


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, required=True, help="new workload directory")
parser.add_argument("--oracle", default="node", help="Node executable for output authentication")
args = parser.parse_args()
output = args.output.resolve()
output.mkdir(parents=True, exist_ok=False)
env = {key: value for key, value in os.environ.items() if key != "FORCE_COLOR"}
env["NO_COLOR"] = "1"
rows = []
for case, size, source in programs():
path = output / (case + ".js")
path.write_text(source)
result = subprocess.run([args.oracle, str(path)], env=env, capture_output=True,
check=True, timeout=30)
if result.stderr:
raise RuntimeError(f"unexpected oracle stderr for {case}: {result.stderr!r}")
rows.append(dict(case=case, size=size, path=str(path),
sha256=hashlib.sha256(path.read_bytes()).hexdigest(),
expected=result.stdout.decode()))
manifest = output / "manifest.json"
manifest.write_text(json.dumps({"metadata": {"workloads": {"workloads": rows}}}, indent=2) + "\n")
print(f"{len(rows)} authenticated fixed workloads: {manifest}")


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion scripts/checks/binary_object/evidence/coercion.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
'src/engine/code/binary_object/function_translate/mod.rs': 'ee4d423731977d088c45a0ee2f0fcf642b119f2c2dd4c6f9243d724a761545d7',
'src/engine/code/binary_object/ordinary_leaf.rs': 'b7553332908929126730993a655bbd55acbe34df3ae7acf7415486f0c3fd81a4',
'src/engine/code/binary_object_publish.rs': 'd7dd67291f5a8e7535ff7e7fb6802aa8348f5186c32235a8411ec79d6c5a0ae7',
'src/engine/heap/runtime/tests.rs': '36ce3fafb354a8c6049bc098069437ea653eed726cd3748f6e00cf30ccacad96',
'src/engine/heap/runtime/tests.rs': '3b7f56cc12f5b2a7cb068274abeb02e1543356a45cccef109eb7b7c0de1a9d0d',
'src/engine/code/binary_object/ordinary_leaf/tests.rs': '8a659586b5dfd17d5b3f1863913ddc952fd46fbb080bbf422133939fedb86630',
'src/engine/heap/runtime/tests/accessors.rs': '7e7aadee6f2248192a6c1438029e26e1153f9412e148cf8c672f7e7d97ddc1d4',
'src/engine/heap/runtime/tests/active_frames.rs': '609cbd20f1c0499710c9494e365fabfa94f3e8c19be359d4a721aff39bfde0bd',
Expand Down
42 changes: 42 additions & 0 deletions scripts/checks/binary_object/rules/ordinary_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Property kernel ownership guards; behavioral coverage remains in Rust/JS."""
import re

FILES = (
"src/engine/object/ordinary_storage.rs",
"src/engine/object/ordinary.rs",
"src/engine/object/internal_methods.rs",
"src/engine/heap/runtime/mod.rs",
"src/engine/heap/object_storage.rs",
)


def check(ctx):
if getattr(ctx, "self_test_marker_authorized", False):
# The existing codec isolation fixtures deliberately omit the property
# engine. Dedicated mutation tests exercise these guards on real files.
return
sources = []
for relative in FILES:
path = ctx.root / relative
if path.is_symlink() or not path.is_file():
ctx.fail("ordinary-property-source", f"missing regular source: {relative}")
return
sources.append(ctx.rust_code_only(path.read_text()))
storage, ordinary, dispatch, runtime, heap = sources
compact = lambda text: re.sub(r"\s+", "", text)
requirements = [
(not re.search(r"pub(?:\([^)]*\))?\s+struct\s+OwnSlot", storage), "slot positions must remain private to the storage owner"),
("(ObjectKind::Ordinary,ObjectPayload::Ordinary)" in compact(storage), "ordinary eligibility must include the semantic class"),
(not re.search(r"\.(?:call_internal|internal_set|materialize_auto_init_property)\s*\(", storage), "storage must not execute callbacks or observable internal methods"),
("ordinary_set_fast_path_available" not in ordinary + dispatch, "ordinary Set must not pre-scan the prototype chain"),
("self.validate_object_and_key(object,key)?" in compact(ordinary) and "self.validate_value_domain(&value," in compact(ordinary) and "self.validate_value_domain(&receiver," in compact(ordinary), "Set must validate object, key, value and receiver domains"),
("rejected_object.as_ref().unwrap_or(receiver)" in compact(ordinary), "Proxy forwarding diagnostics must use the rejected target"),
("if!failure.published{self.release_atoms(atoms)?;}" in compact(runtime), "only pre-publication failures may roll back replacement Atoms"),
]
body, _, _ = ctx.unique_braced_item(heap, re.compile(r"fn\s+replace_object_slot_with_status\s*\([^{}]*\)\s*->[^{}]*\{"), "ordinary-property-transaction", "slot replacement")
retained = body.find("retain_edges_transactionally")
published = body.find("replace_retained_object_slot")
requirements.append((0 <= retained < published, "replacement edges must be retained before publication"))
for accepted, message in requirements:
if not accepted:
ctx.fail("ordinary-property-contract", message)
3 changes: 2 additions & 1 deletion scripts/checks/binary_object/scan.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Ordered rule pipeline. Later rules consume earlier authenticated observations."""
from .context import ScanContext
from .rules import source_setup, surface, native_plan, translation, ordinary_leaf, scalar, publication, runtime_protocols, runtime_contracts, wire_evidence, exception_evidence, coercion, coercion_evidence, reference_oracle, receipts, source_ownership, shared_transport
from .rules import source_setup, surface, native_plan, translation, ordinary_leaf, scalar, publication, runtime_protocols, runtime_contracts, wire_evidence, exception_evidence, coercion, coercion_evidence, reference_oracle, receipts, source_ownership, shared_transport, ordinary_properties

RULES = (
("source_setup", source_setup.check),
Expand All @@ -20,6 +20,7 @@
("receipts", receipts.check),
("source_ownership", source_ownership.check),
("shared_transport", shared_transport.check),
("ordinary_properties", ordinary_properties.check),
)


Expand Down
47 changes: 47 additions & 0 deletions scripts/checks/binary_object/tests/test_ordinary_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from pathlib import Path
import tempfile
import unittest

from binary_object.context import ScanContext
from binary_object.rules import ordinary_properties, source_setup

ROOT = Path(__file__).resolve().parents[4]


class OrdinaryPropertyContracts(unittest.TestCase):
def scan(self, edits=()):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for relative in ordinary_properties.FILES:
source = (ROOT / relative).read_text()
for path, before, after in edits:
if path == relative:
self.assertIn(before, source)
source = source.replace(before, after)
target = root / relative
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(source)
context = ScanContext(root)
source_setup.check(context)
ordinary_properties.check(context)
return context.errors

def test_current_contracts(self):
self.assertEqual(self.scan(), [])

def test_bad_boundaries_are_rejected(self):
storage, ordinary, dispatch, runtime, heap = ordinary_properties.FILES
mutations = [
(storage, "struct OwnSlot", "pub(crate) struct OwnSlot"),
# Replace every occurrence to simulate removal of the shared class gate.
(storage, "ObjectKind::Ordinary", "ObjectKind::ModuleNamespace"),
(storage, "fn locate(", "fn bad() { self.call_internal(); } fn locate("),
(dispatch, "impl Runtime {", "fn ordinary_set_fast_path_available() {} impl Runtime {"),
(ordinary, "self.validate_value_domain(&value,", "self.skip_domain(&value,"),
(ordinary, "rejected_object.as_ref().unwrap_or(receiver)", "receiver"),
(runtime, "if !failure.published", "if failure.published"),
(heap, ".retain_edges_transactionally(&new_edges)", ".skip_retain(&new_edges)"),
]
for mutation in mutations:
with self.subTest(mutation=mutation):
self.assertTrue(self.scan([mutation]))
23 changes: 17 additions & 6 deletions src/engine/builtins/array_buffer/typed_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1614,6 +1614,15 @@ impl Runtime {
snapshot: TypedArraySnapshot,
) -> Result<TypedArrayState, RuntimeError> {
let buffer = self.snapshot_buffer_access(snapshot.buffer)?.state;
Ok(Self::typed_array_state_with_buffer(snapshot, buffer))
}

// Pure bounds calculation, shared with element access. A caller holding
// an access token must not perform observable work before consuming it.
fn typed_array_state_with_buffer(
snapshot: TypedArraySnapshot,
buffer: crate::engine::heap::ArrayBufferState,
) -> TypedArrayState {
let width = u32::from(snapshot.element.byte_length());
let byte_length = if buffer.detached || snapshot.byte_offset > buffer.byte_length {
None
Expand All @@ -1637,13 +1646,13 @@ impl Runtime {
// RAB grows to a byte length not divisible by the element width.
let length = byte_length / width;
let byte_length = length * width;
Ok(TypedArrayState {
TypedArrayState {
snapshot,
length,
byte_length,
out_of_bounds,
resizable: buffer.max_byte_length.is_some(),
})
}
}

pub(crate) fn typed_array_current_length(
Expand Down Expand Up @@ -1786,13 +1795,14 @@ impl Runtime {
object: &ObjectRef,
index: u64,
) -> Result<Option<Value>, RuntimeError> {
let state = self.typed_array_state(object)?;
let snapshot = self.typed_array_snapshot(object)?;
let access = self.snapshot_buffer_access(snapshot.buffer)?;
let state = Self::typed_array_state_with_buffer(snapshot, access.state);
if state.out_of_bounds || index >= u64::from(state.length) {
return Ok(None);
}
let absolute = typed_array_absolute_byte_offset(state.snapshot, index)?;
let width = usize::from(state.snapshot.element.byte_length());
let access = self.snapshot_buffer_access(state.snapshot.buffer)?;
let bytes = self.read_buffer_word(&access, absolute, width)?;
Ok(Some(typed_array_decode(state.snapshot.element, bytes)))
}
Expand Down Expand Up @@ -1898,13 +1908,14 @@ impl Runtime {
index: u64,
bytes: &[u8; 8],
) -> Result<bool, RuntimeError> {
let state = self.typed_array_state(object)?;
let snapshot = self.typed_array_snapshot(object)?;
let access = self.snapshot_buffer_access(snapshot.buffer)?;
let state = Self::typed_array_state_with_buffer(snapshot, access.state);
if state.out_of_bounds || index >= u64::from(state.length) {
return Ok(false);
}
let absolute = typed_array_absolute_byte_offset(state.snapshot, index)?;
let width = usize::from(state.snapshot.element.byte_length());
let access = self.snapshot_buffer_access(state.snapshot.buffer)?;
self.write_buffer_word(&access, absolute, &bytes[..width])?;
Ok(true)
}
Expand Down
35 changes: 32 additions & 3 deletions src/engine/heap/object_storage.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
use super::*;

/// The runtime may undo retained Atoms only before slot publication.
pub(crate) struct SlotReplacementError {
pub(crate) error: HeapError,
pub(crate) published: bool,
}

impl Heap {
/// Read one live object record.
pub fn object(&self, id: ObjectId) -> Result<&ObjectData, HeapError> {
Expand Down Expand Up @@ -602,11 +608,34 @@ impl Heap {
slot_index: usize,
replacement: PropertySlot,
) -> Result<HeapCleanup, HeapError> {
self.validate_replacement_slot(id, slot_index, &replacement)?;
let new_edges = property_slot_edges(&replacement);
self.retain_edges_transactionally(&new_edges)?;
self.replace_object_slot_with_status(id, slot_index, replacement)
.map_err(|failure| failure.error)
}

pub(crate) fn replace_object_slot_with_status(
&mut self,
id: ObjectId,
slot_index: usize,
replacement: PropertySlot,
) -> Result<HeapCleanup, SlotReplacementError> {
self.validate_replacement_slot(id, slot_index, &replacement)
.map_err(|error| SlotReplacementError {
error,
published: false,
})?;
let new_edges = property_slot_edges(&replacement);
self.retain_edges_transactionally(&new_edges)
.map_err(|error| SlotReplacementError {
error,
published: false,
})?;
// Validation and commit are adjacent under &mut Heap. There is no
// mutation/callback between them, so the validated slot cannot vanish.
self.replace_retained_object_slot(id, slot_index, replacement)
.map_err(|error| SlotReplacementError {
error,
published: true,
})
}

/// Commit a validated replacement after its edges have been retained.
Expand Down
23 changes: 23 additions & 0 deletions src/engine/heap/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,29 @@ impl RuntimeState {
self.ensure_dictionary_layout(object)
}

/// One slot transaction, including Atom ownership. Once published, a
/// cleanup failure cannot roll back the new slot's Atom references.
pub(crate) fn replace_property_slot(
&mut self,
object: ObjectId,
index: usize,
replacement: PropertySlot,
) -> Result<(), RuntimeError> {
let atoms = self.retain_slot_atoms(std::slice::from_ref(&replacement))?;
match self
.heap
.replace_object_slot_with_status(object, index, replacement)
{
Ok(cleanup) => self.apply_cleanup(cleanup),
Err(failure) => {
if !failure.published {
self.release_atoms(atoms)?;
}
Err(failure.error.into())
}
}
}

pub(crate) fn apply_cleanup(&mut self, cleanup: HeapCleanup) -> Result<(), RuntimeError> {
self.unlink_finalized_shapes(cleanup.finalized_shape_ids);
self.release_atoms(cleanup.atoms)
Expand Down
4 changes: 2 additions & 2 deletions src/engine/heap/runtime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ fn set_property(
) -> Result<bool, RuntimeError> {
match runtime.prepare_set_property(object, key, value)? {
PropertySetAction::Complete => Ok(true),
PropertySetAction::Rejected(_) => Ok(false),
PropertySetAction::Rejected(_) | PropertySetAction::RejectedProxyTrap => Ok(false),
PropertySetAction::Throw(_) => Err(RuntimeError::Invariant(
"context-free property test produced a JavaScript throw",
)),
Expand All @@ -511,7 +511,7 @@ fn set_property_with_receiver(
) -> Result<bool, RuntimeError> {
match runtime.prepare_set_property_with_receiver(object, key, value, receiver)? {
PropertySetAction::Complete => Ok(true),
PropertySetAction::Rejected(_) => Ok(false),
PropertySetAction::Rejected(_) | PropertySetAction::RejectedProxyTrap => Ok(false),
PropertySetAction::Throw(_) => Err(RuntimeError::Invariant(
"context-free property test produced a JavaScript throw",
)),
Expand Down
Loading