diff --git a/Cargo.lock b/Cargo.lock index 6d626a2..77df459 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,12 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + [[package]] name = "autocfg" version = "1.5.0" @@ -38,16 +44,16 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "boa_gc" version = "1.0.0-dev" -source = "git+https://github.com/boa-dev/boa?branch=main#6ec44883afffa6edbc4df7df35925a6ef888543d" +source = "git+https://github.com/boa-dev/boa.git?branch=main#4fc75c6ae9d85f2b8065c6716f88e9b35318438c" dependencies = [ "boa_macros", - "hashbrown", + "hashbrown 0.17.1", ] [[package]] name = "boa_macros" version = "1.0.0-dev" -source = "git+https://github.com/boa-dev/boa?branch=main#6ec44883afffa6edbc4df7df35925a6ef888543d" +source = "git+https://github.com/boa-dev/boa.git?branch=main#4fc75c6ae9d85f2b8065c6716f88e9b35318438c" dependencies = [ "cfg-if", "cow-utils", @@ -57,6 +63,19 @@ dependencies = [ "synstructure", ] +[[package]] +name = "boa_string" +version = "1.0.0-dev" +source = "git+https://github.com/boa-dev/boa.git?branch=main#4fc75c6ae9d85f2b8065c6716f88e9b35318438c" +dependencies = [ + "fast-float2", + "itoa", + "pastey", + "rustc-hash", + "ryu-js", + "static_assertions", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -200,11 +219,22 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "equivalent" @@ -212,6 +242,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "fast-float2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eb564c5c7423d25c886fb561d1e4ee69f72354d16918afa32c08811f6b6a55" + [[package]] name = "foldhash" version = "0.2.0" @@ -246,12 +282,35 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -259,7 +318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", ] [[package]] @@ -284,9 +343,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" @@ -304,6 +363,12 @@ version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "memchr" version = "2.8.0" @@ -335,9 +400,13 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" name = "oscars" version = "0.1.0" dependencies = [ + "arrayvec", "boa_gc", + "boa_string", "criterion", - "hashbrown", + "either", + "hashbrown 0.16.1", + "icu_locale_core", "oscars_derive", "rustc-hash", "thin-vec", @@ -355,6 +424,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "plotters" version = "0.3.7" @@ -394,9 +469,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -452,9 +527,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustversion" @@ -462,6 +537,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu-js" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04d056b875a9d2e6cb9a61d127afee9ac5999b9f87bcb32079d1318e505be714" + [[package]] name = "same-file" version = "1.0.6" @@ -523,6 +604,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "syn" version = "2.0.117" @@ -566,6 +653,15 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "259cdf8ed4e4aca6f1e9d011e10bd53f524a2d0637d7b28450f6c64ac298c4c6" +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -731,6 +827,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + [[package]] name = "zerocopy" version = "0.8.40" diff --git a/oscars/Cargo.toml b/oscars/Cargo.toml index 37d6221..8b321b1 100644 --- a/oscars/Cargo.toml +++ b/oscars/Cargo.toml @@ -8,6 +8,13 @@ hashbrown = "0.16.1" oscars_derive = { path = "../oscars_derive", version = "0.1.0" } rustc-hash = "2.1.1" thin-vec = { version = "0.2", optional = true } +# Optional Trace/Finalize impls for external types. +# Uses remote git dependency for standalone compilation. +# Override locally via `[patch]` in workspace root. +boa_string = { git = "https://github.com/boa-dev/boa.git", branch = "main", optional = true } +icu_locale_core = { version = "2.2.0", default-features = false, optional = true } +either = { version = "1.16.0", optional = true } +arrayvec = { version = "0.7.6", optional = true } [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } @@ -39,3 +46,7 @@ mark_sweep_branded = ["mark_sweep"] null_collector = ["mark_sweep"] null_collector_branded = ["mark_sweep"] thin-vec = ["dep:thin-vec", "mark_sweep"] +boa_string = ["dep:boa_string", "mark_sweep"] +icu = ["dep:icu_locale_core", "mark_sweep"] +either = ["dep:either", "mark_sweep"] +arrayvec = ["dep:arrayvec", "mark_sweep"] diff --git a/oscars/src/alloc/arena2/alloc.rs b/oscars/src/alloc/arena2/alloc.rs index f1a2c86..ec22503 100644 --- a/oscars/src/alloc/arena2/alloc.rs +++ b/oscars/src/alloc/arena2/alloc.rs @@ -48,6 +48,7 @@ impl ArenaHeapItem { /// /// This avoids creating a `&mut self` reference, which can lead to stacked borrows /// if shared references to the heap item exist + #[allow(dead_code)] pub(crate) fn as_value_ptr(ptr: NonNull) -> *mut T { // SAFETY: `&raw mut` computes the field address without creating a reference unsafe { &raw mut (*ptr.as_ptr()).value } @@ -147,6 +148,7 @@ impl<'arena> ErasedArenaPointer<'arena> { /// SAFETY: /// /// safe because the gc collector owns the arena and keeps it alive + #[allow(dead_code)] pub(crate) unsafe fn extend_lifetime(self) -> ErasedArenaPointer<'static> { ErasedArenaPointer(self.0, PhantomData) } @@ -202,6 +204,7 @@ impl<'arena, T> ArenaPointer<'arena, T> { /// SAFETY: /// /// safe because the gc collector owns the arena and keeps it alive + #[allow(dead_code)] pub(crate) unsafe fn extend_lifetime(self) -> ArenaPointer<'static, T> { // SAFETY: upheld by caller ArenaPointer(unsafe { self.0.extend_lifetime() }, PhantomData) diff --git a/oscars/src/alloc/mempool3/alloc.rs b/oscars/src/alloc/mempool3/alloc.rs index 64e5ba3..813440f 100644 --- a/oscars/src/alloc/mempool3/alloc.rs +++ b/oscars/src/alloc/mempool3/alloc.rs @@ -107,7 +107,7 @@ impl<'pool, T: ?Sized> PoolPointer<'pool, T> { } } -impl<'pool, T> PoolPointer<'pool, T> { +impl<'pool, T: ?Sized> PoolPointer<'pool, T> { pub(crate) unsafe fn from_raw(raw: NonNull>) -> Self { Self(raw, PhantomData) } diff --git a/oscars/src/collectors/mark_sweep/internals/mod.rs b/oscars/src/collectors/mark_sweep/internals/mod.rs index e87ffd1..667f465 100644 --- a/oscars/src/collectors/mark_sweep/internals/mod.rs +++ b/oscars/src/collectors/mark_sweep/internals/mod.rs @@ -4,6 +4,7 @@ mod gc_header; mod vtable; pub(crate) use ephemeron::Ephemeron; +#[allow(unused_imports)] pub(crate) use gc_header::{GcHeader, HeaderColor}; pub(crate) use vtable::{DropFn, FinalizeFn, TraceFn, VTable, vtable_of}; diff --git a/oscars/src/collectors/mark_sweep/trace.rs b/oscars/src/collectors/mark_sweep/trace.rs index 2a3a5ff..00a0662 100644 --- a/oscars/src/collectors/mark_sweep/trace.rs +++ b/oscars/src/collectors/mark_sweep/trace.rs @@ -54,6 +54,14 @@ pub unsafe trait Trace: Finalize { /// - An incorrect implementation may cause undefined behavior unsafe fn trace(&self, color: TraceColor); + /// Unroots handles located in the GC heap. + /// + /// # Safety + /// + /// Must only be called by the garbage collector. + #[inline] + unsafe fn trace_non_roots(&self) {} + fn run_finalizer(&self); } diff --git a/oscars/src/collectors/mark_sweep_branded/cell.rs b/oscars/src/collectors/mark_sweep_branded/cell.rs index 50a6ebe..a67a293 100644 --- a/oscars/src/collectors/mark_sweep_branded/cell.rs +++ b/oscars/src/collectors/mark_sweep_branded/cell.rs @@ -65,10 +65,9 @@ impl DerefMut for GcRefMut<'_, T> { impl Finalize for GcRefCell {} unsafe impl Trace for GcRefCell { + #[inline] unsafe fn trace(&self, tracer: &mut Tracer) { let val = unsafe { &*self.inner.as_ptr() }; - unsafe { - val.trace(tracer); - } + unsafe { val.trace(tracer) }; } } diff --git a/oscars/src/collectors/mark_sweep_branded/gc.rs b/oscars/src/collectors/mark_sweep_branded/gc.rs index 073e706..e220515 100644 --- a/oscars/src/collectors/mark_sweep_branded/gc.rs +++ b/oscars/src/collectors/mark_sweep_branded/gc.rs @@ -60,7 +60,7 @@ impl<'gc, T: Trace + 'gc> Deref for Gc<'gc, T> { } } -impl Finalize for Gc<'_, T> {} +impl Finalize for Gc<'_, T> {} unsafe impl Trace for Gc<'_, T> { unsafe fn trace(&self, tracer: &mut crate::collectors::mark_sweep_branded::trace::Tracer) { tracer.mark(self); diff --git a/oscars/src/collectors/mark_sweep_branded/tests/mod.rs b/oscars/src/collectors/mark_sweep_branded/tests/mod.rs index cea7039..e9da818 100644 --- a/oscars/src/collectors/mark_sweep_branded/tests/mod.rs +++ b/oscars/src/collectors/mark_sweep_branded/tests/mod.rs @@ -6,6 +6,7 @@ struct JsObject { value: i32, } +// SAFETY: JsObject contains no Gc pointers; tracing is a no-op. unsafe impl crate::collectors::mark_sweep_branded::Trace for JsObject { unsafe fn trace(&self, _tracer: &mut crate::collectors::mark_sweep_branded::trace::Tracer) {} } diff --git a/oscars/src/collectors/mark_sweep_branded/tests/uaf.rs b/oscars/src/collectors/mark_sweep_branded/tests/uaf.rs index 921d148..75d6995 100644 --- a/oscars/src/collectors/mark_sweep_branded/tests/uaf.rs +++ b/oscars/src/collectors/mark_sweep_branded/tests/uaf.rs @@ -5,6 +5,7 @@ use core::cell::Cell; struct DetectDrop<'a>(&'a Cell); +// SAFETY: DetectDrop contains no Gc pointers; tracing is a no-op. unsafe impl<'a> Trace for DetectDrop<'a> { unsafe fn trace(&self, _tracer: &mut crate::collectors::mark_sweep_branded::trace::Tracer) {} } diff --git a/oscars/src/collectors/mark_sweep_branded/trace.rs b/oscars/src/collectors/mark_sweep_branded/trace.rs index 8b0118c..f7ec3d0 100644 --- a/oscars/src/collectors/mark_sweep_branded/trace.rs +++ b/oscars/src/collectors/mark_sweep_branded/trace.rs @@ -27,8 +27,20 @@ pub unsafe trait Trace { /// /// # Safety /// - /// See `boa_gc::Trace` for safety contract details. + /// Must only be called by the garbage collector. Implementors must call + /// `Tracer::mark` on all reachable `Gc` fields and avoid other unsafe operations unsafe fn trace(&self, tracer: &mut Tracer); + + /// Unroots handles located in the GC heap. + /// + /// # Safety + /// + /// Must only be called by the garbage collector. + #[inline] + unsafe fn trace_non_roots(&self) {} + + #[inline] + fn run_finalizer(&self) {} } pub(crate) type TraceFn = unsafe fn(core::ptr::NonNull, &mut Tracer<'_>); diff --git a/oscars/src/collectors/null_collector_branded/cell.rs b/oscars/src/collectors/null_collector_branded/cell.rs index cdedb43..a21b431 100644 --- a/oscars/src/collectors/null_collector_branded/cell.rs +++ b/oscars/src/collectors/null_collector_branded/cell.rs @@ -3,7 +3,7 @@ use core::cell::{Ref, RefCell, RefMut}; use core::ops::{Deref, DerefMut}; /// GC aware wrapper around [`RefCell`] -pub struct GcRefCell { +pub struct GcRefCell { inner: RefCell, } @@ -34,40 +34,167 @@ impl GcRefCell { } /// Shared borrow guard returned by [`GcRefCell::borrow`] -pub struct GcRef<'a, T: Trace>(Ref<'a, T>); +pub struct GcRef<'a, T: Trace + ?Sized>(Ref<'a, T>); -impl Deref for GcRef<'_, T> { +/// A mutable borrow guard returned by [`GcRefCell::borrow_mut`] +pub struct GcRefMut<'a, T: Trace + ?Sized>(RefMut<'a, T>); +impl Deref for GcRef<'_, T> { type Target = T; - fn deref(&self) -> &T { + + #[inline] + fn deref(&self) -> &Self::Target { &self.0 } } -/// A mutable borrow guard returned by [`GcRefCell::borrow_mut`] -pub struct GcRefMut<'a, T: Trace>(RefMut<'a, T>); - -impl Deref for GcRefMut<'_, T> { +impl Deref for GcRefMut<'_, T> { type Target = T; - fn deref(&self) -> &T { + + #[inline] + fn deref(&self) -> &Self::Target { &self.0 } } -impl DerefMut for GcRefMut<'_, T> { - fn deref_mut(&mut self) -> &mut T { +impl DerefMut for GcRefMut<'_, T> { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } +impl<'a, T: Trace + ?Sized> GcRef<'a, T> { + pub fn clone(orig: &GcRef<'a, T>) -> GcRef<'a, T> { + GcRef(Ref::clone(&orig.0)) + } + + pub fn map(orig: GcRef<'a, T>, f: F) -> GcRef<'a, U> + where + F: FnOnce(&T) -> &U, + { + GcRef(Ref::map(orig.0, f)) + } + + pub fn try_map(orig: GcRef<'a, T>, f: F) -> Option> + where + F: FnOnce(&T) -> Option<&U>, + { + Ref::filter_map(orig.0, f).ok().map(GcRef) + } + + /// Casts a `GcRef` to a `GcRef` without type checking. + /// + /// # Safety + /// + /// `T` and `U` must be pointer-compatible. The underlying value must be + /// a valid `U`. + pub unsafe fn cast(orig: GcRef<'a, T>) -> GcRef<'a, U> { + GcRef(Ref::map(orig.0, |t| unsafe { + &*((t as *const T).cast::()) + })) + } +} + +impl<'a, T: Trace + ?Sized> GcRefMut<'a, T> { + pub fn map(orig: GcRefMut<'a, T>, f: F) -> GcRefMut<'a, U> + where + F: FnOnce(&mut T) -> &mut U, + { + GcRefMut(RefMut::map(orig.0, f)) + } + + pub fn try_map(orig: GcRefMut<'a, T>, f: F) -> Option> + where + F: FnOnce(&mut T) -> Option<&mut U>, + { + RefMut::filter_map(orig.0, f).ok().map(GcRefMut) + } + + /// Casts a `GcRefMut` to a `GcRefMut` without type checking. + /// + /// # Safety + /// + /// `T` and `U` must be pointer compatible, the underlying value must be + /// a valid `U` + pub unsafe fn cast(orig: GcRefMut<'a, T>) -> GcRefMut<'a, U> { + GcRefMut(RefMut::map(orig.0, |t| unsafe { + &mut *((t as *mut T).cast::()) + })) + } +} + impl Finalize for GcRefCell {} unsafe impl Trace for GcRefCell { + #[inline] unsafe fn trace(&self, tracer: &mut Tracer) { - // SAFETY: We only access the inner value for tracing and do not mutate it. - // The null collector's trace is a no-op, so this is safe. - let val = unsafe { &*self.inner.as_ptr() }; - unsafe { - val.trace(tracer); + unsafe { self.borrow().trace(tracer) }; + } +} + +impl Default for GcRefCell { + #[inline] + fn default() -> Self { + Self::new(T::default()) + } +} + +impl core::fmt::Debug for GcRefCell { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self.inner.try_borrow() { + Ok(borrow) => f.debug_tuple("GcRefCell").field(&*borrow).finish(), + Err(_) => { + struct BorrowedPlaceholder; + impl core::fmt::Debug for BorrowedPlaceholder { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("") + } + } + f.debug_tuple("GcRefCell") + .field(&BorrowedPlaceholder) + .finish() + } } } } + +impl Clone for GcRefCell { + #[inline] + fn clone(&self) -> Self { + Self::new(self.inner.borrow().clone()) + } +} + +impl<'a, T: Trace + core::fmt::Debug + ?Sized> core::fmt::Debug for GcRef<'a, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Debug::fmt(&**self, f) + } +} + +impl<'a, T: Trace + core::fmt::Display + ?Sized> core::fmt::Display for GcRef<'a, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(&**self, f) + } +} + +impl<'a, T: Trace + core::fmt::Debug + ?Sized> core::fmt::Debug for GcRefMut<'a, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Debug::fmt(&**self, f) + } +} + +impl<'a, T: Trace + core::fmt::Display + ?Sized> core::fmt::Display for GcRefMut<'a, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(&**self, f) + } +} + +impl GcRefCell { + pub fn try_borrow(&self) -> Result, core::cell::BorrowError> { + self.inner.try_borrow().map(GcRef) + } + + pub fn try_borrow_mut(&self) -> Result, core::cell::BorrowMutError> { + self.inner.try_borrow_mut().map(GcRefMut) + } +} diff --git a/oscars/src/collectors/null_collector_branded/ephemeron.rs b/oscars/src/collectors/null_collector_branded/ephemeron.rs index 82bf7d1..a887a3b 100644 --- a/oscars/src/collectors/null_collector_branded/ephemeron.rs +++ b/oscars/src/collectors/null_collector_branded/ephemeron.rs @@ -9,14 +9,14 @@ use crate::{ }; use core::marker::PhantomData; -pub struct Ephemeron<'id, K: Trace, V: Trace> { +pub struct Ephemeron<'id, K: Trace + ?Sized, V: Trace> { pub(crate) key_ptr: Option>>, pub(crate) value_ptr: PoolPointer<'static, GcBox>, pub(crate) _marker: PhantomData<*mut &'id ()>, } -impl<'id, K: Trace, V: Trace> Ephemeron<'id, K, V> { - pub(crate) fn new( +impl<'id, K: Trace + ?Sized, V: Trace> Ephemeron<'id, K, V> { + pub(crate) fn new_raw( key_ptr: Option>>, value_ptr: PoolPointer<'static, GcBox>, ) -> Self { @@ -27,27 +27,58 @@ impl<'id, K: Trace, V: Trace> Ephemeron<'id, K, V> { } } - /// Returns the value if the key is alive. - pub fn get_value<'gc>(&self, _cx: &MutationContext<'id, 'gc>) -> Option> { - // In the null collector, everything stays alive until context drops. + pub fn new<'gc>(cx: &MutationContext<'id, 'gc>, key: &Gc<'gc, K>, value: V) -> Self + where + V: Finalize, + { + let value_gc = Gc::new(cx, value); + Self::new_raw(Some(key.ptr), value_gc.ptr) + } + + pub fn get_value<'gc>(&self, cx: &MutationContext<'id, 'gc>) -> Option> { + self.value(cx) + } + + /// Returns the key if still live, or `None` if it has been collected. + /// + /// Note: Under the null collector this always returns `Some` since nothing is ever collected + /// TODO: Hook into the collector's finalization pass to return `None` when the key is + /// only reachable through this ephemeron + pub fn key<'gc>(&self, _cx: &MutationContext<'id, 'gc>) -> Option> { + self.key_ptr.map(|ptr| Gc::with_pointer(ptr)) + } + + /// Returns the value if the key is still live, or `None` if it has been collected. + /// + /// Note: Under the null collector this always returns `Some` since nothing is ever collected + /// TODO: Same as `key()`, must return `None` when the key is collected + pub fn value<'gc>(&self, _cx: &MutationContext<'id, 'gc>) -> Option> { if self.key_ptr.is_some() { Some(Gc::with_pointer(self.value_ptr)) } else { None } } + + /// Returns `true` if the key is still live. + /// + /// Note: under the null collector this always returns true, nothing is ever collected. + /// TODO: Must return `false` once the collector clears `key_ptr` + pub fn has_value(&self) -> bool { + self.key_ptr.is_some() + } } -impl<'id, K: Trace, V: Trace> Clone for Ephemeron<'id, K, V> { +impl<'id, K: Trace + ?Sized, V: Trace> Clone for Ephemeron<'id, K, V> { fn clone(&self) -> Self { *self } } -impl<'id, K: Trace, V: Trace> Copy for Ephemeron<'id, K, V> {} +impl<'id, K: Trace + ?Sized, V: Trace> Copy for Ephemeron<'id, K, V> {} -impl<'id, K: Trace, V: Trace> Finalize for Ephemeron<'id, K, V> {} +impl<'id, K: Trace + ?Sized, V: Trace> Finalize for Ephemeron<'id, K, V> {} -unsafe impl<'id, K: Trace, V: Trace> Trace for Ephemeron<'id, K, V> { +unsafe impl<'id, K: Trace + ?Sized, V: Trace> Trace for Ephemeron<'id, K, V> { unsafe fn trace(&self, _tracer: &mut Tracer) {} } diff --git a/oscars/src/collectors/null_collector_branded/gc.rs b/oscars/src/collectors/null_collector_branded/gc.rs index 37d5cd4..a4137aa 100644 --- a/oscars/src/collectors/null_collector_branded/gc.rs +++ b/oscars/src/collectors/null_collector_branded/gc.rs @@ -33,38 +33,114 @@ impl<'gc, T: Trace + ?Sized + 'gc> Gc<'gc, T> { _marker: PhantomData, } } -} -impl<'gc, T: Trace + ?Sized + 'gc> Gc<'gc, T> { - /// Returns a shared reference to the value. - #[inline] - pub fn get(&self) -> &T { - // SAFETY: `ptr` is non-null and valid for `'gc` by construction. - unsafe { &(*self.ptr.as_ptr().as_ptr()).0.value } + pub fn new<'id>( + mc: &crate::collectors::null_collector_branded::MutationContext<'id, 'gc>, + value: T, + ) -> Self + where + T: Sized + Finalize, + { + mc.try_alloc(value).unwrap() } #[inline] pub fn as_ptr(&self) -> *const T { - self.get() as *const T + self.as_ref() as *const T + } + + pub fn into_raw(self) -> *const T { + let ptr = self.ptr.as_ptr().as_ptr() as *const T; + let _ = self; + ptr + } + + /// Creates a `Gc` from a raw pointer previously obtained from `into_raw`. + /// + /// # Safety + /// + /// `ptr` must have been returned by `Gc::into_raw` for the same `T`. + /// The allocation must be valid and uniquely referenced. + pub unsafe fn from_raw(ptr: *const T) -> Self { + unsafe { + Self::with_pointer(crate::alloc::mempool3::PoolPointer::from_raw( + core::ptr::NonNull::new_unchecked(ptr as *mut _), + )) + } } } impl<'gc, T: Trace + ?Sized + fmt::Display + 'gc> fmt::Display for Gc<'gc, T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(self.get(), f) + fmt::Display::fmt(self.as_ref(), f) + } +} + +impl<'id, T: Trace + ?Sized> Gc<'id, T> { + /// Gets a reference to the inner value. + pub fn as_ref(&self) -> &T { + unsafe { &self.ptr.as_ptr().as_ref().0.value } + } + + /// Converts the `Gc` into a `NonNull` pointer. + pub fn as_non_null(&self) -> core::ptr::NonNull { + let ptr = self.ptr.as_ptr().as_ptr() as *mut T; + unsafe { core::ptr::NonNull::new_unchecked(ptr) } } } impl<'gc, T: Trace + ?Sized + 'gc> Deref for Gc<'gc, T> { type Target = T; fn deref(&self) -> &T { - self.get() + self.as_ref() } } impl Finalize for Gc<'_, T> {} unsafe impl Trace for Gc<'_, T> { - unsafe fn trace(&self, tracer: &mut crate::collectors::null_collector_branded::trace::Tracer) { - tracer.mark(self); + unsafe fn trace(&self, _tracer: &mut crate::collectors::null_collector_branded::trace::Tracer) { + } +} + +impl<'gc, T: Default + Trace + Finalize + 'gc> Default for Gc<'gc, T> { + #[inline] + fn default() -> Self { + Self::new( + &crate::collectors::null_collector_branded::MutationContext::global(), + Default::default(), + ) + } +} + +impl<'gc, T: Trace + ?Sized + 'gc> Gc<'gc, T> { + pub fn ptr_eq(this: &Self, other: &Self) -> bool { + core::ptr::eq(this.ptr.as_ptr().as_ptr(), other.ptr.as_ptr().as_ptr()) + } + + /// Casts a `Gc` to a `Gc` without type checking. + /// + /// # Safety + /// + /// The allocation must contain a valid `U`, `T` and `U` must share + /// the same layout and alignment + pub unsafe fn cast_unchecked(this: Self) -> Gc<'gc, U> { + let ptr = this.ptr.as_ptr().as_ptr() + as *mut crate::alloc::mempool3::PoolItem< + crate::collectors::null_collector_branded::gc_box::GcBox, + >; + let new_ptr = unsafe { + crate::alloc::mempool3::PoolPointer::from_raw(core::ptr::NonNull::new_unchecked(ptr)) + }; + let _ = this; + Gc { + ptr: new_ptr, + _marker: core::marker::PhantomData, + } + } + + pub fn is(this: &Self) -> bool { + // Check if the underlying `GcBox` has type `U` + // SAFETY: `this.ptr` is a valid, initialized pointer to a `GcBox` for the lifetime of `this` + unsafe { this.ptr.as_ptr().as_ref().0.type_name == core::any::type_name::() } } } diff --git a/oscars/src/collectors/null_collector_branded/gc_box.rs b/oscars/src/collectors/null_collector_branded/gc_box.rs index 97f806e..2514b95 100644 --- a/oscars/src/collectors/null_collector_branded/gc_box.rs +++ b/oscars/src/collectors/null_collector_branded/gc_box.rs @@ -2,14 +2,16 @@ use core::ptr::NonNull; use crate::alloc::mempool3::PoolAllocator; -pub(crate) type DropFn = unsafe fn(&mut PoolAllocator<'static>, NonNull); +pub type DropFn = unsafe fn(&mut PoolAllocator<'static>, NonNull); /// Heap wrapper for a garbage collected value. /// /// Allocated via [`PoolAllocator`] -pub(crate) struct GcBox { +pub struct GcBox { /// Type erased finalize and free fn pub(crate) drop_fn: DropFn, + /// Type name of the underlying value + pub(crate) type_name: &'static str, /// User value pub(crate) value: T, } @@ -17,6 +19,10 @@ pub(crate) struct GcBox { impl GcBox { /// Create a [`GcBox`] for `value` pub(crate) fn new(value: T, drop_fn: DropFn) -> Self { - Self { drop_fn, value } + Self { + drop_fn, + type_name: core::any::type_name::(), + value, + } } } diff --git a/oscars/src/collectors/null_collector_branded/mod.rs b/oscars/src/collectors/null_collector_branded/mod.rs index 854ab43..90b0b4c 100644 --- a/oscars/src/collectors/null_collector_branded/mod.rs +++ b/oscars/src/collectors/null_collector_branded/mod.rs @@ -22,7 +22,7 @@ use crate::alloc::mempool3::{PoolAllocError, PoolAllocator}; use core::cell::RefCell; use core::marker::PhantomData; use core::ptr::NonNull; -use gc_box::{DropFn, GcBox}; +pub use gc_box::{DropFn, GcBox}; use rust_alloc::vec::Vec; pub(crate) struct Collector { diff --git a/oscars/src/collectors/null_collector_branded/mutation_ctx.rs b/oscars/src/collectors/null_collector_branded/mutation_ctx.rs index 93a81b8..ced2fd6 100644 --- a/oscars/src/collectors/null_collector_branded/mutation_ctx.rs +++ b/oscars/src/collectors/null_collector_branded/mutation_ctx.rs @@ -18,6 +18,35 @@ pub struct MutationContext<'id, 'gc> { } impl<'id, 'gc> MutationContext<'id, 'gc> { + /// Creates a global thread-local MutationContext for the null collector. + pub fn global() -> Self { + std::thread_local! { + static COLLECTOR: crate::collectors::null_collector_branded::Collector = crate::collectors::null_collector_branded::Collector::new(); + } + COLLECTOR.with(|c| { + let ptr = c as *const crate::collectors::null_collector_branded::Collector; + Self { + collector: unsafe { &*ptr }, + _marker: core::marker::PhantomData, + } + }) + } + + /// Creates a dummy `MutationContext` backed by a dangling pointer. + /// + /// # Safety + /// + /// The returned context is a placeholder and must never be used to + /// allocate or access GC objects. + pub unsafe fn dummy() -> Self { + // SAFETY: We use a dangling pointer for the collector because it's a dummy. + // It should never be used to allocate + Self { + collector: unsafe { &*core::ptr::NonNull::dangling().as_ptr() }, + _marker: PhantomData, + } + } + /// Allocates a value on the GC heap pub fn try_alloc( &self, @@ -43,14 +72,14 @@ impl<'id, 'gc> MutationContext<'id, 'gc> { /// The value is kept alive by the collector as long as the key remains /// reachable from a root. Once the key is collected, `get_value` returns /// `None` and the value is eligible for collection on next cycle. - pub fn alloc_ephemeron( + pub fn alloc_ephemeron( &self, key: Gc<'gc, K>, value: Gc<'gc, V>, ) -> Ephemeron<'id, K, V> { // In the null collector, ephemerons don't need to be registered // since the collector never collects. - Ephemeron::new(Some(key.ptr), value.ptr) + Ephemeron::new_raw(Some(key.ptr), value.ptr) } pub fn collect(&self) { diff --git a/oscars/src/collectors/null_collector_branded/trace.rs b/oscars/src/collectors/null_collector_branded/trace.rs index be4b7d9..ff50048 100644 --- a/oscars/src/collectors/null_collector_branded/trace.rs +++ b/oscars/src/collectors/null_collector_branded/trace.rs @@ -17,14 +17,28 @@ use rust_alloc::vec::Vec; /// /// # Safety /// -/// See `boa_gc::Trace` for safety contract details. +/// Implementors must pass every reachable `Gc` pointer to `Tracer::mark` +/// While the null collector reclaims no memory, implementations must be +/// sound for other collectors to prevent UAF bugs. pub unsafe trait Trace { - /// Marks all Gc pointers reachable from `self`. + /// Marks all `Gc` pointers reachable from `self`. /// /// # Safety /// - /// See `boa_gc::Trace` for safety contract details. + /// Must only be called by the garbage collector. Implementors must call + /// `Tracer::mark` on all reachable `Gc` fields and avoid other unsafe operations. unsafe fn trace(&self, tracer: &mut Tracer); + + /// Unroots handles located in the GC heap. + /// + /// # Safety + /// + /// Must only be called by the garbage collector. + #[inline] + unsafe fn trace_non_roots(&self) {} + + #[inline] + fn run_finalizer(&self) {} } /// Dummy tracer for the null collector @@ -101,7 +115,6 @@ unsafe impl Trace for [T; N] { } unsafe impl Trace for [T] { - #[inline] unsafe fn trace(&self, tracer: &mut Tracer) { for v in self.iter() { v.trace(tracer); @@ -110,22 +123,11 @@ unsafe impl Trace for [T] { } unsafe impl Trace for Box { - #[inline] unsafe fn trace(&self, tracer: &mut Tracer) { (**self).trace(tracer); } } -#[cfg(feature = "thin-vec")] -unsafe impl Trace for thin_vec::ThinVec { - #[inline] - unsafe fn trace(&self, tracer: &mut Tracer) { - for v in self.iter() { - v.trace(tracer); - } - } -} - unsafe impl Trace for Option { unsafe fn trace(&self, tracer: &mut Tracer) { if let Some(v) = self { @@ -159,6 +161,15 @@ unsafe impl Trace for VecDeque { } } +#[cfg(feature = "thin-vec")] +unsafe impl Trace for thin_vec::ThinVec { + unsafe fn trace(&self, tracer: &mut Tracer) { + for v in self.iter() { + v.trace(tracer); + } + } +} + unsafe impl Trace for LinkedList { unsafe fn trace(&self, tracer: &mut Tracer) { for v in self.iter() { @@ -233,6 +244,17 @@ unsafe impl Trace for (A, B, C, D) { } } +unsafe impl Trace for (A, B, C, D, E) { + #[inline] + unsafe fn trace(&self, tracer: &mut Tracer) { + self.0.trace(tracer); + self.1.trace(tracer); + self.2.trace(tracer); + self.3.trace(tracer); + self.4.trace(tracer); + } +} + unsafe impl Trace for rust_alloc::rc::Rc { #[inline] unsafe fn trace(&self, _tracer: &mut Tracer) {} @@ -255,3 +277,147 @@ unsafe impl Trace for BTreeSet { #[inline] unsafe fn trace(&self, _tracer: &mut Tracer) {} } + +#[cfg(feature = "boa_string")] +impl Finalize for boa_string::JsString {} + +#[cfg(feature = "boa_string")] +unsafe impl Trace for boa_string::JsString { + #[inline] + unsafe fn trace(&self, _tracer: &mut Tracer) {} +} + +unsafe impl Trace for str { + #[inline] + unsafe fn trace(&self, _tracer: &mut Tracer) {} +} + +#[cfg(feature = "icu")] +impl Finalize for icu_locale_core::LanguageIdentifier {} + +#[cfg(feature = "icu")] +unsafe impl Trace for icu_locale_core::LanguageIdentifier { + #[inline] + unsafe fn trace(&self, _tracer: &mut Tracer) {} +} + +#[cfg(feature = "icu")] +impl Finalize for icu_locale_core::Locale {} + +#[cfg(feature = "icu")] +unsafe impl Trace for icu_locale_core::Locale { + #[inline] + unsafe fn trace(&self, _tracer: &mut Tracer) {} +} + +#[cfg(feature = "either")] +impl Finalize for either::Either {} + +#[cfg(feature = "either")] +unsafe impl Trace for either::Either { + unsafe fn trace(&self, tracer: &mut Tracer) { + match self { + either::Either::Left(l) => l.trace(tracer), + either::Either::Right(r) => r.trace(tracer), + } + } +} + +#[cfg(feature = "arrayvec")] +impl Finalize for arrayvec::ArrayVec {} + +#[cfg(feature = "arrayvec")] +unsafe impl Trace for arrayvec::ArrayVec { + unsafe fn trace(&self, tracer: &mut Tracer) { + for v in self { + v.trace(tracer); + } + } +} + +unsafe impl Trace for hashbrown::hash_map::HashMap { + #[inline] + unsafe fn trace(&self, tracer: &mut Tracer) { + for v in self.values() { + v.trace(tracer); + } + } +} +// Finalize is already implemented in common.rs + +unsafe impl Trace for hashbrown::hash_set::HashSet { + #[inline] + unsafe fn trace(&self, _tracer: &mut Tracer) {} +} +// Finalize is already implemented in common.rs + +unsafe impl Trace for rust_alloc::collections::BinaryHeap { + #[inline] + unsafe fn trace(&self, _tracer: &mut Tracer) { + // BinaryHeap has no iter_mut(); the null collector's trace is a no-op + // so no values need to be visited here. + } +} +// Finalize is already implemented in common.rs + +#[cfg(feature = "std")] +unsafe impl Trace for std::path::Path { + #[inline] + unsafe fn trace(&self, _tracer: &mut Tracer) {} +} +#[cfg(feature = "std")] +impl Finalize for std::path::Path {} + +#[cfg(feature = "std")] +unsafe impl Trace for std::path::PathBuf { + #[inline] + unsafe fn trace(&self, _tracer: &mut Tracer) {} +} +#[cfg(feature = "std")] +impl Finalize for std::path::PathBuf {} + +#[cfg(feature = "std")] +unsafe impl Trace for std::time::Instant { + #[inline] + unsafe fn trace(&self, _tracer: &mut Tracer) {} +} +#[cfg(feature = "std")] +impl Finalize for std::time::Instant {} + +#[cfg(feature = "std")] +unsafe impl Trace for std::time::SystemTime { + #[inline] + unsafe fn trace(&self, _tracer: &mut Tracer) {} +} +#[cfg(feature = "std")] +impl Finalize for std::time::SystemTime {} + +#[cfg(feature = "std")] +unsafe impl Trace for std::collections::HashMap { + #[inline] + unsafe fn trace(&self, tracer: &mut Tracer) { + for v in self.values() { + v.trace(tracer); + } + } +} + +#[cfg(feature = "std")] +unsafe impl Trace for std::collections::HashSet { + #[inline] + unsafe fn trace(&self, _tracer: &mut Tracer) {} +} + +empty_trace!( + core::sync::atomic::AtomicBool, + core::sync::atomic::AtomicI8, + core::sync::atomic::AtomicU8, + core::sync::atomic::AtomicI16, + core::sync::atomic::AtomicU16, + core::sync::atomic::AtomicI32, + core::sync::atomic::AtomicU32, + core::sync::atomic::AtomicIsize, + core::sync::atomic::AtomicUsize, + core::sync::atomic::AtomicI64, + core::sync::atomic::AtomicU64 +); diff --git a/oscars/src/collectors/null_collector_branded/weak.rs b/oscars/src/collectors/null_collector_branded/weak.rs index db5e56e..012ca87 100644 --- a/oscars/src/collectors/null_collector_branded/weak.rs +++ b/oscars/src/collectors/null_collector_branded/weak.rs @@ -22,6 +22,16 @@ impl<'id, T: Trace + ?Sized> WeakGc<'id, T> { } } + pub fn new<'gc>( + mc: &crate::collectors::null_collector_branded::MutationContext<'id, 'gc>, + inner: &Gc<'gc, T>, + ) -> Self + where + T: Sized + Finalize, + { + mc.alloc_weak(*inner) + } + /// Attempts to upgrade to a strong `Gc<'gc, T>` pub fn upgrade<'gc>( &self, @@ -30,6 +40,10 @@ impl<'id, T: Trace + ?Sized> WeakGc<'id, T> { // In the null collector, everything stays alive until context drops. Some(Gc::with_pointer(self.ptr)) } + + pub fn is_upgradable(&self) -> bool { + true + } } impl<'id, T: Trace + ?Sized> Clone for WeakGc<'id, T> { @@ -40,6 +54,20 @@ impl<'id, T: Trace + ?Sized> Clone for WeakGc<'id, T> { impl<'id, T: Trace + ?Sized> Copy for WeakGc<'id, T> {} +impl<'id, T: Trace + ?Sized> core::fmt::Debug for WeakGc<'id, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "WeakGc({:p})", self.ptr.as_ptr()) + } +} + +impl<'id, T: Trace + ?Sized> PartialEq for WeakGc<'id, T> { + fn eq(&self, other: &Self) -> bool { + core::ptr::addr_eq(self.ptr.as_ptr().as_ptr(), other.ptr.as_ptr().as_ptr()) + } +} + +impl<'id, T: Trace + ?Sized> Eq for WeakGc<'id, T> {} + impl<'id, T: Trace + ?Sized> Finalize for WeakGc<'id, T> {} unsafe impl<'id, T: Trace + ?Sized> Trace for WeakGc<'id, T> { unsafe fn trace(&self, _tracer: &mut crate::collectors::null_collector_branded::trace::Tracer) {