@@ -93,6 +93,7 @@ fn cursor_ptr(value: f64) -> Option<*mut ObjectHeader> {
9393 Some ( obj)
9494}
9595
96+ #[ cfg( test) ]
9697#[ inline( always) ]
9798fn num_field ( obj : * mut ObjectHeader , index : u32 ) -> usize {
9899 let bits = crate :: object:: js_object_get_field ( obj, index) ;
@@ -104,6 +105,49 @@ fn num_field(obj: *mut ObjectHeader, index: u32) -> usize {
104105 }
105106}
106107
108+ /// Direct view of the cursor's fixed inline payload. `cursor_ptr` has already
109+ /// established the class id, and every cursor is allocated with exactly
110+ /// `CURSOR_FIELDS`, so these indexed reads need neither a shape-table lookup
111+ /// nor an overflow check.
112+ #[ derive( Clone , Copy ) ]
113+ struct CursorFields ( * mut JSValue ) ;
114+
115+ impl CursorFields {
116+ #[ inline( always) ]
117+ unsafe fn from_cursor ( cursor : * mut ObjectHeader ) -> Self {
118+ Self ( ( cursor as * mut u8 ) . add ( std:: mem:: size_of :: < ObjectHeader > ( ) ) as * mut JSValue )
119+ }
120+
121+ #[ inline( always) ]
122+ unsafe fn value ( self , index : u32 ) -> JSValue {
123+ * self . 0 . add ( index as usize )
124+ }
125+
126+ #[ inline( always) ]
127+ unsafe fn number ( self , index : u32 ) -> usize {
128+ let n = self . value ( index) . to_number ( ) ;
129+ if n. is_finite ( ) && n >= 0.0 {
130+ n as usize
131+ } else {
132+ 0
133+ }
134+ }
135+
136+ /// Store a value whose representation is provably an IEEE number. Cursor
137+ /// slots 1..=4 are initialized as numbers and no writer stores any other
138+ /// kind, so there is no child edge for the write barrier to remember.
139+ #[ inline( always) ]
140+ unsafe fn set_number ( self , index : u32 , value : usize ) {
141+ debug_assert ! ( ( F_BYTE_START ..=F_UTF16_LEN ) . contains( & index) ) ;
142+ // GC_STORE_AUDIT(NUMBER): `JSValue::number` cannot carry a heap edge;
143+ // `cursor_position_fields_are_never_pointer_typed` pins the invariant
144+ // across every product writer.
145+ self . 0
146+ . add ( index as usize )
147+ . write ( JSValue :: number ( value as f64 ) ) ;
148+ }
149+ }
150+
107151#[ inline( always) ]
108152fn set_num_field ( obj : * mut ObjectHeader , index : u32 , value : usize ) {
109153 crate :: object:: js_object_set_field ( obj, index, JSValue :: number ( value as f64 ) ) ;
@@ -136,8 +180,11 @@ fn set_num_field(obj: *mut ObjectHeader, index: u32, value: usize) {
136180/// A `debug_assert` re-checks it in debug builds, which is where a future
137181/// fourth writer to the slot would be caught.
138182#[ inline]
139- fn with_input < R > ( cursor : * mut ObjectHeader , f : impl FnOnce ( & str ) -> R ) -> Option < R > {
140- let value = crate :: object:: js_object_get_field ( cursor, F_INPUT ) ;
183+ fn with_input < R > ( fields : CursorFields , f : impl FnOnce ( & str ) -> R ) -> Option < R > {
184+ // SAFETY: `fields` was derived at entry from a branded cursor, and slot 0
185+ // is the traced input value. Reading it here is the §9a re-derivation; no
186+ // address derived from it is retained beyond this call.
187+ let value = unsafe { fields. value ( F_INPUT ) } ;
141188 let mut sso = [ 0u8 ; crate :: value:: SHORT_STRING_MAX_LEN ] ;
142189 let bytes =
143190 unsafe { crate :: string:: js_string_key_bytes ( JSValue :: from_bits ( value. bits ( ) ) , & mut sso) } ?;
@@ -249,27 +296,31 @@ pub extern "C" fn js_segments_view_open(segmenter: f64, input: f64) -> f64 {
249296}
250297
251298/// Advance to the next grapheme boundary. `1.0` if a segment is now current,
252- /// `0.0` at the end. **Allocation-free by contract**: three integer field
299+ /// `0.0` at the end. **Allocation-free by contract**: four integer field
253300/// writes and a UAX #29 boundary scan, no arena allocation, no owned `String`,
254301/// no descriptor insert — which is also why it cannot collect.
255302#[ no_mangle]
256303pub extern "C" fn js_segments_view_next ( cursor : f64 ) -> f64 {
257304 let Some ( c) = cursor_ptr ( cursor) else {
258305 return 0.0 ;
259306 } ;
260- let from = num_field ( c, F_BYTE_END ) ;
261- let utf16_start = num_field ( c, F_UTF16_START ) + num_field ( c, F_UTF16_LEN ) ;
262- let step = with_input ( c, |text| {
307+ // SAFETY: `cursor_ptr` proved the fixed cursor layout.
308+ let fields = unsafe { CursorFields :: from_cursor ( c) } ;
309+ let from = unsafe { fields. number ( F_BYTE_END ) } ;
310+ let utf16_start = unsafe { fields. number ( F_UTF16_START ) + fields. number ( F_UTF16_LEN ) } ;
311+ let step = with_input ( fields, |text| {
263312 next_boundary ( text, from) . map ( |next| ( next, super :: segmenter:: utf16_len ( & text[ from..next] ) ) )
264313 } )
265314 . flatten ( ) ;
266315 let Some ( ( next, seg_u16) ) = step else {
267316 return 0.0 ;
268317 } ;
269- set_num_field ( c, F_BYTE_START , from) ;
270- set_num_field ( c, F_UTF16_START , utf16_start) ;
271- set_num_field ( c, F_BYTE_END , next) ;
272- set_num_field ( c, F_UTF16_LEN , seg_u16 as usize ) ;
318+ unsafe {
319+ fields. set_number ( F_BYTE_START , from) ;
320+ fields. set_number ( F_UTF16_START , utf16_start) ;
321+ fields. set_number ( F_BYTE_END , next) ;
322+ fields. set_number ( F_UTF16_LEN , seg_u16 as usize ) ;
323+ }
273324 bump ( & NEXTS ) ;
274325 1.0
275326}
@@ -291,14 +342,16 @@ pub extern "C" fn js_segments_view_code_point_at(cursor: f64, k: f64) -> f64 {
291342 if !k. is_finite ( ) || k < 0.0 || k. fract ( ) != 0.0 {
292343 return undef;
293344 }
345+ // SAFETY: `cursor_ptr` proved the fixed cursor layout.
346+ let fields = unsafe { CursorFields :: from_cursor ( c) } ;
294347 let k = k as usize ;
295- if k >= num_field ( c , F_UTF16_LEN ) {
348+ if k >= unsafe { fields . number ( F_UTF16_LEN ) } {
296349 return undef;
297350 }
298- let start = num_field ( c , F_BYTE_START ) ;
299- let end = num_field ( c , F_BYTE_END ) ;
351+ let start = unsafe { fields . number ( F_BYTE_START ) } ;
352+ let end = unsafe { fields . number ( F_BYTE_END ) } ;
300353 bump ( & CODE_POINT_ATS ) ;
301- with_input ( c , |text| {
354+ with_input ( fields , |text| {
302355 let seg = & text[ start..end] ;
303356 let mut utf16_pos = 0usize ;
304357 for ch in seg. chars ( ) {
@@ -329,14 +382,16 @@ pub extern "C" fn js_segments_view_segment(cursor: f64) -> f64 {
329382 let Some ( c) = cursor_ptr ( cursor) else {
330383 return undef;
331384 } ;
332- let start = num_field ( c, F_BYTE_START ) ;
333- let end = num_field ( c, F_BYTE_END ) ;
385+ // SAFETY: `cursor_ptr` proved the fixed cursor layout.
386+ let fields = unsafe { CursorFields :: from_cursor ( c) } ;
387+ let start = unsafe { fields. number ( F_BYTE_START ) } ;
388+ let end = unsafe { fields. number ( F_BYTE_END ) } ;
334389 bump ( & MATERIALISE_SEGMENT ) ;
335390 // The allocation happens INSIDE the borrow, so the borrow must not outlive
336391 // it: take the bytes out first, then allocate from a copy on the stack path
337392 // `js_string_from_bytes` performs. Nothing derived from the input survives
338393 // this call.
339- let made = with_input ( c , |text| {
394+ let made = with_input ( fields , |text| {
340395 let seg = & text[ start..end] ;
341396 crate :: string:: js_string_from_bytes ( seg. as_ptr ( ) , seg. len ( ) as u32 )
342397 } ) ;
@@ -398,9 +453,11 @@ pub extern "C" fn js_segments_view_regexp_test(cursor: f64, regex: f64) -> f64 {
398453 bump ( & REGEXP_TEST_DECLINED ) ;
399454 return undef;
400455 }
401- let start = num_field ( c, F_BYTE_START ) ;
402- let end = num_field ( c, F_BYTE_END ) ;
403- let verdict = with_input ( c, |text| {
456+ // SAFETY: `cursor_ptr` proved the fixed cursor layout.
457+ let fields = unsafe { CursorFields :: from_cursor ( c) } ;
458+ let start = unsafe { fields. number ( F_BYTE_START ) } ;
459+ let end = unsafe { fields. number ( F_BYTE_END ) } ;
460+ let verdict = with_input ( fields, |text| {
404461 crate :: regex:: regexp_test_str_bounded ( re, & text[ start..end] )
405462 } )
406463 . flatten ( ) ;
@@ -618,6 +675,40 @@ mod view_mode_tests {
618675 ) ;
619676 }
620677
678+ /// SABOTAGE-SHAPED: slots 1..=4 are the proof that `_next` may bypass the
679+ /// generic JSValue store barrier. A future writer that puts a pointer in
680+ /// any position slot makes this fail at the exact step where the invariant
681+ /// is broken; slot 0 is intentionally excluded because it is the traced
682+ /// input string.
683+ #[ test]
684+ fn cursor_position_fields_are_never_pointer_typed ( ) {
685+ let cursor = js_segments_view_open (
686+ grapheme_segmenter ( ) ,
687+ js_string ( "a\u{301} b\u{1f469} \u{200d} \u{1f4bb} cd" ) ,
688+ ) ;
689+ assert ! ( cursor != 0.0 ) ;
690+ let c = cursor_ptr ( cursor) . expect ( "branded cursor" ) ;
691+ let fields = unsafe { CursorFields :: from_cursor ( c) } ;
692+ let mut steps = 0usize ;
693+ loop {
694+ for index in F_BYTE_START ..=F_UTF16_LEN {
695+ let value = unsafe { fields. value ( index) } ;
696+ assert ! (
697+ value. is_number( ) && !value. is_pointer( ) ,
698+ "cursor position slot {index} must be number-only before step {steps}"
699+ ) ;
700+ }
701+ if js_segments_view_next ( cursor) != 1.0 {
702+ break ;
703+ }
704+ steps += 1 ;
705+ }
706+ assert ! (
707+ steps >= 4 ,
708+ "the invariant must be checked across real steps"
709+ ) ;
710+ }
711+
621712 /// The rooting obligation of §9e, exercised rather than asserted: `open`
622713 /// allocates the cursor while holding the input, so a collection landing in
623714 /// that window must not leave a dead value in the traced slot. Force a
@@ -850,10 +941,12 @@ mod view_mode_tests {
850941 ( 0 ..count)
851942 . find( |& i| {
852943 let key = crate :: array:: js_array_get_f64( keys, i) ;
853- crate :: string:: js_string_key_matches_bytes(
854- JSValue :: from_bits( key. to_bits( ) ) ,
855- b"test" ,
856- )
944+ unsafe {
945+ crate :: string:: js_string_key_matches_bytes(
946+ JSValue :: from_bits( key. to_bits( ) ) ,
947+ b"test" ,
948+ )
949+ }
857950 } )
858951 . expect( "test key" ) as u32
859952 } )
0 commit comments