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}