From f9d86c19c053fb2aceadddff3edd932716825393 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Mon, 12 Oct 2026 05:07:28 +0000 Subject: [PATCH 1/5] fix: ignore encoding for ArrayBuffer toString --- .../src/object/buffer_dispatch.rs | 8 ++++ .../src/value/to_string_buffer_tests.rs | 46 +++++++++++++++++++ ...19_arraybuffer_to_string_short_encoding.ts | 25 ++++++++++ 3 files changed, 79 insertions(+) create mode 100644 test-files/test_gap_11519_arraybuffer_to_string_short_encoding.ts diff --git a/crates/perry-runtime/src/object/buffer_dispatch.rs b/crates/perry-runtime/src/object/buffer_dispatch.rs index 49878fc91a..03e4fa30f2 100644 --- a/crates/perry-runtime/src/object/buffer_dispatch.rs +++ b/crates/perry-runtime/src/object/buffer_dispatch.rs @@ -575,6 +575,14 @@ pub unsafe fn dispatch_buffer_method( let s = crate::string::js_string_from_bytes(b"[object KeyObject]".as_ptr(), 18); f64::from_bits(JSValue::string_ptr(s).bits()) } + // ArrayBuffer, SharedArrayBuffer, and DataView inherit + // Object.prototype.toString. They share BufferHeader storage and + // reach this dispatcher, but their `toString` ignores every argument; + // interpreting the first argument as a Buffer encoding leaks their + // backing bytes instead of returning the object brand. + "toString" if crate::buffer::is_non_indexed_buffer_view(addr) => { + crate::object::js_object_to_string(buf_f64) + } "toString" => { let enc = if !args.is_empty() { crate::buffer::js_encoding_tag_from_value(args[0]) diff --git a/crates/perry-runtime/src/value/to_string_buffer_tests.rs b/crates/perry-runtime/src/value/to_string_buffer_tests.rs index 91ed12b6c4..4c77b8e550 100644 --- a/crates/perry-runtime/src/value/to_string_buffer_tests.rs +++ b/crates/perry-runtime/src/value/to_string_buffer_tests.rs @@ -53,3 +53,49 @@ fn array_buffer_backed_values_use_their_object_tags() { "6869" ); } + +#[test] +fn buffer_dispatch_to_string_uses_object_tags_for_non_indexed_views() { + use crate::buffer::BufferHeader; + + let ab = with_printable_bytes(crate::buffer::js_array_buffer_new(2)); + let sab = with_printable_bytes(crate::buffer::js_shared_array_buffer_new(2)); + let value = crate::buffer::bytes::from_slice(crate::buffer::bytes::Brand::DataView, b"hi"); + let dv = JSValue::from_bits(value.to_bits()) + .as_pointer::() + .cast_mut(); + let dv = with_printable_bytes(dv); + let short_hex = f64::from_bits(JSValue::short_string_unchecked(b"hex").bits()); + + // The short encoding and the literal both must be ignored for the three + // non-indexed byte-family receivers. + for (value, expected) in [ + (ab, "[object ArrayBuffer]"), + (sab, "[object SharedArrayBuffer]"), + (dv, "[object DataView]"), + ] { + let addr = JSValue::from_bits(value.to_bits()).as_pointer::() as usize; + for encoding in [ + short_hex, + f64::from_bits( + JSValue::string_ptr(crate::string::js_string_from_bytes(b"hex".as_ptr(), 3)).bits(), + ), + ] { + let result = + unsafe { crate::object::dispatch_buffer_method(addr, "toString", &encoding, 1) }; + assert_eq!( + text(crate::value::js_get_string_pointer_unified(result)), + expected + ); + } + } + + // Control: Buffer.toString continues to consume the same short encoding. + let buffer = with_printable_bytes(crate::buffer::js_buffer_alloc(2, 0)); + let addr = JSValue::from_bits(buffer.to_bits()).as_pointer::() as usize; + let result = unsafe { crate::object::dispatch_buffer_method(addr, "toString", &short_hex, 1) }; + assert_eq!( + text(crate::value::js_get_string_pointer_unified(result)), + "6869" + ); +} diff --git a/test-files/test_gap_11519_arraybuffer_to_string_short_encoding.ts b/test-files/test_gap_11519_arraybuffer_to_string_short_encoding.ts new file mode 100644 index 0000000000..1cb7ceac33 --- /dev/null +++ b/test-files/test_gap_11519_arraybuffer_to_string_short_encoding.ts @@ -0,0 +1,25 @@ +// ArrayBuffer-family toString ignores its arguments; Buffer.toString uses them. +// S() produces an SSO string at runtime rather than a heap literal. +const S = (s: string): string => s.charAt(0) + s.slice(1); + +const ab = new ArrayBuffer(1); +new Uint8Array(ab)[0] = 65; +const sab = new SharedArrayBuffer(1); +new Uint8Array(sab)[0] = 65; +const dv = new DataView(ab); +const objectWithThrowingToString = { toString() { throw new Error('must-not-coerce'); } }; + +for (const [name, value] of [['ArrayBuffer', ab], ['SharedArrayBuffer', sab], ['DataView', dv]] as const) { + const anyValue = value as any; + console.log(name, 'no-args', JSON.stringify(anyValue.toString())); + console.log(name, 'runtime-short', JSON.stringify(anyValue.toString(S('hex')))); + console.log(name, 'heap-literal', JSON.stringify(anyValue.toString('hex'))); + console.log(name, 'extra-range', JSON.stringify(anyValue.toString(S('hex'), 0, 1))); + try { console.log(name, 'throwing-arg', JSON.stringify(anyValue.toString(objectWithThrowingToString))); } + catch (error) { console.log(name, 'throwing-arg', 'THREW', (error as Error).message); } +} + +const buffer = Buffer.from([65]); +console.log('Buffer runtime-short', JSON.stringify(buffer.toString(S('hex')))); +console.log('Buffer heap-literal', JSON.stringify(buffer.toString('hex'))); +console.log('Buffer extra-range', JSON.stringify(buffer.toString('hex', 0, 1))); From bb154789600c483aaaf305bc6879ba2a62f71fc8 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Mon, 12 Oct 2026 05:08:28 +0000 Subject: [PATCH 2/5] test: root byte view dispatch fixtures --- .../src/value/to_string_buffer_tests.rs | 71 ++++++++++++++----- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/crates/perry-runtime/src/value/to_string_buffer_tests.rs b/crates/perry-runtime/src/value/to_string_buffer_tests.rs index 4c77b8e550..db0d6dcbc0 100644 --- a/crates/perry-runtime/src/value/to_string_buffer_tests.rs +++ b/crates/perry-runtime/src/value/to_string_buffer_tests.rs @@ -57,32 +57,55 @@ fn array_buffer_backed_values_use_their_object_tags() { #[test] fn buffer_dispatch_to_string_uses_object_tags_for_non_indexed_views() { use crate::buffer::BufferHeader; + use crate::gc::RuntimeHandleScope; + + let scope = RuntimeHandleScope::new(); + let ab = scope.root_nanbox_f64(boxed(crate::buffer::js_array_buffer_new(2))); + crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes_mut( + crate::value::js_nanbox_pointer(ab.get_nanbox_f64().to_bits() as i64), + scope, + ) + .unwrap() + .copy_from_slice(b"hi"); + }); + + let sab = scope.root_nanbox_f64(boxed(crate::buffer::js_shared_array_buffer_new(2))); + crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes_mut( + crate::value::js_nanbox_pointer(sab.get_nanbox_f64().to_bits() as i64), + scope, + ) + .unwrap() + .copy_from_slice(b"hi"); + }); - let ab = with_printable_bytes(crate::buffer::js_array_buffer_new(2)); - let sab = with_printable_bytes(crate::buffer::js_shared_array_buffer_new(2)); let value = crate::buffer::bytes::from_slice(crate::buffer::bytes::Brand::DataView, b"hi"); - let dv = JSValue::from_bits(value.to_bits()) + let dv_ptr = JSValue::from_bits(value.to_bits()) .as_pointer::() .cast_mut(); - let dv = with_printable_bytes(dv); - let short_hex = f64::from_bits(JSValue::short_string_unchecked(b"hex").bits()); + let dv = scope.root_nanbox_f64(boxed(dv_ptr)); + + let short_hex = scope.root_nanbox_f64(f64::from_bits( + JSValue::short_string_unchecked(b"hex").bits(), + )); + let heap_hex = scope.root_nanbox_f64(f64::from_bits( + JSValue::string_ptr(crate::string::js_string_from_bytes(b"hex".as_ptr(), 3)).bits(), + )); // The short encoding and the literal both must be ignored for the three // non-indexed byte-family receivers. for (value, expected) in [ - (ab, "[object ArrayBuffer]"), - (sab, "[object SharedArrayBuffer]"), - (dv, "[object DataView]"), + (&ab, "[object ArrayBuffer]"), + (&sab, "[object SharedArrayBuffer]"), + (&dv, "[object DataView]"), ] { - let addr = JSValue::from_bits(value.to_bits()).as_pointer::() as usize; - for encoding in [ - short_hex, - f64::from_bits( - JSValue::string_ptr(crate::string::js_string_from_bytes(b"hex".as_ptr(), 3)).bits(), - ), - ] { + for encoding in [&short_hex, &heap_hex] { + let current = value.get_nanbox_f64(); + let addr = JSValue::from_bits(current.to_bits()).as_pointer::() as usize; + let argument = encoding.get_nanbox_f64(); let result = - unsafe { crate::object::dispatch_buffer_method(addr, "toString", &encoding, 1) }; + unsafe { crate::object::dispatch_buffer_method(addr, "toString", &argument, 1) }; assert_eq!( text(crate::value::js_get_string_pointer_unified(result)), expected @@ -91,9 +114,19 @@ fn buffer_dispatch_to_string_uses_object_tags_for_non_indexed_views() { } // Control: Buffer.toString continues to consume the same short encoding. - let buffer = with_printable_bytes(crate::buffer::js_buffer_alloc(2, 0)); - let addr = JSValue::from_bits(buffer.to_bits()).as_pointer::() as usize; - let result = unsafe { crate::object::dispatch_buffer_method(addr, "toString", &short_hex, 1) }; + let buffer = scope.root_nanbox_f64(boxed(crate::buffer::js_buffer_alloc(2, 0))); + crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes_mut( + crate::value::js_nanbox_pointer(buffer.get_nanbox_f64().to_bits() as i64), + scope, + ) + .unwrap() + .copy_from_slice(b"hi"); + }); + let current = buffer.get_nanbox_f64(); + let addr = JSValue::from_bits(current.to_bits()).as_pointer::() as usize; + let argument = short_hex.get_nanbox_f64(); + let result = unsafe { crate::object::dispatch_buffer_method(addr, "toString", &argument, 1) }; assert_eq!( text(crate::value::js_get_string_pointer_unified(result)), "6869" From 210e8b72caddf77d45de81cc5c731e7b58b7a453 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Mon, 12 Oct 2026 06:55:45 +0000 Subject: [PATCH 3/5] test: fix byte-view string pointer typing --- .../perry-runtime/src/value/to_string_buffer_tests.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/perry-runtime/src/value/to_string_buffer_tests.rs b/crates/perry-runtime/src/value/to_string_buffer_tests.rs index db0d6dcbc0..3d0d68a05e 100644 --- a/crates/perry-runtime/src/value/to_string_buffer_tests.rs +++ b/crates/perry-runtime/src/value/to_string_buffer_tests.rs @@ -2,7 +2,7 @@ use super::*; -fn text(ptr: *mut crate::string::StringHeader) -> String { +fn text(ptr: *const crate::string::StringHeader) -> String { unsafe { crate::object::has_own_helpers::str_from_string_header(ptr) } .expect("string conversion returns UTF-8") .to_string() @@ -106,8 +106,12 @@ fn buffer_dispatch_to_string_uses_object_tags_for_non_indexed_views() { let argument = encoding.get_nanbox_f64(); let result = unsafe { crate::object::dispatch_buffer_method(addr, "toString", &argument, 1) }; + let result = scope.root_nanbox_f64(result); assert_eq!( - text(crate::value::js_get_string_pointer_unified(result)), + text( + crate::value::js_get_string_pointer_unified(result.get_nanbox_f64()) + as *const _ + ), expected ); } @@ -127,8 +131,9 @@ fn buffer_dispatch_to_string_uses_object_tags_for_non_indexed_views() { let addr = JSValue::from_bits(current.to_bits()).as_pointer::() as usize; let argument = short_hex.get_nanbox_f64(); let result = unsafe { crate::object::dispatch_buffer_method(addr, "toString", &argument, 1) }; + let result = scope.root_nanbox_f64(result); assert_eq!( - text(crate::value::js_get_string_pointer_unified(result)), + text(crate::value::js_get_string_pointer_unified(result.get_nanbox_f64()) as *const _), "6869" ); } From 5e41d1d8b19c030754975afeca52a9c277eda739 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Mon, 12 Oct 2026 07:01:48 +0000 Subject: [PATCH 4/5] test: mark byte-view writes unsafe --- .../src/value/to_string_buffer_tests.rs | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/crates/perry-runtime/src/value/to_string_buffer_tests.rs b/crates/perry-runtime/src/value/to_string_buffer_tests.rs index 3d0d68a05e..5dabc2d18a 100644 --- a/crates/perry-runtime/src/value/to_string_buffer_tests.rs +++ b/crates/perry-runtime/src/value/to_string_buffer_tests.rs @@ -62,22 +62,30 @@ fn buffer_dispatch_to_string_uses_object_tags_for_non_indexed_views() { let scope = RuntimeHandleScope::new(); let ab = scope.root_nanbox_f64(boxed(crate::buffer::js_array_buffer_new(2))); crate::buffer::bytes::no_gc(|scope| { - crate::buffer::bytes::bytes_mut( - crate::value::js_nanbox_pointer(ab.get_nanbox_f64().to_bits() as i64), - scope, - ) - .unwrap() - .copy_from_slice(b"hi"); + // SAFETY: this test owns the rooted byte view and no other view can + // access its bytes during this no-GC borrow. + unsafe { + crate::buffer::bytes::bytes_mut( + crate::value::js_nanbox_pointer(ab.get_nanbox_f64().to_bits() as i64), + scope, + ) + .unwrap() + .copy_from_slice(b"hi"); + } }); let sab = scope.root_nanbox_f64(boxed(crate::buffer::js_shared_array_buffer_new(2))); crate::buffer::bytes::no_gc(|scope| { - crate::buffer::bytes::bytes_mut( - crate::value::js_nanbox_pointer(sab.get_nanbox_f64().to_bits() as i64), - scope, - ) - .unwrap() - .copy_from_slice(b"hi"); + // SAFETY: this test owns the rooted shared view and no other thread + // or view accesses its bytes during this no-GC borrow. + unsafe { + crate::buffer::bytes::bytes_mut( + crate::value::js_nanbox_pointer(sab.get_nanbox_f64().to_bits() as i64), + scope, + ) + .unwrap() + .copy_from_slice(b"hi"); + } }); let value = crate::buffer::bytes::from_slice(crate::buffer::bytes::Brand::DataView, b"hi"); @@ -120,12 +128,16 @@ fn buffer_dispatch_to_string_uses_object_tags_for_non_indexed_views() { // Control: Buffer.toString continues to consume the same short encoding. let buffer = scope.root_nanbox_f64(boxed(crate::buffer::js_buffer_alloc(2, 0))); crate::buffer::bytes::no_gc(|scope| { - crate::buffer::bytes::bytes_mut( - crate::value::js_nanbox_pointer(buffer.get_nanbox_f64().to_bits() as i64), - scope, - ) - .unwrap() - .copy_from_slice(b"hi"); + // SAFETY: this test owns the rooted Buffer and no other view can + // access its bytes during this no-GC borrow. + unsafe { + crate::buffer::bytes::bytes_mut( + crate::value::js_nanbox_pointer(buffer.get_nanbox_f64().to_bits() as i64), + scope, + ) + .unwrap() + .copy_from_slice(b"hi"); + } }); let current = buffer.get_nanbox_f64(); let addr = JSValue::from_bits(current.to_bits()).as_pointer::() as usize; From 6f2f622f228f93ce6d56026b5041c7b456ca2195 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Mon, 12 Oct 2026 08:06:01 +0000 Subject: [PATCH 5/5] docs: add changelog fragment for12456 --- changelog.d/12456-byte-view-tostring.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/12456-byte-view-tostring.md diff --git a/changelog.d/12456-byte-view-tostring.md b/changelog.d/12456-byte-view-tostring.md new file mode 100644 index 0000000000..de04e4dd7c --- /dev/null +++ b/changelog.d/12456-byte-view-tostring.md @@ -0,0 +1 @@ +Keep inherited `toString` behavior consistent for `ArrayBuffer`, `SharedArrayBuffer`, and `DataView` when dispatched through Buffer helpers. These views now return their object tags without interpreting or coercing encoding and range arguments, including short strings, while Buffer encoding behavior is preserved.