myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

services_hardening_runtime_supervision.rs (5758B)


      1 #![forbid(unsafe_code)]
      2 
      3 use std::{
      4     error::Error,
      5     sync::{
      6         Arc,
      7         atomic::{AtomicBool, AtomicUsize, Ordering},
      8     },
      9 };
     10 
     11 use myc::{
     12     MYC_CRITICAL_TASK_MAX_COUNT, MycCriticalTask, MycCriticalTaskError, MycLogRecord,
     13     MycProcessResult, MycRuntimeSupervisionErrorKind, MycSupervisedRuntime,
     14 };
     15 
     16 fn waiting_peer(joined: Arc<AtomicBool>) -> MycCriticalTask {
     17     MycCriticalTask::new(move |cancellation| async move {
     18         cancellation.cancelled().await;
     19         assert!(cancellation.is_cancelled());
     20         joined.store(true, Ordering::SeqCst);
     21         Ok(())
     22     })
     23 }
     24 
     25 #[test]
     26 fn task_inventory_is_nonempty_bounded_and_stops_at_maximum_plus_one() {
     27     let empty = MycSupervisedRuntime::new([]).expect_err("empty graph must fail closed");
     28     assert_eq!(empty.kind(), MycRuntimeSupervisionErrorKind::EmptyTaskSet);
     29 
     30     let maximum =
     31         (0..MYC_CRITICAL_TASK_MAX_COUNT).map(|_| MycCriticalTask::new(|_| async { Ok(()) }));
     32     assert_eq!(
     33         MycSupervisedRuntime::new(maximum)
     34             .expect("exact maximum")
     35             .task_count(),
     36         MYC_CRITICAL_TASK_MAX_COUNT
     37     );
     38 
     39     let generated = Arc::new(AtomicUsize::new(0));
     40     let count = Arc::clone(&generated);
     41     let unbounded = std::iter::repeat_with(move || {
     42         count.fetch_add(1, Ordering::SeqCst);
     43         MycCriticalTask::new(|_| async { Ok(()) })
     44     });
     45     let too_many = MycSupervisedRuntime::new(unbounded).expect_err("maximum plus one");
     46     assert_eq!(
     47         too_many.kind(),
     48         MycRuntimeSupervisionErrorKind::TooManyTasks
     49     );
     50     assert_eq!(
     51         generated.load(Ordering::SeqCst),
     52         MYC_CRITICAL_TASK_MAX_COUNT + 1
     53     );
     54 }
     55 
     56 #[test]
     57 fn registration_without_a_tokio_runtime_fails_before_any_task_can_detach() {
     58     let runtime = MycSupervisedRuntime::new([MycCriticalTask::new(|_| async { Ok(()) })])
     59         .expect("bounded graph");
     60     let error = futures_executor::block_on(runtime.run()).expect_err("Tokio runtime required");
     61     assert_eq!(
     62         error.kind(),
     63         MycRuntimeSupervisionErrorKind::TaskRegistration
     64     );
     65 }
     66 
     67 #[tokio::test]
     68 async fn task_error_coordinates_peer_cancellation_and_observes_every_join() {
     69     let peer_joined = Arc::new(AtomicBool::new(false));
     70     let runtime = MycSupervisedRuntime::new([
     71         waiting_peer(Arc::clone(&peer_joined)),
     72         MycCriticalTask::new(|_| async { Err(MycCriticalTaskError::failed()) }),
     73     ])
     74     .expect("bounded graph");
     75 
     76     let error = runtime.run().await.expect_err("critical task failed");
     77     assert_eq!(
     78         error.kind(),
     79         MycRuntimeSupervisionErrorKind::TaskReturnedError
     80     );
     81     assert_eq!(error.process_result(), MycProcessResult::UnexpectedInternal);
     82     assert_eq!(error.diagnostic(), MycLogRecord::critical_task_failed());
     83     assert!(peer_joined.load(Ordering::SeqCst));
     84     assert!(Error::source(&error).is_none());
     85 }
     86 
     87 #[tokio::test]
     88 async fn early_success_and_panic_are_fatal_and_join_their_cancelled_peer() {
     89     let early_peer = Arc::new(AtomicBool::new(false));
     90     let early = MycSupervisedRuntime::new([
     91         waiting_peer(Arc::clone(&early_peer)),
     92         MycCriticalTask::new(|_| async { Ok(()) }),
     93     ])
     94     .expect("bounded graph")
     95     .run()
     96     .await
     97     .expect_err("critical task returned before cancellation");
     98     assert_eq!(
     99         early.kind(),
    100         MycRuntimeSupervisionErrorKind::UnexpectedCompletion
    101     );
    102     assert!(early_peer.load(Ordering::SeqCst));
    103 
    104     let panic_peer = Arc::new(AtomicBool::new(false));
    105     let panicked = MycSupervisedRuntime::new([
    106         waiting_peer(Arc::clone(&panic_peer)),
    107         MycCriticalTask::new(|_| async {
    108             panic!("fixed critical-task test panic");
    109             #[allow(unreachable_code)]
    110             Ok(())
    111         }),
    112     ])
    113     .expect("bounded graph")
    114     .run()
    115     .await
    116     .expect_err("critical task panicked");
    117     assert_eq!(
    118         panicked.kind(),
    119         MycRuntimeSupervisionErrorKind::TaskPanicked
    120     );
    121     assert!(panic_peer.load(Ordering::SeqCst));
    122 }
    123 
    124 #[test]
    125 fn task_and_error_diagnostics_are_source_free_and_redacted() {
    126     let secret = "caller-task-secret";
    127     let task = MycCriticalTask::new(move |_| async move {
    128         let _ = secret;
    129         Ok(())
    130     });
    131     let runtime = MycSupervisedRuntime::new([task]).expect("bounded graph");
    132     assert_eq!(
    133         format!("{runtime:?}"),
    134         "MycSupervisedRuntime { task_count: 1, tasks: \"[sealed]\" }"
    135     );
    136     assert!(!format!("{runtime:?}").contains(secret));
    137     assert_eq!(
    138         format!("{:?}", MycCriticalTaskError::failed()),
    139         "MycCriticalTaskError"
    140     );
    141     assert!(Error::source(&MycCriticalTaskError::failed()).is_none());
    142 
    143     let codes = [
    144         (
    145             MycRuntimeSupervisionErrorKind::EmptyTaskSet,
    146             "runtime_task_set_empty",
    147         ),
    148         (
    149             MycRuntimeSupervisionErrorKind::TooManyTasks,
    150             "runtime_task_set_too_large",
    151         ),
    152         (
    153             MycRuntimeSupervisionErrorKind::TaskRegistration,
    154             "runtime_task_registration_failed",
    155         ),
    156         (
    157             MycRuntimeSupervisionErrorKind::TaskReturnedError,
    158             "runtime_task_returned_error",
    159         ),
    160         (
    161             MycRuntimeSupervisionErrorKind::TaskPanicked,
    162             "runtime_task_panicked",
    163         ),
    164         (
    165             MycRuntimeSupervisionErrorKind::UnexpectedCompletion,
    166             "runtime_task_completed_early",
    167         ),
    168         (
    169             MycRuntimeSupervisionErrorKind::UnexpectedCancellation,
    170             "runtime_task_cancelled_unexpectedly",
    171         ),
    172         (
    173             MycRuntimeSupervisionErrorKind::JoinFailed,
    174             "runtime_task_join_failed",
    175         ),
    176     ];
    177     for (kind, code) in codes {
    178         assert_eq!(kind.code(), code);
    179     }
    180 }