1#![deny(unsafe_op_in_unsafe_fn)]
2#![warn(missing_docs, rust_2018_idioms, unreachable_pub)]
3
4use std::alloc::{GlobalAlloc, Layout, System};
57use std::any::Any;
58use std::cell::Cell;
59use std::error::Error;
60use std::fmt;
61use std::ops::Range;
62use std::panic::{self, AssertUnwindSafe};
63use std::ptr;
64use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering};
65
66const MODE_DISABLED: u8 = 0;
67const MODE_COUNTING: u8 = 1;
68const MODE_FAILING: u8 = 2;
69
70const NO_TARGET: usize = usize::MAX;
71const NO_INJECTED_INDEX: usize = usize::MAX;
72const NO_REALLOC_NEW_SIZE: usize = usize::MAX;
73
74const ALLOC_OP_NONE: u8 = 0;
75const ALLOC_OP_ALLOC: u8 = 1;
76const ALLOC_OP_ALLOC_ZEROED: u8 = 2;
77const ALLOC_OP_REALLOC: u8 = 3;
78
79static MODE: AtomicU8 = AtomicU8::new(MODE_DISABLED);
80static TARGET: AtomicUsize = AtomicUsize::new(NO_TARGET);
81static SEEN: AtomicUsize = AtomicUsize::new(0);
82static INJECTED: AtomicBool = AtomicBool::new(false);
83static INJECTED_INDEX: AtomicUsize = AtomicUsize::new(NO_INJECTED_INDEX);
84static INJECTED_OP: AtomicU8 = AtomicU8::new(ALLOC_OP_NONE);
85static INJECTED_SIZE: AtomicUsize = AtomicUsize::new(0);
86static INJECTED_ALIGN: AtomicUsize = AtomicUsize::new(0);
87static INJECTED_NEW_SIZE: AtomicUsize = AtomicUsize::new(NO_REALLOC_NEW_SIZE);
88static CHECK_ACTIVE: AtomicBool = AtomicBool::new(false);
89static ALLOCATOR_WAS_USED: AtomicBool = AtomicBool::new(false);
90
91std::thread_local! {
92 static TRACK_ALLOCATIONS_ON_THREAD: Cell<bool> = const { Cell::new(false) };
93}
94
95pub struct ChaosAllocator<A = System> {
108 inner: A,
109}
110
111impl ChaosAllocator<System> {
112 #[must_use]
114 pub const fn system() -> Self {
115 Self { inner: System }
116 }
117}
118
119impl<A> ChaosAllocator<A> {
120 #[must_use]
122 pub const fn new(inner: A) -> Self {
123 Self { inner }
124 }
125}
126
127impl<A> fmt::Debug for ChaosAllocator<A> {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 f.debug_struct("ChaosAllocator").finish_non_exhaustive()
130 }
131}
132
133unsafe impl<A> GlobalAlloc for ChaosAllocator<A>
139where
140 A: GlobalAlloc,
141{
142 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
143 if should_inject_failure(AllocOp::Alloc, layout, None) {
144 ptr::null_mut()
145 } else {
146 unsafe { self.inner.alloc(layout) }
149 }
150 }
151
152 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
153 if should_inject_failure(AllocOp::AllocZeroed, layout, None) {
154 ptr::null_mut()
155 } else {
156 unsafe { self.inner.alloc_zeroed(layout) }
159 }
160 }
161
162 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
163 unsafe { self.inner.dealloc(ptr, layout) }
166 }
167
168 unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
169 if should_inject_failure(AllocOp::Realloc, layout, Some(new_size)) {
170 ptr::null_mut()
171 } else {
172 unsafe { self.inner.realloc(ptr, layout, new_size) }
175 }
176 }
177}
178
179pub fn check<F>(f: F) -> Report
189where
190 F: Fn(),
191{
192 Check::new().run(f)
193}
194
195pub fn try_check<F>(f: F) -> Result<Report, AlreadyRunning>
197where
198 F: Fn(),
199{
200 Check::new().try_run(f)
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205#[must_use]
206pub struct Check {
207 max_failures: Option<usize>,
208 stop_on_failure: bool,
209 target_start: usize,
210 target_end: Option<usize>,
211 stability_runs: usize,
212}
213
214impl Check {
215 pub const fn new() -> Self {
218 Self {
219 max_failures: None,
220 stop_on_failure: false,
221 target_start: 0,
222 target_end: None,
223 stability_runs: 1,
224 }
225 }
226
227 pub const fn max_failures(mut self, max_failures: usize) -> Self {
234 self.max_failures = Some(max_failures);
235 self
236 }
237
238 pub const fn unlimited_failures(mut self) -> Self {
240 self.max_failures = None;
241 self
242 }
243
244 pub fn only_failure(mut self, target: usize) -> Self {
255 self.target_start = target;
256 self.target_end = Some(
257 target
258 .checked_add(1)
259 .expect("alloc-chaos failure target must be less than usize::MAX"),
260 );
261 self
262 }
263
264 pub fn failure_range(mut self, range: Range<usize>) -> Self {
274 assert!(
275 range.start <= range.end,
276 "alloc-chaos failure range start must be less than or equal to range end"
277 );
278
279 self.target_start = range.start;
280 self.target_end = Some(range.end);
281 self
282 }
283
284 pub const fn all_failures(mut self) -> Self {
287 self.target_start = 0;
288 self.target_end = None;
289 self
290 }
291
292 pub const fn stability_runs(mut self, runs: usize) -> Self {
300 self.stability_runs = if runs == 0 { 1 } else { runs };
301 self
302 }
303
304 pub const fn stop_on_failure(mut self, enabled: bool) -> Self {
310 self.stop_on_failure = enabled;
311 self
312 }
313
314 pub fn run<F>(self, f: F) -> Report
321 where
322 F: Fn(),
323 {
324 self.try_run(f)
325 .expect("alloc-chaos check is already active in this process")
326 }
327
328 pub fn try_run<F>(self, f: F) -> Result<Report, AlreadyRunning>
330 where
331 F: Fn(),
332 {
333 let _active = ActiveCheck::enter()?;
334
335 let allocator_installed = probe_allocator_installed();
336 let baseline = run_counting(&f);
337 let baseline_allocations = baseline.observed_allocations;
338
339 let mut stability_baselines = Vec::new();
340 let mut baseline_stable = true;
341
342 if baseline.outcome.is_completed() {
343 stability_baselines.reserve_exact(self.stability_runs.saturating_sub(1));
344
345 for _ in 1..self.stability_runs {
346 let candidate = run_counting(&f);
347 if !baseline_matches(&baseline, &candidate) {
348 baseline_stable = false;
349 }
350 stability_baselines.push(candidate);
351 }
352 }
353
354 let mut attempts = Vec::new();
355 let mut truncated = baseline_allocations > 0;
356
357 if baseline.outcome.is_completed() && baseline_stable {
358 let plan = FailurePlan::for_check(self, baseline_allocations);
359 truncated = !plan.is_exhaustive_for(baseline_allocations);
360 attempts.reserve_exact(plan.len());
361
362 for target in plan.targets() {
363 let attempt = run_failing(target, &f);
364 let stop = self.stop_on_failure && !attempt.is_success();
365 attempts.push(attempt);
366
367 if stop {
368 truncated = !plan.is_exhaustive_after_stop(target, baseline_allocations);
369 break;
370 }
371 }
372 }
373
374 Ok(Report {
375 baseline,
376 stability_baselines,
377 baseline_stable,
378 attempts,
379 truncated,
380 allocator_installed,
381 })
382 }
383}
384
385impl Default for Check {
386 fn default() -> Self {
387 Self::new()
388 }
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
394pub struct AlreadyRunning;
395
396impl fmt::Display for AlreadyRunning {
397 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
398 f.write_str("an alloc-chaos check is already active in this process")
399 }
400}
401
402impl Error for AlreadyRunning {}
403
404#[derive(Debug, Clone, Copy, PartialEq, Eq)]
406#[must_use]
407pub enum AllocOp {
408 Alloc,
410
411 AllocZeroed,
413
414 Realloc,
416}
417
418impl AllocOp {
419 const fn as_u8(self) -> u8 {
420 match self {
421 Self::Alloc => ALLOC_OP_ALLOC,
422 Self::AllocZeroed => ALLOC_OP_ALLOC_ZEROED,
423 Self::Realloc => ALLOC_OP_REALLOC,
424 }
425 }
426
427 const fn from_u8(value: u8) -> Option<Self> {
428 match value {
429 ALLOC_OP_ALLOC => Some(Self::Alloc),
430 ALLOC_OP_ALLOC_ZEROED => Some(Self::AllocZeroed),
431 ALLOC_OP_REALLOC => Some(Self::Realloc),
432 _ => None,
433 }
434 }
435}
436
437impl fmt::Display for AllocOp {
438 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
439 match self {
440 Self::Alloc => f.write_str("alloc"),
441 Self::AllocZeroed => f.write_str("alloc_zeroed"),
442 Self::Realloc => f.write_str("realloc"),
443 }
444 }
445}
446
447#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449#[must_use]
450pub struct Allocation {
451 index: usize,
452 operation: AllocOp,
453 size: usize,
454 align: usize,
455 new_size: Option<usize>,
456}
457
458impl Allocation {
459 #[must_use]
461 pub fn index(&self) -> usize {
462 self.index
463 }
464
465 pub fn operation(&self) -> AllocOp {
467 self.operation
468 }
469
470 #[must_use]
472 pub fn size(&self) -> usize {
473 self.size
474 }
475
476 #[must_use]
478 pub fn align(&self) -> usize {
479 self.align
480 }
481
482 #[must_use]
484 pub fn new_size(&self) -> Option<usize> {
485 self.new_size
486 }
487}
488
489impl fmt::Display for Allocation {
490 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491 write!(f, "{} size={} align={}", self.operation, self.size, self.align)?;
492
493 if let Some(new_size) = self.new_size {
494 write!(f, " new_size={new_size}")?;
495 }
496
497 Ok(())
498 }
499}
500
501#[derive(Debug, Clone, PartialEq, Eq)]
503#[must_use]
504pub struct Report {
505 baseline: Baseline,
506 stability_baselines: Vec<Baseline>,
507 baseline_stable: bool,
508 attempts: Vec<Attempt>,
509 truncated: bool,
510 allocator_installed: bool,
511}
512
513impl Report {
514 pub fn baseline(&self) -> &Baseline {
516 &self.baseline
517 }
518
519 pub fn stability_baselines(&self) -> &[Baseline] {
521 &self.stability_baselines
522 }
523
524 #[must_use]
527 pub fn baseline_is_stable(&self) -> bool {
528 self.baseline_stable
529 }
530
531 #[must_use]
534 pub fn baseline_allocations(&self) -> usize {
535 self.baseline.observed_allocations
536 }
537
538 pub fn attempts(&self) -> &[Attempt] {
540 &self.attempts
541 }
542
543 pub fn failed_attempts(&self) -> impl Iterator<Item = &Attempt> {
545 self.attempts.iter().filter(|attempt| !attempt.is_success())
546 }
547
548 #[must_use]
551 pub fn first_failure(&self) -> Option<&Attempt> {
552 self.attempts.iter().find(|attempt| !attempt.is_success())
553 }
554
555 #[must_use]
557 pub fn tested_failures(&self) -> usize {
558 self.attempts.len()
559 }
560
561 #[must_use]
564 pub fn injected_failures(&self) -> usize {
565 self.attempts
566 .iter()
567 .filter(|attempt| attempt.injected())
568 .count()
569 }
570
571 #[must_use]
574 pub fn untested_failures(&self) -> usize {
575 self.baseline_allocations()
576 .saturating_sub(self.injected_failures())
577 }
578
579 #[must_use]
586 pub fn is_truncated(&self) -> bool {
587 self.truncated
588 }
589
590 #[must_use]
595 pub fn allocator_installed(&self) -> bool {
596 self.allocator_installed
597 }
598
599 #[must_use]
611 pub fn is_success(&self) -> bool {
612 self.allocator_installed
613 && self.baseline.outcome.is_completed()
614 && self.baseline_stable
615 && !self.truncated
616 && self.tested_failures() == self.baseline_allocations()
617 && self.attempts.iter().all(Attempt::is_success)
618 }
619
620 pub fn assert_success(&self) {
622 assert!(self.is_success(), "{self}");
623 }
624}
625
626impl fmt::Display for Report {
627 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
628 writeln!(f, "alloc-chaos report")?;
629 writeln!(
630 f,
631 " baseline: {}, {} allocation attempt(s)",
632 self.baseline.outcome, self.baseline.observed_allocations
633 )?;
634
635 if !self.stability_baselines.is_empty() {
636 writeln!(
637 f,
638 " stability: {} across {} baseline run(s)",
639 if self.baseline_stable {
640 "stable"
641 } else {
642 "unstable"
643 },
644 self.stability_baselines.len() + 1
645 )?;
646
647 if !self.baseline_stable {
648 for (index, baseline) in self.stability_baselines.iter().enumerate() {
649 writeln!(
650 f,
651 " baseline #{}: {}, {} allocation attempt(s)",
652 index + 2,
653 baseline.outcome,
654 baseline.observed_allocations
655 )?;
656 }
657 }
658 }
659
660 writeln!(
661 f,
662 " tested: {}/{} allocation failure target(s){}",
663 self.tested_failures(),
664 self.baseline.observed_allocations,
665 if self.truncated { " (truncated)" } else { "" }
666 )?;
667
668 let injected_failures = self.injected_failures();
669 if injected_failures != self.tested_failures() {
670 writeln!(
671 f,
672 " injected: {injected_failures}/{} selected target(s)",
673 self.tested_failures()
674 )?;
675 }
676
677 if self.is_truncated() || injected_failures != self.baseline_allocations() {
678 writeln!(f, " untested: {} allocation failure(s)", self.untested_failures())?;
679 }
680 writeln!(f, " allocator installed: {}", self.allocator_installed)?;
681
682 if self.is_success() {
683 writeln!(f, " status: exhaustive success")?;
684 } else if !self.allocator_installed {
685 writeln!(f, " status: invalid check: allocator wrapper not observed")?;
686 } else if !self.baseline.outcome.is_completed() {
687 writeln!(f, " status: failure")?;
688 writeln!(f, " baseline failed: {}", self.baseline.outcome)?;
689 } else if !self.baseline_stable {
690 writeln!(f, " status: invalid check: unstable baseline")?;
691 } else if self.first_failure().is_some() {
692 writeln!(f, " status: failure")?;
693
694 for attempt in self.failed_attempts() {
695 write!(
696 f,
697 " allocation #{}: {}, injected={}, observed={} allocation attempt(s)",
698 attempt.target_allocation,
699 attempt.outcome,
700 attempt.injected,
701 attempt.observed_allocations
702 )?;
703
704 if let Some(allocation) = attempt.injected_allocation {
705 write!(f, ", {allocation}")?;
706 }
707
708 writeln!(f)?;
709 }
710 } else if self.truncated {
711 writeln!(f, " status: partial success")?;
712 } else {
713 writeln!(f, " status: failure")?;
714 }
715
716 Ok(())
717 }
718}
719
720#[derive(Debug, Clone, PartialEq, Eq)]
722#[must_use]
723pub struct Baseline {
724 observed_allocations: usize,
725 outcome: Outcome,
726}
727
728impl Baseline {
729 #[must_use]
731 pub fn observed_allocations(&self) -> usize {
732 self.observed_allocations
733 }
734
735 pub fn outcome(&self) -> &Outcome {
737 &self.outcome
738 }
739}
740
741#[derive(Debug, Clone, PartialEq, Eq)]
743#[must_use]
744pub struct Attempt {
745 target_allocation: usize,
746 observed_allocations: usize,
747 injected: bool,
748 injected_allocation: Option<Allocation>,
749 outcome: Outcome,
750}
751
752impl Attempt {
753 #[must_use]
755 pub fn target_allocation(&self) -> usize {
756 self.target_allocation
757 }
758
759 #[must_use]
761 pub fn observed_allocations(&self) -> usize {
762 self.observed_allocations
763 }
764
765 #[must_use]
768 pub fn injected(&self) -> bool {
769 self.injected
770 }
771
772 #[must_use]
775 pub fn injected_allocation(&self) -> Option<Allocation> {
776 self.injected_allocation
777 }
778
779 pub fn outcome(&self) -> &Outcome {
781 &self.outcome
782 }
783
784 #[must_use]
787 pub fn is_success(&self) -> bool {
788 self.injected && self.outcome.is_completed()
789 }
790}
791
792#[derive(Debug, Clone, PartialEq, Eq)]
794#[must_use]
795pub enum Outcome {
796 Completed,
798
799 Panicked(Panic),
801}
802
803impl Outcome {
804 #[must_use]
806 pub fn is_completed(&self) -> bool {
807 matches!(self, Self::Completed)
808 }
809
810 #[must_use]
812 pub fn is_panicked(&self) -> bool {
813 matches!(self, Self::Panicked(_))
814 }
815}
816
817impl fmt::Display for Outcome {
818 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
819 match self {
820 Self::Completed => f.write_str("completed"),
821 Self::Panicked(panic) => write!(f, "panicked: {panic}"),
822 }
823 }
824}
825
826#[derive(Debug, Clone, PartialEq, Eq)]
828#[must_use]
829pub struct Panic {
830 message: Option<String>,
831}
832
833impl Panic {
834 #[must_use]
837 pub fn message(&self) -> Option<&str> {
838 self.message.as_deref()
839 }
840}
841
842impl fmt::Display for Panic {
843 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
844 match self.message() {
845 Some(message) => f.write_str(message),
846 None => f.write_str("non-string panic payload"),
847 }
848 }
849}
850
851#[derive(Debug, Clone, Copy, PartialEq, Eq)]
852struct FailurePlan {
853 start: usize,
854 end: usize,
855}
856
857impl FailurePlan {
858 fn for_check(check: Check, baseline_allocations: usize) -> Self {
859 let selection_start = check.target_start.min(baseline_allocations);
860 let selection_end = check
861 .target_end
862 .unwrap_or(baseline_allocations)
863 .min(baseline_allocations);
864 let selected_failures = selection_end.saturating_sub(selection_start);
865 let failure_limit = check
866 .max_failures
867 .map_or(selected_failures, |max| max.min(selected_failures));
868
869 Self {
870 start: selection_start,
871 end: selection_start + failure_limit,
872 }
873 }
874
875 fn len(self) -> usize {
876 self.end - self.start
877 }
878
879 fn targets(self) -> Range<usize> {
880 self.start..self.end
881 }
882
883 fn is_exhaustive_for(self, baseline_allocations: usize) -> bool {
884 self.start == 0 && self.end == baseline_allocations
885 }
886
887 fn is_exhaustive_after_stop(self, stopped_target: usize, baseline_allocations: usize) -> bool {
888 self.start == 0 && stopped_target.saturating_add(1) >= baseline_allocations
889 }
890}
891
892#[derive(Debug, Clone, Copy)]
893enum RunMode {
894 Count,
895 Fail { target: usize },
896}
897
898#[derive(Debug, Clone, Copy)]
899struct RunStats {
900 observed_allocations: usize,
901 injected: bool,
902 injected_allocation: Option<Allocation>,
903}
904
905#[derive(Debug)]
906struct ActiveCheck;
907
908impl ActiveCheck {
909 fn enter() -> Result<Self, AlreadyRunning> {
910 CHECK_ACTIVE
911 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
912 .map(|_| Self)
913 .map_err(|_| AlreadyRunning)
914 }
915}
916
917impl Drop for ActiveCheck {
918 fn drop(&mut self) {
919 MODE.store(MODE_DISABLED, Ordering::SeqCst);
920 CHECK_ACTIVE.store(false, Ordering::SeqCst);
921 }
922}
923
924#[derive(Debug)]
925struct ModeGuard {
926 finished: bool,
927 previous_thread_tracking: bool,
928}
929
930impl ModeGuard {
931 fn enter(mode: RunMode) -> Self {
932 let previous_thread_tracking = current_thread_tracking_enabled();
933 set_current_thread_tracking(true);
934
935 SEEN.store(0, Ordering::SeqCst);
936 INJECTED.store(false, Ordering::SeqCst);
937 INJECTED_INDEX.store(NO_INJECTED_INDEX, Ordering::SeqCst);
938 INJECTED_OP.store(ALLOC_OP_NONE, Ordering::SeqCst);
939 INJECTED_SIZE.store(0, Ordering::SeqCst);
940 INJECTED_ALIGN.store(0, Ordering::SeqCst);
941 INJECTED_NEW_SIZE.store(NO_REALLOC_NEW_SIZE, Ordering::SeqCst);
942
943 match mode {
944 RunMode::Count => {
945 TARGET.store(NO_TARGET, Ordering::SeqCst);
946 MODE.store(MODE_COUNTING, Ordering::SeqCst);
947 }
948 RunMode::Fail { target } => {
949 TARGET.store(target, Ordering::SeqCst);
950 MODE.store(MODE_FAILING, Ordering::SeqCst);
951 }
952 }
953
954 Self {
955 finished: false,
956 previous_thread_tracking,
957 }
958 }
959
960 fn finish(mut self) -> RunStats {
961 MODE.store(MODE_DISABLED, Ordering::SeqCst);
962
963 let injected = INJECTED.load(Ordering::SeqCst);
964 let injected_allocation = if injected {
965 AllocOp::from_u8(INJECTED_OP.load(Ordering::SeqCst)).map(|operation| Allocation {
966 index: INJECTED_INDEX.load(Ordering::SeqCst),
967 operation,
968 size: INJECTED_SIZE.load(Ordering::SeqCst),
969 align: INJECTED_ALIGN.load(Ordering::SeqCst),
970 new_size: non_sentinel_usize(INJECTED_NEW_SIZE.load(Ordering::SeqCst)),
971 })
972 } else {
973 None
974 };
975
976 let stats = RunStats {
977 observed_allocations: SEEN.load(Ordering::SeqCst),
978 injected,
979 injected_allocation,
980 };
981
982 set_current_thread_tracking(self.previous_thread_tracking);
983 self.finished = true;
984 stats
985 }
986}
987
988impl Drop for ModeGuard {
989 fn drop(&mut self) {
990 if !self.finished {
991 MODE.store(MODE_DISABLED, Ordering::SeqCst);
992 set_current_thread_tracking(self.previous_thread_tracking);
993 }
994 }
995}
996
997fn run_counting<F>(f: &F) -> Baseline
998where
999 F: Fn(),
1000{
1001 let (stats, outcome) = run_in_mode(RunMode::Count, f);
1002 Baseline {
1003 observed_allocations: stats.observed_allocations,
1004 outcome,
1005 }
1006}
1007
1008fn run_failing<F>(target_allocation: usize, f: &F) -> Attempt
1009where
1010 F: Fn(),
1011{
1012 let (stats, outcome) = run_in_mode(
1013 RunMode::Fail {
1014 target: target_allocation,
1015 },
1016 f,
1017 );
1018
1019 Attempt {
1020 target_allocation,
1021 observed_allocations: stats.observed_allocations,
1022 injected: stats.injected,
1023 injected_allocation: stats.injected_allocation,
1024 outcome,
1025 }
1026}
1027
1028fn run_in_mode<F>(mode: RunMode, f: &F) -> (RunStats, Outcome)
1029where
1030 F: Fn(),
1031{
1032 let guard = ModeGuard::enter(mode);
1033 let result = panic::catch_unwind(AssertUnwindSafe(f));
1034 let stats = guard.finish();
1035
1036 let outcome = match result {
1037 Ok(()) => Outcome::Completed,
1038 Err(payload) => Outcome::Panicked(panic_from_payload(payload)),
1039 };
1040
1041 (stats, outcome)
1042}
1043
1044fn panic_from_payload(payload: Box<dyn Any + Send>) -> Panic {
1045 let message = if let Some(message) = payload.downcast_ref::<&'static str>() {
1046 Some((*message).to_owned())
1047 } else {
1048 payload.downcast_ref::<String>().cloned()
1049 };
1050
1051 Panic { message }
1052}
1053
1054fn baseline_matches(reference: &Baseline, candidate: &Baseline) -> bool {
1055 reference.outcome.is_completed()
1056 && candidate.outcome.is_completed()
1057 && reference.observed_allocations == candidate.observed_allocations
1058}
1059
1060fn set_current_thread_tracking(enabled: bool) {
1061 TRACK_ALLOCATIONS_ON_THREAD.with(|tracking| tracking.set(enabled));
1062}
1063
1064fn current_thread_tracking_enabled() -> bool {
1065 TRACK_ALLOCATIONS_ON_THREAD
1066 .try_with(Cell::get)
1067 .unwrap_or(false)
1068}
1069
1070fn should_inject_failure(operation: AllocOp, layout: Layout, new_size: Option<usize>) -> bool {
1071 ALLOCATOR_WAS_USED.store(true, Ordering::SeqCst);
1072
1073 if std::thread::panicking() {
1077 return false;
1078 }
1079
1080 let mode = MODE.load(Ordering::SeqCst);
1081
1082 if mode == MODE_DISABLED || !current_thread_tracking_enabled() {
1083 return false;
1084 }
1085
1086 let index = SEEN.fetch_add(1, Ordering::SeqCst);
1087
1088 if mode == MODE_FAILING && index == TARGET.load(Ordering::SeqCst) {
1089 INJECTED_INDEX.store(index, Ordering::SeqCst);
1090 INJECTED_OP.store(operation.as_u8(), Ordering::SeqCst);
1091 INJECTED_SIZE.store(layout.size(), Ordering::SeqCst);
1092 INJECTED_ALIGN.store(layout.align(), Ordering::SeqCst);
1093 INJECTED_NEW_SIZE.store(new_size.unwrap_or(NO_REALLOC_NEW_SIZE), Ordering::SeqCst);
1094 INJECTED.store(true, Ordering::SeqCst);
1095 true
1096 } else {
1097 false
1098 }
1099}
1100
1101fn probe_allocator_installed() -> bool {
1102 ALLOCATOR_WAS_USED.store(false, Ordering::SeqCst);
1103
1104 let layout = Layout::from_size_align(1, 1).expect("valid allocator probe layout");
1105
1106 let ptr = unsafe { std::alloc::alloc(layout) };
1110
1111 if !ptr.is_null() {
1112 unsafe { std::alloc::dealloc(ptr, layout) };
1115 }
1116
1117 ALLOCATOR_WAS_USED.load(Ordering::SeqCst)
1118}
1119
1120fn non_sentinel_usize(value: usize) -> Option<usize> {
1121 if value == NO_REALLOC_NEW_SIZE {
1122 None
1123 } else {
1124 Some(value)
1125 }
1126}
1127
1128#[cfg(test)]
1129#[global_allocator]
1130static TEST_ALLOCATOR: ChaosAllocator = ChaosAllocator::system();
1131
1132#[cfg(test)]
1133mod tests {
1134 use super::*;
1135 use std::sync::atomic::{AtomicUsize, Ordering};
1136 use std::sync::{Mutex, MutexGuard};
1137
1138 static TEST_LOCK: Mutex<()> = Mutex::new(());
1139
1140 fn test_lock() -> MutexGuard<'static, ()> {
1141 TEST_LOCK
1142 .lock()
1143 .unwrap_or_else(|poisoned| poisoned.into_inner())
1144 }
1145
1146 fn two_fallible_allocations() {
1147 let mut first = Vec::<u8>::new();
1148 let mut second = Vec::<u8>::new();
1149
1150 let _ = first.try_reserve_exact(64);
1151 let _ = second.try_reserve_exact(64);
1152 }
1153
1154 fn with_quiet_expected_panics<R>(f: impl FnOnce() -> R) -> R {
1155 let previous_hook = std::panic::take_hook();
1156 std::panic::set_hook(Box::new(|_| {}));
1157
1158 let result = std::panic::catch_unwind(AssertUnwindSafe(f));
1159
1160 std::panic::set_hook(previous_hook);
1161
1162 match result {
1163 Ok(value) => value,
1164 Err(payload) => std::panic::resume_unwind(payload),
1165 }
1166 }
1167
1168 #[test]
1169 fn reports_zero_allocations_for_empty_closure() {
1170 let _lock = test_lock();
1171
1172 let report = check(|| {});
1173
1174 assert_eq!(report.baseline_allocations(), 0);
1175 assert!(report.attempts().is_empty());
1176 assert!(report.allocator_installed());
1177 assert!(report.is_success(), "{report}");
1178 }
1179
1180 #[test]
1181 fn injects_try_reserve_failure() {
1182 let _lock = test_lock();
1183
1184 let report = check(|| {
1185 let mut values = Vec::<u8>::new();
1186 let _ = values.try_reserve_exact(1024);
1187 });
1188
1189 assert_eq!(report.baseline_allocations(), 1, "{report}");
1190 assert_eq!(report.attempts().len(), 1);
1191 assert!(report.attempts()[0].injected(), "{report}");
1192 assert!(report.attempts()[0].injected_allocation().is_some());
1193 assert!(report.is_success(), "{report}");
1194 }
1195
1196 #[test]
1197 fn records_injected_allocation_metadata() {
1198 let _lock = test_lock();
1199
1200 let report = check(|| {
1201 let mut values = Vec::<u8>::new();
1202 let _ = values.try_reserve_exact(1024);
1203 });
1204
1205 let allocation = report.attempts()[0]
1206 .injected_allocation()
1207 .expect("allocation metadata should be recorded");
1208
1209 assert_eq!(allocation.index(), 0);
1210 assert_eq!(allocation.operation(), AllocOp::Alloc);
1211 assert_eq!(allocation.size(), 1024);
1212 assert_eq!(allocation.align(), 1);
1213 assert_eq!(allocation.new_size(), None);
1214 }
1215
1216 #[test]
1217 fn reports_baseline_panic() {
1218 let _lock = test_lock();
1219
1220 let report = with_quiet_expected_panics(|| check(|| panic!("baseline failed")));
1221
1222 assert!(!report.is_success());
1223 assert!(report.attempts().is_empty());
1224 assert!(matches!(report.baseline().outcome(), Outcome::Panicked(_)));
1225 }
1226
1227 #[test]
1228 fn assert_success_panics_on_failure_report() {
1229 let _lock = test_lock();
1230
1231 let report = with_quiet_expected_panics(|| check(|| panic!("expected test panic")));
1232 let panic =
1233 with_quiet_expected_panics(|| std::panic::catch_unwind(|| report.assert_success()));
1234
1235 assert!(panic.is_err());
1236 }
1237
1238 #[test]
1239 fn rejects_nested_checks() {
1240 let _lock = test_lock();
1241
1242 let report = check(|| {
1243 assert_eq!(try_check(|| {}).unwrap_err(), AlreadyRunning);
1244 });
1245
1246 assert!(report.is_success(), "{report}");
1247 }
1248
1249 #[test]
1250 fn max_failures_limits_attempts_and_marks_report_truncated() {
1251 let _lock = test_lock();
1252
1253 let report = Check::new().max_failures(1).run(two_fallible_allocations);
1254
1255 assert!(report.baseline_allocations() >= 2, "{report}");
1256 assert_eq!(report.tested_failures(), 1);
1257 assert_eq!(report.attempts().len(), 1);
1258 assert!(report.is_truncated());
1259 assert_eq!(
1260 report.untested_failures(),
1261 report.baseline_allocations() - report.tested_failures()
1262 );
1263 assert!(!report.is_success(), "{report}");
1264 }
1265
1266 #[test]
1267 fn unlimited_failures_removes_limit() {
1268 let _lock = test_lock();
1269
1270 let report = Check::new()
1271 .max_failures(1)
1272 .unlimited_failures()
1273 .run(two_fallible_allocations);
1274
1275 assert!(report.baseline_allocations() >= 2, "{report}");
1276 assert_eq!(report.tested_failures(), report.baseline_allocations());
1277 assert!(!report.is_truncated(), "{report}");
1278 assert!(report.is_success(), "{report}");
1279 }
1280
1281 #[test]
1282 fn only_failure_reproduces_one_target_and_marks_report_truncated() {
1283 let _lock = test_lock();
1284
1285 let report = Check::new().only_failure(1).run(two_fallible_allocations);
1286
1287 assert!(report.baseline_allocations() >= 2, "{report}");
1288 assert_eq!(report.tested_failures(), 1);
1289 assert_eq!(report.attempts()[0].target_allocation(), 1);
1290 assert!(report.attempts()[0].is_success(), "{report}");
1291 assert!(report.is_truncated(), "{report}");
1292 assert!(!report.is_success(), "{report}");
1293 }
1294
1295 #[test]
1296 fn failure_range_selects_requested_targets() {
1297 let _lock = test_lock();
1298
1299 let report = Check::new()
1300 .failure_range(1..2)
1301 .run(two_fallible_allocations);
1302
1303 assert!(report.baseline_allocations() >= 2, "{report}");
1304 assert_eq!(report.tested_failures(), 1);
1305 assert_eq!(report.attempts()[0].target_allocation(), 1);
1306 assert!(report.is_truncated(), "{report}");
1307 }
1308
1309 #[test]
1310 fn failure_range_rejects_reversed_ranges() {
1311 let _lock = test_lock();
1312
1313 let panic = std::panic::catch_unwind(|| {
1314 #[allow(clippy::reversed_empty_ranges)]
1315 let _ = Check::new().failure_range(2..1);
1316 });
1317
1318 assert!(panic.is_err());
1319 }
1320
1321 #[test]
1322 fn stop_on_failure_preserves_range_truncation() {
1323 let _lock = test_lock();
1324
1325 let report = Check::new()
1326 .failure_range(1..2)
1327 .stop_on_failure(true)
1328 .run(|| {
1329 let mut first = Vec::<u8>::new();
1330 let _ = first.try_reserve_exact(64);
1331
1332 let mut second = Vec::<u8>::new();
1333 if second.try_reserve_exact(64).is_err() {
1334 panic!("second allocation failed");
1335 }
1336 });
1337
1338 assert!(report.baseline_allocations() >= 2, "{report}");
1339 assert_eq!(report.tested_failures(), 1, "{report}");
1340 assert_eq!(report.attempts()[0].target_allocation(), 1);
1341 assert!(report.is_truncated(), "{report}");
1342 assert!(!report.is_success(), "{report}");
1343 }
1344
1345 #[test]
1346 fn reports_unstable_baseline() {
1347 let _lock = test_lock();
1348 let runs = AtomicUsize::new(0);
1349
1350 let report = Check::new().stability_runs(2).run(|| {
1351 if runs.fetch_add(1, Ordering::SeqCst) == 0 {
1352 let mut values = Vec::<u8>::new();
1353 let _ = values.try_reserve_exact(64);
1354 }
1355 });
1356
1357 assert!(!report.baseline_is_stable(), "{report}");
1358 assert_eq!(report.stability_baselines().len(), 1);
1359 assert!(report.attempts().is_empty(), "{report}");
1360 assert!(report.is_truncated(), "{report}");
1361 assert!(!report.is_success(), "{report}");
1362 }
1363
1364 #[test]
1365 fn reports_panic_in_injected_failure_run() {
1366 let _lock = test_lock();
1367
1368 let report = with_quiet_expected_panics(|| {
1369 Check::new().max_failures(1).run(|| {
1370 let mut values = Vec::<u8>::new();
1371 if values.try_reserve_exact(1024).is_err() {
1372 panic!("allocation failure was not handled");
1373 }
1374 })
1375 });
1376
1377 assert!(report.baseline().outcome().is_completed(), "{report}");
1378 assert_eq!(report.attempts().len(), 1);
1379 assert!(report.attempts()[0].injected(), "{report}");
1380 assert!(matches!(
1381 report.attempts()[0].outcome(),
1382 Outcome::Panicked(panic)
1383 if panic.message() == Some("allocation failure was not handled")
1384 ));
1385 assert!(!report.is_success());
1386 }
1387
1388 #[test]
1389 fn stop_on_failure_stops_after_first_failed_attempt() {
1390 let _lock = test_lock();
1391
1392 let report = with_quiet_expected_panics(|| {
1393 Check::new().stop_on_failure(true).run(|| {
1394 let mut first = Vec::<u8>::new();
1395 if first.try_reserve_exact(64).is_err() {
1396 panic!("first allocation failed");
1397 }
1398
1399 let mut second = Vec::<u8>::new();
1400 let _ = second.try_reserve_exact(64);
1401 })
1402 });
1403
1404 assert!(report.baseline_allocations() >= 2, "{report}");
1405 assert_eq!(report.tested_failures(), 1, "{report}");
1406 assert!(report.is_truncated(), "{report}");
1407 assert!(!report.is_success());
1408 }
1409
1410 #[test]
1411 fn exposes_failed_attempts() {
1412 let _lock = test_lock();
1413
1414 let report = with_quiet_expected_panics(|| {
1415 Check::new().max_failures(1).run(|| {
1416 let mut values = Vec::<u8>::new();
1417 if values.try_reserve_exact(1024).is_err() {
1418 panic!("not handled");
1419 }
1420 })
1421 });
1422
1423 assert!(report.first_failure().is_some());
1424 assert_eq!(report.failed_attempts().count(), 1);
1425 }
1426
1427 #[test]
1428 fn reports_targets_that_are_not_reached_as_untested() {
1429 let _lock = test_lock();
1430 let run = AtomicUsize::new(0);
1431
1432 let report = Check::new().run(|| {
1433 let current_run = run.fetch_add(1, Ordering::SeqCst);
1434
1435 let mut first = Vec::<u8>::new();
1436 let _ = first.try_reserve_exact(64);
1437
1438 if current_run != 2 {
1439 let mut second = Vec::<u8>::new();
1440 let _ = second.try_reserve_exact(64);
1441 }
1442 });
1443
1444 assert_eq!(report.baseline_allocations(), 2, "{report}");
1445 assert_eq!(report.tested_failures(), 2, "{report}");
1446 assert_eq!(report.injected_failures(), 1, "{report}");
1447 assert_eq!(report.untested_failures(), 1, "{report}");
1448 assert!(!report.is_success(), "{report}");
1449 }
1450
1451 #[test]
1452 fn display_mentions_untested_failures_when_truncated() {
1453 let _lock = test_lock();
1454
1455 let report = Check::new().max_failures(1).run(two_fallible_allocations);
1456 let rendered = report.to_string();
1457
1458 assert!(rendered.contains("truncated"), "{rendered}");
1459 assert!(rendered.contains("untested:"), "{rendered}");
1460 assert!(rendered.contains("partial success"), "{rendered}");
1461 }
1462
1463 #[test]
1464 fn display_mentions_unstable_baseline() {
1465 let _lock = test_lock();
1466 let runs = AtomicUsize::new(0);
1467
1468 let report = Check::new().stability_runs(2).run(|| {
1469 if runs.fetch_add(1, Ordering::SeqCst) == 0 {
1470 let mut values = Vec::<u8>::new();
1471 let _ = values.try_reserve_exact(64);
1472 }
1473 });
1474 let rendered = report.to_string();
1475
1476 assert!(rendered.contains("unstable baseline"), "{rendered}");
1477 }
1478}