Repository navigation
Conversation
Quest-Claim: bac16fc78697e71a
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pointee::Bytes is now Pointee::Array, which covers every array as an rs.Slice, so the address of a pointer to an array is refused as a view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An id is the address of the rs.Arc allocation whose value a pointer points at, freed for reuse when its strong and weak counts reach 0, as Rust frees it, so churning addressed allocations never exhausts wasm32's 4 GiB. Reading the address of a type the program keeps anywhere else (a local it borrows, drops, or gives std, a field, a Box, a collection, a static, or what JS is given whole), or writes whole in place, is refused, as is moving one out of its Arc by into_inner or make_mut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Design change: addresses only of an
Review rounds restart with (Written by Claude Opus 5.5) |
A pointer read only after its allocation was freed took a block then, so two freed together could share an address, and frees before the first read reserved blocks for good. Now each Arc or Rc of a type whose address the program reads gets its address as it is made, by rs.arcAt, which a run settles like the comparisons it makes copies for, and keeps it once freed, as Rust's pointer does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed iterate session Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Design change: closing this PR. Decided with the maintainer on 2026-11-07: pointer addresses stay refused.
The quest's deletion and these decisions are in a separate PR, which records them in the model epic's Plan. (Written by Claude Opus 5.5 with high effort) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
rust2-ts refused every read of a pointer's address (the 2026-10-28 decision against pointer-to-integer casts), so a crate keying a map by
Arc::as_ptr(a) as usizecould not translate. moq-net's model does this six times at the pin:Arc::as_ptr/Weak::as_ptrcasts inmodel/cache.rsandmodel/frame.rs, andptr::from_ref(self) as usize/self as *const Self as usizein theDropofTrackandFrameContent, which moq builds only asArc::new(Self { .. }).Decisions
cfg(rust2_ts)twin in moq).Arc's orRc's value (rejected: free at each type's Rust drop; keep freeing ids at JS GC). Ids belong to thers.Arcallocation and are freed when its strong and weak counts reach 0, exactly as Rust deallocates. Reading the address of a type the program keeps anywhere else, and moving one out of itsArc, is refused. Freeing at GC was rejected because GC never runs within one synchronous job, so churning allocations could exhaust wasm32's 4 GiB where Rust reuses freed memory, a runtime failure Rust doesn't have. A counter that never reuses was rejected becauseusizeis 32 bits on wasm32, so a long-running program would exhaust the address space where Rust reuses freed blocks.Both are recorded in the ts-model epic's Plan, since this PR deletes the quest. The design change is recorded in this comment under
Design change:.Approach
rs.arcAtnumbers a newArc's orRc's value as it is made, for each type whose address the program reads, as an allocator places the allocation: after its two counts, at a multiple of the value's alignment, in a block no other live allocation overlaps.rs.unaddressfrees the block when the strong and weak counts reach 0. Numbering at creation keeps allocations whose lives overlap apart even when a pointer is read only after its allocation is freed.rs.addrreads it: null is 0, a dangling pointer is at its type's alignment,Weak::new's is atusize::MAX. An exposed address casts to its destination integer type (as isize,as u8).p as usize,addr,expose_provenance, a raw pointer'sHashandOrd(rs.hashAddr,rs.cmpAddr), and==,ptr::eq,PartialEqand hash-key equality (rs.sameAddr), so a pointer to a freed allocation equals one to an allocation that reuses its address, as in Rust.Weak::as_ptrtranslates (rs.weakAsPtr).Arc's orRc's value (a local it borrows, drops, or gives std; a field, aBox, a collection; a static or constant; what JS is given or gives a root whole) or writes whole in place (as throughget_mut);into_innerandmake_mutof such a value (try_unwrapandunwrap_or_clonetranslate for no type);Weak::as_ptrof a valueArc::new_cyclicmakes, as JS has no object until the closure returns; and pointers made of addresses (with_addr,map_addr,without_provenance,usize as *const T).Census of moq-net's model at the pin, as the epic records: 73 → 66 refusals. All six address reads are gone; crossbeam-epoch's casts stay refused, rightly, and it was already refused for good.
Tests:
lang::pointersgainsaddresses, run against native Rust:Arcs keyed by address in a B-tree, a hash map, and sets of raw pointers, removed inDropbyptr::from_ref(self)andself as *const Self, checked for uniqueness and alignment among the live, reuse after free, order agreement between pointers and their addresses,==/ptr::eq/ hash-set lookup /cmpof a freed pointer against a new one agreeing with whether the address was reused, twoArcs made and freed together and read only after, andWeaks keeping their address after the value drops. A mutation makingsameAddrcompare identity failed it with 94 differences.ts/tests/pointers.tschecksrs.arcAtreuse directly.rejectgainsaddresses.rsfor each refusal.Impact
p as usize,addr,expose_provenance, raw-pointerHash/Ord/equality/ordering, andWeak::as_ptrnow translate where exact; anArc::new/Rc::newof an addressed type goes throughrs.arcAt.rs.addr,rs.addressAt(was private),rs.addrNew,rs.unaddress,rs.arcAt,rs.sameAddr,rs.hashAddr,rs.cmpAddr,rs.weakAsPtr.with_addr/map_addr/without_provenancesay "a pointer made of an address, which JS has no memory at"; address refusals say "the address of a pointer toT, ...".Follow-ups
{:p}andDebugof wakers still refuse writing addresses. They could printrs.addr, but the output then differs from native, so they need their own decision: plan it once a crate needs one.Waiting
questbuild that stored finding counts, not lists, so the current build could not resume it) reported 4 findings, listed under Review. Three are one class: the whole-program proof that an addressed type lives only as anArc's value enumerates every place a value can be stored, and keeps missing some: a generic instance reached only through an address read (fn local<T>(v: T) -> usize), a&Fooa root takes from JS, and a temporary an iterator callback makes andfilterborrows. Each lets a non-Arcvalue take an id nothing frees, so churn aborts at 4 GiB. The fourth is separate: a*const ()cast of an addressed pointer still compares by JS identity, so it disagrees with==on the typed pointers after an address is reused. Options:Tmay be kept, prove everyTis born in anArc: each aggregate ofTin a reached body must move straight intoArc::new/Rc::new/new_cyclic's closure result, andTmust not beCopy, appear in a root's signature, or be made byptr::read,transmute,zeroed, orassume_init. Construction sites are a finite set the MIR shows, so the three findings fall out (each makes aToutside anArc), and theKeptwalk and storage tables incopy.rsgo away. The existing move-out refusals stay. moq passes:TrackandFrameContentare built asArc::new(Self { .. }). It refuses aTbuilt in a local and passed toArc::newthrough a generic helper. Fix the erasure finding alongside by refusing a cast of an addressed type's pointer to another pointee.Arc(Arc::as_ptr,Weak::as_ptr,into_raw), refusingselfinDrop. Simplest and local, but drops two of moq's six reads (cache.rs:1026,frame.rs:510), which then need a moq-side change.cfg(rust2_ts)twin: contradicts the 2026-11-04 decision.Models
Review
Rewritten by
quest reviewafter each round.01a1b3a1-e7bc-7661-b313-72bd16991d4f, round 1Three non-Arc storage paths bypass validation, and erased-pointer equality disagrees with numeric addresses after allocation reuse.ts/src/lower/copy.rs:609:[P2] Record generic instances required by storage validationts/src/lower/address.rs:289:[P2] Preserve external-storage provenance through referencests/src/lower/pointer.rs:142:[P2] Preserve numeric pointer equality through erasurets/src/lower/address.rs:295:[P2] Validate temporary values produced by iterator callbacks(Written by Claude Opus 5.5 with high)
🤖 Generated with Claude Code