Skip to main content

rusteron_client/
lib.rs

1#![allow(improper_ctypes_definitions)]
2#![allow(non_upper_case_globals)]
3#![allow(non_camel_case_types)]
4#![allow(non_snake_case)]
5#![allow(clippy::all)]
6#![allow(unused_unsafe)]
7#![allow(unused_variables)]
8#![allow(unsafe_op_in_unsafe_fn)]
9#![doc = include_str!("../README.md")]
10//! # Features
11//!
12//! - **`static`**: When enabled, this feature statically links the Aeron C code.
13//!   By default, the library uses dynamic linking to the Aeron C libraries.
14//! - **`backtrace`**: When enabled will log a backtrace for each AeronCError
15//! - **`extra-logging`**: When enabled will log when resource is created and destroyed. Useful if you're seeing a segfault due to a resource being closed
16//! - **`log-c-bindings`**: When enabled will log every C binding call with arguments and return values. Useful for debugging FFI interactions
17//! - **`precompile`**: When enabled will use precompiled C code instead of requiring cmake and java to be installed
18
19#[allow(improper_ctypes_definitions)]
20#[allow(unpredictable_function_pointer_comparisons)]
21pub mod bindings {
22    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
23}
24
25use bindings::*;
26use std::time::Duration;
27
28/// Result codes returned by [`AeronPublication::offer`] / `try_claim` (Aeron `aeronc.h`). A
29/// positive value is the resulting log position; the negatives classify the failure.
30///
31/// **Fatal** (stop offering): [`PUBLICATION_CLOSED`], [`PUBLICATION_MAX_POSITION_EXCEEDED`],
32/// [`PUBLICATION_ERROR`]. **Transient** (retry, ideally with an idle strategy):
33/// [`PUBLICATION_BACK_PRESSURED`], [`PUBLICATION_NOT_CONNECTED`], [`PUBLICATION_ADMIN_ACTION`].
34/// Mirrors how Aeron's own samples (`BasicPublisher` / `basic_publisher.c`) classify offer results.
35pub const PUBLICATION_NOT_CONNECTED: i64 = bindings::AERON_PUBLICATION_NOT_CONNECTED as i64;
36pub const PUBLICATION_BACK_PRESSURED: i64 = bindings::AERON_PUBLICATION_BACK_PRESSURED as i64;
37pub const PUBLICATION_ADMIN_ACTION: i64 = bindings::AERON_PUBLICATION_ADMIN_ACTION as i64;
38pub const PUBLICATION_CLOSED: i64 = bindings::AERON_PUBLICATION_CLOSED as i64;
39pub const PUBLICATION_MAX_POSITION_EXCEEDED: i64 = bindings::AERON_PUBLICATION_MAX_POSITION_EXCEEDED as i64;
40pub const PUBLICATION_ERROR: i64 = bindings::AERON_PUBLICATION_ERROR as i64;
41
42include!(concat!(env!("OUT_DIR"), "/aeron.rs"));
43include!(concat!(env!("OUT_DIR"), "/aeron_custom.rs"));
44
45// ---------------------------------------------------------------------------
46// Idle strategies for poll loops. Pure-Rust port of Aeron's `IdleStrategy`
47// (Java/C++): pass the work count from the last `poll` to `idle(work_count)`; it returns
48// immediately when work was done, otherwise backs off the CPU (spin / yield / sleep).
49// ---------------------------------------------------------------------------
50
51/// Back off a poll loop when the last cycle did no work. Mirrors Aeron's `IdleStrategy`:
52/// `idle(work_count)` returns immediately when `work_count > 0`, otherwise spins / yields /
53/// sleeps depending on the implementation.
54pub trait IdleStrategy {
55    /// Called with the work/fragment count from the last operation. Implementations return
56    /// immediately when `work_count > 0` (work was done) and back off otherwise.
57    fn idle(&mut self, work_count: i32);
58
59    /// Reset accumulated backoff state after a productive period. Default: no-op.
60    fn reset(&mut self) {}
61}
62
63/// Spin with a CPU pause hint. Lowest latency, pins a core. (Aeron `BusySpinIdleStrategy`.)
64pub struct BusySpinIdleStrategy;
65impl BusySpinIdleStrategy {
66    pub fn new() -> Self {
67        Self
68    }
69}
70impl Default for BusySpinIdleStrategy {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75impl IdleStrategy for BusySpinIdleStrategy {
76    #[inline]
77    fn idle(&mut self, work_count: i32) {
78        if work_count > 0 {
79            return;
80        }
81        std::hint::spin_loop();
82    }
83}
84
85/// No-op — never yields the core. (Aeron `NoOpIdleStrategy`.)
86pub struct NoOpIdleStrategy;
87impl NoOpIdleStrategy {
88    pub fn new() -> Self {
89        Self
90    }
91}
92impl Default for NoOpIdleStrategy {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97impl IdleStrategy for NoOpIdleStrategy {
98    #[inline]
99    fn idle(&mut self, _work_count: i32) {}
100}
101
102/// Yield the OS thread when idle. Lower CPU than busy-spin, slightly higher latency.
103/// (Aeron `YieldingIdleStrategy`.)
104pub struct YieldingIdleStrategy;
105impl YieldingIdleStrategy {
106    pub fn new() -> Self {
107        Self
108    }
109}
110impl Default for YieldingIdleStrategy {
111    fn default() -> Self {
112        Self::new()
113    }
114}
115impl IdleStrategy for YieldingIdleStrategy {
116    #[inline]
117    fn idle(&mut self, work_count: i32) {
118        if work_count > 0 {
119            return;
120        }
121        std::thread::yield_now();
122    }
123}
124
125/// Sleep for a fixed duration when idle. (Aeron `SleepingIdleStrategy`.)
126pub struct SleepingIdleStrategy {
127    duration: Duration,
128}
129impl SleepingIdleStrategy {
130    pub fn new(duration: Duration) -> Self {
131        Self { duration }
132    }
133}
134impl IdleStrategy for SleepingIdleStrategy {
135    #[inline]
136    fn idle(&mut self, work_count: i32) {
137        if work_count == 0 {
138            std::thread::sleep(self.duration);
139        }
140    }
141}
142
143/// Adaptive backoff: spin a few times, then yield a few times, then sleep with an exponentially
144/// growing park up to a max. A good general-purpose strategy. (Aeron `BackoffIdleStrategy`.)
145///
146/// Defaults match Aeron: `max_spins = 10`, `max_yields = 20`, `min_park = 1µs`, `max_park = 1ms`.
147pub struct BackoffIdleStrategy {
148    max_spins: i64,
149    max_yields: i64,
150    min_park: Duration,
151    max_park: Duration,
152    spins: i64,
153    yields: i64,
154    park: Duration,
155    state: u8,
156}
157
158const BACKOFF_NOT_IDLE: u8 = 0;
159const BACKOFF_SPINNING: u8 = 1;
160const BACKOFF_YIELDING: u8 = 2;
161const BACKOFF_PARKING: u8 = 3;
162
163impl BackoffIdleStrategy {
164    /// Defaults match Aeron: 10 spins, 20 yields, park 1µs..1ms.
165    pub fn new() -> Self {
166        Self::with(10, 20, Duration::from_micros(1), Duration::from_millis(1))
167    }
168
169    /// Full control over the backoff parameters.
170    pub fn with(max_spins: i64, max_yields: i64, min_park: Duration, max_park: Duration) -> Self {
171        Self {
172            max_spins,
173            max_yields,
174            min_park,
175            max_park,
176            spins: 0,
177            yields: 0,
178            park: min_park,
179            state: BACKOFF_NOT_IDLE,
180        }
181    }
182
183    #[inline]
184    fn idle_one(&mut self) {
185        match self.state {
186            BACKOFF_NOT_IDLE => {
187                self.state = BACKOFF_SPINNING;
188                self.spins += 1;
189            }
190            BACKOFF_SPINNING => {
191                std::hint::spin_loop();
192                self.spins += 1;
193                if self.spins > self.max_spins {
194                    self.state = BACKOFF_YIELDING;
195                    self.yields = 0;
196                }
197            }
198            BACKOFF_YIELDING => {
199                self.yields += 1;
200                if self.yields > self.max_yields {
201                    self.state = BACKOFF_PARKING;
202                    self.park = self.min_park;
203                } else {
204                    std::thread::yield_now();
205                }
206            }
207            _ => {
208                // PARKING — sleep then double the park period up to the max.
209                std::thread::sleep(self.park);
210                self.park = std::cmp::min(self.park.saturating_mul(2), self.max_park);
211            }
212        }
213    }
214}
215
216impl Default for BackoffIdleStrategy {
217    fn default() -> Self {
218        Self::new()
219    }
220}
221
222impl IdleStrategy for BackoffIdleStrategy {
223    #[inline]
224    fn idle(&mut self, work_count: i32) {
225        if work_count > 0 {
226            self.reset();
227        } else {
228            self.idle_one();
229        }
230    }
231
232    fn reset(&mut self) {
233        self.spins = 0;
234        self.yields = 0;
235        self.park = self.min_park;
236        self.state = BACKOFF_NOT_IDLE;
237    }
238}
239
240// rustdoc sets cfg(doctest), not cfg(test), so this compiles the root README's code blocks.
241#[cfg(doctest)]
242#[doc = include_str!("../../README.md")]
243mod readme_doctests {}
244
245#[cfg(doctest)]
246#[doc = include_str!("../../docs/mdc_mds_guide.md")]
247mod mdc_mds_guide_doctests {}
248
249#[cfg(doctest)]
250#[doc = include_str!("../../docs/media_driver_configuration_and_backpressure_guide.md")]
251mod media_driver_guide_doctests {}
252
253#[cfg(test)]
254mod idle_strategy_tests {
255    use super::*;
256    use std::time::Instant;
257
258    #[test]
259    fn idle_strategy_kind_round_trips_through_context() {
260        let ctx = AeronContext::new().unwrap();
261        for (kind, name) in [
262            (AeronIdleStrategyKind::Sleeping, "sleeping"),
263            (AeronIdleStrategyKind::Yielding, "yield"),
264            (AeronIdleStrategyKind::BusySpin, "spin"),
265            (AeronIdleStrategyKind::NoOp, "noop"),
266            (AeronIdleStrategyKind::Backoff, "backoff"),
267        ] {
268            ctx.set_idle_strategy_kind(kind).unwrap();
269            assert_eq!(name, ctx.get_idle_strategy(), "kind {kind:?}");
270        }
271    }
272
273    #[test]
274    fn rust_backoff_idles_correctly() {
275        let mut idle = BackoffIdleStrategy::new();
276        // work done -> effectively free
277        idle.idle(1);
278        // no work: walk spin -> yield -> park without crashing; parking must take real time
279        let start = Instant::now();
280        for _ in 0..100 {
281            idle.idle(0);
282        }
283        assert!(
284            start.elapsed() >= Duration::from_micros(50),
285            "backoff should have parked"
286        );
287    }
288
289    #[test]
290    fn rust_stateless_strategies_are_safe() {
291        let strategies: Vec<Box<dyn IdleStrategy>> = vec![
292            Box::new(BusySpinIdleStrategy::default()),
293            Box::new(YieldingIdleStrategy::default()),
294            Box::new(NoOpIdleStrategy::default()),
295        ];
296        for mut s in strategies {
297            s.idle(1);
298            s.idle(0);
299        }
300    }
301
302    #[test]
303    fn rust_backoff_matches_c_defaults() {
304        // Verify BackoffIdleStrategy uses Aeron's canonical backoff defaults
305        // (AERON_IDLE_STRATEGY_BACKOFF_*: 10 spins, 20 yields, park 1µs..1ms).
306        let idle = BackoffIdleStrategy::new();
307        assert_eq!(idle.max_spins, 10, "max_spins should be 10");
308        assert_eq!(idle.max_yields, 20, "max_yields should be 20");
309        assert_eq!(idle.min_park, Duration::from_micros(1), "min_park should be 1µs");
310        assert_eq!(idle.max_park, Duration::from_millis(1), "max_park should be 1ms");
311    }
312
313    #[test]
314    fn busy_spin_and_yield_return_on_work() {
315        let mut s = BusySpinIdleStrategy;
316        s.idle(5); // work done -> must not block
317        s.idle(0); // no work -> just a pause hint, returns immediately
318
319        let mut y = YieldingIdleStrategy;
320        y.idle(3); // work done
321        y.idle(0); // yields once, returns
322    }
323
324    #[test]
325    fn no_op_never_blocks() {
326        let mut s = NoOpIdleStrategy;
327        for _ in 0..1000 {
328            s.idle(0);
329        }
330    }
331
332    #[test]
333    fn sleeping_idle_only_sleeps_when_idle() {
334        let mut s = SleepingIdleStrategy::new(Duration::from_micros(10));
335        let t = std::time::Instant::now();
336        s.idle(1); // work done -> no sleep
337        assert!(t.elapsed() < Duration::from_millis(1));
338
339        let t = std::time::Instant::now();
340        s.idle(0); // idle -> sleeps ~10µs
341        assert!(t.elapsed() >= Duration::from_micros(10));
342    }
343
344    #[test]
345    fn backoff_resets_on_work_and_progresses_to_park() {
346        let mut s = BackoffIdleStrategy::with(2, 2, Duration::from_micros(1), Duration::from_millis(1));
347        // Work done -> resets state (no backoff progression).
348        s.idle(1);
349        assert_eq!(s.state, BACKOFF_NOT_IDLE);
350        assert_eq!(s.spins, 0);
351
352        // Spin a few, then yield, then park: driving it enough with 0 work must reach PARKING.
353        for _ in 0..1000 {
354            s.idle(0);
355        }
356        assert_eq!(s.state, BACKOFF_PARKING);
357        // Park period grows but is capped at max_park.
358        assert!(s.park <= Duration::from_millis(1));
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    use crate::test_alloc::current_allocs;
366    use hdrhistogram::Histogram;
367    use log::{error, info};
368    use rusteron_media_driver::AeronDriverContext;
369    use serial_test::serial;
370    use std::error;
371    use std::error::Error;
372    use std::io::Write;
373    use std::os::raw::c_int;
374    use std::sync::Arc;
375    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
376    use std::thread::{JoinHandle, sleep};
377    use std::time::{Duration, Instant};
378
379    #[test]
380    fn fragment_assembler_keeps_delegate_alive() -> Result<(), Box<dyn Error>> {
381        struct Delegate(Arc<AtomicBool>);
382        impl Drop for Delegate {
383            fn drop(&mut self) {
384                self.0.store(true, Ordering::SeqCst);
385            }
386        }
387        impl AeronFragmentHandlerCallback for Delegate {
388            fn handle_aeron_fragment_handler(&mut self, _: &[u8], _: AeronHeader) {}
389        }
390
391        let dropped = Arc::new(AtomicBool::new(false));
392        let (assembler, delegate) = Handler::with_fragment_assembler(Delegate(dropped.clone()))?;
393        drop(delegate);
394        assert!(
395            !dropped.load(Ordering::SeqCst),
396            "delegate freed while the assembler still points at it"
397        );
398        drop(assembler);
399        assert!(dropped.load(Ordering::SeqCst));
400        Ok(())
401    }
402
403    /// Client-level handlers registered from temporaries stay alive until the client
404    /// closes, and the close handler fires during that close.
405    #[test]
406    #[serial]
407    fn client_keeps_counter_and_close_handlers_alive() -> Result<(), Box<dyn Error>> {
408        struct Watched {
409            closed: Arc<AtomicUsize>,
410            dropped: Arc<AtomicUsize>,
411        }
412        impl Drop for Watched {
413            fn drop(&mut self) {
414                self.dropped.fetch_add(1, Ordering::SeqCst);
415            }
416        }
417        impl AeronCloseClientCallback for Watched {
418            fn handle_aeron_on_close_client(&mut self) {
419                self.closed.fetch_add(1, Ordering::SeqCst);
420            }
421        }
422        impl AeronAvailableCounterCallback for Watched {
423            fn handle_aeron_on_available_counter(&mut self, _: AeronCountersReader, _: i64, _: i32) {}
424        }
425        impl AeronUnavailableCounterCallback for Watched {
426            fn handle_aeron_on_unavailable_counter(&mut self, _: AeronCountersReader, _: i64, _: i32) {}
427        }
428
429        let media_driver_ctx = AeronDriverContext::new()?;
430        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
431        media_driver_ctx.set_dir_delete_on_start(true)?;
432        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
433        let (stop, driver_handle) =
434            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
435
436        let ctx = AeronContext::new()?;
437        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
438        let aeron = Aeron::new(&ctx)?;
439        aeron.start()?;
440
441        let closed = Arc::new(AtomicUsize::new(0));
442        let dropped = Arc::new(AtomicUsize::new(0));
443        let watched = || {
444            Handler::new(Watched {
445                closed: closed.clone(),
446                dropped: dropped.clone(),
447            })
448        };
449        aeron.add_close_handler(&AeronCloseClientPair::new(Some(&watched()))?)?;
450        aeron.add_available_counter_handler(&AeronAvailableCounterPair::new(Some(&watched()))?)?;
451        aeron.add_unavailable_counter_handler(&AeronUnavailableCounterPair::new(Some(&watched()))?)?;
452        assert_eq!(
453            dropped.load(Ordering::SeqCst),
454            0,
455            "handlers freed while the client still points at them"
456        );
457
458        drop(aeron);
459        assert_eq!(closed.load(Ordering::SeqCst), 1);
460        assert_eq!(dropped.load(Ordering::SeqCst), 3);
461
462        stop.store(true, Ordering::SeqCst);
463        let _ = driver_handle.join().unwrap();
464        Ok(())
465    }
466
467    #[derive(Default, Debug)]
468    struct ErrorCount {
469        error_count: usize,
470    }
471
472    impl AeronErrorHandlerCallback for ErrorCount {
473        fn handle_aeron_error_handler(&mut self, error_code: c_int, msg: &str) {
474            error!("Aeron error {}: {}", error_code, msg);
475            self.error_count += 1;
476        }
477    }
478
479    struct CloseNotificationCount {
480        count: Arc<AtomicUsize>,
481    }
482
483    impl AeronNotificationCallback for CloseNotificationCount {
484        fn handle_aeron_notification(&mut self) {
485            self.count.fetch_add(1, Ordering::SeqCst);
486        }
487    }
488
489    fn running_under_valgrind() -> bool {
490        std::env::var_os("RUSTERON_VALGRIND").is_some()
491    }
492
493    /// A client of its own for a test thread: without `multi-threaded` a handle stays on the
494    /// thread whose client created it.
495    fn thread_client(dir: &str, driver_timeout_ms: u64) -> Result<Aeron, AeronCError> {
496        let ctx = AeronContext::new()?;
497        ctx.set_dir(&dir.into_c_string())?;
498        ctx.set_driver_timeout_ms(driver_timeout_ms)?;
499        let aeron = Aeron::new(&ctx)?;
500        aeron.start()?;
501        Ok(aeron)
502    }
503
504    #[test]
505    fn string_into_keeps_only_valid_written_bytes() -> Result<(), Box<dyn error::Error>> {
506        let builder = AeronUriStringBuilder::new_zeroed_on_heap();
507        builder.init_new()?;
508        let mut dst = String::with_capacity(64);
509        // No media set, so sprint fails before writing anything.
510        assert!(builder.sprint_into(&mut dst).is_err());
511        assert_eq!(dst, "");
512
513        builder.media(Media::Ipc)?.put(c"alias", c"\u{e9}")?;
514        // "aeron:ipc?alias=" is 16 bytes, so 17 bytes of output split the 2-byte char.
515        let mut dst = String::with_capacity(18);
516        builder.sprint_into(&mut dst)?;
517        assert_eq!(dst, "aeron:ipc?alias=");
518        Ok(())
519    }
520
521    #[test]
522    #[serial]
523    fn version_check() -> Result<(), Box<dyn error::Error>> {
524        unsafe {
525            aeron_randomised_int32();
526        }
527        // Let background teardown from previous #[serial] tests settle before taking
528        // the baseline: a driver thread still stopping can emit a captured log line
529        // inside our window, which counts as a live allocation and skews the check.
530        let settle_start = Instant::now();
531        let mut alloc_count = current_allocs();
532        loop {
533            sleep(Duration::from_millis(50));
534            let now = current_allocs();
535            if now == alloc_count || settle_start.elapsed() > Duration::from_secs(2) {
536                alloc_count = now;
537                break;
538            }
539            alloc_count = now;
540        }
541
542        {
543            let major = unsafe { crate::aeron_version_major() };
544            let minor = unsafe { crate::aeron_version_minor() };
545            let patch = unsafe { crate::aeron_version_patch() };
546
547            let cargo_version = "1.53.3";
548            let aeron_version = format!("{}.{}.{}", major, minor, patch);
549            assert_eq!(aeron_version, cargo_version);
550
551            let ctx = AeronContext::new()?;
552            let handler = Handler::new(ErrorCount::default());
553            ctx.set_error_handler(Some(handler.clone()))?;
554
555            assert!(Aeron::epoch_clock() > 0);
556            // the context holds a clone of the handler; dropping both frees the
557            // callback value exactly once (verified by the alloc-count check below)
558        }
559
560        assert!(
561            current_allocs() <= alloc_count,
562            "allocations {} > {alloc_count}",
563            current_allocs()
564        );
565
566        Ok(())
567    }
568
569    #[test]
570    #[serial]
571    fn async_publication_invalid_interface_poll_then_drop() -> Result<(), Box<dyn error::Error>> {
572        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
573
574        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
575        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
576        media_driver_ctx.set_dir_delete_on_start(true)?;
577        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
578        let (stop, driver_handle) =
579            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
580
581        let ctx = AeronContext::new()?;
582        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
583        let error_handler = Handler::new(ErrorCount::default());
584        ctx.set_error_handler(Some(error_handler.clone()))?;
585        let aeron = Aeron::new(&ctx)?;
586        aeron.start()?;
587
588        let channel = String::from("aeron:udp?endpoint=203.0.113.1:54321");
589
590        // Create async publication and subscription pollers on the same invalid channel and
591        // attempt to resolve them. If both are created, try a small send/receive cycle and then exit.
592        let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), 4321)?;
593        let sub_poller =
594            aeron.async_add_subscription(&channel.into_c_string(), 4321, Handlers::NONE, Handlers::NONE)?;
595
596        let mut publication: Option<AeronPublication> = None;
597        let mut subscription: Option<AeronSubscription> = None;
598        let start = Instant::now();
599        while start.elapsed() < Duration::from_secs(2) {
600            if publication.is_none() {
601                match pub_poller.poll() {
602                    Ok(Some(p)) => publication = Some(p),
603                    Ok(None) | Err(_) => {}
604                }
605            }
606            if subscription.is_none() {
607                match sub_poller.poll() {
608                    Ok(Some(s)) => subscription = Some(s),
609                    Ok(None) | Err(_) => {}
610                }
611            }
612            if publication.is_some() && subscription.is_some() {
613                break;
614            }
615            #[cfg(debug_assertions)]
616            std::thread::sleep(Duration::from_millis(10));
617        }
618
619        info!("publication: {:?}", publication);
620        info!("subscription: {:?}", subscription);
621
622        if let (Some(publisher), Some(subscription)) = (publication, subscription) {
623            let payload = b"hello-aeron";
624            let send_start = Instant::now();
625            let mut sent = false;
626            while send_start.elapsed() < Duration::from_millis(500) {
627                let res = publisher.offer_raw(payload, Handlers::NONE);
628                if res >= payload.len() as i64 {
629                    sent = true;
630                    info!("sent {:?}", payload);
631                    break;
632                }
633                std::thread::sleep(Duration::from_millis(10));
634            }
635
636            if sent {
637                let mut got = false;
638                let read_start = Instant::now();
639                while read_start.elapsed() < Duration::from_millis(500) {
640                    let _ = subscription.poll_fn(
641                        |msg, _hdr| {
642                            if msg == payload {
643                                got = true;
644                                info!("received {:?}", payload);
645                            }
646                        },
647                        1024,
648                    );
649                    if got {
650                        break;
651                    }
652                    std::thread::sleep(Duration::from_millis(10));
653                }
654                // We don't assert on got, just exercise the path.
655            }
656        }
657
658        // Shutdown
659        stop.store(true, Ordering::SeqCst);
660        let _ = driver_handle.join().unwrap();
661        Ok(())
662    }
663
664    /// Exercises the additive ergonomics API end-to-end:
665    /// `offer_result_simple`, `try_claim_owned` + `commit`, the `session_id` /
666    /// `stream_id` header accessors, and `status()` on publication/subscription.
667    #[test]
668    #[serial]
669    fn offer_result_and_claim_roundtrip_with_header_accessors() -> Result<(), Box<dyn error::Error>> {
670        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
671
672        let media_driver_ctx = AeronDriverContext::new()?;
673        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
674        media_driver_ctx.set_dir_delete_on_start(true)?;
675        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
676        let (stop, driver_handle) =
677            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
678
679        let ctx = AeronContext::new()?;
680        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
681        let error_handler = Handler::new(ErrorCount::default());
682        ctx.set_error_handler(Some(error_handler.clone()))?;
683        let aeron = Aeron::new(&ctx)?;
684        aeron.start()?;
685
686        let channel = String::from("aeron:ipc");
687        let stream_id: i32 = 9123;
688        let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), stream_id)?;
689        let sub_poller =
690            aeron.async_add_subscription(&channel.into_c_string(), stream_id, Handlers::NONE, Handlers::NONE)?;
691
692        let mut publication: Option<AeronPublication> = None;
693        let mut subscription: Option<AeronSubscription> = None;
694        let start = Instant::now();
695        while start.elapsed() < Duration::from_secs(2) {
696            if publication.is_none() {
697                if let Ok(Some(p)) = pub_poller.poll() {
698                    publication = Some(p);
699                }
700            }
701            if subscription.is_none() {
702                if let Ok(Some(s)) = sub_poller.poll() {
703                    subscription = Some(s);
704                }
705            }
706            if publication.is_some() && subscription.is_some() {
707                break;
708            }
709            #[cfg(debug_assertions)]
710            sleep(Duration::from_millis(10));
711        }
712
713        let (publisher, subscription) = match (publication, subscription) {
714            (Some(p), Some(s)) => (p, s),
715            _ => panic!("publication/subscription did not come up"),
716        };
717
718        // Wait for the IPC images to connect before asserting status.
719        let conn_start = Instant::now();
720        while !publisher.is_connected() && conn_start.elapsed() < Duration::from_secs(2) {
721            #[cfg(debug_assertions)]
722            sleep(Duration::from_millis(10));
723        }
724        assert_eq!(publisher.status(), AeronStatus::Connected);
725
726        // 1) Publish via the Result-returning offer variant.
727        let payload = b"hello-result";
728        let offer_start = Instant::now();
729        let mut offered = false;
730        while offer_start.elapsed() < Duration::from_secs(2) {
731            if let Ok(pos) = publisher.offer(payload) {
732                if pos >= payload.len() as i64 {
733                    offered = true;
734                    break;
735                }
736            }
737            #[cfg(debug_assertions)]
738            sleep(Duration::from_millis(10));
739        }
740        assert!(offered, "offer_result_simple never succeeded");
741
742        // 2) Publish via the RAII claim: write into the claimed buffer and commit.
743        let claim_payload = b"hello-claim";
744        let claim_start = Instant::now();
745        let mut committed = false;
746        while claim_start.elapsed() < Duration::from_secs(2) {
747            if let Ok(mut claim) = publisher.try_claim_owned(claim_payload.len()) {
748                claim.data()[..claim_payload.len()].copy_from_slice(claim_payload);
749                if claim.commit().is_ok() {
750                    committed = true;
751                    break;
752                }
753            }
754            #[cfg(debug_assertions)]
755            sleep(Duration::from_millis(10));
756        }
757        assert!(committed, "try_claim_owned + commit never succeeded");
758
759        // 3) Publish via the zero-alloc gathering offer: header + payload parts must
760        // arrive as ONE contiguous message.
761        let parts_header = b"hdr:";
762        let parts_payload = b"gathered-body";
763        let parts_expected: Vec<u8> = [parts_header.as_slice(), parts_payload.as_slice()].concat();
764        let parts_start = Instant::now();
765        let mut parts_offered = false;
766        while parts_start.elapsed() < Duration::from_secs(2) {
767            if publisher.offer_parts(&[parts_header, parts_payload]).is_ok() {
768                parts_offered = true;
769                break;
770            }
771            #[cfg(debug_assertions)]
772            sleep(Duration::from_millis(10));
773        }
774        assert!(parts_offered, "offer_parts never succeeded");
775        // more parts than the stack iovec capacity must be rejected, not truncated
776        let too_many = [b"x".as_slice(); MAX_OFFER_PARTS + 1];
777        assert!(
778            publisher.offer_parts(&too_many).is_err(),
779            "over-capacity offer_parts must fail"
780        );
781
782        // 4) Receive all three and assert the header accessors.
783        let received_offer = std::cell::Cell::new(false);
784        let received_claim = std::cell::Cell::new(false);
785        let received_parts = std::cell::Cell::new(false);
786        let header_ids = std::cell::Cell::new(Option::<(i32, i32)>::None);
787        let read_start = Instant::now();
788        while read_start.elapsed() < Duration::from_secs(2)
789            && !(received_offer.get() && received_claim.get() && received_parts.get())
790        {
791            let _ = subscription.poll_fn(
792                |msg, header| {
793                    header_ids.set(Some((
794                        header.session_id().unwrap_or(0),
795                        header.stream_id().unwrap_or(0),
796                    )));
797                    if msg == payload {
798                        received_offer.set(true);
799                    }
800                    if msg == claim_payload {
801                        received_claim.set(true);
802                    }
803                    if msg == parts_expected.as_slice() {
804                        received_parts.set(true);
805                    }
806                },
807                1024,
808            );
809            #[cfg(debug_assertions)]
810            sleep(Duration::from_millis(10));
811        }
812        assert!(received_offer.get(), "did not receive offer_result message");
813        assert!(received_claim.get(), "did not receive claim message");
814        assert!(
815            received_parts.get(),
816            "did not receive offer_parts message as one contiguous body"
817        );
818        let (session_id, recv_stream_id) = header_ids.get().expect("no header captured");
819        assert_eq!(recv_stream_id, stream_id, "header stream_id should match");
820        assert_ne!(session_id, 0, "session_id should be populated");
821
822        assert_eq!(subscription.status(), AeronStatus::Connected);
823
824        // Shutdown
825        stop.store(true, Ordering::SeqCst);
826        let _ = driver_handle.join().unwrap();
827        Ok(())
828    }
829    /// A committed claim arrives with its bytes and reserved value, at the position it claimed.
830    #[test]
831    #[serial]
832    fn aeron_claim_commit_roundtrip_is_received() -> Result<(), Box<dyn error::Error>> {
833        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
834
835        let media_driver_ctx = AeronDriverContext::new()?;
836        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
837        media_driver_ctx.set_dir_delete_on_start(true)?;
838        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
839        let (stop, driver_handle) =
840            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
841
842        let ctx = AeronContext::new()?;
843        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
844        let error_handler = Handler::new(ErrorCount::default());
845        ctx.set_error_handler(Some(error_handler.clone()))?;
846        let aeron = Aeron::new(&ctx)?;
847        aeron.start()?;
848
849        let channel = String::from("aeron:ipc");
850        let stream_id: i32 = 9124;
851        let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), stream_id)?;
852        let sub_poller =
853            aeron.async_add_subscription(&channel.into_c_string(), stream_id, Handlers::NONE, Handlers::NONE)?;
854
855        let mut publication: Option<AeronPublication> = None;
856        let mut subscription: Option<AeronSubscription> = None;
857        let start = Instant::now();
858        while start.elapsed() < Duration::from_secs(2) {
859            if publication.is_none() {
860                if let Ok(Some(p)) = pub_poller.poll() {
861                    publication = Some(p);
862                }
863            }
864            if subscription.is_none() {
865                if let Ok(Some(s)) = sub_poller.poll() {
866                    subscription = Some(s);
867                }
868            }
869            if publication.is_some() && subscription.is_some() {
870                break;
871            }
872            #[cfg(debug_assertions)]
873            sleep(Duration::from_millis(10));
874        }
875
876        let (publisher, subscription) = match (publication, subscription) {
877            (Some(p), Some(s)) => (p, s),
878            _ => panic!("publication/subscription did not come up"),
879        };
880
881        // Wait for the IPC images to connect before asserting status.
882        let conn_start = Instant::now();
883        while !publisher.is_connected() && conn_start.elapsed() < Duration::from_secs(2) {
884            #[cfg(debug_assertions)]
885            sleep(Duration::from_millis(10));
886        }
887        assert_eq!(publisher.status(), AeronStatus::Connected);
888
889        // Claim a buffer, write into it, stamp it, and commit.
890        let claim_payload = b"claim-commit-test";
891        let stamp = 0x0123_4567_89ab_cdef_i64;
892        let claim_start = Instant::now();
893        let mut committed_pos = None;
894        while claim_start.elapsed() < Duration::from_secs(2) {
895            if let Ok(mut claim) = publisher.try_claim_owned(claim_payload.len()) {
896                claim.data()[..claim_payload.len()].copy_from_slice(claim_payload);
897                claim.set_reserved_value(stamp);
898                let pos = claim.position();
899                let commit_pos = claim.commit()?;
900                committed_pos = Some((commit_pos, pos));
901                break;
902            }
903            #[cfg(debug_assertions)]
904            sleep(Duration::from_millis(10));
905        }
906        let (commit_pos, claim_pos) = committed_pos.expect("try_claim_owned + commit never succeeded");
907        assert_eq!(commit_pos, claim_pos, "commit() return should match claim.position()");
908
909        // Receive the committed message and assert the exact bytes and stamp.
910        let received = std::cell::Cell::new(None);
911        let read_start = Instant::now();
912        while read_start.elapsed() < Duration::from_secs(2) && received.get().is_none() {
913            let _ = subscription.poll_fn(
914                |msg, header| {
915                    if msg == claim_payload {
916                        received.set(Some(header.reserved_value()));
917                        // the in-place reads must agree with Aeron C's own copy of the header
918                        let c = header.get_values().expect("aeron_header_values");
919                        let frame = c.frame();
920                        assert_eq!(header.session_id(), Some(frame.session_id()));
921                        assert_eq!(header.stream_id(), Some(frame.stream_id()));
922                        assert_eq!(header.term_id(), Some(frame.term_id()));
923                        assert_eq!(header.term_offset(), Some(frame.term_offset()));
924                        assert_eq!(header.reserved_value(), Some(frame.reserved_value()));
925                    }
926                },
927                1024,
928            );
929            #[cfg(debug_assertions)]
930            sleep(Duration::from_millis(10));
931        }
932        assert_eq!(received.get(), Some(Some(stamp)), "claim message with its stamp");
933
934        assert_eq!(subscription.status(), AeronStatus::Connected);
935
936        // Shutdown
937        stop.store(true, Ordering::SeqCst);
938        let _ = driver_handle.join().unwrap();
939        Ok(())
940    }
941
942    /// A claim dropped uncommitted is aborted: never delivered, and the publication stays usable.
943    #[test]
944    #[serial]
945    fn aeron_claim_dropped_without_commit_is_aborted() -> Result<(), Box<dyn error::Error>> {
946        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
947
948        let media_driver_ctx = AeronDriverContext::new()?;
949        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
950        media_driver_ctx.set_dir_delete_on_start(true)?;
951        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
952        let (stop, driver_handle) =
953            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
954
955        let ctx = AeronContext::new()?;
956        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
957        let error_handler = Handler::new(ErrorCount::default());
958        ctx.set_error_handler(Some(error_handler.clone()))?;
959        let aeron = Aeron::new(&ctx)?;
960        aeron.start()?;
961
962        let channel = String::from("aeron:ipc");
963        let stream_id: i32 = 9125;
964        let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), stream_id)?;
965        let sub_poller =
966            aeron.async_add_subscription(&channel.into_c_string(), stream_id, Handlers::NONE, Handlers::NONE)?;
967
968        let mut publication: Option<AeronPublication> = None;
969        let mut subscription: Option<AeronSubscription> = None;
970        let start = Instant::now();
971        while start.elapsed() < Duration::from_secs(2) {
972            if publication.is_none() {
973                if let Ok(Some(p)) = pub_poller.poll() {
974                    publication = Some(p);
975                }
976            }
977            if subscription.is_none() {
978                if let Ok(Some(s)) = sub_poller.poll() {
979                    subscription = Some(s);
980                }
981            }
982            if publication.is_some() && subscription.is_some() {
983                break;
984            }
985            #[cfg(debug_assertions)]
986            sleep(Duration::from_millis(10));
987        }
988
989        let (publisher, subscription) = match (publication, subscription) {
990            (Some(p), Some(s)) => (p, s),
991            _ => panic!("publication/subscription did not come up"),
992        };
993
994        // Wait for the IPC images to connect.
995        let conn_start = Instant::now();
996        while !publisher.is_connected() && conn_start.elapsed() < Duration::from_secs(2) {
997            #[cfg(debug_assertions)]
998            sleep(Duration::from_millis(10));
999        }
1000        assert_eq!(publisher.status(), AeronStatus::Connected);
1001
1002        // Claim a buffer, write into it, then DROP without commit/abort.
1003        let dropped_payload = b"dropped-claim-payload";
1004        let claim_start = Instant::now();
1005        let mut claimed = false;
1006        while claim_start.elapsed() < Duration::from_secs(2) && !claimed {
1007            if let Ok(mut claim) = publisher.try_claim_owned(dropped_payload.len()) {
1008                claim.data()[..dropped_payload.len()].copy_from_slice(dropped_payload);
1009                // Explicitly drop the claim here — it should be aborted, not committed.
1010                drop(claim);
1011                claimed = true;
1012            }
1013            #[cfg(debug_assertions)]
1014            sleep(Duration::from_millis(10));
1015        }
1016        assert!(claimed, "try_claim_owned never succeeded");
1017
1018        // Publish a second, different message via the standard offer path.
1019        let marker_payload = b"marker-after-dropped-claim";
1020        let offer_start = Instant::now();
1021        let mut offered = false;
1022        while offer_start.elapsed() < Duration::from_secs(2) && !offered {
1023            if let Ok(pos) = publisher.offer(marker_payload) {
1024                if pos >= marker_payload.len() as i64 {
1025                    offered = true;
1026                }
1027            }
1028            #[cfg(debug_assertions)]
1029            sleep(Duration::from_millis(10));
1030        }
1031        assert!(offered, "offer_result_simple never succeeded after dropped claim");
1032
1033        // Receive only the marker — the dropped claim should have been aborted.
1034        let received_dropped = std::cell::Cell::new(false);
1035        let received_marker = std::cell::Cell::new(false);
1036        let read_start = Instant::now();
1037        while read_start.elapsed() < Duration::from_secs(2) && !(received_marker.get() || received_dropped.get()) {
1038            let _ = subscription.poll_fn(
1039                |msg, _header| {
1040                    if msg == dropped_payload {
1041                        received_dropped.set(true);
1042                    }
1043                    if msg == marker_payload {
1044                        received_marker.set(true);
1045                    }
1046                },
1047                1024,
1048            );
1049            #[cfg(debug_assertions)]
1050            sleep(Duration::from_millis(10));
1051        }
1052        assert!(!received_dropped.get(), "dropped claim was erroneously delivered");
1053        assert!(
1054            received_marker.get(),
1055            "marker message was not received after dropped claim"
1056        );
1057
1058        // Confirm the publication is still usable by offering one more message.
1059        let final_payload = b"final-after-all";
1060        let final_start = Instant::now();
1061        let mut final_offered = false;
1062        while final_start.elapsed() < Duration::from_secs(2) && !final_offered {
1063            if let Ok(pos) = publisher.offer(final_payload) {
1064                if pos >= final_payload.len() as i64 {
1065                    final_offered = true;
1066                }
1067            }
1068            #[cfg(debug_assertions)]
1069            sleep(Duration::from_millis(10));
1070        }
1071        assert!(final_offered, "publication became unusable after dropped claim");
1072
1073        assert_eq!(subscription.status(), AeronStatus::Connected);
1074
1075        // Shutdown
1076        stop.store(true, Ordering::SeqCst);
1077        let _ = driver_handle.join().unwrap();
1078        Ok(())
1079    }
1080
1081    /// An oversized `try_claim_owned` returns an error without building or aborting a claim.
1082    #[test]
1083    #[serial]
1084    fn try_claim_owned_failure_is_clean() -> Result<(), Box<dyn error::Error>> {
1085        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1086
1087        let media_driver_ctx = AeronDriverContext::new()?;
1088        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1089        media_driver_ctx.set_dir_delete_on_start(true)?;
1090        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1091        let (stop, driver_handle) =
1092            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1093
1094        let ctx = AeronContext::new()?;
1095        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1096        let error_handler = Handler::new(ErrorCount::default());
1097        ctx.set_error_handler(Some(error_handler.clone()))?;
1098        let aeron = Aeron::new(&ctx)?;
1099        aeron.start()?;
1100
1101        let channel = String::from("aeron:ipc");
1102        let stream_id: i32 = 9126;
1103        let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), stream_id)?;
1104        let sub_poller =
1105            aeron.async_add_subscription(&channel.into_c_string(), stream_id, Handlers::NONE, Handlers::NONE)?;
1106
1107        let mut publication: Option<AeronPublication> = None;
1108        let mut subscription: Option<AeronSubscription> = None;
1109        let start = Instant::now();
1110        while start.elapsed() < Duration::from_secs(2) {
1111            if publication.is_none() {
1112                if let Ok(Some(p)) = pub_poller.poll() {
1113                    publication = Some(p);
1114                }
1115            }
1116            if subscription.is_none() {
1117                if let Ok(Some(s)) = sub_poller.poll() {
1118                    subscription = Some(s);
1119                }
1120            }
1121            if publication.is_some() && subscription.is_some() {
1122                break;
1123            }
1124            #[cfg(debug_assertions)]
1125            sleep(Duration::from_millis(10));
1126        }
1127
1128        let (publisher, _subscription) = match (publication, subscription) {
1129            (Some(p), Some(s)) => (p, s),
1130            _ => panic!("publication/subscription did not come up"),
1131        };
1132
1133        // Wait for the IPC images to connect.
1134        let conn_start = Instant::now();
1135        while !publisher.is_connected() && conn_start.elapsed() < Duration::from_secs(2) {
1136            #[cfg(debug_assertions)]
1137            sleep(Duration::from_millis(10));
1138        }
1139        assert_eq!(publisher.status(), AeronStatus::Connected);
1140
1141        // Get the max_message_length and attempt to claim more than that.
1142        let constants = publisher.get_constants().expect("publication constants");
1143        let max_len = constants.max_message_length;
1144        assert!(max_len > 0, "max_message_length should be positive");
1145
1146        // Try to claim an absurdly large length — should return Err without panicking.
1147        let oversized_claim = publisher.try_claim_owned(usize::MAX);
1148        assert!(
1149            oversized_claim.is_err(),
1150            "try_claim_owned(usize::MAX) should return Err"
1151        );
1152
1153        // Try to claim exactly one byte over the limit — should also return Err.
1154        let over_limit_claim = publisher.try_claim_owned(max_len as usize + 1);
1155        assert!(
1156            over_limit_claim.is_err(),
1157            "try_claim_owned(max_len + 1) should return Err"
1158        );
1159
1160        // Verify the publication is still usable after the failed claims.
1161        let valid_payload = b"valid-after-failed-claim";
1162        let offer_start = Instant::now();
1163        let mut offered = false;
1164        while offer_start.elapsed() < Duration::from_secs(2) && !offered {
1165            if let Ok(pos) = publisher.offer(valid_payload) {
1166                if pos >= valid_payload.len() as i64 {
1167                    offered = true;
1168                }
1169            }
1170            #[cfg(debug_assertions)]
1171            sleep(Duration::from_millis(10));
1172        }
1173        assert!(offered, "publication became unusable after failed try_claim_owned");
1174
1175        // Shutdown
1176        stop.store(true, Ordering::SeqCst);
1177        let _ = driver_handle.join().unwrap();
1178        Ok(())
1179    }
1180
1181    /// The publish hot path (`offer` with the no-op reserved-value supplier) allocates nothing.
1182    #[test]
1183    #[serial]
1184    fn publish_hot_path_is_allocation_free() -> Result<(), Box<dyn error::Error>> {
1185        let media_driver_ctx = AeronDriverContext::new()?;
1186        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1187        media_driver_ctx.set_dir_delete_on_start(true)?;
1188        media_driver_ctx.set_dir(&format!("{}alloc-guard", media_driver_ctx.get_dir()).into_c_string())?;
1189        let (stop, driver_handle) =
1190            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1191
1192        let ctx = AeronContext::new()?;
1193        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1194        let error_handler = Handler::new(ErrorCount::default());
1195        ctx.set_error_handler(Some(error_handler.clone()))?;
1196        let aeron = Aeron::new(&ctx)?;
1197        aeron.start()?;
1198
1199        let channel = String::from("aeron:ipc");
1200        let stream_id: i32 = 7777;
1201        let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), stream_id)?;
1202
1203        // Bring the publication up + connected before measuring.
1204        let publisher: AeronPublication = {
1205            let start = Instant::now();
1206            loop {
1207                if let Ok(Some(p)) = pub_poller.poll() {
1208                    let conn_start = Instant::now();
1209                    while !p.is_connected() && conn_start.elapsed() < Duration::from_secs(2) {
1210                        #[cfg(debug_assertions)]
1211                        sleep(Duration::from_millis(10));
1212                    }
1213                    break p;
1214                }
1215                if start.elapsed() > Duration::from_secs(3) {
1216                    panic!("publication did not come up");
1217                }
1218                #[cfg(debug_assertions)]
1219                sleep(Duration::from_millis(10));
1220            }
1221        };
1222
1223        let payload = b"alloc-guard-payload";
1224        // Warm up (first offer may touch lazy publication state).
1225        let _ = publisher.offer_raw(payload, Handlers::NONE);
1226
1227        // Assert a tight offer loop is allocation-free (publish hot path).
1228        crate::test_alloc::assert_no_allocation(|| {
1229            for _ in 0..200 {
1230                let _ = publisher.offer_raw(payload, Handlers::NONE);
1231            }
1232        });
1233
1234        stop.store(true, Ordering::SeqCst);
1235        let _ = driver_handle.join().unwrap();
1236        Ok(())
1237    }
1238
1239    /// `from_code` + `Clone` must stay allocation-free so retry loops that keep
1240    /// failing on `-1` don't tax the hot path. Message capture is opt-in.
1241    #[test]
1242    #[serial]
1243    fn repeated_c_errors_are_allocation_free() {
1244        crate::test_alloc::assert_no_allocation(|| {
1245            for _ in 0..200 {
1246                let err = AeronCError::from_code(-1);
1247                assert_eq!(err.code, -1);
1248                assert!(err.message().is_none());
1249                let cloned = err.clone();
1250                assert_eq!(cloned, err);
1251            }
1252        });
1253    }
1254
1255    #[test]
1256    #[serial]
1257    fn async_pub_sub_invalid_endpoint_create_drop_stress() -> Result<(), Box<dyn error::Error>> {
1258        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1259
1260        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1261        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1262        media_driver_ctx.set_dir_delete_on_start(true)?;
1263        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1264        let (stop, driver_handle) =
1265            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1266
1267        let ctx = AeronContext::new()?;
1268        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1269        let error_handler = Handler::new(ErrorCount::default());
1270        ctx.set_error_handler(Some(error_handler.clone()))?;
1271        let aeron = Aeron::new(&ctx)?;
1272        aeron.start()?;
1273
1274        const STRESS_ITERS: u16 = 60;
1275        const POLL_TIMEOUT: Duration = Duration::from_secs(10);
1276        const POLL_SLEEP: Duration = Duration::from_millis(10);
1277
1278        // Stress: repeatedly create async pub/sub on an invalid endpoint and drive each
1279        // poller to a terminal state before dropping it.
1280        for i in 0..STRESS_ITERS {
1281            let port = 55000u16 + i;
1282            let channel = format!("aeron:udp?endpoint=203.0.113.1:{}", port);
1283            let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), 4500 + i as i32)?;
1284            let sub_poller = aeron.async_add_subscription(
1285                &channel.into_c_string(),
1286                4500 + i as i32,
1287                Handlers::NONE,
1288                Handlers::NONE,
1289            )?;
1290
1291            let start = Instant::now();
1292            let mut publication_done = false;
1293            let mut subscription_done = false;
1294
1295            while !(publication_done && subscription_done) && start.elapsed() < POLL_TIMEOUT {
1296                if !publication_done {
1297                    match pub_poller.poll() {
1298                        Ok(Some(pub_)) => {
1299                            let _ = pub_;
1300                            publication_done = true;
1301                        }
1302                        Ok(None) => {}
1303                        Err(err) => {
1304                            info!("publication async add finished with error on iteration {i}: {err:?}");
1305                            publication_done = true;
1306                        }
1307                    }
1308                }
1309
1310                if !subscription_done {
1311                    match sub_poller.poll() {
1312                        Ok(Some(sub_)) => {
1313                            let _ = sub_;
1314                            subscription_done = true;
1315                        }
1316                        Ok(None) => {}
1317                        Err(err) => {
1318                            info!("subscription async add finished with error on iteration {i}: {err:?}");
1319                            subscription_done = true;
1320                        }
1321                    }
1322                }
1323
1324                if !(publication_done && subscription_done) {
1325                    std::thread::sleep(POLL_SLEEP);
1326                }
1327            }
1328
1329            assert!(
1330                publication_done,
1331                "publication async add did not complete on iteration {i} within {:?}",
1332                POLL_TIMEOUT
1333            );
1334            assert!(
1335                subscription_done,
1336                "subscription async add did not complete on iteration {i} within {:?}",
1337                POLL_TIMEOUT
1338            );
1339        }
1340
1341        drop(aeron);
1342        stop.store(true, Ordering::SeqCst);
1343        let _ = driver_handle.join().unwrap();
1344        Ok(())
1345    }
1346
1347    #[test]
1348    #[serial]
1349    fn async_subscription_invalid_interface_poll_then_drop() -> Result<(), Box<dyn error::Error>> {
1350        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1351
1352        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1353        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1354        media_driver_ctx.set_dir_delete_on_start(true)?;
1355        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1356        let (stop, driver_handle) =
1357            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1358
1359        let ctx = AeronContext::new()?;
1360        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1361        let error_handler = Handler::new(ErrorCount::default());
1362        ctx.set_error_handler(Some(error_handler.clone()))?;
1363        let aeron = Aeron::new(&ctx)?;
1364        aeron.start()?;
1365
1366        // Invalid remote endpoint only (no interface)
1367        let channel = String::from("aeron:udp?endpoint=203.0.113.1:54323");
1368
1369        let poller = aeron.async_add_subscription(&channel.into_c_string(), 4323, Handlers::NONE, Handlers::NONE)?;
1370
1371        let start = Instant::now();
1372        while start.elapsed() < Duration::from_millis(250) {
1373            let _ = poller.poll();
1374            #[cfg(debug_assertions)]
1375            std::thread::sleep(Duration::from_millis(10));
1376        }
1377
1378        stop.store(true, Ordering::SeqCst);
1379        let _ = driver_handle.join().unwrap();
1380        Ok(())
1381    }
1382
1383    #[test]
1384    #[serial]
1385    fn blocking_add_subscription_invalid_interface_timeout() -> Result<(), Box<dyn error::Error>> {
1386        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1387
1388        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1389        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1390        media_driver_ctx.set_dir_delete_on_start(true)?;
1391        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1392        let (stop, driver_handle) =
1393            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1394
1395        let ctx = AeronContext::new()?;
1396        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1397        let error_handler = Handler::new(ErrorCount::default());
1398        ctx.set_error_handler(Some(error_handler.clone()))?;
1399        let aeron = Aeron::new(&ctx)?;
1400        aeron.start()?;
1401
1402        let channel = String::from("aeron:udp?endpoint=203.0.113.1:54324");
1403
1404        let result = aeron.add_subscription(
1405            &channel.into_c_string(),
1406            4324,
1407            Handlers::NONE,
1408            Handlers::NONE,
1409            Duration::from_millis(300),
1410        );
1411
1412        assert!(result.is_err(), "expected error for invalid interface");
1413        drop(aeron);
1414        stop.store(true, Ordering::SeqCst);
1415        let _ = driver_handle.join().unwrap();
1416        Ok(())
1417    }
1418
1419    #[test]
1420    #[serial]
1421    fn async_publication_invalid_bind_poll_then_drop() -> Result<(), Box<dyn error::Error>> {
1422        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1423
1424        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1425        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1426        media_driver_ctx.set_dir_delete_on_start(true)?;
1427        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1428        let (stop, driver_handle) =
1429            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1430
1431        let ctx = AeronContext::new()?;
1432        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1433        let error_handler = Handler::new(ErrorCount::default());
1434        ctx.set_error_handler(Some(error_handler.clone()))?;
1435        let aeron = Aeron::new(&ctx)?;
1436        aeron.start()?;
1437
1438        // Use an invalid bind on publication (bind is not valid for publication, and the IP is unowned).
1439        let channel = format!("aeron:udp?endpoint=127.0.0.1:54330|bind=203.0.113.1:60000");
1440
1441        let poller = aeron.async_add_publication(&channel.into_c_string(), 4330)?;
1442        let start = Instant::now();
1443        while start.elapsed() < Duration::from_millis(250) {
1444            let _ = poller.poll();
1445            #[cfg(debug_assertions)]
1446            std::thread::sleep(Duration::from_millis(10));
1447        }
1448        stop.store(true, Ordering::SeqCst);
1449        let _ = driver_handle.join().unwrap();
1450        Ok(())
1451    }
1452
1453    #[test]
1454    #[serial]
1455    pub fn simple_large_send() -> Result<(), Box<dyn error::Error>> {
1456        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1457        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1458        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1459        media_driver_ctx.set_dir_delete_on_start(true)?;
1460        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1461        let (liveness_ns, unblock_ns, driver_timeout_ms) = if running_under_valgrind() {
1462            (180_000_000_000u64, 185_000_000_000u64, 180_000)
1463        } else {
1464            (60_000_000_000u64, 65_000_000_000u64, 60_000)
1465        };
1466        media_driver_ctx.set_client_liveness_timeout_ns(liveness_ns)?;
1467        media_driver_ctx.set_image_liveness_timeout_ns(liveness_ns)?;
1468        media_driver_ctx.set_publication_unblock_timeout_ns(unblock_ns)?;
1469        media_driver_ctx.set_driver_timeout_ms(driver_timeout_ms)?;
1470        let (stop, driver_handle) =
1471            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1472
1473        let ctx = AeronContext::new()?;
1474        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1475        assert_eq!(media_driver_ctx.get_dir(), ctx.get_dir());
1476        // Keep client-side keepalive threshold aligned with the slow Valgrind environment.
1477        ctx.set_driver_timeout_ms(driver_timeout_ms)?;
1478        // Handlers are reference-counted: the context/resources keep clones alive,
1479        // and the values are freed automatically when the last reference drops.
1480        let error_handler = Handler::new(ErrorCount::default());
1481        let new_pub_handler = Handler::new(AeronNewPublicationLogger);
1482        let avail_counter_handler1 = Handler::new(AeronAvailableCounterLogger);
1483        let close_client_handler = Handler::new(AeronCloseClientLogger);
1484        let new_sub_handler = Handler::new(AeronNewSubscriptionLogger);
1485        let unavail_counter_handler = Handler::new(AeronUnavailableCounterLogger);
1486        let avail_counter_handler2 = Handler::new(AeronAvailableCounterLogger);
1487        let excl_pub_handler = Handler::new(AeronNewPublicationLogger);
1488        ctx.set_error_handler(Some(error_handler.clone()))?;
1489        ctx.set_on_new_publication(Some(new_pub_handler.clone()))?;
1490        ctx.set_on_available_counter(Some(avail_counter_handler1.clone()))?;
1491        ctx.set_on_close_client(Some(close_client_handler.clone()))?;
1492        ctx.set_on_new_subscription(Some(new_sub_handler.clone()))?;
1493        ctx.set_on_unavailable_counter(Some(unavail_counter_handler.clone()))?;
1494        ctx.set_on_available_counter(Some(avail_counter_handler2.clone()))?;
1495        ctx.set_on_new_exclusive_publication(Some(excl_pub_handler.clone()))?;
1496
1497        info!("creating client [simple_large_send test]");
1498        let aeron = Aeron::new(&ctx)?;
1499        info!("starting client");
1500
1501        aeron.start()?;
1502        info!("client started");
1503
1504        assert!(AeronCncMetadata::load_from_file(ctx.get_dir())?.pid > 0);
1505        let cstr = std::ffi::CString::new(ctx.get_dir()).unwrap();
1506        AeronCncMetadata::read_from_file(&cstr, |cnc| {
1507            assert!(cnc.pid > 0);
1508        })?;
1509        assert!(AeronCnc::open(&ctx.get_dir().into_c_string())?.get_to_driver_heartbeat_ms()? > 0);
1510        let cstr = std::ffi::CString::new(ctx.get_dir()).unwrap();
1511        for _ in 0..50 {
1512            AeronCnc::read(&cstr, |cnc| {
1513                assert!(cnc.get_to_driver_heartbeat_ms().unwrap() > 0);
1514            })?;
1515        }
1516
1517        let subscription = aeron.add_subscription(
1518            AERON_IPC_STREAM,
1519            123,
1520            Handlers::NONE,
1521            Handlers::NONE,
1522            Duration::from_secs(5),
1523        )?;
1524        info!("created subscription");
1525
1526        subscription.poll_fn(|msg, header| println!("foo"), 1024).unwrap();
1527
1528        // pick a large enough size to confirm fragement assembler is working
1529        let string_len = media_driver_ctx.ipc_mtu_length * 100;
1530        info!("string length: {}", string_len);
1531
1532        let stop_publisher = Arc::new(AtomicBool::new(false));
1533
1534        let publisher_handler = {
1535            let stop_publisher = stop_publisher.clone();
1536            let dir = ctx.get_dir().to_string();
1537            std::thread::spawn(move || {
1538                let aeron = thread_client(&dir, driver_timeout_ms).expect("publisher client");
1539                let publisher = aeron
1540                    .add_publication(AERON_IPC_STREAM, 123, Duration::from_secs(5))
1541                    .expect("publisher");
1542                info!("created publisher");
1543                let binding = "1".repeat(string_len);
1544                let large_msg = binding.as_bytes();
1545                loop {
1546                    if stop_publisher.load(Ordering::Acquire) || publisher.is_closed() {
1547                        break;
1548                    }
1549                    let result = publisher.offer_raw(large_msg, Handlers::NONE);
1550
1551                    assert_eq!(123, publisher.get_constants().unwrap().stream_id);
1552
1553                    if result < large_msg.len() as i64 {
1554                        let error = AeronCError::from_code(result as i32);
1555                        match error.kind() {
1556                            AeronErrorType::PublicationBackPressured | AeronErrorType::PublicationAdminAction => {
1557                                // ignore
1558                            }
1559                            _ => {
1560                                error!(
1561                                    "ERROR: failed to send message {:?}",
1562                                    AeronCError::from_code(result as i32)
1563                                );
1564                            }
1565                        }
1566                        sleep(Duration::from_millis(500));
1567                    }
1568                }
1569                info!("stopping publisher thread");
1570            })
1571        };
1572
1573        let mut assembler = AeronFragmentClosureAssembler::new()?;
1574
1575        struct Context {
1576            count: Arc<AtomicUsize>,
1577            stop: Arc<AtomicBool>,
1578            string_len: usize,
1579        }
1580
1581        let count = Arc::new(AtomicUsize::new(0usize));
1582        let mut context = Context {
1583            count: count.clone(),
1584            stop: stop.clone(),
1585            string_len,
1586        };
1587
1588        // Start the timer
1589        let start_time = Instant::now();
1590
1591        // Use break-with-value so cleanup (handler release, driver stop) always runs.
1592        // 120-second timeout: under Valgrind execution is ~10× slower, so 30 s is too tight.
1593        let loop_result: Result<(), Box<dyn error::Error>> = loop {
1594            if start_time.elapsed() > Duration::from_secs(120) {
1595                info!("Failed: exceeded 120-second timeout");
1596                break Err(Box::new(std::io::Error::new(
1597                    std::io::ErrorKind::TimedOut,
1598                    "Timeout exceeded",
1599                )));
1600            }
1601            let c = count.load(Ordering::SeqCst);
1602            if c > 100 {
1603                break Ok(());
1604            }
1605
1606            fn process_msg(ctx: &mut Context, buffer: &[u8], header: AeronHeader) {
1607                ctx.count.fetch_add(1, Ordering::SeqCst);
1608
1609                let values = header.get_values().unwrap();
1610                assert_ne!(values.frame.session_id, 0);
1611
1612                if buffer.len() != ctx.string_len {
1613                    ctx.stop.store(true, Ordering::SeqCst);
1614                    error!(
1615                        "ERROR: message was {} but was expecting {} [header={:?}]",
1616                        buffer.len(),
1617                        ctx.string_len,
1618                        header
1619                    );
1620                    sleep(Duration::from_secs(1));
1621                }
1622
1623                assert_eq!(buffer.len(), ctx.string_len);
1624                assert_eq!(buffer, "1".repeat(ctx.string_len).as_bytes());
1625            }
1626
1627            assembler.poll(&subscription, &mut context, process_msg, 128)?;
1628            assert_eq!(123, subscription.stream_id().unwrap());
1629        };
1630
1631        subscription.close()?;
1632
1633        info!("stopping client");
1634        stop_publisher.store(true, Ordering::SeqCst);
1635
1636        let _ = publisher_handler.join().unwrap();
1637        drop(aeron);
1638
1639        stop.store(true, Ordering::SeqCst);
1640        let _ = driver_handle.join().unwrap();
1641
1642        // Release all context handlers now that Aeron and the driver are fully stopped.
1643
1644        let cnc = AeronCnc::open(&ctx.get_dir().into_c_string())?;
1645        cnc.counters_reader()
1646            .foreach_counter_fn(|value: i64, id: i32, type_id: i32, key: &[u8], label: &str| {
1647                println!(
1648                    "counter reader id={id}, type_id={type_id}, key={key:?}, label={label}, value={value} [type={:?}]",
1649                    AeronSystemCounterType::try_from(type_id)
1650                );
1651            });
1652        cnc.error_log_read_fn(| observation_count: i32,
1653                                     first_observation_timestamp: i64,
1654                                     last_observation_timestamp: i64,
1655                                     error: &str| {
1656            println!("error: {error} observationCount={observation_count}, first_observation_timestamp={first_observation_timestamp}, last_observation_timestamp={last_observation_timestamp}");
1657        }, 0);
1658        cnc.loss_reporter_read_fn(|    observation_count: i64,
1659                                    total_bytes_lost: i64,
1660                                    first_observation_timestamp: i64,
1661                                    last_observation_timestamp: i64,
1662                                    session_id: i32,
1663                                    stream_id: i32,
1664                                    channel: &str,
1665                                    source: &str,| {
1666            println!("loss reporter observationCount={observation_count}, totalBytesLost={total_bytes_lost}, first_observed={first_observation_timestamp}, last_observed={last_observation_timestamp}, session_id={session_id}, stream_id={stream_id}, channel={channel} source={source}");
1667        })?;
1668
1669        loop_result?;
1670        Ok(())
1671    }
1672
1673    #[test]
1674    #[serial]
1675    pub fn try_claim() -> Result<(), Box<dyn error::Error>> {
1676        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1677        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1678        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1679        media_driver_ctx.set_dir_delete_on_start(true)?;
1680        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1681        let (liveness_ns, unblock_ns, driver_timeout_ms) = if running_under_valgrind() {
1682            (180_000_000_000u64, 185_000_000_000u64, 180_000)
1683        } else {
1684            (60_000_000_000u64, 65_000_000_000u64, 60_000)
1685        };
1686        media_driver_ctx.set_client_liveness_timeout_ns(liveness_ns)?;
1687        media_driver_ctx.set_image_liveness_timeout_ns(liveness_ns)?;
1688        media_driver_ctx.set_publication_unblock_timeout_ns(unblock_ns)?;
1689        media_driver_ctx.set_driver_timeout_ms(driver_timeout_ms)?;
1690        let (stop, driver_handle) =
1691            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1692
1693        let ctx = AeronContext::new()?;
1694        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1695        assert_eq!(media_driver_ctx.get_dir(), ctx.get_dir());
1696        // Keep client-side keepalive threshold aligned with slow Valgrind environment.
1697        ctx.set_driver_timeout_ms(driver_timeout_ms)?;
1698        let error_handler = Handler::new(ErrorCount::default());
1699        ctx.set_error_handler(Some(error_handler.clone()))?;
1700
1701        info!("creating client [try_claim test]");
1702        let aeron = Aeron::new(&ctx)?;
1703        info!("starting client");
1704
1705        aeron.start()?;
1706        info!("client started");
1707        const STREAM_ID: i32 = 123;
1708
1709        let subscription = aeron.add_subscription(
1710            AERON_IPC_STREAM,
1711            STREAM_ID,
1712            Handlers::NONE,
1713            Handlers::NONE,
1714            Duration::from_secs(5),
1715        )?;
1716        info!("created subscription");
1717
1718        // pick a large enough size to confirm fragement assembler is working
1719        let string_len = 156;
1720        info!("string length: {}", string_len);
1721
1722        let stop_publisher = Arc::new(AtomicBool::new(false));
1723
1724        let publisher_handler = {
1725            let stop_publisher = stop_publisher.clone();
1726            let dir = ctx.get_dir().to_string();
1727            std::thread::spawn(move || {
1728                let aeron = thread_client(&dir, driver_timeout_ms).expect("publisher client");
1729                let publisher = aeron
1730                    .add_publication(AERON_IPC_STREAM, STREAM_ID, Duration::from_secs(5))
1731                    .expect("publisher");
1732                info!("created publisher");
1733                let binding = "1".repeat(string_len);
1734                let msg = binding.as_bytes();
1735                let buffer = AeronBufferClaim::default();
1736                loop {
1737                    if stop_publisher.load(Ordering::Acquire) || publisher.is_closed() {
1738                        break;
1739                    }
1740
1741                    let result = publisher.try_claim_raw(string_len, &buffer);
1742
1743                    if result < msg.len() as i64 {
1744                        error!(
1745                            "ERROR: failed to send message {:?}",
1746                            AeronCError::from_code(result as i32)
1747                        );
1748                    } else {
1749                        buffer.data().write_all(&msg).unwrap();
1750                        buffer.commit().unwrap();
1751                    }
1752                }
1753                info!("stopping publisher thread");
1754            })
1755        };
1756
1757        let count = Arc::new(AtomicUsize::new(0usize));
1758        let count_copy = Arc::clone(&count);
1759        let stop2 = stop.clone();
1760
1761        struct FragmentHandler {
1762            count_copy: Arc<AtomicUsize>,
1763            stop2: Arc<AtomicBool>,
1764            string_len: usize,
1765        }
1766
1767        impl AeronFragmentHandlerCallback for FragmentHandler {
1768            fn handle_aeron_fragment_handler(&mut self, buffer: &[u8], header: AeronHeader) {
1769                assert_eq!(STREAM_ID, header.get_values().unwrap().frame.stream_id);
1770                let header = header.get_values().unwrap();
1771                let frame = header.frame();
1772                let stream_id = frame.stream_id();
1773                assert_eq!(STREAM_ID, stream_id);
1774
1775                self.count_copy.fetch_add(1, Ordering::SeqCst);
1776
1777                if buffer.len() != self.string_len {
1778                    self.stop2.store(true, Ordering::SeqCst);
1779                    error!(
1780                        "ERROR: message was {} but was expecting {} [header={:?}]",
1781                        buffer.len(),
1782                        self.string_len,
1783                        header
1784                    );
1785                    sleep(Duration::from_secs(1));
1786                }
1787
1788                assert_eq!(buffer.len(), self.string_len);
1789                assert_eq!(buffer, "1".repeat(self.string_len).as_bytes());
1790            }
1791        }
1792
1793        let (closure, inner_handler) = Handler::with_fragment_assembler(FragmentHandler {
1794            count_copy,
1795            stop2,
1796            string_len,
1797        })?;
1798        let loop_result: Result<(), Box<dyn error::Error>> = {
1799            let start_time = Instant::now();
1800            loop {
1801                if start_time.elapsed() > Duration::from_secs(120) {
1802                    info!("Failed: exceeded 120-second timeout");
1803                    break Err(Box::new(std::io::Error::new(
1804                        std::io::ErrorKind::TimedOut,
1805                        "Timeout exceeded",
1806                    )));
1807                }
1808                let c = count.load(Ordering::SeqCst);
1809                if c > 100 {
1810                    break Ok(());
1811                }
1812                subscription.poll(Some(&closure), 128)?;
1813            }
1814        };
1815
1816        info!("stopping client");
1817
1818        stop_publisher.store(true, Ordering::SeqCst);
1819
1820        let _ = publisher_handler.join().unwrap();
1821
1822        stop.store(true, Ordering::SeqCst);
1823        let _ = driver_handle.join().unwrap();
1824        loop_result?;
1825        Ok(())
1826    }
1827
1828    #[test]
1829    #[serial]
1830    pub fn counters() -> Result<(), Box<dyn error::Error>> {
1831        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1832        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1833        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1834        media_driver_ctx.set_dir_delete_on_start(true)?;
1835        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1836        let (stop, driver_handle) =
1837            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1838
1839        let ctx = AeronContext::new()?;
1840        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1841        assert_eq!(media_driver_ctx.get_dir(), ctx.get_dir());
1842        let error_handler = Handler::new(ErrorCount::default());
1843        ctx.set_error_handler(Some(error_handler.clone()))?;
1844        let unavailable_counter_handler = Handler::new(AeronUnavailableCounterLogger);
1845        ctx.set_on_unavailable_counter(Some(unavailable_counter_handler.clone()))?;
1846
1847        struct AvailableCounterHandler {
1848            found_counter: bool,
1849        }
1850
1851        impl AeronAvailableCounterCallback for AvailableCounterHandler {
1852            fn handle_aeron_on_available_counter(
1853                &mut self,
1854                counters_reader: AeronCountersReader,
1855                registration_id: i64,
1856                counter_id: i32,
1857            ) -> () {
1858                info!(
1859                    "on counter key={:?}, label={:?} registration_id={registration_id}, counter_id={counter_id}, value={}, {counters_reader:?}",
1860                    String::from_utf8(counters_reader.get_counter_key(counter_id).unwrap()),
1861                    counters_reader.get_counter_label(counter_id, 1000),
1862                    counters_reader.addr(counter_id)
1863                );
1864
1865                assert_eq!(
1866                    counters_reader.counter_registration_id(counter_id).unwrap(),
1867                    registration_id
1868                );
1869
1870                if let Ok(label) = counters_reader.get_counter_label(counter_id, 1000) {
1871                    if label == "label_buffer" {
1872                        self.found_counter = true;
1873                        assert_eq!(&counters_reader.get_counter_key(counter_id).unwrap(), "key".as_bytes());
1874                    }
1875                }
1876            }
1877        }
1878
1879        let available_counter_handler = Handler::new(AvailableCounterHandler { found_counter: false });
1880        ctx.set_on_available_counter(Some(available_counter_handler.clone()))?;
1881
1882        info!("creating client");
1883        let aeron = Aeron::new(&ctx)?;
1884        info!("starting client");
1885
1886        aeron.start()?;
1887        info!("client started [counters test]");
1888
1889        let counter = aeron.add_counter(123, "key".as_bytes(), "label_buffer", Duration::from_secs(5))?;
1890        let constants = counter.get_constants()?;
1891        let counter_id = constants.counter_id;
1892
1893        for _ in 0..150 {
1894            counter.addr_atomic().fetch_add(1, Ordering::SeqCst);
1895        }
1896        assert_eq!(counter.addr_atomic().load(Ordering::SeqCst), 150);
1897
1898        info!("stopping client");
1899
1900        #[cfg(not(target_os = "windows"))] // not sure why windows version doesn't fire event
1901        assert!(available_counter_handler.found_counter);
1902
1903        let reader = aeron.counters_reader();
1904        assert_eq!(reader.get_counter_label(counter_id, 256)?, "label_buffer");
1905        assert_eq!(reader.get_counter_key(counter_id)?, "key".as_bytes());
1906
1907        let mut dst = String::with_capacity(64);
1908        assert!(reader.get_counter_label_into(-1, &mut dst).is_err());
1909        assert_eq!(dst.as_bytes(), b"");
1910        let utf8_counter = aeron.add_counter(124, &[], "lbl\u{e9}", Duration::from_secs(5))?;
1911        let utf8_id = utf8_counter.get_constants()?.counter_id;
1912        // 4 bytes split the 2-byte char, so only the valid prefix is kept.
1913        assert_eq!(reader.get_counter_label(utf8_id, 4)?.as_bytes(), b"lbl");
1914        assert_eq!(reader.get_counter_label(utf8_id, 5)?, "lbl\u{e9}");
1915        let buffers = AeronCountersReaderBuffers::default();
1916        reader.get_buffers(&buffers)?;
1917
1918        stop.store(true, Ordering::SeqCst);
1919        let _ = driver_handle.join().unwrap();
1920        Ok(())
1921    }
1922
1923    /// A simple error counter for testing error callback invocation.
1924    #[derive(Default, Debug)]
1925    struct TestErrorCount {
1926        pub error_count: usize,
1927    }
1928
1929    impl Drop for TestErrorCount {
1930        fn drop(&mut self) {
1931            info!("TestErrorCount dropped with {} errors", self.error_count);
1932        }
1933    }
1934
1935    impl AeronErrorHandlerCallback for TestErrorCount {
1936        fn handle_aeron_error_handler(&mut self, error_code: c_int, msg: &str) {
1937            error!("Aeron error {}: {}", error_code, msg);
1938            self.error_count += 1;
1939        }
1940    }
1941
1942    #[test]
1943    #[serial]
1944    pub fn backpressure_recovery_test() -> Result<(), Box<dyn error::Error>> {
1945        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1946
1947        let under_valgrind = running_under_valgrind();
1948        let driver_timeout_ms = if under_valgrind { 180_000 } else { 60_000 };
1949        let liveness_timeout_ns = if under_valgrind {
1950            180_000_000_000
1951        } else {
1952            60_000_000_000
1953        };
1954        let poll_timeout = Duration::from_millis(driver_timeout_ms as u64);
1955
1956        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1957        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1958        media_driver_ctx.set_dir_delete_on_start(true)?;
1959        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1960        media_driver_ctx.set_client_liveness_timeout_ns(liveness_timeout_ns)?;
1961        media_driver_ctx.set_image_liveness_timeout_ns(liveness_timeout_ns)?;
1962        media_driver_ctx.set_publication_unblock_timeout_ns(liveness_timeout_ns + 5_000_000_000)?;
1963        media_driver_ctx.set_driver_timeout_ms(driver_timeout_ms)?;
1964        let (stop, driver_handle) =
1965            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1966
1967        let ctx = AeronContext::new()?;
1968        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1969        ctx.set_driver_timeout_ms(driver_timeout_ms)?;
1970        let error_handler = Handler::new(TestErrorCount::default());
1971        ctx.set_error_handler(Some(error_handler.clone()))?;
1972
1973        let aeron = Aeron::new(&ctx)?;
1974        aeron.start()?;
1975
1976        let subscription = aeron.add_subscription(
1977            AERON_IPC_STREAM,
1978            123,
1979            Handlers::NONE,
1980            Handlers::NONE,
1981            Duration::from_secs(5),
1982        )?;
1983
1984        let count = Arc::new(AtomicUsize::new(0));
1985        let start_time = Instant::now();
1986
1987        let stop_publisher = Arc::new(AtomicBool::new(false));
1988
1989        // Spawn a publisher thread that repeatedly sends "test" messages.
1990        let publisher_thread = {
1991            let stop_publisher = stop_publisher.clone();
1992            let dir = ctx.get_dir().to_string();
1993            std::thread::spawn(move || {
1994                let aeron = thread_client(&dir, driver_timeout_ms).expect("publisher client");
1995                let publisher = aeron
1996                    .add_publication(AERON_IPC_STREAM, 123, Duration::from_secs(5))
1997                    .expect("publisher");
1998                while !stop_publisher.load(Ordering::Acquire) {
1999                    let msg = b"test";
2000                    let result = publisher.offer_raw(msg, Handlers::NONE);
2001                    // If backpressure is encountered, sleep a bit.
2002                    if result == AeronErrorType::PublicationBackPressured.code() as i64 {
2003                        sleep(Duration::from_millis(50));
2004                    }
2005                    if publisher.is_closed() {
2006                        break;
2007                    }
2008                }
2009            })
2010        };
2011
2012        // Poll using the inline closure via poll_fn until we receive at least 50 messages.
2013        while count.load(Ordering::SeqCst) < 50 && start_time.elapsed() < poll_timeout {
2014            let _ = subscription.poll_fn(
2015                |_msg, _header| {
2016                    count.fetch_add(1, Ordering::SeqCst);
2017                },
2018                128,
2019            )?;
2020        }
2021
2022        stop_publisher.store(true, Ordering::SeqCst);
2023        publisher_thread.join().unwrap();
2024        stop.store(true, Ordering::SeqCst);
2025        let _ = driver_handle.join().unwrap();
2026
2027        assert!(
2028            count.load(Ordering::SeqCst) >= 50,
2029            "Expected at least 50 messages received"
2030        );
2031        Ok(())
2032    }
2033
2034    #[test]
2035    #[serial]
2036    pub fn multi_subscription_test() -> Result<(), Box<dyn error::Error>> {
2037        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
2038
2039        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
2040        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
2041        media_driver_ctx.set_dir_delete_on_start(true)?;
2042        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
2043        media_driver_ctx.set_client_liveness_timeout_ns(60_000_000_000)?;
2044        media_driver_ctx.set_image_liveness_timeout_ns(60_000_000_000)?;
2045        media_driver_ctx.set_publication_unblock_timeout_ns(65_000_000_000)?;
2046        media_driver_ctx.set_driver_timeout_ms(60_000)?;
2047        let (_stop, driver_handle) =
2048            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
2049
2050        let ctx = AeronContext::new()?;
2051        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
2052        let error_handler = Handler::new(TestErrorCount::default());
2053        ctx.set_error_handler(Some(error_handler.clone()))?;
2054
2055        let aeron = Aeron::new(&ctx)?;
2056        aeron.start()?;
2057        let publisher = aeron.add_publication(AERON_IPC_STREAM, 123, Duration::from_secs(5))?;
2058
2059        // Create two subscriptions on the same channel.
2060        let subscription1 = aeron.add_subscription(
2061            AERON_IPC_STREAM,
2062            123,
2063            Handlers::NONE,
2064            Handlers::NONE,
2065            Duration::from_secs(5),
2066        )?;
2067        let subscription2 = aeron.add_subscription(
2068            AERON_IPC_STREAM,
2069            123,
2070            Handlers::NONE,
2071            Handlers::NONE,
2072            Duration::from_secs(5),
2073        )?;
2074
2075        let count1 = Arc::new(AtomicUsize::new(0));
2076        let count2 = Arc::new(AtomicUsize::new(0));
2077
2078        // Publish a single message.
2079        let msg = b"hello multi-subscription";
2080        let result = publisher.offer_raw(msg, Handlers::NONE);
2081        assert!(result >= msg.len() as i64, "Message should be sent successfully");
2082
2083        let start_time = Instant::now();
2084        // Poll both subscriptions with inline closures until each has received at least one message.
2085        while (count1.load(Ordering::SeqCst) < 1 || count2.load(Ordering::SeqCst) < 1)
2086            && start_time.elapsed() < Duration::from_secs(5)
2087        {
2088            let _ = subscription1.poll_fn(
2089                |_msg, _header| {
2090                    count1.fetch_add(1, Ordering::SeqCst);
2091                },
2092                128,
2093            )?;
2094            let _ = subscription2.poll_fn(
2095                |_msg, _header| {
2096                    count2.fetch_add(1, Ordering::SeqCst);
2097                },
2098                128,
2099            )?;
2100        }
2101
2102        assert!(
2103            count1.load(Ordering::SeqCst) >= 1,
2104            "Subscription 1 did not receive the message"
2105        );
2106        assert!(
2107            count2.load(Ordering::SeqCst) >= 1,
2108            "Subscription 2 did not receive the message"
2109        );
2110
2111        drop(subscription2);
2112        drop(subscription1);
2113        drop(publisher);
2114        drop(aeron);
2115        _stop.store(true, Ordering::SeqCst);
2116        let _ = driver_handle.join().unwrap();
2117        Ok(())
2118    }
2119
2120    #[test]
2121    #[serial]
2122    pub fn should_be_able_to_drop_after_close_manually_being_closed() -> Result<(), Box<dyn error::Error>> {
2123        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
2124
2125        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
2126        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
2127        media_driver_ctx.set_dir_delete_on_start(true)?;
2128        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
2129        let (_stop, driver_handle) =
2130            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
2131
2132        let ctx = AeronContext::new()?;
2133        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
2134        let error_handler = Handler::new(AeronErrorHandlerLogger);
2135        ctx.set_error_handler(Some(error_handler.clone()))?;
2136
2137        let aeron = Aeron::new(&ctx)?;
2138        aeron.start()?;
2139
2140        {
2141            let publisher = aeron.add_publication(AERON_IPC_STREAM, 123, Duration::from_secs(5))?;
2142            info!("created publication [sessionId={}]", publisher.session_id());
2143            publisher.close()?;
2144        }
2145
2146        {
2147            let publisher = aeron.add_publication(AERON_IPC_STREAM, 124, Duration::from_secs(5))?;
2148            info!("created publication [sessionId={}]", publisher.session_id());
2149            publisher.close()?;
2150        }
2151
2152        {
2153            let publisher = aeron.add_publication(AERON_IPC_STREAM, 125, Duration::from_secs(5))?;
2154            info!("created publication [sessionId={}]", publisher.session_id());
2155            publisher.close()?;
2156        }
2157
2158        drop(aeron);
2159        _stop.store(true, Ordering::SeqCst);
2160        let _ = driver_handle.join().unwrap();
2161        Ok(())
2162    }
2163
2164    #[test]
2165    #[serial]
2166    pub fn structural_close_safety_test() -> Result<(), Box<dyn error::Error>> {
2167        // Under the new design, close(self) consumes the handle — use-after-close
2168        // is a compile error, not a runtime check.  This test verifies that
2169        // structural teardown (the ManagedCResource::Drop path) properly frees
2170        // the C resource without errors.
2171        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
2172
2173        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
2174        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
2175        media_driver_ctx.set_dir_delete_on_start(true)?;
2176        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
2177        let (_stop, driver_handle) =
2178            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
2179
2180        let ctx = AeronContext::new()?;
2181        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
2182        let error_handler = Handler::new(TestErrorCount::default());
2183        ctx.set_error_handler(Some(error_handler.clone()))?;
2184
2185        let aeron = Aeron::new(&ctx)?;
2186        aeron.start()?;
2187        let publisher = aeron.add_publication(AERON_IPC_STREAM, 123, Duration::from_secs(5))?;
2188
2189        // Consume the publication handle — close(self) drops it, and the cleanup
2190        // closure (wired at construction) calls aeron_publication_close on Drop
2191        // of the last Rc.  No further use of `publisher` is possible at compile
2192        // time (the binding is moved into close()).
2193        publisher.close()?;
2194
2195        drop(aeron);
2196        _stop.store(true, Ordering::SeqCst);
2197        let _ = driver_handle.join().unwrap();
2198        Ok(())
2199    }
2200
2201    /// Test sending and receiving an empty (zero-length) message using inline closures with poll_fn.
2202    #[test]
2203    #[serial]
2204    pub fn empty_message_test() -> Result<(), Box<dyn error::Error>> {
2205        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
2206
2207        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
2208        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
2209        media_driver_ctx.set_dir_delete_on_start(true)?;
2210        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
2211        let (_stop, driver_handle) =
2212            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
2213
2214        let ctx = AeronContext::new()?;
2215        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
2216        let error_handler = Handler::new(TestErrorCount::default());
2217        ctx.set_error_handler(Some(error_handler.clone()))?;
2218
2219        let aeron = Aeron::new(&ctx)?;
2220        aeron.start()?;
2221        let publisher = aeron.add_publication(AERON_IPC_STREAM, 123, Duration::from_secs(5))?;
2222        let subscription = aeron.add_subscription(
2223            AERON_IPC_STREAM,
2224            123,
2225            Handlers::NONE,
2226            Handlers::NONE,
2227            Duration::from_secs(5),
2228        )?;
2229
2230        let empty_received = Arc::new(AtomicBool::new(false));
2231        let start_time = Instant::now();
2232
2233        let result = publisher.offer_raw(b"", Handlers::NONE);
2234        assert!(result > 0);
2235
2236        while !empty_received.load(Ordering::SeqCst) && start_time.elapsed() < Duration::from_secs(5) {
2237            let _ = subscription.poll_fn(
2238                |msg, _header| {
2239                    if msg.is_empty() {
2240                        empty_received.store(true, Ordering::SeqCst);
2241                    }
2242                },
2243                128,
2244            )?;
2245        }
2246
2247        assert!(empty_received.load(Ordering::SeqCst), "Empty message was not received");
2248        drop(subscription);
2249        drop(publisher);
2250        drop(aeron);
2251        _stop.store(true, Ordering::SeqCst);
2252        let _ = driver_handle.join().unwrap();
2253        Ok(())
2254    }
2255
2256    #[derive(Default, Debug)]
2257    struct MdcTotals {
2258        gap_events: u64,
2259        missing_messages: u64,
2260        received_messages: u64,
2261    }
2262
2263    #[derive(Debug)]
2264    struct MdcWindowStats {
2265        expected_seq: Option<u64>,
2266        gap_events: u64,
2267        missing_messages: u64,
2268        received_messages: u64,
2269        histogram: Histogram<u64>,
2270    }
2271
2272    impl MdcWindowStats {
2273        fn new() -> Result<Self, Box<dyn error::Error>> {
2274            Ok(Self {
2275                expected_seq: None,
2276                gap_events: 0,
2277                missing_messages: 0,
2278                received_messages: 0,
2279                histogram: Histogram::new(3)?,
2280            })
2281        }
2282
2283        fn observe(&mut self, seq: u64, sent_ts_ns: u64) {
2284            self.received_messages += 1;
2285
2286            match self.expected_seq {
2287                None => self.expected_seq = Some(seq.saturating_add(1)),
2288                Some(expected) if seq > expected => {
2289                    self.gap_events += 1;
2290                    self.missing_messages += seq - expected;
2291                    self.expected_seq = Some(seq.saturating_add(1));
2292                }
2293                Some(expected) if seq == expected => {
2294                    self.expected_seq = Some(expected.saturating_add(1));
2295                }
2296                Some(_) => {
2297                    // Ignore out-of-order/late packets for gap counting in this window.
2298                }
2299            }
2300
2301            let now_ns = Aeron::nano_clock().max(0) as u64;
2302            let latency_ns = now_ns.saturating_sub(sent_ts_ns);
2303            let _ = self.histogram.record(latency_ns);
2304        }
2305
2306        fn print_and_reset(&mut self, window_number: usize, interval: Duration, totals: &mut MdcTotals) {
2307            totals.gap_events += self.gap_events;
2308            totals.missing_messages += self.missing_messages;
2309            totals.received_messages += self.received_messages;
2310
2311            if self.histogram.len() > 0 {
2312                let min_us = self.histogram.min() / 1_000;
2313                let p50_us = self.histogram.value_at_quantile(0.50) / 1_000;
2314                let p99_us = self.histogram.value_at_quantile(0.99) / 1_000;
2315                let max_us = self.histogram.max() / 1_000;
2316                println!(
2317                    "[mdc-window-{window_number}] interval={interval:?} received={} gaps={} missing={} latency_us[min={}, p50={}, p99={}, max={}]",
2318                    self.received_messages, self.gap_events, self.missing_messages, min_us, p50_us, p99_us, max_us,
2319                );
2320            } else {
2321                println!(
2322                    "[mdc-window-{window_number}] interval={interval:?} received=0 gaps=0 missing=0 latency_us[min=n/a, p50=n/a, p99=n/a, max=n/a]"
2323                );
2324            }
2325
2326            self.expected_seq = None;
2327            self.gap_events = 0;
2328            self.missing_messages = 0;
2329            self.received_messages = 0;
2330            self.histogram.reset();
2331        }
2332    }
2333
2334    /// Run with loss profile on macOS:
2335    /// `just mdc-loss-run 120 10 0.10`
2336    ///
2337    /// The recipe handles PF setup and cleanup automatically.
2338    ///
2339    /// Manual cleanup (if needed):
2340    /// `sudo pfctl -a com.apple/rusteron-mdc-loss -F all`
2341    /// `sudo dnctl -q flush`
2342    /// `sudo pfctl -f /etc/pf.conf`
2343    #[test]
2344    #[serial]
2345    #[ignore] // Long-running diagnostics test for manual MDC with rolling latency/gap reports.
2346    pub fn mdc_unreliable_gap_latency_histogram_report() -> Result<(), Box<dyn error::Error>> {
2347        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
2348
2349        const STREAM_ID: i32 = 32931;
2350        const CONTROL_PORT: u16 = 32929;
2351        const SUBSCRIBER_PORT: u16 = 32930;
2352        const MESSAGE_LEN: usize = 130;
2353
2354        let report_interval = Duration::from_secs(
2355            std::env::var("RUSTERON_MDC_REPORT_INTERVAL_SECS")
2356                .ok()
2357                .and_then(|value| value.parse::<u64>().ok())
2358                .unwrap_or(10),
2359        );
2360        let test_duration = Duration::from_secs(
2361            std::env::var("RUSTERON_MDC_TEST_DURATION_SECS")
2362                .ok()
2363                .and_then(|value| value.parse::<u64>().ok())
2364                .unwrap_or(300),
2365        );
2366        let mdc_host = std::env::var("RUSTERON_MDC_HOST").unwrap_or("127.0.0.1".to_string());
2367
2368        let publication_channel = format!("aeron:udp?control-mode=manual|control={}:{CONTROL_PORT}", mdc_host);
2369        // - group=false: do not apply MDC receiver-group semantics.
2370        // - nak-delay=100us: shorten unreliable-stream gap-fill decision latency.
2371        let subscription_channel = format!(
2372            "aeron:udp?endpoint={}:{SUBSCRIBER_PORT}|reliable=false|tether=false|group=false|nak-delay=500us",
2373            mdc_host
2374        );
2375        let destination_uri = format!("aeron:udp?endpoint={}:{SUBSCRIBER_PORT}", mdc_host);
2376
2377        let (media_driver_ctx, stop_driver, driver_handle) = start_media_driver(32930)?;
2378        let aeron_dir = media_driver_ctx.get_dir().to_string();
2379
2380        println!(
2381            "[mdc-start] publication={} subscription={} duration={:?} report_interval={:?}",
2382            publication_channel, subscription_channel, test_duration, report_interval
2383        );
2384
2385        let running = Arc::new(AtomicBool::new(true));
2386
2387        let subscriber_dir = aeron_dir.clone();
2388        let subscriber_channel = subscription_channel.clone();
2389        let subscriber_running = Arc::clone(&running);
2390        let subscriber_thread = std::thread::spawn(move || -> MdcTotals {
2391            let (_ctx, aeron) =
2392                create_client_for_dir(&subscriber_dir).expect("failed to create subscriber aeron client");
2393
2394            let subscription = aeron
2395                .add_subscription(
2396                    &subscriber_channel.into_c_string(),
2397                    STREAM_ID,
2398                    Handlers::NONE,
2399                    Handlers::NONE,
2400                    Duration::from_secs(5),
2401                )
2402                .expect("failed to create subscriber");
2403
2404            let mut totals = MdcTotals::default();
2405            let mut window_stats = MdcWindowStats::new().expect("failed to create histogram");
2406            let test_start = Instant::now();
2407            let mut window_start = test_start;
2408            let mut window_number = 1usize;
2409
2410            while test_start.elapsed() < test_duration {
2411                let _ = subscription
2412                    .poll_fn(
2413                        |msg, _header| {
2414                            if msg.len() < 16 {
2415                                return;
2416                            }
2417
2418                            let seq = u64::from_le_bytes(msg[0..8].try_into().unwrap());
2419                            let sent_ts_ns = u64::from_le_bytes(msg[8..16].try_into().unwrap());
2420                            window_stats.observe(seq, sent_ts_ns);
2421                        },
2422                        10_000,
2423                    )
2424                    .expect("subscriber poll failed");
2425
2426                if window_start.elapsed() >= report_interval {
2427                    window_stats.print_and_reset(window_number, report_interval, &mut totals);
2428                    window_number += 1;
2429                    window_start = Instant::now();
2430                }
2431            }
2432
2433            window_stats.print_and_reset(window_number, report_interval, &mut totals);
2434            subscriber_running.store(false, Ordering::SeqCst);
2435            totals
2436        });
2437
2438        // Ensure subscriber has started before publisher setup.
2439        sleep(Duration::from_millis(250));
2440
2441        let publisher_dir = aeron_dir;
2442        let publisher_channel = publication_channel.clone();
2443        let publisher_destination = destination_uri.clone();
2444        let publisher_running = Arc::clone(&running);
2445        let publisher_thread = std::thread::spawn(move || -> u64 {
2446            let (_ctx, aeron) = create_client_for_dir(&publisher_dir).expect("failed to create publisher aeron client");
2447
2448            let publication = aeron
2449                .add_exclusive_publication(&publisher_channel.into_c_string(), STREAM_ID, Duration::from_secs(5))
2450                .expect("failed to create publication");
2451
2452            let add_destination = AeronAsyncDestination::aeron_exclusive_publication_async_add_destination(
2453                &aeron,
2454                &publication,
2455                &publisher_destination.into_c_string(),
2456            )
2457            .expect("failed to add manual MDC destination");
2458
2459            let add_destination_start = Instant::now();
2460            while add_destination
2461                .aeron_exclusive_publication_async_destination_poll()
2462                .expect("destination add poll failed")
2463                == 0
2464            {
2465                assert!(
2466                    add_destination_start.elapsed() <= Duration::from_secs(5),
2467                    "Timed out adding manual MDC destination"
2468                );
2469                sleep(Duration::from_millis(10));
2470            }
2471
2472            let connect_start = Instant::now();
2473            while !publication.is_connected() && connect_start.elapsed() < Duration::from_secs(5) {
2474                sleep(Duration::from_millis(10));
2475            }
2476            assert!(
2477                publication.is_connected(),
2478                "manual MDC publication did not connect to subscriber destination"
2479            );
2480
2481            let mut seq: u64 = 0;
2482            let mut payload = [0u8; MESSAGE_LEN];
2483            while publisher_running.load(Ordering::Acquire) {
2484                payload[0..8].copy_from_slice(&seq.to_le_bytes());
2485                let ts_ns = Aeron::nano_clock().max(0) as u64;
2486                payload[8..16].copy_from_slice(&ts_ns.to_le_bytes());
2487
2488                let result = publication.offer_raw(&payload, Handlers::NONE);
2489                if result > 0 {
2490                    seq = seq.wrapping_add(1);
2491                }
2492                sleep(Duration::from_millis(1));
2493            }
2494            seq
2495        });
2496
2497        let totals = subscriber_thread.join().unwrap();
2498        running.store(false, Ordering::SeqCst);
2499        let sent_messages = publisher_thread.join().unwrap();
2500        stop_driver.store(true, Ordering::SeqCst);
2501        let _ = driver_handle.join().unwrap();
2502
2503        println!(
2504            "[mdc-summary] sent={} received={} total_gaps={} total_missing={}",
2505            sent_messages, totals.received_messages, totals.gap_events, totals.missing_messages
2506        );
2507
2508        assert!(sent_messages > 0, "publisher failed to send any messages");
2509        assert!(totals.received_messages > 0, "subscriber did not receive any messages");
2510        Ok(())
2511    }
2512
2513    #[test]
2514    #[serial]
2515    #[ignore] // need to work to get tags working properly, its more of testing issue then tag issue
2516    pub fn tags() -> Result<(), Box<dyn error::Error>> {
2517        rusteron_code_gen::test_logger::init(log::LevelFilter::Debug);
2518
2519        let (md_ctx, stop, md) = start_media_driver(1)?;
2520
2521        let (_a_ctx2, aeron_sub) = create_client(&md_ctx)?;
2522
2523        info!("creating suscriber 1");
2524        let sub = aeron_sub
2525            .add_subscription(
2526                c"aeron:udp?tags=100",
2527                123,
2528                Handlers::NONE,
2529                Handlers::NONE,
2530                Duration::from_secs(50),
2531            )
2532            .map_err(|e| {
2533                error!("aeron error={}", Aeron::errmsg());
2534                e
2535            })?;
2536
2537        let ctx = AeronContext::new()?;
2538        ctx.set_dir(&aeron_sub.context().get_dir().into_c_string())?;
2539        let aeron = Aeron::new(&ctx)?;
2540        aeron.start()?;
2541
2542        info!("creating suscriber 2");
2543        let sub2 = aeron_sub.add_subscription(
2544            c"aeron:udp?tags=100",
2545            123,
2546            Handlers::NONE,
2547            Handlers::NONE,
2548            Duration::from_secs(50),
2549        )?;
2550
2551        let (_a_ctx1, aeron_pub) = create_client(&md_ctx)?;
2552        info!("creating publisher");
2553        assert!(!aeron_pub.is_closed());
2554        let publisher = aeron_pub
2555            .add_publication(
2556                c"aeron:udp?endpoint=localhost:4040|alias=test|tags=100",
2557                123,
2558                Duration::from_secs(5),
2559            )
2560            .map_err(|e| {
2561                error!("aeron error={}", Aeron::errmsg());
2562                e
2563            })?;
2564
2565        info!("publishing msg");
2566
2567        loop {
2568            let result = publisher.offer_raw("213".as_bytes(), Handlers::NONE);
2569            if result < 0 {
2570                error!("failed to publish {:?}", AeronCError::from_code(result as i32));
2571            } else {
2572                break;
2573            }
2574        }
2575
2576        sub.poll_fn(
2577            |msg, _header| {
2578                println!("Received message: {:?}", msg);
2579            },
2580            128,
2581        )?;
2582        sub2.poll_fn(
2583            |msg, _header| {
2584                println!("Received message: {:?}", msg);
2585            },
2586            128,
2587        )?;
2588
2589        stop.store(true, Ordering::SeqCst);
2590
2591        Ok(())
2592    }
2593
2594    fn create_client_for_dir(dir: &str) -> Result<(AeronContext, Aeron), Box<dyn Error>> {
2595        info!("creating aeron client [dir={}]", dir);
2596        let ctx = AeronContext::new()?;
2597        ctx.set_dir(&dir.into_c_string())?;
2598        let aeron = Aeron::new(&ctx)?;
2599        aeron.start()?;
2600        Ok((ctx, aeron))
2601    }
2602
2603    fn create_client(media_driver_ctx: &AeronDriverContext) -> Result<(AeronContext, Aeron), Box<dyn Error>> {
2604        let dir = media_driver_ctx.get_dir().to_string();
2605        create_client_for_dir(&dir)
2606    }
2607
2608    fn start_media_driver(
2609        instance: u64,
2610    ) -> Result<
2611        (
2612            AeronDriverContext,
2613            Arc<AtomicBool>,
2614            JoinHandle<Result<(), rusteron_media_driver::AeronCError>>,
2615        ),
2616        Box<dyn Error>,
2617    > {
2618        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
2619        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
2620        media_driver_ctx.set_dir_delete_on_start(true)?;
2621        media_driver_ctx
2622            .set_dir(&format!("{}{}-{}", media_driver_ctx.get_dir(), Aeron::epoch_clock(), instance).into_c_string())?;
2623        let (stop, driver_handle) =
2624            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
2625        Ok((media_driver_ctx, stop, driver_handle))
2626    }
2627
2628    /// `AeronExclusivePublication`'s offer and claim results, and the
2629    /// `AeronPublicationConstants` accessors, end to end.
2630    #[test]
2631    #[serial]
2632    fn exclusive_publication_result_variants_and_constants() -> Result<(), Box<dyn error::Error>> {
2633        let (media_driver_ctx, stop, _driver_handle) = start_media_driver(8)?;
2634        let (_ctx, aeron) = create_client(&media_driver_ctx)?;
2635
2636        let stream_id: i32 = 4321;
2637        let exclusive = aeron.add_exclusive_publication(AERON_IPC_STREAM, stream_id, Duration::from_secs(5))?;
2638
2639        // Add a matching subscription so the IPC image connects and offers succeed.
2640        let sub_poller = aeron.async_add_subscription(
2641            &"aeron:ipc".to_string().into_c_string(),
2642            stream_id,
2643            Handlers::NONE,
2644            Handlers::NONE,
2645        )?;
2646        let mut subscription: Option<AeronSubscription> = None;
2647        let sub_start = Instant::now();
2648        while sub_start.elapsed() < Duration::from_secs(2) && subscription.is_none() {
2649            if let Ok(Some(s)) = sub_poller.poll() {
2650                subscription = Some(s);
2651            }
2652            #[cfg(debug_assertions)]
2653            sleep(Duration::from_millis(10));
2654        }
2655        let _subscription = subscription.expect("subscription did not come up");
2656
2657        let conn = Instant::now();
2658        while !exclusive.is_connected() && conn.elapsed() < Duration::from_secs(2) {
2659            #[cfg(debug_assertions)]
2660            sleep(Duration::from_millis(10));
2661        }
2662        assert_eq!(exclusive.status(), AeronStatus::Connected);
2663
2664        // 1) offer_result_simple (typed Result variant on the exclusive pub).
2665        let payload = b"exclusive-result";
2666        let mut offered_pos = None;
2667        let offer_start = Instant::now();
2668        while offer_start.elapsed() < Duration::from_secs(2) && offered_pos.is_none() {
2669            if let Ok(pos) = exclusive.offer(payload) {
2670                offered_pos = Some(pos);
2671            }
2672            #[cfg(debug_assertions)]
2673            sleep(Duration::from_millis(10));
2674        }
2675        let pos = offered_pos.expect("exclusive offer_result_simple never succeeded");
2676        assert!(pos >= payload.len() as i64);
2677
2678        // 2) RAII zero-copy claim on the exclusive pub.
2679        let claim_payload = b"exclusive-claim";
2680        let mut committed = false;
2681        let claim_start = Instant::now();
2682        while claim_start.elapsed() < Duration::from_secs(2) && !committed {
2683            if let Ok(mut claim) = exclusive.try_claim_owned(claim_payload.len()) {
2684                claim.data()[..claim_payload.len()].copy_from_slice(claim_payload);
2685                committed = claim.commit().is_ok();
2686            }
2687            #[cfg(debug_assertions)]
2688            sleep(Duration::from_millis(10));
2689        }
2690        assert!(committed, "exclusive try_claim_owned + commit never succeeded");
2691
2692        // 3) Generated constants accessors return sane, consistent values.
2693        let constants = exclusive.get_constants().expect("publication constants");
2694        assert_eq!(constants.stream_id, stream_id);
2695        // channel is a *const c_char into the C struct; just assert it's a valid C string.
2696        assert!(!constants.channel.is_null());
2697        assert_eq!(
2698            unsafe { std::ffi::CStr::from_ptr(constants.channel) }.to_str().unwrap(),
2699            "aeron:ipc"
2700        );
2701        assert!(constants.max_possible_position > 0);
2702        assert!(constants.term_buffer_length > 0);
2703        assert!(constants.max_message_length > 0);
2704        assert!(constants.max_payload_length > 0);
2705        // position() advances with the offers.
2706        assert!(exclusive.position() >= pos);
2707
2708        stop.store(true, Ordering::SeqCst);
2709        Ok(())
2710    }
2711
2712    /// Property tests for the pure-Rust logic (no driver).
2713    mod property_tests {
2714        use crate::{
2715            AeronCError, AeronErrorType, AeronOfferError, AeronStatus, AeronStatusTracker,
2716            validate_endpoint_for_aeron_udp,
2717        };
2718        use proptest::prelude::*;
2719
2720        /// Fuzz: arbitrary endpoint strings must never panic — only Ok/Err.
2721        #[test]
2722        fn validate_endpoint_never_panics_on_arbitrary_input() {
2723            proptest!(|(s in ".{0,40}")| {
2724                let _ = validate_endpoint_for_aeron_udp(&s);
2725            });
2726        }
2727
2728        /// Any hostname/IPv4 + port in 0..=65535 is accepted.
2729        #[test]
2730        fn validate_endpoint_accepts_well_formed_host_port() {
2731            let host = "[a-z][a-z0-9-]{0,20}(\\.[a-z0-9-]{1,20}){0,3}";
2732            proptest!(|(host in host, port in 0u16..=65535)| {
2733                let ep = format!("{host}:{port}");
2734                prop_assert!(
2735                    validate_endpoint_for_aeron_udp(&ep).is_ok(),
2736                    "expected ok for {ep}"
2737                );
2738            });
2739        }
2740
2741        /// Any valid-looking host with a port > 65535 is rejected.
2742        #[test]
2743        fn validate_endpoint_rejects_out_of_range_port() {
2744            proptest!(|(port in 65536u32..=200_000)| {
2745                let ep = format!("localhost:{port}");
2746                prop_assert!(validate_endpoint_for_aeron_udp(&ep).is_err());
2747            });
2748        }
2749
2750        /// Tracker emits iff the status differs from the previous one, and
2751        /// `last_status` is always the most recently observed.
2752        #[test]
2753        fn tracker_emits_iff_transition_and_tracks_last() {
2754            let status = prop::sample::select(vec![
2755                AeronStatus::Disconnected,
2756                AeronStatus::Connected,
2757                AeronStatus::BackPressured,
2758                AeronStatus::Closed,
2759            ]);
2760            proptest!(|(seq in prop::collection::vec(status, 0..40))| {
2761                let mut t = AeronStatusTracker::new();
2762                let mut prev: Option<AeronStatus> = None;
2763                for &s in &seq {
2764                    let emitted = t.observe(s);
2765                    let expected_emits = prev != Some(s);
2766                    prop_assert_eq!(emitted.is_some(), expected_emits);
2767                    if let Some(e) = emitted {
2768                        prop_assert_eq!(e, s);
2769                    }
2770                    prev = Some(s);
2771                }
2772                prop_assert_eq!(t.last_status(), seq.last().copied());
2773            });
2774        }
2775
2776        /// `AeronOfferError::from_position`: non-negative → Ok(position); the five
2777        /// documented sentinels map to their variants; any other negative keeps its
2778        /// code inside `Error`. Every error is classified retryable xor fatal.
2779        #[test]
2780        fn from_position_and_classification_are_consistent() {
2781            proptest!(|(pos in -1_000_000i64..=1_000_000)| {
2782                match AeronOfferError::from_position(pos) {
2783                    Ok(p) => prop_assert!(p >= 0 && p == pos),
2784                    Err(e) => {
2785                        prop_assert!(pos < 0);
2786                        prop_assert!(e.is_retryable() ^ e.is_fatal());
2787                        match (pos, &e) {
2788                            (-1, AeronOfferError::NotConnected)
2789                            | (-2, AeronOfferError::BackPressured)
2790                            | (-3, AeronOfferError::AdminAction)
2791                            | (-4, AeronOfferError::Closed)
2792                            | (-5, AeronOfferError::MaxPositionExceeded) => {}
2793                            (p, AeronOfferError::Error(inner)) if p < -5 || p == 0 => {
2794                                prop_assert_eq!(inner.code, p as i32);
2795                            }
2796                            (p, other) => prop_assert!(false, "unexpected mapping {} -> {:?}", p, other),
2797                        }
2798                    }
2799                }
2800            });
2801        }
2802
2803        /// Sanity: every documented Aeron error code maps back to itself.
2804        #[test]
2805        fn known_error_codes_round_trip() {
2806            for code in [-1, -2, -3, -4, -5, -6, -1000, -1001, -1002, -1003] {
2807                let err = AeronCError::from_code(code);
2808                assert_eq!(err.code, code);
2809                assert_eq!(AeronErrorType::from_code(code).code(), code);
2810            }
2811        }
2812    }
2813
2814    #[cfg(test)]
2815    mod spin_poll_tests {
2816        use super::*;
2817        use crate::test_alloc::assert_no_allocation;
2818        use rusteron_media_driver::AeronDriverContext;
2819        use serial_test::serial;
2820
2821        /// Tests the `poll_fn` closure-poll on `AeronSubscription`.
2822        /// Verifies that it receives all published messages without
2823        /// allocations on the hot path.
2824        #[test]
2825        #[serial]
2826        fn poll_fn_receives_all_messages_no_alloc() -> Result<(), Box<dyn error::Error>> {
2827            rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
2828
2829            let media_driver_ctx = AeronDriverContext::new()?;
2830            media_driver_ctx.set_dir_delete_on_shutdown(true)?;
2831            media_driver_ctx.set_dir_delete_on_start(true)?;
2832            media_driver_ctx
2833                .set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
2834            let (stop, driver_handle) =
2835                rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
2836
2837            let ctx = AeronContext::new()?;
2838            ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
2839            let error_handler = Handler::new(ErrorCount::default());
2840            ctx.set_error_handler(Some(error_handler.clone()))?;
2841            let aeron = Aeron::new(&ctx)?;
2842            aeron.start()?;
2843
2844            let channel = String::from("aeron:ipc");
2845            let stream_id: i32 = 9999;
2846
2847            let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), stream_id)?;
2848            let sub_poller =
2849                aeron.async_add_subscription(&channel.into_c_string(), stream_id, Handlers::NONE, Handlers::NONE)?;
2850
2851            let mut publication: Option<AeronPublication> = None;
2852            let mut subscription: Option<AeronSubscription> = None;
2853            let start = Instant::now();
2854            while start.elapsed() < Duration::from_secs(2) {
2855                if publication.is_none() {
2856                    if let Ok(Some(p)) = pub_poller.poll() {
2857                        publication = Some(p);
2858                    }
2859                }
2860                if subscription.is_none() {
2861                    if let Ok(Some(s)) = sub_poller.poll() {
2862                        subscription = Some(s);
2863                    }
2864                }
2865                if publication.is_some() && subscription.is_some() {
2866                    break;
2867                }
2868                #[cfg(debug_assertions)]
2869                sleep(Duration::from_millis(10));
2870            }
2871
2872            let (publisher, subscription) = match (publication, subscription) {
2873                (Some(p), Some(s)) => (p, s),
2874                _ => panic!("publication/subscription did not come up"),
2875            };
2876
2877            // Wait for IPC images to connect
2878            let conn_start = Instant::now();
2879            while !publisher.is_connected() && conn_start.elapsed() < Duration::from_secs(2) {
2880                #[cfg(debug_assertions)]
2881                sleep(Duration::from_millis(10));
2882            }
2883
2884            // Publish N distinct messages
2885            const NUM_MESSAGES: usize = 10;
2886            let payloads: Vec<Vec<u8>> = (0..NUM_MESSAGES)
2887                .map(|i| format!("message-{}", i).into_bytes())
2888                .collect();
2889
2890            for payload in &payloads {
2891                let offer_start = Instant::now();
2892                let mut offered = false;
2893                while offer_start.elapsed() < Duration::from_secs(2) {
2894                    if let Ok(pos) = publisher.offer(payload) {
2895                        if pos >= payload.len() as i64 {
2896                            offered = true;
2897                            break;
2898                        }
2899                    }
2900                    #[cfg(debug_assertions)]
2901                    sleep(Duration::from_millis(10));
2902                }
2903                assert!(offered, "Failed to offer message");
2904            }
2905
2906            // Use poll_fn to receive all messages (spin-poll pattern)
2907            let mut received_count = 0;
2908            let max_iterations = 1000;
2909
2910            for _ in 0..max_iterations {
2911                let fragments = subscription.poll_fn(
2912                    |data, _header| {
2913                        received_count += 1;
2914                        info!("Received fragment {} bytes", data.len());
2915                    },
2916                    1024,
2917                )?;
2918
2919                if fragments > 0 {
2920                    // Got some messages, exit spin
2921                    break;
2922                }
2923
2924                #[cfg(debug_assertions)]
2925                sleep(Duration::from_micros(100));
2926            }
2927
2928            assert_eq!(
2929                received_count, NUM_MESSAGES,
2930                "Expected to receive {} messages, got {}",
2931                NUM_MESSAGES, received_count
2932            );
2933
2934            // Now test that the hot path is allocation-free
2935            let alloc_before = current_allocs();
2936
2937            let mut alloc_free_count = 0;
2938            let spin_alloc_test = || {
2939                for _ in 0..10 {
2940                    let _ = subscription.poll_fn(
2941                        |data, _header| {
2942                            alloc_free_count += 1;
2943                        },
2944                        1024,
2945                    );
2946                }
2947            };
2948
2949            assert_no_allocation(spin_alloc_test);
2950
2951            let alloc_after = current_allocs();
2952            assert!(
2953                (alloc_after - alloc_before).abs() < 10,
2954                "Expected no net allocation in poll_fn hot path"
2955            );
2956
2957            // Cleanup
2958            stop.store(true, Ordering::SeqCst);
2959            let _ = driver_handle.join().unwrap();
2960
2961            Ok(())
2962        }
2963    }
2964
2965    // ── Use-after-free tests: aeron dropped before children ───────────────
2966    //
2967    // When `drop(aeron)` fires first, the C side calls `aeron_close()` which
2968    // frees every registered resource (publications, subscriptions, counters,
2969    // exclusive publications) via `aeron_client_conductor_on_close()`.
2970    //
2971    // The Rust wrappers don't know this happened — their `close_already_called`
2972    // flag is still `false`, so any subsequent method call that goes through
2973    // `get_inner()` dereferences a dangling pointer.
2974    //
2975    // These tests save the raw C pointer before `drop(aeron)`, then call C
2976    // functions directly on it afterwards to eliminate intermediate Drop
2977    // machinery that could trigger re-allocation.
2978
2979    /// Helper: set up an embedded media driver + Aeron client.
2980    fn setup_aeron_for_uaf_test() -> (
2981        Aeron,
2982        rusteron_media_driver::testing::EmbeddedDriver,
2983        Handler<TestErrorCount>,
2984    ) {
2985        setup_aeron_for_uaf_test_with(|_| Ok(()))
2986    }
2987
2988    fn setup_aeron_for_uaf_test_with(
2989        configure: impl FnOnce(&AeronDriverContext) -> Result<(), rusteron_media_driver::AeronCError>,
2990    ) -> (
2991        Aeron,
2992        rusteron_media_driver::testing::EmbeddedDriver,
2993        Handler<TestErrorCount>,
2994    ) {
2995        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
2996
2997        let driver = rusteron_media_driver::testing::EmbeddedDriver::launch_with(configure).unwrap();
2998
2999        let ctx = AeronContext::new().unwrap();
3000        ctx.set_dir(&driver.dir().into_c_string()).unwrap();
3001        let error_handler = Handler::new(TestErrorCount::default());
3002        ctx.set_error_handler(Some(error_handler.clone())).unwrap();
3003
3004        let aeron = Aeron::new(&ctx).unwrap();
3005        aeron.start().unwrap();
3006
3007        (aeron, driver, error_handler)
3008    }
3009
3010    fn teardown_aeron_after_uaf_test(
3011        driver: rusteron_media_driver::testing::EmbeddedDriver,
3012        error_handler: Handler<TestErrorCount>,
3013    ) {
3014        drop(driver); // stops + joins on Drop
3015    }
3016
3017    /// `drop(aeron)` while children hold `Rc<Aeron>` references must not call
3018    /// `aeron_close()` until the last child drops — the raw C pointer reads below
3019    /// stay valid because close is deferred.
3020    #[test]
3021    #[serial]
3022    fn drop_client_before_children_is_safe_test() {
3023        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3024
3025        // Create all resource types.
3026        let publisher = aeron
3027            .add_publication(AERON_IPC_STREAM, 1001, Duration::from_secs(5))
3028            .unwrap();
3029        let subscription = aeron
3030            .add_subscription(
3031                AERON_IPC_STREAM,
3032                1002,
3033                Handlers::NONE,
3034                Handlers::NONE,
3035                Duration::from_secs(5),
3036            )
3037            .unwrap();
3038        let counter = aeron
3039            .add_counter(1003, &[1u8, 2, 3, 4], "test counter", Duration::from_secs(5))
3040            .unwrap();
3041        let excl_pub = aeron
3042            .add_exclusive_publication(AERON_IPC_STREAM, 1004, Duration::from_secs(5))
3043            .unwrap();
3044
3045        // Save raw C pointers while still valid.
3046        let pub_ptr: *mut aeron_publication_t = publisher.get_inner();
3047        let sub_ptr: *mut aeron_subscription_t = subscription.get_inner();
3048        let counter_ptr: *mut aeron_counter_t = counter.get_inner();
3049        let excl_pub_ptr: *mut aeron_exclusive_publication_t = excl_pub.get_inner();
3050
3051        // SAFE: drop(aeron) does NOT call aeron_close() — children still
3052        // hold Rc references, keeping the C client alive.
3053        drop(aeron);
3054
3055        // Read raw pointers — these access VALID (not freed) memory.
3056        let _closed = unsafe { aeron_publication_is_closed(pub_ptr) };
3057        let _connected = unsafe { aeron_subscription_is_connected(sub_ptr) };
3058        let _counter_closed = unsafe { aeron_counter_is_closed(counter_ptr) };
3059        let _excl_closed = unsafe { aeron_exclusive_publication_is_closed(excl_pub_ptr) };
3060        let _channel = unsafe { aeron_publication_channel(pub_ptr) };
3061
3062        // Reaching here proves structural safety: no UAF after drop(aeron).
3063
3064        // Now drop children properly — last one releases Rc<Aeron> →
3065        // aeron_close() fires in correct order.
3066        drop(publisher);
3067        drop(subscription);
3068        drop(counter);
3069        drop(excl_pub);
3070
3071        // Teardown.
3072        teardown_aeron_after_uaf_test(driver, error_handler);
3073    }
3074
3075    #[test]
3076    #[serial]
3077    fn cloned_leaf_handles_close_once_and_null_all_clones() {
3078        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3079
3080        let publisher = aeron
3081            .add_publication(AERON_IPC_STREAM, 1101, Duration::from_secs(5))
3082            .unwrap();
3083        let publisher_clone = publisher.clone();
3084        assert!(!publisher_clone.get_inner().is_null());
3085        assert!(publisher.close().is_ok());
3086        assert!(publisher_clone.get_inner().is_null());
3087        assert!(publisher_clone.close().is_ok());
3088
3089        let subscription = aeron
3090            .add_subscription(
3091                AERON_IPC_STREAM,
3092                1102,
3093                Handlers::NONE,
3094                Handlers::NONE,
3095                Duration::from_secs(5),
3096            )
3097            .unwrap();
3098        let subscription_clone = subscription.clone();
3099        assert!(!subscription_clone.get_inner().is_null());
3100        assert!(subscription.close().is_ok());
3101        assert!(subscription_clone.get_inner().is_null());
3102        assert!(subscription_clone.close().is_ok());
3103
3104        let counter = aeron
3105            .add_counter(1103, &[1u8, 2, 3, 4], "close clone counter", Duration::from_secs(5))
3106            .unwrap();
3107        let counter_clone = counter.clone();
3108        assert!(!counter_clone.get_inner().is_null());
3109        assert!(counter.close().is_ok());
3110        assert!(counter_clone.get_inner().is_null());
3111        assert!(counter_clone.close().is_ok());
3112
3113        let exclusive_publisher = aeron
3114            .add_exclusive_publication(AERON_IPC_STREAM, 1104, Duration::from_secs(5))
3115            .unwrap();
3116        let exclusive_publisher_clone = exclusive_publisher.clone();
3117        assert!(!exclusive_publisher_clone.get_inner().is_null());
3118        assert!(exclusive_publisher.close().is_ok());
3119        assert!(exclusive_publisher_clone.get_inner().is_null());
3120        assert!(exclusive_publisher_clone.close().is_ok());
3121
3122        assert!(aeron.close().is_ok());
3123        teardown_aeron_after_uaf_test(driver, error_handler);
3124    }
3125
3126    #[test]
3127    #[serial]
3128    fn close_with_handler_invokes_publication_close_notification_and_nulls_clones() {
3129        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3130
3131        let notification_count = Arc::new(AtomicUsize::new(0));
3132        let close_handler = Handler::new(CloseNotificationCount {
3133            count: notification_count.clone(),
3134        });
3135
3136        let publisher = aeron
3137            .add_publication(AERON_IPC_STREAM, 1151, Duration::from_secs(5))
3138            .unwrap();
3139        let publisher_clone = publisher.clone();
3140
3141        assert!(publisher.close_with_handler(Some(&close_handler)).is_ok());
3142        assert!(publisher_clone.get_inner().is_null());
3143
3144        let start = Instant::now();
3145        while notification_count.load(Ordering::SeqCst) == 0 && start.elapsed() < Duration::from_secs(5) {
3146            sleep(Duration::from_millis(10));
3147        }
3148        assert_eq!(1, notification_count.load(Ordering::SeqCst));
3149
3150        assert!(publisher_clone.close_with_handler(Some(&close_handler)).is_ok());
3151        assert_eq!(1, notification_count.load(Ordering::SeqCst));
3152
3153        assert!(aeron.close().is_ok());
3154        teardown_aeron_after_uaf_test(driver, error_handler);
3155    }
3156
3157    #[test]
3158    #[serial]
3159    fn explicit_client_close_defers_while_children_are_alive() {
3160        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3161
3162        let publisher = aeron
3163            .add_publication(AERON_IPC_STREAM, 1201, Duration::from_secs(5))
3164            .unwrap();
3165        let subscription = aeron
3166            .add_subscription(
3167                AERON_IPC_STREAM,
3168                1202,
3169                Handlers::NONE,
3170                Handlers::NONE,
3171                Duration::from_secs(5),
3172            )
3173            .unwrap();
3174        let counter = aeron
3175            .add_counter(
3176                1203,
3177                &[5u8, 6, 7, 8],
3178                "deferred client close counter",
3179                Duration::from_secs(5),
3180            )
3181            .unwrap();
3182        let exclusive_publisher = aeron
3183            .add_exclusive_publication(AERON_IPC_STREAM, 1204, Duration::from_secs(5))
3184            .unwrap();
3185
3186        let pub_ptr = publisher.get_inner();
3187        let sub_ptr = subscription.get_inner();
3188        let counter_ptr = counter.get_inner();
3189        let excl_ptr = exclusive_publisher.get_inner();
3190
3191        assert!(aeron.clone().close().is_ok());
3192
3193        assert!(!publisher.get_inner().is_null());
3194        assert!(!subscription.get_inner().is_null());
3195        assert!(!counter.get_inner().is_null());
3196        assert!(!exclusive_publisher.get_inner().is_null());
3197
3198        let _ = unsafe { aeron_publication_is_closed(pub_ptr) };
3199        let _ = unsafe { aeron_subscription_is_connected(sub_ptr) };
3200        let _ = unsafe { aeron_counter_is_closed(counter_ptr) };
3201        let _ = unsafe { aeron_exclusive_publication_is_closed(excl_ptr) };
3202
3203        drop(publisher);
3204        drop(subscription);
3205        drop(counter);
3206        drop(exclusive_publisher);
3207
3208        assert!(aeron.close().is_ok());
3209        teardown_aeron_after_uaf_test(driver, error_handler);
3210    }
3211
3212    #[test]
3213    #[serial]
3214    fn explicit_client_clone_close_defers_and_original_remains_usable() {
3215        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3216
3217        let aeron_clone = aeron.clone();
3218        assert!(aeron_clone.close().is_ok());
3219
3220        let publisher = aeron
3221            .add_publication(AERON_IPC_STREAM, 1301, Duration::from_secs(5))
3222            .expect("original client should remain usable after closing a clone");
3223        assert!(!publisher.get_inner().is_null());
3224        assert!(publisher.close().is_ok());
3225
3226        assert!(aeron.close().is_ok());
3227        teardown_aeron_after_uaf_test(driver, error_handler);
3228    }
3229
3230    #[test]
3231    #[serial]
3232    fn closing_leaf_resource_does_not_poison_client_or_siblings() {
3233        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3234
3235        let publisher = aeron
3236            .add_publication(AERON_IPC_STREAM, 1401, Duration::from_secs(5))
3237            .unwrap();
3238        let subscription = aeron
3239            .add_subscription(
3240                AERON_IPC_STREAM,
3241                1402,
3242                Handlers::NONE,
3243                Handlers::NONE,
3244                Duration::from_secs(5),
3245            )
3246            .unwrap();
3247
3248        assert!(publisher.close().is_ok());
3249        assert!(!subscription.get_inner().is_null());
3250        let _ = unsafe { aeron_subscription_is_connected(subscription.get_inner()) };
3251
3252        let next_publisher = aeron
3253            .add_publication(AERON_IPC_STREAM, 1403, Duration::from_secs(5))
3254            .expect("client should create new resources after a leaf close");
3255        assert!(!next_publisher.get_inner().is_null());
3256
3257        assert!(subscription.close().is_ok());
3258        assert!(next_publisher.close().is_ok());
3259        assert!(aeron.close().is_ok());
3260        teardown_aeron_after_uaf_test(driver, error_handler);
3261    }
3262
3263    /// After `aeron.close()` the surviving handles must remain fully usable —
3264    /// not merely safe to drop. A complete offer/poll roundtrip is the strongest
3265    /// proof that the deferred close left the C client fully operational.
3266    #[test]
3267    #[serial]
3268    fn client_close_defers_and_children_remain_fully_usable() {
3269        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3270
3271        let publisher = aeron
3272            .add_publication(AERON_IPC_STREAM, 1501, Duration::from_secs(5))
3273            .unwrap();
3274        let subscription = aeron
3275            .add_subscription(
3276                AERON_IPC_STREAM,
3277                1501,
3278                Handlers::NONE,
3279                Handlers::NONE,
3280                Duration::from_secs(5),
3281            )
3282            .unwrap();
3283
3284        assert!(aeron.clone().close().is_ok());
3285
3286        let payload = b"post-close-roundtrip";
3287        let send_start = Instant::now();
3288        let mut sent = false;
3289        while send_start.elapsed() < Duration::from_secs(5) {
3290            if publisher.offer_raw(payload, Handlers::NONE) >= payload.len() as i64 {
3291                sent = true;
3292                break;
3293            }
3294            sleep(Duration::from_millis(10));
3295        }
3296        assert!(sent, "offer() must still work after a deferred client close");
3297
3298        let received = Arc::new(AtomicUsize::new(0));
3299        let received_copy = received.clone();
3300        let read_start = Instant::now();
3301        while received.load(Ordering::SeqCst) == 0 && read_start.elapsed() < Duration::from_secs(5) {
3302            subscription
3303                .poll_fn(
3304                    |buffer, _header| {
3305                        assert_eq!(buffer, payload);
3306                        received_copy.fetch_add(1, Ordering::SeqCst);
3307                    },
3308                    16,
3309                )
3310                .unwrap();
3311            sleep(Duration::from_millis(10));
3312        }
3313        assert_eq!(
3314            1,
3315            received.load(Ordering::SeqCst),
3316            "poll() must still deliver after a deferred client close"
3317        );
3318
3319        drop(publisher);
3320        drop(subscription);
3321        assert!(aeron.close().is_ok());
3322        teardown_aeron_after_uaf_test(driver, error_handler);
3323    }
3324
3325    struct CountingAvailableImageHandler {
3326        available: Arc<AtomicUsize>,
3327        drops: Arc<AtomicUsize>,
3328    }
3329
3330    impl AeronAvailableImageCallback for CountingAvailableImageHandler {
3331        fn handle_aeron_on_available_image(&mut self, _subscription: AeronSubscription, _image: AeronImage) {
3332            self.available.fetch_add(1, Ordering::SeqCst);
3333        }
3334    }
3335
3336    impl Drop for CountingAvailableImageHandler {
3337        fn drop(&mut self) {
3338            self.drops.fetch_add(1, Ordering::SeqCst);
3339        }
3340    }
3341
3342    /// A retained callback must stay alive as long as the C client can invoke it,
3343    /// even when the caller drops their `Handler` immediately after registration:
3344    /// the subscription owns a clone via its dependency list. It must be freed
3345    /// exactly once when the subscription (and its async poller) are gone.
3346    #[test]
3347    #[serial]
3348    fn retained_image_handler_outlives_callers_drop_and_frees_exactly_fn() {
3349        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3350
3351        let available = Arc::new(AtomicUsize::new(0));
3352        let drops = Arc::new(AtomicUsize::new(0));
3353
3354        let subscription = {
3355            let handler = Handler::new(CountingAvailableImageHandler {
3356                available: available.clone(),
3357                drops: drops.clone(),
3358            });
3359            let subscription = aeron
3360                .add_subscription(
3361                    AERON_IPC_STREAM,
3362                    1601,
3363                    Some(&handler),
3364                    Handlers::NONE,
3365                    Duration::from_secs(5),
3366                )
3367                .unwrap();
3368            // caller's reference goes away here — the subscription's dependency
3369            // clone must keep the callback value alive for the conductor thread
3370            drop(handler);
3371            subscription
3372        };
3373        assert_eq!(
3374            0,
3375            drops.load(Ordering::SeqCst),
3376            "handler freed while C can still call it"
3377        );
3378
3379        let publisher = aeron
3380            .add_publication(AERON_IPC_STREAM, 1601, Duration::from_secs(5))
3381            .unwrap();
3382
3383        let start = Instant::now();
3384        while available.load(Ordering::SeqCst) == 0 && start.elapsed() < Duration::from_secs(5) {
3385            let _ = publisher.offer_raw(b"wake", Handlers::NONE);
3386            sleep(Duration::from_millis(10));
3387        }
3388        assert!(
3389            available.load(Ordering::SeqCst) > 0,
3390            "image-available callback should have fired after the caller dropped its Handler"
3391        );
3392        assert_eq!(0, drops.load(Ordering::SeqCst));
3393
3394        drop(publisher);
3395        drop(subscription);
3396        drop(aeron);
3397        assert_eq!(
3398            1,
3399            drops.load(Ordering::SeqCst),
3400            "handler must be freed exactly once after its owning resources close"
3401        );
3402        teardown_aeron_after_uaf_test(driver, error_handler);
3403    }
3404
3405    /// Dropping an async poller *without polling it* auto-cancels the pending add (see
3406    /// `dropping_unpolled_async_subscription_does_not_leak_driver_counter`), so the
3407    /// subscription never completes and a later publication must NOT fire the image
3408    /// handler. That must hold *without* freeing the handler early: it stays anchored to
3409    /// the client (whose lifetime matches the conductor's) until the client itself is
3410    /// dropped, so a callback racing with cancellation can never observe a freed handler.
3411    #[test]
3412    #[serial]
3413    fn unpolled_async_subscription_drop_keeps_image_handler_alive() {
3414        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3415
3416        let available = Arc::new(AtomicUsize::new(0));
3417        let drops = Arc::new(AtomicUsize::new(0));
3418
3419        {
3420            let handler = Handler::new(CountingAvailableImageHandler {
3421                available: available.clone(),
3422                drops: drops.clone(),
3423            });
3424            let poller = aeron
3425                .async_add_subscription(AERON_IPC_STREAM, 1611, Some(&handler), Handlers::NONE)
3426                .unwrap();
3427            // both the caller's handler and the never-polled poller go away here, which
3428            // auto-cancels the pending add (see the `auto-cancelling ...` warn log)
3429        }
3430        assert_eq!(
3431            0,
3432            drops.load(Ordering::SeqCst),
3433            "handler freed while the conductor could still (transiently) reference it (UAF)"
3434        );
3435
3436        // the add was cancelled before it ever completed, so connecting a publication on
3437        // the same stream must never fire the image-available callback
3438        let publisher = aeron
3439            .add_publication(AERON_IPC_STREAM, 1611, Duration::from_secs(5))
3440            .unwrap();
3441        let start = Instant::now();
3442        while start.elapsed() < Duration::from_secs(2) {
3443            let _ = publisher.offer_raw(b"wake", Handlers::NONE);
3444            sleep(Duration::from_millis(10));
3445        }
3446        assert_eq!(
3447            0,
3448            available.load(Ordering::SeqCst),
3449            "cancelled subscription must never complete/connect after being dropped unpolled"
3450        );
3451        assert_eq!(0, drops.load(Ordering::SeqCst));
3452
3453        drop(publisher);
3454        drop(aeron);
3455        assert_eq!(1, drops.load(Ordering::SeqCst), "freed exactly once at client close");
3456        teardown_aeron_after_uaf_test(driver, error_handler);
3457    }
3458
3459    /// A failed async add (invalid URI) must fail cleanly: the error surfaces from
3460    /// new()/poll(), later polls are inert (the C client freed the async struct on
3461    /// the errored poll), and the client stays fully usable afterwards.
3462    #[test]
3463    #[serial]
3464    fn async_add_subscription_invalid_uri_fails_cleanly() {
3465        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3466
3467        // the driver rejects the port before resolving the host, so no DNS lookup slows this down
3468        let bad_uri = c"aeron:udp?endpoint=127.0.0.1:99999";
3469        match aeron.async_add_subscription(bad_uri, 1621, Handlers::NONE, Handlers::NONE) {
3470            Err(_) => {} // rejected synchronously — fine
3471            Ok(poller) => {
3472                let mut saw_error = false;
3473                let start = Instant::now();
3474                while start.elapsed() < Duration::from_secs(5) {
3475                    match poller.poll() {
3476                        Err(_) => {
3477                            saw_error = true;
3478                            break;
3479                        }
3480                        Ok(Some(_)) => panic!("subscription must not be created for an invalid uri"),
3481                        Ok(None) => sleep(Duration::from_millis(10)),
3482                    }
3483                }
3484                assert!(saw_error, "poll should surface the async add error");
3485                // the C client freed the async struct on the errored poll; further
3486                // polls must be inert, not use-after-free
3487                assert!(matches!(poller.poll(), Ok(None)));
3488                assert!(matches!(poller.poll(), Ok(None)));
3489                drop(poller);
3490            }
3491        }
3492
3493        // client unaffected: normal roundtrip still works
3494        let publisher = aeron
3495            .add_publication(AERON_IPC_STREAM, 1622, Duration::from_secs(5))
3496            .unwrap();
3497        assert!(!publisher.get_inner().is_null());
3498        assert!(publisher.close().is_ok());
3499        assert!(aeron.close().is_ok());
3500        teardown_aeron_after_uaf_test(driver, error_handler);
3501    }
3502
3503    /// The full dependency graph at once — context (with error handler), client + clone,
3504    /// publication + clone, exclusive publication, subscription with a retained image
3505    /// handler, counter, and an unpolled async poller — closed/dropped in an adversarial
3506    /// order, with every surviving handle exercised after each step. Deferred close must
3507    /// keep the C client alive until the *last* reference, and every retained handler must
3508    /// be freed exactly once at the end.
3509    #[test]
3510    #[serial]
3511    fn complex_object_graph_close_is_safe_in_any_order() {
3512        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
3513
3514        let media_driver_ctx = AeronDriverContext::new().unwrap();
3515        media_driver_ctx.set_dir_delete_on_shutdown(true).unwrap();
3516        media_driver_ctx.set_dir_delete_on_start(true).unwrap();
3517        media_driver_ctx
3518            .set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())
3519            .unwrap();
3520        let (stop, driver_handle) =
3521            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
3522
3523        let errors = Arc::new(AtomicUsize::new(0));
3524        let image_drops = Arc::new(AtomicUsize::new(0));
3525        let unpolled_drops = Arc::new(AtomicUsize::new(0));
3526        let available = Arc::new(AtomicUsize::new(0));
3527
3528        let ctx = AeronContext::new().unwrap();
3529        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string()).unwrap();
3530        let errors_cb = errors.clone();
3531        ctx.set_error_handler(Some(move |code: i32, msg: &str| {
3532            errors_cb.fetch_add(1, Ordering::SeqCst);
3533            log::error!("client error {code}: {msg}");
3534        }))
3535        .unwrap();
3536        let aeron = Aeron::new(&ctx).unwrap();
3537        aeron.start().unwrap();
3538        let aeron_clone = aeron.clone();
3539        drop(ctx); // the client's internal clone keeps the C context alive
3540
3541        // children of every kind
3542        let publication = aeron
3543            .add_publication(AERON_IPC_STREAM, 1801, Duration::from_secs(5))
3544            .unwrap();
3545        let publication_clone = publication.clone();
3546        let exclusive = aeron
3547            .add_exclusive_publication(AERON_IPC_STREAM, 1801, Duration::from_secs(5))
3548            .unwrap();
3549        let image_handler = Handler::new(CountingAvailableImageHandler {
3550            available: available.clone(),
3551            drops: image_drops.clone(),
3552        });
3553        let subscription = aeron
3554            .add_subscription(
3555                AERON_IPC_STREAM,
3556                1801,
3557                Some(&image_handler),
3558                Handlers::NONE,
3559                Duration::from_secs(5),
3560            )
3561            .unwrap();
3562        drop(image_handler); // subscription + client keep it alive
3563        let counter = aeron
3564            .add_counter(1802, &[1, 2, 3], "graph counter", Duration::from_secs(5))
3565            .unwrap();
3566        let unpolled_handler = Handler::new(CountingAvailableImageHandler {
3567            available: available.clone(),
3568            drops: unpolled_drops.clone(),
3569        });
3570        let unpolled_poller = aeron
3571            .async_add_subscription(AERON_IPC_STREAM, 1803, Some(&unpolled_handler), Handlers::NONE)
3572            .unwrap();
3573        drop(unpolled_handler);
3574        drop(unpolled_poller); // never polled — its handler must stay alive via the client
3575
3576        // adversarial order: close the client FIRST (defers), then use everything
3577        assert!(aeron.close().is_ok());
3578
3579        let payload = b"graph-roundtrip";
3580        let start = Instant::now();
3581        let mut sent = false;
3582        while start.elapsed() < Duration::from_secs(5) {
3583            if publication.offer(payload).is_ok() {
3584                sent = true;
3585                break;
3586            }
3587            sleep(Duration::from_millis(5));
3588        }
3589        assert!(sent, "publication must work after deferred client close");
3590        assert!(exclusive.offer(payload).is_ok() || !exclusive.is_connected());
3591        let received = Arc::new(AtomicUsize::new(0));
3592        let received_copy = received.clone();
3593        let read_start = Instant::now();
3594        while received.load(Ordering::SeqCst) == 0 && read_start.elapsed() < Duration::from_secs(5) {
3595            subscription
3596                .poll_fn(
3597                    |_buf, _hdr| {
3598                        received_copy.fetch_add(1, Ordering::SeqCst);
3599                    },
3600                    16,
3601                )
3602                .unwrap();
3603            sleep(Duration::from_millis(5));
3604        }
3605        assert!(
3606            received.load(Ordering::SeqCst) >= 1,
3607            "subscription must deliver after deferred close"
3608        );
3609        counter.addr_atomic().store(42, std::sync::atomic::Ordering::SeqCst);
3610        assert_eq!(42, counter.addr_atomic().load(std::sync::atomic::Ordering::SeqCst));
3611
3612        // close a leaf, then its clone must be inert but safe
3613        assert!(publication.close().is_ok());
3614        assert!(publication_clone.get_inner().is_null());
3615        assert!(publication_clone.close().is_ok());
3616
3617        // remaining teardown in shuffled order, exercising the clone last
3618        drop(subscription);
3619        assert_eq!(
3620            0,
3621            image_drops.load(Ordering::SeqCst),
3622            "client anchor still holds the image handler"
3623        );
3624        drop(exclusive);
3625        assert!(counter.close().is_ok());
3626        assert!(aeron_clone.close().is_ok()); // the LAST reference — the real aeron_close runs here
3627
3628        assert_eq!(
3629            1,
3630            image_drops.load(Ordering::SeqCst),
3631            "image handler freed exactly once"
3632        );
3633        assert_eq!(
3634            1,
3635            unpolled_drops.load(Ordering::SeqCst),
3636            "unpolled poller's handler freed exactly once"
3637        );
3638        assert_eq!(
3639            0,
3640            errors.load(Ordering::SeqCst),
3641            "no client errors during graph teardown"
3642        );
3643
3644        stop.store(true, Ordering::SeqCst);
3645        let _ = driver_handle.join();
3646    }
3647
3648    /// Typed counter navigation: publisher-limit and subscriber-position counters are
3649    /// found by (type id, registration id), mirroring Java's CountersReader lookups.
3650    #[test]
3651    #[serial]
3652    fn counters_can_be_found_by_type_and_registration_id() {
3653        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3654
3655        let publisher = aeron
3656            .add_publication(AERON_IPC_STREAM, 1951, Duration::from_secs(5))
3657            .unwrap();
3658        let subscription = aeron
3659            .add_subscription(
3660                AERON_IPC_STREAM,
3661                1951,
3662                Handlers::NONE,
3663                Handlers::NONE,
3664                Duration::from_secs(5),
3665            )
3666            .unwrap();
3667        let start = Instant::now();
3668        while !publisher.is_connected() && start.elapsed() < Duration::from_secs(5) {
3669            sleep(Duration::from_millis(10));
3670        }
3671
3672        let counters = aeron.counters_reader();
3673        let pub_registration_id = publisher.get_constants().unwrap().registration_id;
3674        let limit_counter = counters
3675            .find_by_type_and_registration_id(AERON_COUNTER_PUBLISHER_LIMIT_TYPE_ID as i32, pub_registration_id)
3676            .expect("publisher limit counter");
3677        assert!(
3678            counters.get_counter_value(limit_counter) > 0,
3679            "publisher limit should be positive once connected"
3680        );
3681
3682        let sub_registration_id = subscription.get_constants().unwrap().registration_id();
3683        let position_counter = counters
3684            .find_by_type_and_registration_id(AERON_COUNTER_SUBSCRIPTION_POSITION_TYPE_ID as i32, sub_registration_id)
3685            .expect("subscriber position counter");
3686        assert!(counters.get_counter_value(position_counter) >= 0);
3687
3688        assert!(
3689            counters
3690                .find_by_type_and_registration_id(AERON_COUNTER_PUBLISHER_LIMIT_TYPE_ID as i32, -12345)
3691                .is_none()
3692        );
3693
3694        drop(publisher);
3695        drop(subscription);
3696        drop(aeron);
3697        teardown_aeron_after_uaf_test(driver, error_handler);
3698    }
3699
3700    #[test]
3701    #[serial]
3702    fn dropping_unpolled_async_subscription_does_not_leak_driver_counter() {
3703        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3704
3705        let stream_id = 1901;
3706        let registration_id = {
3707            let poller = aeron
3708                .async_add_subscription(AERON_IPC_STREAM, stream_id, Handlers::NONE, Handlers::NONE)
3709                .unwrap();
3710            let registration_id = poller.get_registration_id();
3711            drop(poller); // never polled
3712            registration_id
3713        };
3714
3715        // A subscription's position counter is only materialised once it has an image
3716        // (i.e. a connected publication) — so connect one to force the conductor to fully
3717        // complete whatever it did with the abandoned add. If the add wasn't cancelled,
3718        // the leaked subscription will connect just like a normal one and get a counter.
3719        let publisher = aeron
3720            .add_publication(AERON_IPC_STREAM, stream_id, Duration::from_secs(5))
3721            .unwrap();
3722        let counters = aeron.counters_reader();
3723        let start = Instant::now();
3724        let mut leaked_counter = None;
3725        while start.elapsed() < Duration::from_secs(5) {
3726            let _ = publisher.offer_raw(b"probe", Handlers::NONE);
3727            if let Some(counter) = counters
3728                .find_by_type_and_registration_id(AERON_COUNTER_SUBSCRIPTION_POSITION_TYPE_ID as i32, registration_id)
3729            {
3730                leaked_counter = Some(counter);
3731                break;
3732            }
3733            sleep(Duration::from_millis(20));
3734        }
3735
3736        assert!(
3737            leaked_counter.is_none(),
3738            "subscription for registration_id={registration_id} is still registered with the \
3739             driver (visible in its counters, as `aeron-stats` would show) even though the \
3740             async poller was dropped before poll() ever resolved it — this is the issue #60 leak"
3741        );
3742
3743        // client stays fully usable afterwards
3744        assert!(!publisher.get_inner().is_null());
3745        drop(publisher);
3746        drop(aeron);
3747        teardown_aeron_after_uaf_test(driver, error_handler);
3748    }
3749
3750    #[test]
3751    #[serial]
3752    fn async_add_subscription_cancel_is_explicit_and_idempotent() {
3753        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3754
3755        let stream_id = 1902;
3756        let poller = aeron
3757            .async_add_subscription(AERON_IPC_STREAM, stream_id, Handlers::NONE, Handlers::NONE)
3758            .unwrap();
3759        let registration_id = poller.get_registration_id();
3760
3761        poller.cancel().expect("explicit cancel should succeed");
3762        // idempotent: cancelling again (and the eventual drop) must be a no-op, not a
3763        // double-free / double-cancel error
3764        poller.cancel().expect("second cancel must be a harmless no-op");
3765
3766        let publisher = aeron
3767            .add_publication(AERON_IPC_STREAM, stream_id, Duration::from_secs(5))
3768            .unwrap();
3769        let counters = aeron.counters_reader();
3770        let start = Instant::now();
3771        let mut leaked_counter = None;
3772        while start.elapsed() < Duration::from_secs(5) {
3773            let _ = publisher.offer_raw(b"probe", Handlers::NONE);
3774            if let Some(counter) = counters
3775                .find_by_type_and_registration_id(AERON_COUNTER_SUBSCRIPTION_POSITION_TYPE_ID as i32, registration_id)
3776            {
3777                leaked_counter = Some(counter);
3778                break;
3779            }
3780            sleep(Duration::from_millis(20));
3781        }
3782        assert!(
3783            leaked_counter.is_none(),
3784            "explicit .cancel() should release the pending registration just like drop does"
3785        );
3786
3787        drop(poller); // must not re-cancel / panic after two explicit cancels
3788        drop(publisher);
3789        drop(aeron);
3790        teardown_aeron_after_uaf_test(driver, error_handler);
3791    }
3792
3793    /// A poller that *did* resolve via `poll()` must never be cancelled afterwards —
3794    /// calling `.cancel()` (or dropping) post-resolution must be inert and must not tear
3795    /// down the now-live subscription it produced.
3796    #[test]
3797    #[serial]
3798    fn async_add_subscription_cancel_after_resolved_poll_is_inert() {
3799        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3800
3801        let stream_id = 1903;
3802        let poller = aeron
3803            .async_add_subscription(AERON_IPC_STREAM, stream_id, Handlers::NONE, Handlers::NONE)
3804            .unwrap();
3805
3806        let subscription = {
3807            let start = Instant::now();
3808            loop {
3809                if let Some(subscription) = poller.poll().unwrap() {
3810                    break subscription;
3811                }
3812                assert!(start.elapsed() < Duration::from_secs(5), "poll() never resolved");
3813                sleep(Duration::from_millis(10));
3814            }
3815        };
3816
3817        // resolved: cancel must now be a no-op and must not close the live subscription
3818        poller
3819            .cancel()
3820            .expect(".cancel() after a resolved poll() must be a harmless no-op");
3821        drop(poller);
3822
3823        assert!(
3824            !subscription.get_inner().is_null(),
3825            "cancel()/drop after poll() resolved must not tear down the live subscription"
3826        );
3827        drop(subscription);
3828        drop(aeron);
3829        teardown_aeron_after_uaf_test(driver, error_handler);
3830    }
3831
3832    /// Same drop-before-poll leak proof as
3833    /// `dropping_unpolled_async_subscription_does_not_leak_driver_counter`, but for
3834    /// `AeronAsyncAddPublication` — the publisher limit counter must never appear for a
3835    /// registration id whose async add was dropped unpolled.
3836    #[test]
3837    #[serial]
3838    fn dropping_unpolled_async_publication_does_not_leak_driver_counter() {
3839        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3840
3841        let stream_id = 1910;
3842        let registration_id = {
3843            let poller = aeron.async_add_publication(AERON_IPC_STREAM, stream_id).unwrap();
3844            let registration_id = poller.get_registration_id();
3845            drop(poller); // never polled
3846            registration_id
3847        };
3848
3849        // Freeing an already-materialised publication's counter goes through the same
3850        // driver-side unlink+linger machinery as a normal close, so (unlike the
3851        // subscription-position counter, which never appears at all here because nothing
3852        // ever connects) give it a few seconds to actually disappear rather than expecting
3853        // it to be instantaneous.
3854        let counters = aeron.counters_reader();
3855        let start = Instant::now();
3856        let mut leaked_counter = None;
3857        while start.elapsed() < Duration::from_secs(5) {
3858            leaked_counter = counters
3859                .find_by_type_and_registration_id(AERON_COUNTER_PUBLISHER_LIMIT_TYPE_ID as i32, registration_id);
3860            if leaked_counter.is_none() {
3861                break;
3862            }
3863            sleep(Duration::from_millis(50));
3864        }
3865        assert!(
3866            leaked_counter.is_none(),
3867            "publication for registration_id={registration_id} is still registered with the \
3868             driver even though the async poller was dropped before poll() ever resolved it"
3869        );
3870
3871        // client stays fully usable afterwards
3872        let publisher = aeron
3873            .add_publication(AERON_IPC_STREAM, stream_id, Duration::from_secs(5))
3874            .unwrap();
3875        assert!(!publisher.get_inner().is_null());
3876        drop(publisher);
3877        drop(aeron);
3878        teardown_aeron_after_uaf_test(driver, error_handler);
3879    }
3880
3881    /// Same drop-before-poll leak proof, but for `AeronAsyncAddExclusivePublication`.
3882    #[test]
3883    #[serial]
3884    fn dropping_unpolled_async_exclusive_publication_does_not_leak_driver_counter() {
3885        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3886
3887        let stream_id = 1911;
3888        let registration_id = {
3889            let poller = aeron
3890                .async_add_exclusive_publication(AERON_IPC_STREAM, stream_id)
3891                .unwrap();
3892            let registration_id = poller.get_registration_id();
3893            drop(poller); // never polled
3894            registration_id
3895        };
3896
3897        // Same rationale as the publication test above: give the driver-side unlink a
3898        // few seconds instead of expecting it to be instantaneous.
3899        let counters = aeron.counters_reader();
3900        let start = Instant::now();
3901        let mut leaked_counter = None;
3902        while start.elapsed() < Duration::from_secs(5) {
3903            leaked_counter = counters
3904                .find_by_type_and_registration_id(AERON_COUNTER_PUBLISHER_LIMIT_TYPE_ID as i32, registration_id);
3905            if leaked_counter.is_none() {
3906                break;
3907            }
3908            sleep(Duration::from_millis(50));
3909        }
3910        assert!(
3911            leaked_counter.is_none(),
3912            "exclusive publication for registration_id={registration_id} is still registered \
3913             with the driver even though the async poller was dropped before poll() ever \
3914             resolved it"
3915        );
3916
3917        drop(aeron);
3918        teardown_aeron_after_uaf_test(driver, error_handler);
3919    }
3920
3921    /// Same drop-before-poll leak proof, but for `AeronAsyncAddCounter` — a user counter's
3922    /// key buffer (not the registration id) is the only thing we control, so match on the
3923    /// exact key bytes instead of `find_by_type_and_registration_id`.
3924    #[test]
3925    #[serial]
3926    fn dropping_unpolled_async_counter_does_not_leak_driver_counter() {
3927        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3928
3929        let type_id = 987_654;
3930        let key_buffer = [0xABu8, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89];
3931        {
3932            let poller = aeron
3933                .async_add_counter(type_id, &key_buffer, "leak-test-counter")
3934                .unwrap();
3935            drop(poller); // never polled
3936        }
3937
3938        let counters = aeron.counters_reader();
3939        let start = Instant::now();
3940        let mut leaked_counter = None;
3941        while start.elapsed() < Duration::from_secs(2) {
3942            if let Some(counter) = counters.find_by_type_id(type_id, |key| key == key_buffer) {
3943                leaked_counter = Some(counter);
3944                break;
3945            }
3946            sleep(Duration::from_millis(20));
3947        }
3948        assert!(
3949            leaked_counter.is_none(),
3950            "user counter is still registered with the driver even though the async poller \
3951             was dropped before poll() ever resolved it"
3952        );
3953
3954        drop(aeron);
3955        teardown_aeron_after_uaf_test(driver, error_handler);
3956    }
3957
3958    /// Explicit `.cancel()` for `AeronAsyncAddPublication`, `AeronAsyncAddExclusivePublication`
3959    /// and `AeronAsyncAddCounter` must behave like the subscription case: idempotent, and a
3960    /// no-op once `poll()` has already resolved.
3961    #[test]
3962    #[serial]
3963    fn async_add_publication_exclusive_publication_and_counter_cancel_is_explicit_idempotent_and_inert_after_resolve() {
3964        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3965
3966        // publication: explicit cancel before resolve is idempotent and releases the counter
3967        {
3968            let stream_id = 1912;
3969            let poller = aeron.async_add_publication(AERON_IPC_STREAM, stream_id).unwrap();
3970            let registration_id = poller.get_registration_id();
3971            poller.cancel().expect("explicit cancel should succeed");
3972            poller.cancel().expect("second cancel must be a harmless no-op");
3973
3974            let counters = aeron.counters_reader();
3975            let start = Instant::now();
3976            let mut still_present = counters
3977                .find_by_type_and_registration_id(AERON_COUNTER_PUBLISHER_LIMIT_TYPE_ID as i32, registration_id);
3978            while still_present.is_some() && start.elapsed() < Duration::from_secs(5) {
3979                sleep(Duration::from_millis(50));
3980                still_present = counters
3981                    .find_by_type_and_registration_id(AERON_COUNTER_PUBLISHER_LIMIT_TYPE_ID as i32, registration_id);
3982            }
3983            assert!(
3984                still_present.is_none(),
3985                "explicit .cancel() should release the pending publication registration"
3986            );
3987            drop(poller); // must not re-cancel / panic after two explicit cancels
3988        }
3989
3990        // exclusive publication: cancel after a resolved poll() must be inert
3991        {
3992            let stream_id = 1913;
3993            let poller = aeron
3994                .async_add_exclusive_publication(AERON_IPC_STREAM, stream_id)
3995                .unwrap();
3996            let publication = {
3997                let start = Instant::now();
3998                loop {
3999                    if let Some(publication) = poller.poll().unwrap() {
4000                        break publication;
4001                    }
4002                    assert!(start.elapsed() < Duration::from_secs(5), "poll() never resolved");
4003                    sleep(Duration::from_millis(10));
4004                }
4005            };
4006            poller
4007                .cancel()
4008                .expect(".cancel() after a resolved poll() must be a harmless no-op");
4009            drop(poller);
4010            assert!(
4011                !publication.get_inner().is_null(),
4012                "cancel()/drop after poll() resolved must not tear down the live publication"
4013            );
4014            drop(publication);
4015        }
4016
4017        // counter: explicit cancel before resolve is idempotent
4018        {
4019            let type_id = 987_655;
4020            let key_buffer = [1u8, 2, 3, 4, 5, 6, 7, 8];
4021            let poller = aeron
4022                .async_add_counter(type_id, &key_buffer, "explicit-cancel-counter")
4023                .unwrap();
4024            poller.cancel().expect("explicit cancel should succeed");
4025            poller.cancel().expect("second cancel must be a harmless no-op");
4026            drop(poller);
4027
4028            let counters = aeron.counters_reader();
4029            assert!(
4030                counters.find_by_type_id(type_id, |key| key == key_buffer).is_none(),
4031                "explicit .cancel() should release the pending counter registration"
4032            );
4033        }
4034
4035        drop(aeron);
4036        teardown_aeron_after_uaf_test(driver, error_handler);
4037    }
4038
4039    /// A failed async add (invalid URI) must fail cleanly for publications too: the error
4040    /// surfaces from `poll()`, later polls are inert (no use-after-free), the auto-cancel
4041    /// cleanup path must not double-free the C struct the errored poll already released,
4042    /// and the client stays fully usable afterwards. Mirrors
4043    /// `async_add_subscription_invalid_uri_fails_cleanly` for the publication side.
4044    #[test]
4045    #[serial]
4046    fn async_add_publication_invalid_uri_fails_cleanly() {
4047        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
4048
4049        // the driver rejects the port before resolving the host, so no DNS lookup slows this down
4050        let bad_uri = c"aeron:udp?endpoint=127.0.0.1:99999";
4051        match aeron.async_add_publication(bad_uri, 1622) {
4052            Err(_) => {} // rejected synchronously — fine
4053            Ok(poller) => {
4054                let mut saw_error = false;
4055                let start = Instant::now();
4056                while start.elapsed() < Duration::from_secs(5) {
4057                    match poller.poll() {
4058                        Err(_) => {
4059                            saw_error = true;
4060                            break;
4061                        }
4062                        Ok(Some(_)) => panic!("publication must not be created for an invalid uri"),
4063                        Ok(None) => sleep(Duration::from_millis(10)),
4064                    }
4065                }
4066                assert!(saw_error, "poll should surface the async add error");
4067                // the C client freed the async struct on the errored poll; further polls
4068                // must be inert, and the eventual drop must not re-cancel a freed pointer
4069                assert!(matches!(poller.poll(), Ok(None)));
4070                assert!(matches!(poller.poll(), Ok(None)));
4071                drop(poller);
4072            }
4073        }
4074
4075        // client unaffected: normal roundtrip still works
4076        let publisher = aeron
4077            .add_publication(AERON_IPC_STREAM, 1623, Duration::from_secs(5))
4078            .unwrap();
4079        assert!(!publisher.get_inner().is_null());
4080        drop(publisher);
4081        drop(aeron);
4082        teardown_aeron_after_uaf_test(driver, error_handler);
4083    }
4084
4085    /// Builds a `control-mode=manual` (MDS/MDC) UDP channel string.
4086    fn manual_control_mode_channel() -> std::ffi::CString {
4087        let builder = AeronUriStringBuilder::new_zeroed_on_heap();
4088        builder.init_new().unwrap();
4089        builder
4090            .media(Media::Udp)
4091            .unwrap()
4092            .control_mode(ControlMode::Manual)
4093            .unwrap();
4094        cformat!("{}", builder.build(256).unwrap())
4095    }
4096
4097    /// Builds a plain `aeron:udp?endpoint=127.0.0.1:PORT` destination/channel string.
4098    fn udp_endpoint_channel(port: u16) -> std::ffi::CString {
4099        cformat!(
4100            "{}",
4101            AeronUriStringBuilder::udp(&format!("127.0.0.1:{port}"))
4102                .unwrap()
4103                .build(256)
4104                .unwrap()
4105        )
4106    }
4107
4108    /// Repeatedly offers `msg` (ignoring transient/retryable send failures) while polling
4109    /// every subscription in `subs`, until every one of them has observed it (or `timeout`
4110    /// elapses). Re-offering — rather than a single blind send — absorbs the real-world
4111    /// race between "the destination/image is nominally connected" and "the driver has
4112    /// actually finished wiring up delivery to it", which a single fixed-count send can hit
4113    /// even after `is_connected()` reports true.
4114    fn assert_all_subs_eventually_receive(
4115        offer: impl Fn(&[u8]) -> Result<i64, AeronOfferError>,
4116        msg: &str,
4117        subs: &[&AeronSubscription],
4118        timeout: Duration,
4119    ) {
4120        let mut seen: Vec<std::collections::HashSet<String>> = subs.iter().map(|_| Default::default()).collect();
4121        let deadline = Instant::now() + timeout;
4122        while Instant::now() < deadline && !seen.iter().all(|s| s.contains(msg)) {
4123            let _ = offer(msg.as_bytes());
4124            for (i, sub) in subs.iter().enumerate() {
4125                let _ = sub.poll_fn(
4126                    |buf, _hdr| {
4127                        seen[i].insert(String::from_utf8_lossy(buf).to_string());
4128                    },
4129                    16,
4130                );
4131            }
4132            sleep(Duration::from_millis(20));
4133        }
4134        for (i, s) in seen.iter().enumerate() {
4135            assert!(s.contains(msg), "subs[{i}] never received {msg:?}, saw {s:?}");
4136        }
4137    }
4138
4139    /// Repeatedly offers freshly-tagged probe messages while polling both `removed_sub`
4140    /// (expected to stop receiving) and every `alive_subs` entry (expected to keep
4141    /// receiving), until one probe is confirmed delivered to all `alive_subs` but absent
4142    /// from `removed_sub`. This proves the destination removal has genuinely taken effect
4143    /// driver-side — not merely that `remove_destination(...)` returned `Ok(())` — while
4144    /// tolerating the small, real round-trip delay between that call succeeding and the
4145    /// driver actually tearing down delivery to the removed destination (the same kind of
4146    /// delay already observed for publication/counter cancellation elsewhere in this suite).
4147    /// Panics if no such probe is confirmed within `timeout`.
4148    fn assert_destination_removal_takes_effect(
4149        offer: impl Fn(&[u8]) -> Result<i64, AeronOfferError>,
4150        removed_sub: &AeronSubscription,
4151        alive_subs: &[&AeronSubscription],
4152        timeout: Duration,
4153    ) {
4154        let deadline = Instant::now() + timeout;
4155        let mut attempt = 0usize;
4156        loop {
4157            attempt += 1;
4158            let msg = format!("removal-probe-{attempt}");
4159            let mut alive_seen: Vec<std::collections::HashSet<String>> =
4160                alive_subs.iter().map(|_| Default::default()).collect();
4161            let mut removed_seen = std::collections::HashSet::new();
4162            let probe_deadline = Instant::now() + Duration::from_millis(300).min(timeout);
4163            while Instant::now() < probe_deadline
4164                && (!alive_seen.iter().all(|s| s.contains(&msg)) || removed_seen.is_empty())
4165            {
4166                let _ = offer(msg.as_bytes());
4167                for (i, sub) in alive_subs.iter().enumerate() {
4168                    let _ = sub.poll_fn(
4169                        |buf, _hdr| {
4170                            alive_seen[i].insert(String::from_utf8_lossy(buf).to_string());
4171                        },
4172                        16,
4173                    );
4174                }
4175                let _ = removed_sub.poll_fn(
4176                    |buf, _hdr| {
4177                        removed_seen.insert(String::from_utf8_lossy(buf).to_string());
4178                    },
4179                    16,
4180                );
4181                sleep(Duration::from_millis(10));
4182            }
4183            if alive_seen.iter().all(|s| s.contains(&msg)) && !removed_seen.contains(&msg) {
4184                return;
4185            }
4186            assert!(
4187                Instant::now() < deadline,
4188                "destination removal never took effect after {attempt} probes: alive_seen={alive_seen:?} removed_seen={removed_seen:?}"
4189            );
4190        }
4191    }
4192
4193    #[test]
4194    #[serial]
4195    fn subscription_mds_add_and_remove_destination_gates_real_data_flow() {
4196        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
4197        let stream_id = 1904;
4198
4199        let subscription = aeron
4200            .async_add_subscription(
4201                &manual_control_mode_channel(),
4202                stream_id,
4203                Handlers::NONE,
4204                Handlers::NONE,
4205            )
4206            .unwrap()
4207            .poll_blocking(Duration::from_secs(5))
4208            .unwrap();
4209
4210        let port_a = rusteron_media_driver::testing::find_unused_udp_port(20300).expect("no free port");
4211        let port_b = rusteron_media_driver::testing::find_unused_udp_port(port_a + 1).expect("no free port");
4212        let dest_a = udp_endpoint_channel(port_a);
4213        let dest_b = udp_endpoint_channel(port_b);
4214
4215        // Removing a destination that was never added must not panic/hang — Aeron's C API
4216        // doesn't track membership up front for this call.
4217        let result = subscription.remove_destination(&dest_a, Duration::from_secs(2));
4218        assert!(
4219            result.is_ok() || result.is_err(),
4220            "removing an unknown destination must resolve cleanly, got {result:?}"
4221        );
4222
4223        subscription
4224            .add_destination(&dest_a, Duration::from_secs(5))
4225            .expect("adding destination A should succeed");
4226        subscription
4227            .add_destination(&dest_b, Duration::from_secs(5))
4228            .expect("adding destination B should succeed");
4229
4230        let publisher_a = aeron
4231            .async_add_publication(&dest_a, stream_id)
4232            .unwrap()
4233            .poll_blocking(Duration::from_secs(5))
4234            .unwrap();
4235        let publisher_b = aeron
4236            .async_add_publication(&dest_b, stream_id)
4237            .unwrap()
4238            .poll_blocking(Duration::from_secs(5))
4239            .unwrap();
4240
4241        let start = Instant::now();
4242        while (!publisher_a.is_connected() || !publisher_b.is_connected()) && start.elapsed() < Duration::from_secs(10)
4243        {
4244            sleep(Duration::from_millis(10));
4245        }
4246        assert!(
4247            publisher_a.is_connected() && publisher_b.is_connected(),
4248            "publishers did not connect through the MDS destinations"
4249        );
4250
4251        // Both feeds must arrive through the single aggregating subscription. Retrying the
4252        // offer (rather than a single blind send) absorbs the race between "is_connected()
4253        // reports true" and the driver actually finishing delivery wiring.
4254        let mut seen = std::collections::HashSet::new();
4255        let deadline = Instant::now() + Duration::from_secs(5);
4256        while Instant::now() < deadline && !(seen.contains("feed-A-1") && seen.contains("feed-B-1")) {
4257            let _ = publisher_a.offer(b"feed-A-1");
4258            let _ = publisher_b.offer(b"feed-B-1");
4259            let _ = subscription.poll_fn(
4260                |buf, _hdr| {
4261                    seen.insert(String::from_utf8_lossy(buf).to_string());
4262                },
4263                16,
4264            );
4265            sleep(Duration::from_millis(20));
4266        }
4267        assert!(
4268            seen.contains("feed-A-1") && seen.contains("feed-B-1"),
4269            "expected both destinations to deliver before any removal, got {seen:?}"
4270        );
4271
4272        // Remove destination A; its messages must genuinely stop arriving while B keeps
4273        // flowing. Probe repeatedly (rather than a single send) to tolerate the small
4274        // real round trip between `remove_destination()` returning and the driver
4275        // actually tearing down delivery for that destination.
4276        subscription
4277            .remove_destination(&dest_a, Duration::from_secs(5))
4278            .expect("removing destination A should succeed");
4279
4280        let deadline = Instant::now() + Duration::from_secs(5);
4281        let mut attempt = 0usize;
4282        loop {
4283            attempt += 1;
4284            let tag_a = format!("removal-probe-A-{attempt}");
4285            let tag_b = format!("removal-probe-B-{attempt}");
4286            let mut probe_seen = std::collections::HashSet::new();
4287            let probe_deadline = Instant::now() + Duration::from_millis(300);
4288            while Instant::now() < probe_deadline && !probe_seen.contains(&tag_b) {
4289                let _ = publisher_a.offer(tag_a.as_bytes());
4290                let _ = publisher_b.offer(tag_b.as_bytes());
4291                let _ = subscription.poll_fn(
4292                    |buf, _hdr| {
4293                        probe_seen.insert(String::from_utf8_lossy(buf).to_string());
4294                    },
4295                    16,
4296                );
4297                sleep(Duration::from_millis(10));
4298            }
4299            if probe_seen.contains(&tag_b) && !probe_seen.contains(&tag_a) {
4300                break;
4301            }
4302            assert!(
4303                Instant::now() < deadline,
4304                "destination A removal never took effect after {attempt} probes: {probe_seen:?}"
4305            );
4306        }
4307
4308        // Removing it again (already gone) must still resolve cleanly.
4309        let result = subscription.remove_destination(&dest_a, Duration::from_secs(2));
4310        assert!(
4311            result.is_ok() || result.is_err(),
4312            "removing an already-removed destination must resolve cleanly, got {result:?}"
4313        );
4314
4315        drop(publisher_a);
4316        drop(publisher_b);
4317        drop(subscription);
4318        drop(aeron);
4319        teardown_aeron_after_uaf_test(driver, error_handler);
4320    }
4321
4322    #[test]
4323    #[serial]
4324    fn publication_mdc_add_and_remove_destination_gates_real_data_flow() {
4325        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
4326        let stream_id = 1905;
4327
4328        let publication = aeron
4329            .async_add_publication(&manual_control_mode_channel(), stream_id)
4330            .unwrap()
4331            .poll_blocking(Duration::from_secs(5))
4332            .unwrap();
4333
4334        let port_a = rusteron_media_driver::testing::find_unused_udp_port(20320).expect("no free port");
4335        let port_b = rusteron_media_driver::testing::find_unused_udp_port(port_a + 1).expect("no free port");
4336        let port_c = rusteron_media_driver::testing::find_unused_udp_port(port_b + 1).expect("no free port");
4337        let dest_a = udp_endpoint_channel(port_a); // removed via remove_destination(uri)
4338        let dest_b = udp_endpoint_channel(port_b); // removed via remove_destination_by_id
4339        let dest_c = udp_endpoint_channel(port_c); // control: never removed
4340
4341        // Removing a destination that was never added must not panic/hang.
4342        let result = publication.remove_destination(&dest_a, Duration::from_secs(2));
4343        assert!(result.is_ok() || result.is_err());
4344
4345        let sub_a = aeron
4346            .add_subscription(
4347                &dest_a,
4348                stream_id,
4349                Handlers::NONE,
4350                Handlers::NONE,
4351                Duration::from_secs(5),
4352            )
4353            .unwrap();
4354        let sub_c = aeron
4355            .add_subscription(
4356                &dest_c,
4357                stream_id,
4358                Handlers::NONE,
4359                Handlers::NONE,
4360                Duration::from_secs(5),
4361            )
4362            .unwrap();
4363
4364        publication
4365            .add_destination(&dest_a, Duration::from_secs(5))
4366            .expect("adding destination A should succeed");
4367        publication
4368            .add_destination(&dest_c, Duration::from_secs(5))
4369            .expect("adding destination C should succeed");
4370
4371        // Capture destination B's registration id via the low-level async API (not
4372        // exposed by the `add_destination` convenience wrapper) so we can remove it by id
4373        // later — mirrors how a caller would drive this from their own event loop.
4374        let add_b = publication.async_add_destination(&aeron, &dest_b).unwrap();
4375        let dest_b_registration_id = add_b.get_registration_id();
4376        let start = Instant::now();
4377        loop {
4378            let poll_result = add_b.aeron_publication_async_destination_poll();
4379            if matches!(poll_result, Ok(n) if n > 0) {
4380                break;
4381            }
4382            assert!(
4383                start.elapsed() < Duration::from_secs(5),
4384                "add_destination(B) never resolved"
4385            );
4386            sleep(Duration::from_millis(5));
4387        }
4388        // Resolved: there's no C free for this resource once the driver acknowledges it
4389        // (mirrors the wrapper functions in `aeron_custom.rs`) — mark it closed so `Drop`
4390        // doesn't treat this as a leak under `strict-lifecycle`.
4391        let _ = add_b.inner.close_resource();
4392        let sub_b = aeron
4393            .add_subscription(
4394                &dest_b,
4395                stream_id,
4396                Handlers::NONE,
4397                Handlers::NONE,
4398                Duration::from_secs(5),
4399            )
4400            .unwrap();
4401
4402        let start = Instant::now();
4403        while !publication.is_connected() && start.elapsed() < Duration::from_secs(10) {
4404            sleep(Duration::from_millis(10));
4405        }
4406        assert!(
4407            publication.is_connected(),
4408            "publication never connected to any MDC destination"
4409        );
4410
4411        // Everything is wired: a message sent (retried) now must reach all three
4412        // subscribers — retrying absorbs the race between the destination nominally
4413        // connecting and the driver actually finishing delivery wiring for it.
4414        assert_all_subs_eventually_receive(
4415            |b| publication.offer(b),
4416            "to-ABC",
4417            &[&sub_a, &sub_b, &sub_c],
4418            Duration::from_secs(5),
4419        );
4420
4421        // Remove A by URI: subsequent messages must stop reaching sub A while B and C
4422        // keep receiving.
4423        publication
4424            .remove_destination(&dest_a, Duration::from_secs(5))
4425            .expect("remove_destination(A) should succeed");
4426        assert_destination_removal_takes_effect(
4427            |b| publication.offer(b),
4428            &sub_a,
4429            &[&sub_b, &sub_c],
4430            Duration::from_secs(5),
4431        );
4432
4433        // Remove B by registration id: the C API call itself must succeed (it's correctly
4434        // wired end-to-end to `aeron_publication_async_remove_destination_by_id`), but data
4435        // flow to sub B is **not** expected to stop here.
4436        //
4437        // Confirmed upstream bug (still present in aeron-io/aeron `master` as of this
4438        // writing, and in the vendored 1.53.3 submodule): `aeron_client_conductor_
4439        // on_cmd_destination_by_id()` in `aeron-client/src/main/c/aeron_client_conductor.c`
4440        // populates `command->destination_registration_id` with the *parent publication's*
4441        // own `resource_registration_id` instead of the destination's actual
4442        // `destination_registration_id` (the value this function received as a parameter
4443        // and had already stored correctly on the async command struct) —
4444        // `command->destination_registration_id = resource_registration_id;` should read
4445        // `command->destination_registration_id = async->destination_registration_id;`.
4446        // Since the driver's `aeron_udp_destination_tracker_remove_destination_by_id()`
4447        // matches purely on `entry->registration_id == destination_registration_id`, and a
4448        // publication's own registration id never equals one of its destinations'
4449        // registration ids, the driver silently finds no match and removes nothing — while
4450        // still reporting the command as succeeded. So today, `remove_destination_by_id`
4451        // is effectively a no-op at the driver level regardless of caller; only
4452        // `remove_destination` (by URI) actually works. Verified by direct byte-for-byte
4453        // comparison against the current aeron-io/aeron GitHub `master` source — this is
4454        // not specific to our vendored version and should be reported upstream.
4455        // `remove_destination_by_id` now deliberately surfaces this as an `Err` (rather
4456        // than a misleading `Ok(())`) precisely because of the bug above — assert that
4457        // honest failure instead of a successful data-flow stop.
4458        let err = publication
4459            .remove_destination_by_id(dest_b_registration_id, Duration::from_secs(5))
4460            .expect_err("remove_destination_by_id(B) must report the known upstream no-op as an error, not Ok(())");
4461        assert!(
4462            format!("{err:?}").contains("upstream Aeron C bug"),
4463            "unexpected error from remove_destination_by_id: {err:?}"
4464        );
4465
4466        drop(sub_a);
4467        drop(sub_b);
4468        drop(sub_c);
4469        drop(publication);
4470        drop(aeron);
4471        teardown_aeron_after_uaf_test(driver, error_handler);
4472    }
4473
4474    /// Same MDC round trip as `publication_mdc_add_and_remove_destination_gates_real_data_flow`,
4475    /// but for [`AeronExclusivePublication`] — the `add_destination`/`remove_destination`/
4476    /// `remove_destination_by_id` trio is a separate impl block generated from a separate set
4477    /// of C functions, so it needs its own real-data-flow proof rather than assuming parity
4478    /// with the regular-publication path.
4479    #[test]
4480    #[serial]
4481    fn exclusive_publication_mdc_add_and_remove_destination_gates_real_data_flow() {
4482        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
4483        let stream_id = 1906;
4484
4485        let publication = aeron
4486            .async_add_exclusive_publication(&manual_control_mode_channel(), stream_id)
4487            .unwrap()
4488            .poll_blocking(Duration::from_secs(5))
4489            .unwrap();
4490
4491        let port_a = rusteron_media_driver::testing::find_unused_udp_port(20340).expect("no free port");
4492        let port_b = rusteron_media_driver::testing::find_unused_udp_port(port_a + 1).expect("no free port");
4493        let dest_a = udp_endpoint_channel(port_a); // removed via remove_destination(uri)
4494        let dest_b = udp_endpoint_channel(port_b); // removed via remove_destination_by_id
4495
4496        let result = publication.remove_destination(&dest_a, Duration::from_secs(2));
4497        assert!(result.is_ok() || result.is_err());
4498
4499        let sub_a = aeron
4500            .add_subscription(
4501                &dest_a,
4502                stream_id,
4503                Handlers::NONE,
4504                Handlers::NONE,
4505                Duration::from_secs(5),
4506            )
4507            .unwrap();
4508
4509        publication
4510            .add_destination(&dest_a, Duration::from_secs(5))
4511            .expect("adding destination A should succeed");
4512
4513        let add_b = publication.async_add_destination(&aeron, &dest_b).unwrap();
4514        let dest_b_registration_id = add_b.get_registration_id();
4515        let start = Instant::now();
4516        loop {
4517            let poll_result = add_b.aeron_exclusive_publication_async_destination_poll();
4518            if matches!(poll_result, Ok(n) if n > 0) {
4519                break;
4520            }
4521            assert!(
4522                start.elapsed() < Duration::from_secs(5),
4523                "add_destination(B) never resolved"
4524            );
4525            sleep(Duration::from_millis(5));
4526        }
4527        // Resolved: there's no C free for this resource once the driver acknowledges it
4528        // (mirrors the wrapper functions in `aeron_custom.rs`) — mark it closed so `Drop`
4529        // doesn't treat this as a leak under `strict-lifecycle`.
4530        let _ = add_b.inner.close_resource();
4531        let sub_b = aeron
4532            .add_subscription(
4533                &dest_b,
4534                stream_id,
4535                Handlers::NONE,
4536                Handlers::NONE,
4537                Duration::from_secs(5),
4538            )
4539            .unwrap();
4540
4541        let start = Instant::now();
4542        while !publication.is_connected() && start.elapsed() < Duration::from_secs(10) {
4543            sleep(Duration::from_millis(10));
4544        }
4545        assert!(
4546            publication.is_connected(),
4547            "exclusive publication never connected to any MDC destination"
4548        );
4549
4550        assert_all_subs_eventually_receive(
4551            |b| publication.offer(b),
4552            "to-AB",
4553            &[&sub_a, &sub_b],
4554            Duration::from_secs(5),
4555        );
4556
4557        // Remove A by URI: only B should keep receiving.
4558        publication
4559            .remove_destination(&dest_a, Duration::from_secs(5))
4560            .expect("remove_destination(A) should succeed");
4561        assert_destination_removal_takes_effect(|b| publication.offer(b), &sub_a, &[&sub_b], Duration::from_secs(5));
4562
4563        // Remove B by registration id: the call succeeds at the API level (correctly
4564        // wired to `aeron_exclusive_publication_async_remove_destination_by_id`), but see
4565        // the identical, extensively-documented note in
4566        // `publication_mdc_add_and_remove_destination_gates_real_data_flow` above — a
4567        // confirmed upstream Aeron C bug (`aeron_client_conductor_on_cmd_destination_by_id`
4568        // in `aeron_client_conductor.c`, reproduced against the current aeron-io/aeron
4569        // `master`) makes `remove_destination_by_id` a driver-level no-op today regardless
4570        // of publication type, so data-flow gating is intentionally not asserted here.
4571        // `remove_destination_by_id` now deliberately surfaces this as an `Err` (rather
4572        // than a misleading `Ok(())`) precisely because of the bug above — assert that
4573        // honest failure instead of a successful data-flow stop.
4574        let err = publication
4575            .remove_destination_by_id(dest_b_registration_id, Duration::from_secs(5))
4576            .expect_err("remove_destination_by_id(B) must report the known upstream no-op as an error, not Ok(())");
4577        assert!(
4578            format!("{err:?}").contains("upstream Aeron C bug"),
4579            "unexpected error from remove_destination_by_id: {err:?}"
4580        );
4581
4582        drop(sub_a);
4583        drop(sub_b);
4584        drop(publication);
4585        drop(aeron);
4586        teardown_aeron_after_uaf_test(driver, error_handler);
4587    }
4588
4589    /// Retained images are released back to the subscription automatically when the
4590    /// handle drops — in either order: image-then-subscription (normal release) or
4591    /// subscription-then-image (release skipped; the C client already reclaimed it).
4592    #[test]
4593    #[serial]
4594    fn retained_images_release_automatically_in_any_drop_order() {
4595        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
4596
4597        let publisher = aeron
4598            .add_publication(AERON_IPC_STREAM, 1901, Duration::from_secs(5))
4599            .unwrap();
4600        let subscription = aeron
4601            .add_subscription(
4602                AERON_IPC_STREAM,
4603                1901,
4604                Handlers::NONE,
4605                Handlers::NONE,
4606                Duration::from_secs(5),
4607            )
4608            .unwrap();
4609        let start = Instant::now();
4610        while subscription.image_count().unwrap_or(0) == 0 && start.elapsed() < Duration::from_secs(5) {
4611            let _ = publisher.offer(b"image-wake");
4612            sleep(Duration::from_millis(10));
4613        }
4614        assert!(subscription.image_count().unwrap() >= 1);
4615
4616        // borrow-scoped iteration: no bookkeeping
4617        let mut seen = 0;
4618        subscription.for_each_image(|image| {
4619            assert!(!image.get_inner().is_null());
4620            seen += 1;
4621        });
4622        assert!(seen >= 1);
4623
4624        // normal order: image handle dropped while the subscription is alive
4625        {
4626            let image = subscription.image_at_index(0).expect("image at 0");
4627            let session_id = image.get_constants().unwrap().session_id();
4628            assert!(subscription.image_by_session_id(session_id).is_some());
4629            assert!(subscription.image_by_session_id(session_id ^ 0x5555_5555).is_none());
4630            drop(image); // releases the image
4631        }
4632        // still healthy afterwards: roundtrip works
4633        assert!(subscription.poll_fn(|_, _| {}, 4).is_ok());
4634
4635        // adversarial order: subscription closed while a retained image handle lives
4636        let image = subscription.image_at_index(0).expect("image at 0");
4637        drop(subscription);
4638        drop(image); // must be a safe no-op, not a UAF release
4639
4640        drop(publisher);
4641        drop(aeron);
4642        teardown_aeron_after_uaf_test(driver, error_handler);
4643    }
4644
4645    /// A retained image that has become unavailable is still released when its handle
4646    /// drops, rather than lingering, log buffer mapped, until the client closes.
4647    #[test]
4648    #[serial]
4649    fn retained_image_is_released_after_it_becomes_unavailable() {
4650        unsafe extern "C" {
4651            fn aeron_image_refcnt_acquire(image: *mut aeron_image_t) -> i64;
4652        }
4653        let driver = rusteron_media_driver::testing::EmbeddedDriver::launch_with(|ctx| {
4654            ctx.set_publication_linger_timeout_ns(Duration::from_millis(50).as_nanos() as u64)
4655                .map(|_| ())
4656        })
4657        .unwrap();
4658        let ctx = AeronContext::new().unwrap();
4659        ctx.set_dir(&driver.dir().into_c_string()).unwrap();
4660        let aeron = Aeron::new(&ctx).unwrap();
4661        aeron.start().unwrap();
4662        let publication = aeron
4663            .add_publication(AERON_IPC_STREAM, 1902, Duration::from_secs(5))
4664            .unwrap();
4665        let subscription = aeron
4666            .add_subscription(
4667                AERON_IPC_STREAM,
4668                1902,
4669                Handlers::NONE,
4670                Handlers::NONE,
4671                Duration::from_secs(5),
4672            )
4673            .unwrap();
4674        let deadline = Instant::now() + Duration::from_secs(5);
4675        while subscription.image_count().unwrap_or(0) == 0 && Instant::now() < deadline {
4676            sleep(Duration::from_millis(10));
4677        }
4678        let first = subscription.image_at_index(0).expect("image at 0");
4679        let second = subscription.image_at_index(0).expect("image at 0");
4680        // SAFETY: `second` keeps the image retained for every read.
4681        let refcnt = || unsafe { aeron_image_refcnt_acquire(second.get_inner()) };
4682        assert_eq!(refcnt(), 3, "the subscription and both handles hold the image");
4683
4684        drop(publication);
4685        let deadline = Instant::now() + Duration::from_secs(10);
4686        while refcnt() > 2 && Instant::now() < deadline {
4687            // polling lets the client drop the subscription's own reference
4688            let _ = subscription.poll_fn(|_, _| {}, 1);
4689            sleep(Duration::from_millis(10));
4690        }
4691        assert!(second.is_closed());
4692        assert_eq!(refcnt(), 2, "only the two handles hold the unavailable image");
4693
4694        drop(first);
4695        assert_eq!(refcnt(), 1, "dropping a handle releases the unavailable image");
4696        drop(second);
4697        drop(subscription);
4698        drop(aeron);
4699        drop(driver);
4700    }
4701
4702    /// Closing the subscription releases a retained image with it, and the C client then
4703    /// frees the image, so dropping the handle afterwards must not release it again.
4704    #[test]
4705    #[serial]
4706    fn retained_image_dropped_after_its_subscription_closes_is_not_released_again() {
4707        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
4708        let publication = aeron
4709            .add_publication(AERON_IPC_STREAM, 1903, Duration::from_secs(5))
4710            .unwrap();
4711        let subscription = aeron
4712            .add_subscription(
4713                AERON_IPC_STREAM,
4714                1903,
4715                Handlers::NONE,
4716                Handlers::NONE,
4717                Duration::from_secs(5),
4718            )
4719            .unwrap();
4720        let deadline = Instant::now() + Duration::from_secs(5);
4721        while subscription.image_count().unwrap_or(0) == 0 && Instant::now() < deadline {
4722            sleep(Duration::from_millis(10));
4723        }
4724        let image = subscription.image_at_index(0).expect("image at 0");
4725
4726        let closed = Arc::new(AtomicUsize::new(0));
4727        let close_handler = Handler::new(CloseNotificationCount { count: closed.clone() });
4728        subscription.close_with_handler(Some(&close_handler)).unwrap();
4729        let deadline = Instant::now() + Duration::from_secs(5);
4730        while closed.load(Ordering::SeqCst) == 0 && Instant::now() < deadline {
4731            sleep(Duration::from_millis(10));
4732        }
4733        assert_eq!(1, closed.load(Ordering::SeqCst), "the C close finished before the drop");
4734        drop(image);
4735
4736        drop(publication);
4737        drop(aeron);
4738        teardown_aeron_after_uaf_test(driver, error_handler);
4739    }
4740
4741    /// `close_now` is the unsafe escape hatch: the C close runs immediately.
4742    /// Clones of the client handle share the nulled pointer, so they stay safe
4743    /// to drop or close (children would dangle — none exist here).
4744    #[test]
4745    #[serial]
4746    fn close_now_with_clones_only_is_safe() {
4747        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
4748
4749        let clone = aeron.clone();
4750        unsafe {
4751            assert!(aeron.close_now().is_ok());
4752        }
4753        assert!(clone.get_inner().is_null(), "clones must see the nulled pointer");
4754        assert!(clone.close().is_ok(), "closing an already-closed clone is a no-op");
4755        teardown_aeron_after_uaf_test(driver, error_handler);
4756    }
4757
4758    /// `close_now` with a complex object graph where Rust drops children before
4759    /// the parent. In 0.2's deferred-close model, children close immediately when
4760    /// dropped, so calling `close_now()` on a parent after all children have been
4761    /// dropped must be safe — the C client handles already-closed resources
4762    /// gracefully. This test verifies no segfaults or double-frees (CI runs this
4763    /// under ASan which catches use-after-free).
4764    ///
4765    /// WARNING: if any child handle survives `close_now()`, its C resources are
4766    /// freed and the handle becomes unsafe to use (dangling pointer). You MUST
4767    /// drop all children before calling `close_now()`, or explicitly `std::mem::forget`
4768    /// them and accept the UB.
4769    #[test]
4770    #[serial]
4771    fn close_now_after_all_children_dropped_is_safe() {
4772        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
4773        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
4774
4775        // Build a complex object graph: publications, subscriptions, exclusive publications
4776        let pub1 = aeron
4777            .add_publication(AERON_IPC_STREAM, 1001, Duration::from_secs(5))
4778            .expect("add publication 1");
4779        let pub2 = aeron
4780            .add_publication(AERON_IPC_STREAM, 1002, Duration::from_secs(5))
4781            .expect("add publication 2");
4782
4783        let sub1 = aeron
4784            .add_subscription(
4785                AERON_IPC_STREAM,
4786                1001,
4787                Handlers::NONE,
4788                Handlers::NONE,
4789                Duration::from_secs(5),
4790            )
4791            .expect("add subscription 1");
4792        let sub2 = aeron
4793            .add_subscription(
4794                AERON_IPC_STREAM,
4795                1002,
4796                Handlers::NONE,
4797                Handlers::NONE,
4798                Duration::from_secs(5),
4799            )
4800            .expect("add subscription 2");
4801
4802        let excl_pub = aeron
4803            .add_exclusive_publication(AERON_IPC_STREAM, 1003, Duration::from_secs(5))
4804            .expect("add exclusive publication");
4805
4806        // All children must be dropped before close_now - their C resources are freed
4807        drop(pub1);
4808        drop(pub2);
4809        drop(sub1);
4810        drop(sub2);
4811        drop(excl_pub);
4812
4813        // Call close_now on the parent — must not segfault or double-free
4814        unsafe {
4815            assert!(
4816                aeron.close_now().is_ok(),
4817                "close_now should succeed after all children dropped"
4818            );
4819        }
4820
4821        teardown_aeron_after_uaf_test(driver, error_handler);
4822        // Valgrind will catch any double-frees or use-after-free from this test
4823    }
4824
4825    /// DANGER: This test documents the UB scenario where children survive
4826    /// `close_now()`. This is commented-out because it WILL segfault (ASan would
4827    /// catch it as use-after-free). This proves you MUST drop all children before
4828    /// calling `close_now()`.
4829    ///
4830    /// What happens:
4831    /// 1. `aeron.close_now()` frees the Aeron C client
4832    /// 2. All publications/subscriptions are freed in C
4833    /// 3. But `pub2` still holds a non-null pointer to freed memory
4834    /// 4. `drop(pub2)` calls `aeron_publication_close()` on dangling pointer → UB
4835    ///
4836    /// DO NOT call `close_now()` with surviving children — it's fundamentally unsafe.
4837    #[test]
4838    #[serial]
4839    #[ignore = "this test WILL segfault - it documents the UB scenario"]
4840    fn close_now_with_surviving_children_is_ub_and_segfaults() {
4841        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
4842        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
4843
4844        let pub2 = aeron
4845            .add_publication(AERON_IPC_STREAM, 1001, Duration::from_secs(5))
4846            .expect("add publication");
4847
4848        // Call close_now while pub2 is still alive — UNSAFE
4849        unsafe {
4850            let _ = aeron.close_now();
4851        }
4852
4853        // pub2 now holds a dangling pointer. Dropping it calls
4854        // aeron_publication_close() on freed memory → SEGFAULT
4855        drop(pub2); // <-- BOOM: use-after-free
4856
4857        // Never reached
4858        teardown_aeron_after_uaf_test(driver, error_handler);
4859    }
4860
4861    /// The client stores the raw context pointer in C for its entire life, so the
4862    /// context must outlive the client. Dropping the user's `AeronContext` handle
4863    /// only releases a reference — the client's internal clone keeps the C context
4864    /// alive, and a full roundtrip must still work afterwards.
4865    #[test]
4866    #[serial]
4867    fn context_drop_while_client_alive_is_safe() {
4868        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
4869
4870        let media_driver_ctx = AeronDriverContext::new().unwrap();
4871        media_driver_ctx.set_dir_delete_on_shutdown(true).unwrap();
4872        media_driver_ctx.set_dir_delete_on_start(true).unwrap();
4873        media_driver_ctx
4874            .set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())
4875            .unwrap();
4876        let (stop, driver_handle) =
4877            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
4878
4879        let aeron = {
4880            let ctx = AeronContext::new().unwrap();
4881            ctx.set_dir(&media_driver_ctx.get_dir().into_c_string()).unwrap();
4882            let aeron = Aeron::new(&ctx).unwrap();
4883            drop(ctx); // user's handle gone; the client's clone keeps the C context alive
4884            aeron
4885        };
4886        aeron.start().unwrap();
4887
4888        let publisher = aeron
4889            .add_publication(AERON_IPC_STREAM, 1631, Duration::from_secs(5))
4890            .unwrap();
4891        let subscription = aeron
4892            .add_subscription(
4893                AERON_IPC_STREAM,
4894                1631,
4895                Handlers::NONE,
4896                Handlers::NONE,
4897                Duration::from_secs(5),
4898            )
4899            .unwrap();
4900
4901        let payload = b"ctx-dropped-roundtrip";
4902        let start = Instant::now();
4903        let mut sent = false;
4904        while start.elapsed() < Duration::from_secs(5) {
4905            if publisher.offer_raw(payload, Handlers::NONE) >= payload.len() as i64 {
4906                sent = true;
4907                break;
4908            }
4909            sleep(Duration::from_millis(10));
4910        }
4911        assert!(sent);
4912
4913        let received = Arc::new(AtomicUsize::new(0));
4914        let received_copy = received.clone();
4915        let read_start = Instant::now();
4916        while received.load(Ordering::SeqCst) == 0 && read_start.elapsed() < Duration::from_secs(5) {
4917            subscription
4918                .poll_fn(
4919                    |buffer, _| {
4920                        assert_eq!(buffer, payload);
4921                        received_copy.fetch_add(1, Ordering::SeqCst);
4922                    },
4923                    16,
4924                )
4925                .unwrap();
4926            sleep(Duration::from_millis(10));
4927        }
4928        assert_eq!(1, received.load(Ordering::SeqCst));
4929
4930        stop.store(true, Ordering::SeqCst);
4931        let _ = driver_handle.join();
4932    }
4933
4934    /// Media driver dies mid-flight: the client must surface the failure through
4935    /// the error handler (driver keepalive timeout) and every handle must stay
4936    /// memory-safe — offers turn into errors, never UB — and teardown completes.
4937    #[test]
4938    #[serial]
4939    fn media_driver_shutdown_surfaces_errors_and_client_stays_safe() {
4940        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
4941
4942        let media_driver_ctx = AeronDriverContext::new().unwrap();
4943        media_driver_ctx.set_dir_delete_on_shutdown(true).unwrap();
4944        media_driver_ctx.set_dir_delete_on_start(true).unwrap();
4945        media_driver_ctx
4946            .set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())
4947            .unwrap();
4948        let (stop, driver_handle) =
4949            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
4950
4951        let errors = Arc::new(AtomicUsize::new(0));
4952        let ctx = AeronContext::new().unwrap();
4953        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string()).unwrap();
4954        // short keepalive so the driver loss is detected quickly
4955        ctx.set_driver_timeout_ms(2_000).unwrap();
4956        let errors_cb = errors.clone();
4957        ctx.set_error_handler(Some(move |code: i32, msg: &str| {
4958            errors_cb.fetch_add(1, Ordering::SeqCst);
4959            log::info!("client error {code}: {msg}");
4960        }))
4961        .unwrap();
4962        let aeron = Aeron::new(&ctx).unwrap();
4963        aeron.start().unwrap();
4964
4965        let publisher = aeron
4966            .add_publication(AERON_IPC_STREAM, 1701, Duration::from_secs(5))
4967            .unwrap();
4968        let subscription = aeron
4969            .add_subscription(
4970                AERON_IPC_STREAM,
4971                1701,
4972                Handlers::NONE,
4973                Handlers::NONE,
4974                Duration::from_secs(5),
4975            )
4976            .unwrap();
4977
4978        // sanity roundtrip while the driver is up
4979        let send_start = Instant::now();
4980        while publisher.offer(b"pre-shutdown").is_err() && send_start.elapsed() < Duration::from_secs(5) {
4981            sleep(Duration::from_millis(10));
4982        }
4983
4984        // kill the driver
4985        stop.store(true, Ordering::SeqCst);
4986        let _ = driver_handle.join();
4987
4988        // the conductor must notice the driver is gone and invoke the error handler
4989        let start = Instant::now();
4990        while errors.load(Ordering::SeqCst) == 0 && start.elapsed() < Duration::from_secs(15) {
4991            // keep exercising the handles: must return errors, never crash
4992            let _ = publisher.offer(b"post-shutdown");
4993            let _ = subscription.poll_fn(|_, _| {}, 4);
4994            sleep(Duration::from_millis(50));
4995        }
4996        assert!(
4997            errors.load(Ordering::SeqCst) > 0,
4998            "client error handler should have reported the driver keepalive timeout"
4999        );
5000
5001        // handles remain safe after the client flags the failure
5002        match publisher.offer(b"after-error") {
5003            Ok(_) => {}
5004            Err(e) => assert!(e.is_retryable() || e.is_fatal()), // typed, not UB
5005        }
5006    }
5007
5008    // ── Handler dependency tracking tests ────────────────────────────────
5009    //
5010    // These tests verify that handlers registered via add_dependency()
5011    // are kept alive correctly. Handlers must survive aeron.close() and
5012    // only be released when the Aeron client itself is dropped.
5013    //
5014    // NOTE: Handler<T> wraps Arc<UnsafeCell<T>>, and UnsafeCell deliberately
5015    // doesn't implement Drop (C code mutates handlers through raw pointers).
5016    // We track handler lifetime via Arc::strong_count instead of drop counters.
5017
5018    /// Test that a single error handler survives handler clone drops
5019    /// and persists via dependency tracking.
5020    #[test]
5021    #[serial]
5022    fn handler_dependency_tracking() {
5023        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
5024        let (aeron, driver, _error_handler) = setup_aeron_for_uaf_test();
5025
5026        // Create a test handler
5027        let handler = Handler::new(move |code: i32, msg: &str| {
5028            log::info!("Error handler called: code={}, msg={}", code, msg);
5029        });
5030
5031        // Store a clone to track strong_count
5032        let handler1 = handler.clone();
5033
5034        // Verify initial strong_count is 2 (handler and handler1)
5035        assert_eq!(
5036            Arc::strong_count(&handler1.inner),
5037            2,
5038            "Handler should be held by handler and handler1"
5039        );
5040
5041        // Register the handler with the context
5042        let ctx = aeron.context();
5043        ctx.set_error_handler(Some(handler)).unwrap();
5044
5045        // After registration, handler1 is the only external reference
5046        // (the original handler was moved into the context's dependencies)
5047        assert_eq!(
5048            Arc::strong_count(&handler1.inner),
5049            1,
5050            "Handler should be held only by handler1 (original stored in context)"
5051        );
5052
5053        // Explicitly close the Aeron client
5054        aeron.close().unwrap();
5055
5056        // The handler should STILL be alive because it's stored in context
5057        assert_eq!(
5058            Arc::strong_count(&handler1.inner),
5059            1,
5060            "Handler must survive aeron.close() - still tracked by context"
5061        );
5062
5063        // Drop handler1 - now only context holds the handler
5064        drop(handler1);
5065
5066        teardown_aeron_after_uaf_test(driver, _error_handler);
5067    }
5068
5069    /// Test that subscription image handlers are properly tracked
5070    /// and survive subscription drops.
5071    #[test]
5072    #[serial]
5073    fn subscription_handler_dependency_tracking() {
5074        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
5075        let (aeron, driver, _error_handler) = setup_aeron_for_uaf_test();
5076
5077        let avail_handler = Handler::new(move |subscription: AeronSubscription, image: AeronImage| {
5078            log::info!("Available image callback invoked");
5079        });
5080
5081        let local_avail = avail_handler.clone();
5082
5083        // Create subscription with handlers
5084        let sub = aeron
5085            .add_subscription(
5086                AERON_IPC_STREAM,
5087                2001,
5088                Some(&avail_handler),
5089                None::<&Handler<AeronUnavailableImageLogger>>,
5090                Duration::from_secs(5),
5091            )
5092            .unwrap();
5093
5094        // Handler is held by avail_handler, local_avail, and stored in subscription
5095        let initial_count = Arc::strong_count(&local_avail.inner);
5096        assert!(
5097            initial_count >= 2,
5098            "Handler should be held by at least avail_handler and local_avail"
5099        );
5100
5101        // Close the subscription
5102        drop(sub);
5103
5104        // Handler should STILL be alive - tracked by dependencies
5105        let after_sub_drop = Arc::strong_count(&local_avail.inner);
5106        assert!(
5107            after_sub_drop >= 1,
5108            "Handler must survive subscription drop - still tracked"
5109        );
5110
5111        // Drop the Aeron wrapper
5112        drop(aeron);
5113
5114        teardown_aeron_after_uaf_test(driver, _error_handler);
5115    }
5116
5117    #[test]
5118    #[serial]
5119    fn handler_dependencies_on_client_grow_linearly_and_never_shrink_until_client_drops() {
5120        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
5121        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
5122
5123        let baseline = aeron.inner.dependency_len();
5124
5125        const CYCLES: i32 = 5;
5126        for i in 0..CYCLES {
5127            let handler = Handler::new(move |_subscription: AeronSubscription, _image: AeronImage| {});
5128            let sub = aeron
5129                .add_subscription(
5130                    AERON_IPC_STREAM,
5131                    2300 + i,
5132                    Some(&handler),
5133                    None::<&Handler<AeronUnavailableImageLogger>>,
5134                    Duration::from_secs(5),
5135                )
5136                .unwrap();
5137            // both the subscription and our local handle go away; only the client's
5138            // anchored clone (added inside async_add_subscription) should remain.
5139            drop(sub);
5140            drop(handler);
5141
5142            let expected = baseline + (i as usize + 1);
5143            assert_eq!(
5144                aeron.inner.dependency_len(),
5145                expected,
5146                "cycle {i}: expected exactly one new handler dependency on the client \
5147                 per subscribe/unsubscribe cycle"
5148            );
5149        }
5150
5151        assert_eq!(
5152            aeron.inner.dependency_len(),
5153            baseline + CYCLES as usize,
5154            "{CYCLES} subscribe/unsubscribe cycles must leave exactly {CYCLES} handler \
5155             dependencies anchored on the client (none reclaimed until client drop)"
5156        );
5157
5158        drop(aeron);
5159        teardown_aeron_after_uaf_test(driver, error_handler);
5160    }
5161
5162    #[test]
5163    #[serial]
5164    fn invoker_mode_on_available_image_fires_safely_around_close() {
5165        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
5166        let driver = rusteron_media_driver::testing::EmbeddedDriver::launch().unwrap();
5167
5168        let ctx = AeronContext::new().unwrap();
5169        ctx.set_dir(&driver.dir().into_c_string()).unwrap();
5170        ctx.set_use_conductor_agent_invoker(true).unwrap();
5171        let error_handler = Handler::new(TestErrorCount::default());
5172        ctx.set_error_handler(Some(error_handler.clone())).unwrap();
5173
5174        let aeron = Aeron::new(&ctx).unwrap();
5175        aeron.start().unwrap();
5176
5177        let available = Arc::new(AtomicUsize::new(0));
5178        let drops = Arc::new(AtomicUsize::new(0));
5179        let handler = Handler::new(CountingAvailableImageHandler {
5180            available: available.clone(),
5181            drops: drops.clone(),
5182        });
5183
5184        let deadline = Instant::now() + Duration::from_secs(15);
5185        macro_rules! drive_until {
5186            ($cond:expr, $msg:expr) => {{
5187                loop {
5188                    aeron.main_do_work().unwrap();
5189                    if $cond {
5190                        break;
5191                    }
5192                    assert!(Instant::now() < deadline, $msg);
5193                    sleep(Duration::from_millis(1));
5194                }
5195            }};
5196        }
5197
5198        let sub_poller = aeron
5199            .async_add_subscription(AERON_IPC_STREAM, 2101, Some(&handler), Handlers::NONE)
5200            .unwrap();
5201        #[allow(unused_assignments)]
5202        let mut subscription_result = None;
5203        drive_until!(
5204            {
5205                subscription_result = sub_poller.poll().unwrap();
5206                subscription_result.is_some()
5207            },
5208            "invoker never resolved the async add_subscription"
5209        );
5210        let subscription = subscription_result.unwrap();
5211        drop(sub_poller); // done polling — don't hold its client-Rc clone any longer
5212
5213        let pub_poller = aeron.async_add_publication(AERON_IPC_STREAM, 2101).unwrap();
5214        #[allow(unused_assignments)]
5215        let mut publisher_result = None;
5216        drive_until!(
5217            {
5218                publisher_result = pub_poller.poll().unwrap();
5219                publisher_result.is_some()
5220            },
5221            "invoker never resolved the async add_publication"
5222        );
5223        let publisher = publisher_result.unwrap();
5224        drop(pub_poller);
5225
5226        drive_until!(
5227            {
5228                let _ = publisher.offer_raw(b"wake", Handlers::NONE);
5229                available.load(Ordering::SeqCst) > 0
5230            },
5231            "on_available_image never fired before subscription close"
5232        );
5233
5234        // Only requests the close; the conductor (which we alone drive here) hasn't
5235        // necessarily processed it yet.
5236        drop(subscription);
5237
5238        // Pump the invoker a bit more so the close (and any in-flight callback racing
5239        // with it) is fully processed under our control, proving it's safe either way.
5240        for _ in 0..200 {
5241            aeron.main_do_work().unwrap();
5242            sleep(Duration::from_millis(1));
5243        }
5244
5245        assert_eq!(
5246            unsafe { error_handler.get_mut().error_count },
5247            0,
5248            "no errors should be observed while driving close via the invoker"
5249        );
5250        assert_eq!(
5251            drops.load(Ordering::SeqCst),
5252            0,
5253            "handler must still be anchored to the client after the subscription's async \
5254             close, not freed early"
5255        );
5256
5257        drop(publisher);
5258        // Drop our own local clone of the handler: the client's dependency list holds
5259        // the other clone, which is the one that must keep it alive until the client
5260        // itself drops.
5261        drop(handler);
5262        drop(aeron);
5263        assert_eq!(
5264            drops.load(Ordering::SeqCst),
5265            1,
5266            "handler must be freed exactly once, when the client itself drops"
5267        );
5268
5269        drop(driver);
5270    }
5271
5272    #[test]
5273    #[serial]
5274    fn invoker_mode_blocking_adds_drive_the_conductor() -> Result<(), Box<dyn std::error::Error>> {
5275        let driver = rusteron_media_driver::testing::EmbeddedDriver::launch()?;
5276        let ctx = AeronContext::new()?;
5277        ctx.set_dir(&driver.dir().into_c_string())?;
5278        ctx.set_use_conductor_agent_invoker(true)?;
5279        let aeron = Aeron::new(&ctx)?;
5280        aeron.start()?;
5281
5282        let timeout = Duration::from_secs(2);
5283        let publication = aeron.add_publication(AERON_IPC_STREAM, 2102, timeout)?;
5284        let subscription = aeron
5285            .async_add_subscription(&manual_control_mode_channel(), 2102, Handlers::NONE, Handlers::NONE)?
5286            .poll_blocking(timeout)?;
5287        let port = rusteron_media_driver::testing::find_unused_udp_port(20400).ok_or("no free port")?;
5288        subscription.add_destination(&udp_endpoint_channel(port), timeout)?;
5289
5290        drop(subscription);
5291        drop(publication);
5292        drop(aeron);
5293        drop(driver);
5294        Ok(())
5295    }
5296
5297    // ── Structural teardown verification via memory protection ────────
5298    //
5299    // Under the new design, the Rc dependency graph ensures correct
5300    // teardown order: `aeron_close()` can only fire once every child's
5301    // Rc<Aeron> is released.  This section proves that:
5302    //
5303    //  1. `drop(aeron)` is SAFE while children are alive (Rc keeps the
5304    //     C client alive).
5305    //  2. `drop(publisher)` triggers `aeron_close()` (when its Rc<Aeron>
5306    //     is the last handle), which frees the publication — UAF after
5307    //     the publisher drops is the *correct* C-level behaviour.
5308    //
5309    // To prove (2) we use `mprotect(PROT_NONE)` on the page containing
5310    // the dangling pointer after `aeron_close()`.  Any access then
5311    // triggers SIGBUS/SIGSEGV, proving the pointer targets freed memory.
5312    //
5313    // On Linux/macOS `mprotect` on malloc'd heap memory works at the page
5314    // level.  This test uses `fork()` so the dangerous mprotect runs in a
5315    // child process — the parent test runner is never at risk.
5316    //
5317    // The test passes when the child exits with SIGABRT (teardown proven).
5318
5319    #[cfg(any(target_os = "linux", target_os = "macos"))]
5320    const PROT_NONE: i32 = 0;
5321
5322    #[cfg(target_os = "macos")]
5323    const SYS_PAGESIZE: i32 = 29;
5324    #[cfg(target_os = "linux")]
5325    const SYS_PAGESIZE: i32 = 30;
5326
5327    unsafe extern "C" {
5328        fn mprotect(addr: *mut core::ffi::c_void, len: usize, prot: i32) -> i32;
5329        fn sysconf(name: i32) -> isize;
5330        fn write(fd: i32, buf: *const core::ffi::c_void, count: usize) -> isize;
5331    }
5332
5333    /// Async-signal-safe write of a fixed message to stderr.
5334    unsafe fn uaf_write_msg(msg: &[u8]) {
5335        write(2, msg.as_ptr() as *const core::ffi::c_void, msg.len());
5336    }
5337
5338    /// Signal handler for SIGBUS / SIGSEGV caught during the mprotect test.
5339    /// Converts the crash into a clean abort with a diagnostic message.
5340    extern "C" fn uaf_sigbus_handler(_sig: i32) {
5341        unsafe {
5342            uaf_write_msg(b"\n\n*** CORRECT TEARDOWN *** aeron_close() freed the publication memory\n");
5343            uaf_write_msg(b"*** Rc graph teardown confirmed: the publication's page was protected\n");
5344            uaf_write_msg(b"*** with mprotect(PROT_NONE) after the publisher dropped, proving the\n");
5345            uaf_write_msg(b"*** pointer is dangling because aeron_close freed it, not before.\n\n");
5346        }
5347        std::process::abort();
5348    }
5349
5350    /// Prove the Rc graph teardown frees C memory: after the last child handle
5351    /// drops (triggering deferred `aeron_close()`), `mprotect(PROT_NONE)` on the
5352    /// publication's page causes a SIGBUS on access, confirming the memory was freed.
5353    ///
5354    /// It uses `mprotect(PROT_NONE)`, which affects an entire VM page.  If other live
5355    /// heap allocations share the
5356    /// same page as the freed Aeron resource the process may crash during
5357    /// teardown too, so we call `std::process::abort` on detection and
5358    /// `std::process::exit(0)` on the no-crash path.
5359    ///
5360    /// Uses `fork()` to isolate the dangerous mprotect + access into
5361    /// a child process.  The parent waits for the child's exit status:
5362    ///
5363    /// | Child exit | Meaning | Test result |
5364    /// |---|---|---|
5365    /// | `_exit(0)` | `mprotect` unsupported on this platform | pass (skip) |
5366    /// | SIGABRT | freed memory detected via signal handler | **pass (teardown proven)** |
5367    /// | other signal or non-zero exit | unexpected failure | fail |
5368    #[test]
5369    #[serial]
5370    fn prove_rc_teardown_frees_via_mprotect() {
5371        unsafe extern "C" {
5372            fn signal(sig: i32, handler: unsafe extern "C" fn(i32)) -> usize;
5373            fn fork() -> i32;
5374            fn waitpid(pid: i32, status: *mut i32, options: i32) -> i32;
5375            fn _exit(status: i32) -> !;
5376        }
5377        #[cfg(target_os = "macos")]
5378        const SIGBUS: i32 = 10;
5379        #[cfg(not(target_os = "macos"))]
5380        const SIGBUS: i32 = 7;
5381        const SIGSEGV: i32 = 11;
5382
5383        // This test deliberately forks, mprotects, and `mem::forget`s the driver
5384        // to PROVE Rc teardown frees the C resource — it is not a memory-hygiene
5385        // target, and its non-standard teardown (native-executed child + unusual
5386        // close ordering) confuses Valgrind's leak accounting. Skip under Valgrind.
5387        if running_under_valgrind() {
5388            return;
5389        }
5390
5391        unsafe {
5392            let pid = fork();
5393            assert!(pid >= 0, "fork failed");
5394            if pid == 0 {
5395                // ── CHILD: proof of correct Rc graph teardown ──
5396                // The parent is unaffected (fork makes the child's
5397                // mprotect page-private via copy-on-write).
5398
5399                signal(SIGBUS, uaf_sigbus_handler);
5400                signal(SIGSEGV, uaf_sigbus_handler);
5401
5402                // the child never shuts its driver down, so it reuses one dir that the next
5403                // run's driver deletes on start, rather than leaving a new dir every run
5404                let (aeron, driver, error_handler) = setup_aeron_for_uaf_test_with(|ctx| {
5405                    let dir = std::env::temp_dir().join("aeron-rusteron-mprotect-test");
5406                    ctx.set_dir(&dir.display().to_string().into_c_string()).map(|_| ())
5407                });
5408                let publisher = aeron
5409                    .add_publication(AERON_IPC_STREAM, 1006, Duration::from_secs(5))
5410                    .unwrap();
5411
5412                let raw_ptr: *mut aeron_publication_t = publisher.get_inner();
5413
5414                // Step 1: drop(aeron) — SAFE under new design.  The
5415                // publisher still holds an Rc<Aeron>, so aeron_close()
5416                // is deferred.
5417                drop(aeron);
5418
5419                // Step 2: drop(publisher) — the Rc<Aeron> refcount drops
5420                // to 0, triggering aeron_close().  aeron_close() frees all
5421                // C resources including the publication at raw_ptr.
5422                drop(publisher);
5423
5424                // Step 3: mprotect the page.  raw_ptr is now dangling.
5425                let page_size = sysconf(SYS_PAGESIZE) as usize;
5426                let page = (raw_ptr as usize) & !(page_size - 1);
5427                let rc = mprotect(page as *mut core::ffi::c_void, page_size, PROT_NONE);
5428
5429                if rc == 0 {
5430                    // Page is PROT_NONE — accessing raw_ptr triggers SIGBUS →
5431                    // signal handler → "teardown confirmed" → abort() (SIGABRT).
5432                    let _closed = aeron_publication_is_closed(raw_ptr);
5433                    // If we reach here, mprotect worked but nothing crashed.
5434                    uaf_write_msg(b"*** FAIL: mprotect succeeded but freed access did not crash\n");
5435                    std::process::abort();
5436                }
5437
5438                // mprotect not supported on this platform — clean exit.
5439                drop(error_handler);
5440                std::mem::forget(driver); // child process: no clean driver teardown needed
5441                _exit(0);
5442            }
5443
5444            // ── PARENT: wait for child's result ──
5445            let mut status: i32 = 0;
5446            let waited = waitpid(pid, &mut status as *mut i32, 0);
5447            assert!(waited != -1, "waitpid failed");
5448
5449            if status & 0x7f == 0 {
5450                // Child exited normally (WIFEXITED).
5451                let code = (status >> 8) & 0xff;
5452                if code == 0 {
5453                    eprintln!(
5454                        "note: mprotect(PROT_NONE) not supported on heap \
5455                         memory on this platform — teardown proof skipped"
5456                    );
5457                    return;
5458                }
5459                panic!("child exited with code {} (unexpected — setup error?)", code);
5460            }
5461
5462            // Child was signalled (WIFSIGNALED).
5463            let sig = status & 0x7f;
5464            if sig == 6 {
5465                // SIGABRT from uaf_sigbus_handler → teardown PROVEN.
5466                eprintln!(
5467                    "*** PASS: Rc graph teardown proven — child confirmed freed \
5468                     memory after aeron_close() via mprotect"
5469                );
5470                return;
5471            }
5472
5473            panic!("child killed by signal {} (expected SIGABRT=6 for teardown proof)", sig);
5474        }
5475    }
5476}