Skip to main content

eventp/
thin.rs

1//! Thin pointer implementations.
2
3use std::alloc::{self, Layout};
4use std::marker::PhantomData;
5use std::mem::{self, size_of};
6use std::ops::Deref;
7use std::os::fd::{AsRawFd, RawFd};
8use std::ptr::{self, NonNull};
9
10#[cfg(feature = "mock")]
11use crate::mock::MockEventp;
12use crate::utils::unlikely;
13use crate::{Eventp, EventpOps, Subscriber};
14
15/// Similar to `Box<dyn Subscriber<Ep>>`, but the size of this type is only one usize.
16///
17/// Since epoll allows registering only a `u64` alongside the file descriptor,
18/// only a thin pointer can be stored.
19///
20/// # Memory layout
21///
22/// ```text
23/// +---------+---------+---------+-----------------+--------------------+
24/// |  _pad_  |  raw fd |  _pad_  |       vptr      | dyn Subscriber<Ep> |
25/// +---------+---------+---------+-----------------+--------------------+
26/// ??      ptr-16    ptr-12    ptr-8               ↑                    ??
27///                                                 |
28///                            ThinBoxSubscriber { ptr }
29/// ```
30///
31/// See [technical](crate::_technical) for more information.
32pub struct ThinBoxSubscriber<Ep: EventpOps> {
33    ptr: NonNull<u8>,
34    _marker: PhantomData<dyn Subscriber<Ep>>,
35}
36
37impl<Ep> ThinBoxSubscriber<Ep>
38where
39    Ep: EventpOps,
40{
41    /// Allocates memory on the heap and then places `value` into it.
42    ///
43    /// # Panics
44    ///
45    /// - if combining the `(usize, usize)` header layout with `T`'s layout
46    ///   overflows (`Layout::extend` returns `Err`);
47    /// - if the heap allocation fails (via [`alloc::handle_alloc_error`]).
48    pub fn new<T: Subscriber<Ep>>(value: T) -> Self {
49        #[cfg(not(target_pointer_width = "64"))]
50        compile_error!("Platforms with pointer width other than 64 are not supported.");
51
52        // Verify trait object layout: first 8 bytes for the data pointer,
53        // next 8 bytes for the vtable pointer.
54        const _: () = assert!(size_of::<&dyn Subscriber<Eventp>>() == 16);
55        #[cfg(feature = "mock")]
56        const _: () = assert!(size_of::<&dyn Subscriber<MockEventp>>() == 16);
57
58        // Obtain the fat pointer and extract the vtable address.
59        let fat_ptr = &value as &dyn Subscriber<Ep>;
60        let (_data_ptr, vptr) =
61            unsafe { mem::transmute::<&dyn Subscriber<Ep>, (*const (), *const ())>(fat_ptr) };
62
63        // Read the raw fd before allocating, so any panic from a
64        // user-provided `AsFd` impl cannot leave a partially-initialized heap.
65        let raw_fd = value.as_fd().as_raw_fd();
66
67        // Create a new layout for the raw fd, vptr and data T.
68        let (layout, value_offset) = Layout::new::<(usize, usize)>()
69            .extend(Layout::new::<T>())
70            .expect("Failed to create combined layout");
71
72        let ptr = {
73            // SAFETY: Layout has a non-zero size, because it contains
74            // at least two usizes.
75            let ptr = unsafe { alloc::alloc(layout) };
76            if ptr.is_null() {
77                alloc::handle_alloc_error(layout);
78            }
79
80            // SAFETY: Points to a valid location because the previous allocation succeeded.
81            let ptr = unsafe { ptr.add(value_offset) };
82            unsafe { NonNull::new_unchecked(ptr) }
83        };
84
85        let mut ret = Self {
86            ptr,
87            _marker: PhantomData,
88        };
89
90        // Fill it with the data. No operation may unwind.
91
92        *ret.raw_fd_mut() = raw_fd;
93        *ret.vptr_mut() = vptr;
94
95        // Move the value into the allocated location. No drop occurs.
96        // SAFETY: data_ptr is valid and aligned for writes.
97        unsafe { ret.ptr.as_ptr().cast::<T>().write(value) };
98
99        ret
100    }
101
102    /// Allocates memory on the heap and then moves `value` into it.
103    /// The original [Box] will be consumed.
104    ///
105    /// # Panics
106    ///
107    /// - if combining the `(usize, usize)` header layout with the value's layout
108    ///   overflows (`Layout::extend` returns `Err`);
109    /// - if the heap allocation fails (via [`alloc::handle_alloc_error`]).
110    pub fn from_box_dyn(value: Box<dyn Subscriber<Ep>>) -> Self {
111        // Obtain the fat pointer and extract the vtable address.
112        let fat_ptr = value.deref();
113        let (_data_ptr, vptr) =
114            unsafe { mem::transmute::<&dyn Subscriber<Ep>, (*const (), *const ())>(fat_ptr) };
115
116        // Get the layout of original data.
117        let value_layout = Layout::for_value(value.deref());
118
119        // Read the raw fd before allocating, so any panic from a
120        // user-provided `AsFd` impl cannot leave a partially-initialized heap.
121        let raw_fd = value.as_fd().as_raw_fd();
122
123        // Create a new layout for the raw fd, vptr and data.
124        let (layout, value_offset) = Layout::new::<(usize, usize)>()
125            .extend(value_layout)
126            .expect("Failed to create combined layout");
127
128        let ptr = {
129            // SAFETY: Layout has a non-zero size, because it contains
130            // at least two usizes.
131            let ptr = unsafe { alloc::alloc(layout) };
132            if ptr.is_null() {
133                alloc::handle_alloc_error(layout);
134            }
135
136            // SAFETY: Points to a valid location because the previous allocation succeeded.
137            let ptr = unsafe { ptr.add(value_offset) };
138            unsafe { NonNull::new_unchecked(ptr) }
139        };
140
141        let mut ret = Self {
142            ptr,
143            _marker: PhantomData,
144        };
145
146        // Fill it with the data. No operation may unwind.
147
148        *ret.raw_fd_mut() = raw_fd;
149        *ret.vptr_mut() = vptr;
150
151        // Move the value into the allocated location. No drop occurs.
152        let value = Box::into_raw(value) as *mut u8;
153        // SAFETY: `src` and `dst` are valid and aligned. Because they are from
154        // different allocation, so not overlapped.
155        unsafe {
156            ret.ptr
157                .as_ptr()
158                .cast::<u8>()
159                .copy_from_nonoverlapping(value, value_layout.size())
160        };
161        // SAFETY: `GlobalAlloc` is the allocator of this value and `value_layout` is valid.
162        unsafe { alloc::dealloc(value, value_layout) };
163
164        ret
165    }
166
167    pub(crate) fn raw_fd_ref(&self) -> &RawFd {
168        // SAFETY: See memory layout of docs of this type.
169        unsafe { &*self.ptr.as_ptr().sub(2 * size_of::<usize>()).cast() }
170    }
171
172    fn raw_fd_mut(&mut self) -> &mut RawFd {
173        // SAFETY: See memory layout of docs of this type.
174        unsafe { &mut *self.ptr.as_ptr().sub(2 * size_of::<usize>()).cast() }
175    }
176
177    fn vptr_ref(&self) -> &*const () {
178        // SAFETY: See memory layout of docs of this type.
179        unsafe { &*self.ptr.as_ptr().sub(size_of::<usize>()).cast() }
180    }
181
182    fn vptr_mut(&mut self) -> &mut *const () {
183        // SAFETY: See memory layout of docs of this type.
184        unsafe { &mut *self.ptr.as_ptr().sub(size_of::<usize>()).cast() }
185    }
186
187    fn is_subscriber_dropped(&self) -> bool {
188        *self.raw_fd_ref() == -1
189    }
190
191    fn mark_subscriber_dropped(&mut self) {
192        *self.raw_fd_mut() = -1;
193    }
194
195    /// Dereferences to a trait object regardless of the raw-fd sentinel.
196    ///
197    /// The returned reference is always layout-valid (its address and vtable
198    /// point at the heap slot of the original `T`), so the caller may always
199    /// use it as the argument to `Layout::for_value`, `ptr::drop_in_place`, or
200    /// other operations that only inspect the trait object's layout.
201    ///
202    /// # Safety
203    ///
204    /// - The caller must not invoke any method on the returned trait object
205    ///   when the subscriber has already been dropped in place (i.e. when
206    ///   `raw_fd == -1` after a prior `drop_in_place`/`mark_subscriber_dropped`).
207    /// - The caller must respect Rust's aliasing rules: while this reference
208    ///   exists, no other mutable reference to the same subscriber may exist.
209    unsafe fn deref(&self) -> &dyn Subscriber<Ep> {
210        let data_ptr = self.ptr.as_ptr().cast();
211        let vptr = *self.vptr_ref();
212
213        // SAFETY: `data_ptr` and `vptr` were written together in `new` /
214        // `from_box_dyn`, so reassembling them yields a fat pointer whose layout
215        // matches `*mut dyn Subscriber<Ep>`.
216        let fat_ptr = unsafe {
217            mem::transmute::<(*const (), *const ()), *mut dyn Subscriber<Ep>>((data_ptr, vptr))
218        };
219        // SAFETY: The heap slot is still allocated (it is only freed in `Drop`),
220        // and the caller's `# Safety` contract above guarantees no aliasing
221        // mutable reference exists.
222        unsafe { &mut *fat_ptr }
223    }
224
225    /// Same as [`Self::deref()`], but obtains a mutable reference.
226    unsafe fn deref_mut(&mut self) -> &mut dyn Subscriber<Ep> {
227        let data_ptr = self.ptr.as_ptr().cast();
228        let vptr = *self.vptr_ref();
229
230        // SAFETY: `data_ptr` and `vptr` were written together in `new` /
231        // `from_box_dyn`, so reassembling them yields a fat pointer whose layout
232        // matches `*mut dyn Subscriber<Ep>`.
233        let fat_ptr = unsafe {
234            mem::transmute::<(*const (), *const ()), *mut dyn Subscriber<Ep>>((data_ptr, vptr))
235        };
236        // SAFETY: The heap slot is still allocated (it is only freed in `Drop`),
237        // and the caller's `# Safety` contract above guarantees no aliasing
238        // mutable reference exists.
239        unsafe { &mut *fat_ptr }
240    }
241
242    pub(crate) fn try_deref(&self) -> Option<&dyn Subscriber<Ep>> {
243        if unlikely(self.is_subscriber_dropped()) {
244            return None;
245        }
246        // SAFETY: `raw_fd != -1`, so the subscriber has not been dropped in place;
247        // the caller will receive a normal `&mut dyn Subscriber` and is free to
248        // invoke methods on it. Aliasing is enforced by `&mut self`.
249        unsafe { Some(self.deref()) }
250    }
251
252    pub(crate) fn try_deref_mut(&mut self) -> Option<&mut dyn Subscriber<Ep>> {
253        if unlikely(self.is_subscriber_dropped()) {
254            return None;
255        }
256        // SAFETY: `raw_fd != -1`, so the subscriber has not been dropped in place;
257        // the caller will receive a normal `&mut dyn Subscriber` and is free to
258        // invoke methods on it. Aliasing is enforced by `&mut self`.
259        unsafe { Some(self.deref_mut()) }
260    }
261
262    /// Moves the subscriber out of this thin allocation into a `Box<dyn Subscriber<Ep>>`.
263    ///
264    /// Returns `None` if the subscriber has already been dropped in place
265    /// (via the internal deferred-drop machinery), in which case the value is
266    /// no longer recoverable.
267    ///
268    /// The thin allocation is released as part of this call; the returned
269    /// `Box` owns a freshly-allocated heap slot whose layout matches the
270    /// underlying concrete type, so it is fully compatible with the global
271    /// allocator's `Box<dyn _>` deallocation path.
272    ///
273    /// # Panics
274    ///
275    /// - if allocating the destination `Box` fails (via
276    ///   [`alloc::handle_alloc_error`]).
277    pub fn into_box_dyn(mut self) -> Option<Box<dyn Subscriber<Ep>>> {
278        if self.is_subscriber_dropped() {
279            return None;
280        }
281
282        // SAFETY: The subscriber has not been dropped in place, so its value
283        // is still live. We only inspect the trait object's layout / vtable
284        // here -- we do not invoke any user method on it -- so the
285        // `deref_mut` contract is upheld.
286        let value_ref = unsafe { self.deref_mut() };
287        let value_layout = Layout::for_value(value_ref);
288        let value_ptr = value_ref as *mut dyn Subscriber<Ep> as *mut u8;
289        let vptr = *self.vptr_ref();
290
291        // Allocate a fresh slot sized exactly for the concrete value, so the
292        // resulting `Box` can be deallocated by the global allocator using
293        // the same layout it would have used for `Box::new(value)`.
294        //
295        // For a ZST we must NOT call `alloc::alloc` (it requires a non-zero
296        // layout). Instead, mirror what `Box::<T>::new` does for ZSTs and use
297        // a properly-aligned dangling pointer. `copy_nonoverlapping` of zero
298        // bytes is a no-op and accepts any aligned pointer, and `Box::<T>`
299        // for a ZST never calls the deallocator.
300        //
301        // The `align as *mut u8` cast produces a pointer with no provenance;
302        // under strict provenance this is only sound for zero-sized accesses,
303        // which is exactly the regime we use it in. Once MSRV reaches 1.84
304        // this can be replaced with `ptr::without_provenance_mut(align)`.
305        let dst: *mut u8 = if value_layout.size() == 0 {
306            value_layout.align() as *mut u8
307        } else {
308            // SAFETY: `value_layout` is non-zero here.
309            let p = unsafe { alloc::alloc(value_layout) };
310            if p.is_null() {
311                alloc::handle_alloc_error(value_layout);
312            }
313            p
314        };
315
316        // SAFETY: When `value_layout.size() > 0`, `dst` was just allocated
317        // for `value_layout` and `value_ptr` points at a live value of that
318        // layout in a different allocation, so the regions cannot overlap.
319        // When `value_layout.size() == 0`, this is a no-op and the aligned
320        // dangling pointers are valid for zero-sized reads/writes.
321        unsafe {
322            ptr::copy_nonoverlapping(value_ptr, dst, value_layout.size());
323        }
324
325        // Mark the slot as already-dropped BEFORE handing ownership over, so
326        // that `Drop for ThinBoxSubscriber` only runs the deallocation path
327        // and does not double-drop the value we just moved out.
328        self.mark_subscriber_dropped();
329
330        // SAFETY: `dst` and `vptr` were paired together from the same trait
331        // object, so reassembling them yields a valid `*mut dyn Subscriber<Ep>`
332        // pointing at storage compatible with what `Box<dyn Subscriber<Ep>>`
333        // expects: either a global-allocator slot of `value_layout` (non-ZST)
334        // or an aligned dangling pointer (ZST), matching `Box::<T>::new`'s
335        // own conventions in both cases.
336        let fat_ptr = unsafe {
337            mem::transmute::<(*const (), *const ()), *mut dyn Subscriber<Ep>>((
338                dst.cast::<()>(),
339                vptr,
340            ))
341        };
342        Some(unsafe { Box::from_raw(fat_ptr) })
343    }
344
345    /// Drops the subscriber in place and marks the slot so subsequent
346    /// `try_deref_mut` calls return `None`.
347    pub(crate) fn drop_in_place(&mut self) {
348        if self.is_subscriber_dropped() {
349            return;
350        }
351
352        // The order matters: we mark the slot BEFORE running the destructor so
353        // that even if `T::drop` triggers re-entrancy (e.g. it ends up calling
354        // back through this `ThinBoxSubscriber`), `try_deref_mut` will refuse
355        // to hand out a reference to a half-destructed value.
356        self.mark_subscriber_dropped();
357
358        // SAFETY: We just verified `raw_fd != -1` above, so the subscriber has
359        // not already been dropped. We use the returned reference only to obtain
360        // a `*mut` for `ptr::drop_in_place` -- no method is invoked on it after
361        // the destructor runs, so the `# Safety` contract of `deref_mut` is
362        // upheld even though `raw_fd` has just been set to `-1`.
363        let value = unsafe { self.deref_mut() };
364        let value_ptr = value as *mut _;
365        unsafe { ptr::drop_in_place(value_ptr) };
366    }
367}
368
369impl<Ep> From<Box<dyn Subscriber<Ep>>> for ThinBoxSubscriber<Ep>
370where
371    Ep: EventpOps,
372{
373    fn from(value: Box<dyn Subscriber<Ep>>) -> Self {
374        Self::from_box_dyn(value)
375    }
376}
377
378impl<Ep> TryFrom<ThinBoxSubscriber<Ep>> for Box<dyn Subscriber<Ep>>
379where
380    Ep: EventpOps,
381{
382    type Error = ThinBoxSubscriber<Ep>;
383
384    /// Converts a [`ThinBoxSubscriber`] back into a `Box<dyn Subscriber<Ep>>`.
385    ///
386    /// Returns the original `ThinBoxSubscriber` as the error if the
387    /// subscriber has already been dropped in place and is no longer
388    /// recoverable.
389    fn try_from(value: ThinBoxSubscriber<Ep>) -> Result<Self, Self::Error> {
390        if value.is_subscriber_dropped() {
391            return Err(value);
392        }
393        // SAFETY: We just verified the subscriber is still alive, so
394        // `into_box_dyn` is guaranteed to return `Some`.
395        Ok(unsafe { value.into_box_dyn().unwrap_unchecked() })
396    }
397}
398
399impl<Ep: EventpOps> Drop for ThinBoxSubscriber<Ep> {
400    fn drop(&mut self) {
401        struct DropGuard<Ep: EventpOps> {
402            ptr: NonNull<u8>,
403            value_layout: Layout,
404            _marker: PhantomData<dyn Subscriber<Ep>>,
405        }
406
407        impl<Ep: EventpOps> Drop for DropGuard<Ep> {
408            fn drop(&mut self) {
409                unsafe {
410                    // SAFETY: Layout must have been computable if we're in drop.
411                    let (layout, value_offset) = Layout::new::<(usize, usize)>()
412                        .extend(self.value_layout)
413                        .unwrap_unchecked();
414
415                    // SAFETY: `GlobalAlloc` is the allocator of this space and layout is valid.
416                    alloc::dealloc(self.ptr.as_ptr().sub(value_offset), layout);
417                }
418            }
419        }
420
421        let ptr = self.ptr;
422        let is_subscriber_dropped = self.is_subscriber_dropped();
423
424        // SAFETY: We only need a fat pointer here to recover `Layout::for_value`
425        // (which inspects the vtable for size/align, not the data) and to feed
426        // `ptr::drop_in_place` below. We do not invoke any method on the trait
427        // object, so the `deref_mut` contract is upheld in both states.
428        let value = unsafe { self.deref_mut() };
429        let value_ptr = value as *mut _;
430        let value_layout = Layout::for_value(value);
431
432        // `_guard` will deallocate the memory when dropped, even if `drop_in_place` unwinds.
433        let _guard = DropGuard::<Ep> {
434            ptr,
435            value_layout,
436            _marker: PhantomData,
437        };
438        if !is_subscriber_dropped {
439            // SAFETY: The subscriber has not been dropped in place yet, so its
440            // value is still live and must be destructed exactly once here.
441            unsafe { ptr::drop_in_place(value_ptr) };
442        }
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use std::cell::Cell;
449    use std::os::fd::{AsFd, BorrowedFd};
450    use std::panic::{catch_unwind, AssertUnwindSafe};
451    use std::sync::atomic::{AtomicUsize, Ordering};
452
453    use nix::sys::eventfd::{EfdFlags, EventFd};
454
455    use super::*;
456    use crate::epoll::EpollFlags;
457    use crate::subscriber::{Handler, HasInterest};
458    use crate::{Event, Eventp, Interest, Pinned};
459
460    /// A subscriber backed by a real `eventfd`, parameterized by alignment via
461    /// a zero-sized `repr(align(N))` tag and an arbitrary payload `P`.
462    ///
463    /// `Align` controls the resulting `align_of::<TestSub<_, _>>()` so we can
464    /// exercise the offset-padding logic between the (raw_fd, vptr) header and
465    /// the value slot.
466    #[repr(C)]
467    struct TestSub<Align: Copy, P> {
468        _align: Align,
469        eventfd: EventFd,
470        interest: Cell<Interest>,
471        on_handle: P,
472        on_drop: DropTracker,
473    }
474
475    /// Tracks whether the subscriber's destructor has run, and optionally
476    /// panics from inside `Drop` to exercise the `DropGuard` path.
477    struct DropTracker {
478        counter: &'static AtomicUsize,
479        panic_in_drop: bool,
480    }
481
482    impl Drop for DropTracker {
483        fn drop(&mut self) {
484            self.counter.fetch_add(1, Ordering::SeqCst);
485            if self.panic_in_drop {
486                panic!("intentional panic from DropTracker");
487            }
488        }
489    }
490
491    impl<Align: Copy, P> AsFd for TestSub<Align, P> {
492        fn as_fd(&self) -> BorrowedFd<'_> {
493            self.eventfd.as_fd()
494        }
495    }
496
497    impl<Align: Copy, P> HasInterest for TestSub<Align, P> {
498        fn interest(&self) -> &Cell<Interest> {
499            &self.interest
500        }
501    }
502
503    impl<Align, P> Handler<Eventp> for TestSub<Align, P>
504    where
505        Align: Copy,
506        P: FnMut(),
507    {
508        fn handle(&mut self, _event: Event, _eventp: Pinned<'_, Eventp>) {
509            (self.on_handle)();
510        }
511    }
512
513    fn new_eventfd() -> EventFd {
514        EventFd::from_flags(EfdFlags::EFD_CLOEXEC | EfdFlags::EFD_NONBLOCK).unwrap()
515    }
516
517    fn make_sub<Align: Copy + Default, P: FnMut()>(
518        on_handle: P,
519        counter: &'static AtomicUsize,
520    ) -> TestSub<Align, P> {
521        TestSub {
522            _align: Align::default(),
523            eventfd: new_eventfd(),
524            interest: Cell::new(Interest::new(EpollFlags::empty())),
525            on_handle,
526            on_drop: DropTracker {
527                counter,
528                panic_in_drop: false,
529            },
530        }
531    }
532
533    /// Counter helper: each test uses its own static so tests can run in
534    /// parallel without stepping on each other.
535    macro_rules! drop_counter {
536        () => {{
537            static COUNTER: AtomicUsize = AtomicUsize::new(0);
538            COUNTER.store(0, Ordering::SeqCst);
539            &COUNTER
540        }};
541    }
542
543    #[test]
544    fn new_then_drop_runs_destructor_exactly_once() {
545        let counter = drop_counter!();
546        let sub = make_sub::<(), _>(|| {}, counter);
547
548        let thin = ThinBoxSubscriber::<Eventp>::new(sub);
549        assert_eq!(counter.load(Ordering::SeqCst), 0);
550
551        drop(thin);
552        assert_eq!(counter.load(Ordering::SeqCst), 1);
553    }
554
555    #[test]
556    fn raw_fd_ref_returns_subscribers_fd() {
557        let counter = drop_counter!();
558        let sub = make_sub::<(), _>(|| {}, counter);
559        let expected_fd = sub.eventfd.as_fd().as_raw_fd();
560
561        let thin = ThinBoxSubscriber::<Eventp>::new(sub);
562        assert_eq!(*thin.raw_fd_ref(), expected_fd);
563    }
564
565    #[test]
566    fn try_deref_mut_dispatches_to_handler() {
567        let counter = drop_counter!();
568        let call_count = std::rc::Rc::new(Cell::new(0u32));
569        let cc = call_count.clone();
570        let sub = make_sub::<(), _>(move || cc.set(cc.get() + 1), counter);
571
572        let mut thin = ThinBoxSubscriber::<Eventp>::new(sub);
573
574        // Build a fresh `Eventp` purely to satisfy the `Pinned` parameter.
575        // `handle()` here only calls `on_handle`, never touching the reactor.
576        let mut ep = Eventp::default();
577        // SAFETY: `ep` lives until end of scope and is never moved afterward.
578        let pinned = Pinned(unsafe { std::pin::Pin::new_unchecked(&mut ep) });
579
580        let s = thin.try_deref_mut().expect("must deref while alive");
581        s.handle(Event::new(EpollFlags::empty()), pinned);
582        assert_eq!(call_count.get(), 1);
583    }
584
585    #[test]
586    fn drop_in_place_marks_slot_and_runs_destructor() {
587        let counter = drop_counter!();
588        let sub = make_sub::<(), _>(|| {}, counter);
589
590        let mut thin = ThinBoxSubscriber::<Eventp>::new(sub);
591        assert!(!thin.is_subscriber_dropped());
592
593        thin.drop_in_place();
594        assert_eq!(counter.load(Ordering::SeqCst), 1);
595        assert!(thin.is_subscriber_dropped());
596        assert!(thin.try_deref_mut().is_none());
597
598        // Dropping the thin pointer afterwards must NOT run the destructor again.
599        drop(thin);
600        assert_eq!(counter.load(Ordering::SeqCst), 1);
601    }
602
603    #[test]
604    fn drop_in_place_is_idempotent() {
605        let counter = drop_counter!();
606        let sub = make_sub::<(), _>(|| {}, counter);
607
608        let mut thin = ThinBoxSubscriber::<Eventp>::new(sub);
609        thin.drop_in_place();
610        thin.drop_in_place();
611        thin.drop_in_place();
612        assert_eq!(counter.load(Ordering::SeqCst), 1);
613    }
614
615    #[test]
616    fn drop_guard_releases_heap_when_destructor_panics() {
617        let counter = drop_counter!();
618
619        let sub = TestSub::<(), _> {
620            _align: (),
621            eventfd: new_eventfd(),
622            interest: Cell::new(Interest::new(EpollFlags::empty())),
623            on_handle: || {},
624            on_drop: DropTracker {
625                counter,
626                panic_in_drop: true,
627            },
628        };
629
630        let thin = ThinBoxSubscriber::<Eventp>::new(sub);
631
632        // Drop must unwind, but the `DropGuard` inside `Drop for
633        // ThinBoxSubscriber` should still release the heap allocation. If it
634        // didn't, Miri / a leak sanitizer would catch it; we at least verify
635        // the destructor counter ticks exactly once.
636        let result = catch_unwind(AssertUnwindSafe(move || drop(thin)));
637        assert!(result.is_err(), "drop must propagate the panic");
638        assert_eq!(counter.load(Ordering::SeqCst), 1);
639    }
640
641    /// Builds and round-trips a subscriber whose alignment is `$align`.
642    /// Exercises the padding the layout code inserts between the (raw_fd,
643    /// vptr) header and the value when `align_of::<T>() > align_of::<usize>()`.
644    macro_rules! align_roundtrip_test {
645        ($name:ident, $align:literal) => {
646            #[test]
647            fn $name() {
648                #[derive(Copy, Clone, Default)]
649                #[repr(align($align))]
650                struct A;
651
652                let counter = drop_counter!();
653                let sub = make_sub::<A, _>(|| {}, counter);
654                let expected_fd = sub.eventfd.as_fd().as_raw_fd();
655
656                assert!(std::mem::align_of::<TestSub<A, fn()>>() >= $align);
657
658                let mut thin = ThinBoxSubscriber::<Eventp>::new(sub);
659                assert_eq!(*thin.raw_fd_ref(), expected_fd);
660
661                // Dispatching through the vtable must reach the right value
662                // even when there is padding between the header and the data.
663                let mut ep = Eventp::default();
664                // SAFETY: `ep` lives until end of scope and is never moved.
665                let pinned = Pinned(unsafe { std::pin::Pin::new_unchecked(&mut ep) });
666                let s = thin.try_deref_mut().unwrap();
667                s.handle(Event::new(EpollFlags::empty()), pinned);
668
669                drop(thin);
670                assert_eq!(counter.load(Ordering::SeqCst), 1);
671            }
672        };
673    }
674
675    align_roundtrip_test!(roundtrip_align_8, 8);
676    align_roundtrip_test!(roundtrip_align_16, 16);
677    align_roundtrip_test!(roundtrip_align_32, 32);
678    align_roundtrip_test!(roundtrip_align_64, 64);
679
680    #[test]
681    fn from_box_dyn_matches_new() {
682        let counter = drop_counter!();
683        let sub = make_sub::<(), _>(|| {}, counter);
684        let expected_fd = sub.eventfd.as_fd().as_raw_fd();
685
686        let boxed: Box<dyn Subscriber<Eventp>> = Box::new(sub);
687        let thin = ThinBoxSubscriber::<Eventp>::from_box_dyn(boxed);
688        assert_eq!(*thin.raw_fd_ref(), expected_fd);
689
690        drop(thin);
691        assert_eq!(counter.load(Ordering::SeqCst), 1);
692    }
693
694    #[test]
695    fn from_impl_delegates_to_from_box_dyn() {
696        let counter = drop_counter!();
697        let sub = make_sub::<(), _>(|| {}, counter);
698
699        let boxed: Box<dyn Subscriber<Eventp>> = Box::new(sub);
700        let thin: ThinBoxSubscriber<Eventp> = boxed.into();
701        drop(thin);
702        assert_eq!(counter.load(Ordering::SeqCst), 1);
703    }
704
705    /// A subscriber whose `AsFd::as_fd` always panics. Used to verify that
706    /// `ThinBoxSubscriber::{new, from_box_dyn}` read the fd *before*
707    /// allocating, so that an unwind cannot leave a partially-initialized
708    /// heap slot for `Drop` to crash on.
709    ///
710    /// `Drop` is instrumented so we can assert that the value itself was
711    /// dropped exactly once on unwind (not zero times -> leak, and not twice
712    /// -> double-drop / UB).
713    struct PanickingFdSub {
714        interest: Cell<Interest>,
715        drops: &'static AtomicUsize,
716    }
717
718    impl PanickingFdSub {
719        fn new(drops: &'static AtomicUsize) -> Self {
720            Self {
721                interest: Cell::new(Interest::new(EpollFlags::empty())),
722                drops,
723            }
724        }
725    }
726
727    impl Drop for PanickingFdSub {
728        fn drop(&mut self) {
729            self.drops.fetch_add(1, Ordering::SeqCst);
730        }
731    }
732
733    impl AsFd for PanickingFdSub {
734        fn as_fd(&self) -> BorrowedFd<'_> {
735            panic!("intentional panic from PanickingFdSub::as_fd");
736        }
737    }
738
739    impl HasInterest for PanickingFdSub {
740        fn interest(&self) -> &Cell<Interest> {
741            &self.interest
742        }
743    }
744
745    impl Handler<Eventp> for PanickingFdSub {
746        fn handle(&mut self, _event: Event, _eventp: Pinned<'_, Eventp>) {}
747    }
748
749    /// Regression test for the partial-construction unsoundness: if
750    /// `value.as_fd()` panics inside `ThinBoxSubscriber::new`, the panic must
751    /// propagate cleanly -- without segfaulting from a `Drop` that runs over
752    /// uninitialized memory, and without leaking `value`.
753    #[test]
754    fn new_does_not_segfault_when_as_fd_panics() {
755        let drops = drop_counter!();
756        let sub = PanickingFdSub::new(drops);
757
758        let result = catch_unwind(AssertUnwindSafe(|| {
759            let _ = ThinBoxSubscriber::<Eventp>::new(sub);
760        }));
761        assert!(result.is_err(), "as_fd panic must propagate");
762        // `sub` was moved into `new`, so its Drop must run exactly once on
763        // unwind. If the fix regresses (heap allocated before reading the fd),
764        // either the process segfaults or this counter ends up at 0.
765        assert_eq!(drops.load(Ordering::SeqCst), 1);
766    }
767
768    #[test]
769    fn into_box_dyn_round_trips_value_and_runs_destructor_once() {
770        let counter = drop_counter!();
771        let sub = make_sub::<(), _>(|| {}, counter);
772        let expected_fd = sub.eventfd.as_fd().as_raw_fd();
773
774        let thin = ThinBoxSubscriber::<Eventp>::new(sub);
775        let boxed = thin.into_box_dyn().expect("alive subscriber must convert");
776        // Conversion itself must not run the destructor.
777        assert_eq!(counter.load(Ordering::SeqCst), 0);
778        assert_eq!(boxed.as_fd().as_raw_fd(), expected_fd);
779
780        drop(boxed);
781        assert_eq!(counter.load(Ordering::SeqCst), 1);
782    }
783
784    /// A zero-sized subscriber. Exercises the ZST branch of `into_box_dyn`,
785    /// which must NOT call `alloc::alloc` with a zero layout (UB) and must
786    /// produce a `Box` whose deallocation path matches `Box::<Self>::new`.
787    ///
788    /// The subscriber holds no state, so it cannot own an `EventFd`; we use
789    /// stdin as a borrowed fd just to satisfy `AsFd`, and a leaked
790    /// `Cell<Interest>` to satisfy `HasInterest`. Neither is exercised here.
791    struct ZstSub;
792
793    impl AsFd for ZstSub {
794        fn as_fd(&self) -> BorrowedFd<'_> {
795            // SAFETY: stdin is open for the lifetime of the process; we only
796            // need a borrowed fd token and never perform I/O on it.
797            unsafe { BorrowedFd::borrow_raw(0) }
798        }
799    }
800
801    impl HasInterest for ZstSub {
802        fn interest(&self) -> &Cell<Interest> {
803            // `Cell` is not `Sync`, so it cannot live in a `static`. Leak a
804            // `Box` once instead; the resulting `&'static Cell<Interest>` is
805            // good for the rest of the process. Per-call leaks would be
806            // wasteful, so cache the pointer in a `OnceLock<usize>`.
807            use std::sync::OnceLock;
808            static SLOT: OnceLock<usize> = OnceLock::new();
809            let addr = *SLOT.get_or_init(|| {
810                let leaked: &'static Cell<Interest> =
811                    Box::leak(Box::new(Cell::new(Interest::new(EpollFlags::empty()))));
812                leaked as *const Cell<Interest> as usize
813            });
814            // SAFETY: `addr` was produced from `Box::leak`, which yields a
815            // valid pointer with `'static` lifetime. `Cell` is `!Sync` but
816            // we only ever read `addr` here, not the cell's contents, and
817            // the test never accesses it concurrently.
818            unsafe { &*(addr as *const Cell<Interest>) }
819        }
820    }
821
822    impl Handler<Eventp> for ZstSub {
823        fn handle(&mut self, _event: Event, _eventp: Pinned<'_, Eventp>) {}
824    }
825
826    #[test]
827    fn into_box_dyn_handles_zst_subscriber() {
828        assert_eq!(std::mem::size_of::<ZstSub>(), 0);
829
830        let thin = ThinBoxSubscriber::<Eventp>::new(ZstSub);
831        let boxed = thin.into_box_dyn().expect("alive ZST must convert");
832        // Dropping the `Box` must not call the global allocator with a
833        // zero-sized layout. If it does, Miri / a sanitizer will flag it.
834        drop(boxed);
835    }
836
837    #[test]
838    fn into_box_dyn_round_trips_through_thin_again() {
839        let counter = drop_counter!();
840        let sub = make_sub::<(), _>(|| {}, counter);
841        let expected_fd = sub.eventfd.as_fd().as_raw_fd();
842
843        let thin1 = ThinBoxSubscriber::<Eventp>::new(sub);
844        let boxed = thin1.into_box_dyn().unwrap();
845        let thin2 = ThinBoxSubscriber::<Eventp>::from_box_dyn(boxed);
846        assert_eq!(*thin2.raw_fd_ref(), expected_fd);
847
848        drop(thin2);
849        assert_eq!(counter.load(Ordering::SeqCst), 1);
850    }
851
852    #[test]
853    fn into_box_dyn_returns_none_after_drop_in_place() {
854        let counter = drop_counter!();
855        let sub = make_sub::<(), _>(|| {}, counter);
856
857        let mut thin = ThinBoxSubscriber::<Eventp>::new(sub);
858        thin.drop_in_place();
859        assert_eq!(counter.load(Ordering::SeqCst), 1);
860
861        assert!(thin.into_box_dyn().is_none());
862        // Returning `None` must not run the destructor a second time.
863        assert_eq!(counter.load(Ordering::SeqCst), 1);
864    }
865
866    #[test]
867    fn try_from_thin_returns_err_after_drop_in_place() {
868        let counter = drop_counter!();
869        let sub = make_sub::<(), _>(|| {}, counter);
870
871        let mut thin = ThinBoxSubscriber::<Eventp>::new(sub);
872        thin.drop_in_place();
873
874        let recovered: Result<Box<dyn Subscriber<Eventp>>, _> = thin.try_into();
875        assert!(recovered.is_err());
876        // The destructor must still have run exactly once (from drop_in_place).
877        assert_eq!(counter.load(Ordering::SeqCst), 1);
878    }
879
880    /// Same regression check, but for the `from_box_dyn` path.
881    #[test]
882    fn from_box_dyn_does_not_segfault_when_as_fd_panics() {
883        let drops = drop_counter!();
884        let boxed: Box<dyn Subscriber<Eventp>> = Box::new(PanickingFdSub::new(drops));
885
886        let result = catch_unwind(AssertUnwindSafe(|| {
887            let _ = ThinBoxSubscriber::<Eventp>::from_box_dyn(boxed);
888        }));
889        assert!(result.is_err(), "as_fd panic must propagate");
890        assert_eq!(drops.load(Ordering::SeqCst), 1);
891    }
892}