provider_executor.rs (25794B)
1 //! Sealed execution over the two governed signer-provider implementations. 2 3 #![allow( 4 dead_code, 5 reason = "Step 159 Unit 12 seals the executor before Unit 15 runtime graph wiring" 6 )] 7 8 use core::fmt; 9 use std::{error::Error, sync::Arc}; 10 11 use nostr::{ 12 JsonUtil as _, Keys, PublicKey, SecretKey, UnsignedEvent, 13 nips::{nip04, nip44}, 14 }; 15 use sha2::{Digest as _, Sha256}; 16 17 use crate::provider_local_signer::{ProtectedWireHex, WireCapability, WireProviderResult}; 18 use crate::provider_verification::verify_encrypted_provider_response; 19 use crate::{ 20 MYC_LOCAL_SIGNER_TRANSPORT_CONTRACT_VERSION, MYC_PROVIDER_INPUT_MAX_BYTES, MycConfigDocumentV1, 21 MycDecryptedIdentity, MycLocalSignerClient, MycProviderBinding, MycProviderCapability, 22 MycProviderCorrelationId, MycProviderDeadlineUnixMs, MycProviderKind, MycProviderOperation, 23 MycProviderOperationId, MycProviderOperationInput, MycProviderResponseObservedAtUnixMs, 24 MycProviderRole, MycRuntimeContext, MycTaskCancellation, MycVerifiedProviderResponse, 25 }; 26 #[cfg(any(target_os = "linux", target_os = "macos"))] 27 use crate::{open_myc_encrypted_identity, resolve_myc_wrapping_credential}; 28 29 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 30 pub(crate) enum MycProviderExecutionErrorKind { 31 Binding, 32 Open, 33 Operation, 34 Cancelled, 35 Transport, 36 Verification, 37 UnsupportedPlatform, 38 } 39 40 pub(crate) struct MycProviderExecutionError { 41 kind: MycProviderExecutionErrorKind, 42 } 43 44 impl MycProviderExecutionError { 45 pub(crate) const fn kind(&self) -> MycProviderExecutionErrorKind { 46 self.kind 47 } 48 } 49 50 impl fmt::Debug for MycProviderExecutionError { 51 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 52 formatter 53 .debug_struct("MycProviderExecutionError") 54 .field("kind", &self.kind) 55 .finish() 56 } 57 } 58 59 impl fmt::Display for MycProviderExecutionError { 60 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 61 formatter.write_str("Myc provider execution failed") 62 } 63 } 64 65 impl Error for MycProviderExecutionError {} 66 67 const fn execution_error(kind: MycProviderExecutionErrorKind) -> MycProviderExecutionError { 68 MycProviderExecutionError { kind } 69 } 70 71 struct OwnedBlockingTask<T> { 72 handle: Option<tokio::task::JoinHandle<T>>, 73 } 74 75 impl<T: Send + 'static> OwnedBlockingTask<T> { 76 fn spawn(task: impl FnOnce() -> T + Send + 'static) -> Self { 77 Self { 78 handle: Some(tokio::task::spawn_blocking(task)), 79 } 80 } 81 82 async fn join( 83 &mut self, 84 failure: MycProviderExecutionErrorKind, 85 ) -> Result<T, MycProviderExecutionError> { 86 let result = match self.handle.as_mut() { 87 Some(handle) => handle.await, 88 None => return Err(execution_error(failure)), 89 }; 90 self.handle.take(); 91 result.map_err(|_| execution_error(failure)) 92 } 93 } 94 95 impl<T> Drop for OwnedBlockingTask<T> { 96 fn drop(&mut self) { 97 let Some(handle) = self.handle.take() else { 98 return; 99 }; 100 handle.abort(); 101 while !handle.is_finished() { 102 std::thread::park_timeout(core::time::Duration::from_millis(1)); 103 } 104 } 105 } 106 107 enum ExecutableProvider { 108 EncryptedFile { 109 binding: MycProviderBinding, 110 identity: Arc<MycDecryptedIdentity>, 111 }, 112 LocalSigner { 113 binding: MycProviderBinding, 114 client: Box<MycLocalSignerClient>, 115 }, 116 } 117 118 impl ExecutableProvider { 119 const fn role(&self) -> MycProviderRole { 120 match self { 121 Self::EncryptedFile { binding, .. } | Self::LocalSigner { binding, .. } => { 122 binding.role() 123 } 124 } 125 } 126 } 127 128 pub(crate) struct MycProviderExecutor { 129 providers: Box<[ExecutableProvider]>, 130 } 131 132 impl MycProviderExecutor { 133 pub(crate) async fn open( 134 runtime: &MycRuntimeContext, 135 configuration: &MycConfigDocumentV1, 136 cancellation: &MycTaskCancellation, 137 ) -> Result<Self, MycProviderExecutionError> { 138 #[cfg(not(any(target_os = "linux", target_os = "macos")))] 139 { 140 let _ = (runtime, configuration, cancellation); 141 return Err(execution_error( 142 MycProviderExecutionErrorKind::UnsupportedPlatform, 143 )); 144 } 145 146 #[cfg(any(target_os = "linux", target_os = "macos"))] 147 { 148 let mut providers = 149 Vec::with_capacity(configuration.provider_contract().bindings().len()); 150 for binding in configuration.provider_contract().bindings() { 151 if cancellation.is_cancelled() { 152 return Err(execution_error(MycProviderExecutionErrorKind::Cancelled)); 153 } 154 match binding.kind() { 155 MycProviderKind::EncryptedFile => { 156 let runtime = runtime.clone(); 157 let binding = binding.clone(); 158 let worker_binding = binding.clone(); 159 let mut worker = OwnedBlockingTask::spawn(move || { 160 let credential = 161 resolve_myc_wrapping_credential(&runtime, &worker_binding) 162 .map_err(|_| ())?; 163 open_myc_encrypted_identity(&worker_binding, &credential) 164 .map_err(|_| ()) 165 }); 166 let identity = tokio::select! { 167 result = worker.join(MycProviderExecutionErrorKind::Open) => result? 168 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Open))?, 169 () = cancellation.cancelled() => { 170 let _ = worker.join(MycProviderExecutionErrorKind::Open).await; 171 return Err(execution_error(MycProviderExecutionErrorKind::Cancelled)); 172 } 173 }; 174 providers.push(ExecutableProvider::EncryptedFile { 175 binding, 176 identity: Arc::new(identity), 177 }); 178 } 179 MycProviderKind::LocalSigner => { 180 let client = MycLocalSignerClient::new(binding) 181 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Open))?; 182 providers.push(ExecutableProvider::LocalSigner { 183 binding: binding.clone(), 184 client: Box::new(client), 185 }); 186 } 187 } 188 } 189 if providers.len() != configuration.provider_contract().bindings().len() { 190 return Err(execution_error(MycProviderExecutionErrorKind::Binding)); 191 } 192 Ok(Self { 193 providers: providers.into_boxed_slice(), 194 }) 195 } 196 } 197 198 pub(crate) async fn execute( 199 &self, 200 operation: MycProviderOperation, 201 observed_at: MycProviderResponseObservedAtUnixMs, 202 cancellation: &MycTaskCancellation, 203 ) -> Result<MycVerifiedProviderResponse, MycProviderExecutionError> { 204 let provider = self 205 .providers 206 .iter() 207 .find(|provider| provider.role() == operation.role()) 208 .ok_or_else(|| execution_error(MycProviderExecutionErrorKind::Binding))?; 209 if cancellation.is_cancelled() { 210 return Err(execution_error(MycProviderExecutionErrorKind::Cancelled)); 211 } 212 match provider { 213 ExecutableProvider::EncryptedFile { binding, identity } => { 214 let binding = binding.clone(); 215 let identity = Arc::clone(identity); 216 let mut worker = OwnedBlockingTask::spawn(move || { 217 let result = execute_encrypted(&identity, &operation)?; 218 Ok::<_, MycProviderExecutionError>((operation, result)) 219 }); 220 tokio::select! { 221 joined = worker.join(MycProviderExecutionErrorKind::Operation) => { 222 let (operation, result) = joined??; 223 verify_encrypted_provider_response(&binding, &operation, observed_at, result) 224 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Verification)) 225 } 226 () = cancellation.cancelled() => { 227 // A blocking cryptographic call cannot be abandoned. Join it before 228 // returning cancellation so no protected operation is detached. 229 // The result is deliberately discarded and never becomes domain authority. 230 let _ = worker.join(MycProviderExecutionErrorKind::Operation).await; 231 Err(execution_error(MycProviderExecutionErrorKind::Cancelled)) 232 } 233 } 234 } 235 ExecutableProvider::LocalSigner { binding, client } => { 236 tokio::select! { 237 result = client.execute(&operation) => { 238 let response = result.map_err(|_| execution_error(MycProviderExecutionErrorKind::Transport))?; 239 response 240 .verify(binding, &operation, observed_at) 241 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Verification)) 242 } 243 () = cancellation.cancelled() => { 244 Err(execution_error(MycProviderExecutionErrorKind::Cancelled)) 245 } 246 } 247 } 248 } 249 } 250 251 pub(crate) fn contains_role(&self, role: MycProviderRole) -> bool { 252 self.providers 253 .iter() 254 .any(|provider| provider.role() == role) 255 } 256 257 pub(crate) async fn probe_all( 258 &self, 259 observed_at_unix_ms: u64, 260 seed: [u8; 32], 261 cancellation: &MycTaskCancellation, 262 ) -> Result<(), MycProviderExecutionError> { 263 let observed_at = MycProviderResponseObservedAtUnixMs::new(observed_at_unix_ms) 264 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Binding))?; 265 for (index, provider) in self.providers.iter().enumerate() { 266 let role = provider.role(); 267 let binding = match provider { 268 ExecutableProvider::EncryptedFile { binding, .. } 269 | ExecutableProvider::LocalSigner { binding, .. } => binding, 270 }; 271 let timeout = binding 272 .local_signer_limits() 273 .map_or(15_000, |limits| limits.request_deadline_ms()); 274 let deadline = observed_at_unix_ms 275 .checked_add(timeout) 276 .and_then(|value| MycProviderDeadlineUnixMs::new(value).ok()) 277 .ok_or_else(|| execution_error(MycProviderExecutionErrorKind::Binding))?; 278 let index = u32::try_from(index) 279 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Binding))?; 280 let operation = MycProviderOperation::new( 281 binding, 282 MycProviderOperationId::from_bytes(probe_identifier( 283 b"operation", 284 &seed, 285 index, 286 role, 287 )), 288 MycProviderCorrelationId::from_bytes(probe_identifier( 289 b"correlation", 290 &seed, 291 index, 292 role, 293 )), 294 deadline, 295 MycProviderOperationInput::describe(), 296 ) 297 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Binding))?; 298 let response = self.execute(operation, observed_at, cancellation).await?; 299 if response.role() != role || response.capability() != MycProviderCapability::Describe { 300 return Err(execution_error(MycProviderExecutionErrorKind::Verification)); 301 } 302 } 303 Ok(()) 304 } 305 } 306 307 fn probe_identifier(kind: &[u8], seed: &[u8; 32], index: u32, role: MycProviderRole) -> [u8; 32] { 308 let mut hasher = Sha256::new(); 309 hasher.update(b"radroots.myc.provider_probe.v1\0"); 310 hasher.update(u64::try_from(kind.len()).unwrap_or(u64::MAX).to_be_bytes()); 311 hasher.update(kind); 312 hasher.update(seed); 313 hasher.update(index.to_be_bytes()); 314 hasher.update(role.as_str().as_bytes()); 315 hasher.finalize().into() 316 } 317 318 impl fmt::Debug for MycProviderExecutor { 319 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 320 formatter 321 .debug_struct("MycProviderExecutor") 322 .field("provider_count", &self.providers.len()) 323 .finish() 324 } 325 } 326 327 fn execute_encrypted( 328 identity: &MycDecryptedIdentity, 329 operation: &MycProviderOperation, 330 ) -> Result<WireProviderResult, MycProviderExecutionError> { 331 if operation.provider() != MycProviderKind::EncryptedFile 332 || operation.expected_identity() != identity.public_identity() 333 { 334 return Err(execution_error(MycProviderExecutionErrorKind::Binding)); 335 } 336 let secret = SecretKey::from_slice(identity.secret_bytes()) 337 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Operation))?; 338 let keys = Keys::new(secret); 339 let input = operation.input(); 340 let peer = || { 341 input 342 .peer() 343 .ok_or_else(|| execution_error(MycProviderExecutionErrorKind::Binding)) 344 .and_then(|identity| { 345 PublicKey::from_hex(identity.as_hex()) 346 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Binding)) 347 }) 348 }; 349 let bytes = || { 350 input 351 .bytes() 352 .ok_or_else(|| execution_error(MycProviderExecutionErrorKind::Binding)) 353 }; 354 match input.capability() { 355 MycProviderCapability::Describe => Ok(WireProviderResult::Describe { 356 public_identity: identity.public_identity().as_hex().to_owned(), 357 protocol_version: MYC_LOCAL_SIGNER_TRANSPORT_CONTRACT_VERSION, 358 capabilities: MycProviderCapability::ALL 359 .into_iter() 360 .filter(|capability| capability_allowed_for_role(operation.role(), *capability)) 361 .map(WireCapability::from) 362 .collect(), 363 maximum_request_bytes: MYC_PROVIDER_INPUT_MAX_BYTES as u64, 364 }), 365 MycProviderCapability::PublicIdentity => Ok(WireProviderResult::PublicIdentity { 366 public_identity: identity.public_identity().as_hex().to_owned(), 367 }), 368 MycProviderCapability::SignEvent => { 369 let unsigned: UnsignedEvent = serde_json::from_slice(bytes()?) 370 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Operation))?; 371 let signed = unsigned 372 .sign_with_keys(&keys) 373 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Operation))?; 374 Ok(WireProviderResult::SignEvent { 375 payload_hex: ProtectedWireHex::from_bytes(signed.as_json().as_bytes()), 376 }) 377 } 378 MycProviderCapability::Nip04Encrypt => { 379 let peer = peer()?; 380 let payload = nip04::encrypt(keys.secret_key(), &peer, bytes()?) 381 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Operation))?; 382 Ok(WireProviderResult::Nip04Encrypt { 383 peer: peer.to_hex(), 384 payload_hex: ProtectedWireHex::from_bytes(payload.as_bytes()), 385 }) 386 } 387 MycProviderCapability::Nip04Decrypt => { 388 let peer = peer()?; 389 let payload = core::str::from_utf8(bytes()?) 390 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Operation))?; 391 let plaintext = nip04::decrypt(keys.secret_key(), &peer, payload) 392 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Operation))?; 393 Ok(WireProviderResult::Nip04Decrypt { 394 peer: peer.to_hex(), 395 payload_hex: ProtectedWireHex::from_bytes(plaintext.as_bytes()), 396 }) 397 } 398 MycProviderCapability::Nip44Encrypt => { 399 let peer = peer()?; 400 let payload = nip44::encrypt(keys.secret_key(), &peer, bytes()?, nip44::Version::V2) 401 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Operation))?; 402 Ok(WireProviderResult::Nip44Encrypt { 403 peer: peer.to_hex(), 404 version: 2, 405 payload_hex: ProtectedWireHex::from_bytes(payload.as_bytes()), 406 }) 407 } 408 MycProviderCapability::Nip44Decrypt => { 409 let peer = peer()?; 410 let payload = core::str::from_utf8(bytes()?) 411 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Operation))?; 412 let plaintext = nip44::decrypt(keys.secret_key(), &peer, payload) 413 .map_err(|_| execution_error(MycProviderExecutionErrorKind::Operation))?; 414 Ok(WireProviderResult::Nip44Decrypt { 415 peer: peer.to_hex(), 416 version: 2, 417 payload_hex: ProtectedWireHex::from_bytes(plaintext.as_bytes()), 418 }) 419 } 420 } 421 } 422 423 const fn capability_allowed_for_role( 424 role: MycProviderRole, 425 capability: MycProviderCapability, 426 ) -> bool { 427 match role { 428 MycProviderRole::Transport | MycProviderRole::User => true, 429 MycProviderRole::Discovery => matches!( 430 capability, 431 MycProviderCapability::Describe 432 | MycProviderCapability::PublicIdentity 433 | MycProviderCapability::SignEvent 434 ), 435 } 436 } 437 438 #[cfg(test)] 439 mod tests { 440 use std::error::Error as _; 441 442 use nostr::{JsonUtil as _, Kind, Tag, Timestamp}; 443 444 use super::*; 445 use crate::{ 446 MycConfigProfile, MycProviderCorrelationId, MycProviderDeadlineUnixMs, 447 MycProviderNip44Version, MycProviderOperationId, MycProviderOperationInput, 448 parse_myc_config_v1, 449 }; 450 451 const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); 452 453 fn keys(seed: u8) -> Keys { 454 Keys::parse(&format!("{seed:02x}{}", "00".repeat(31))).expect("test keys") 455 } 456 457 fn secret(seed: u8) -> [u8; 32] { 458 let mut secret = [0_u8; 32]; 459 secret[0] = seed; 460 secret 461 } 462 463 fn configuration() -> MycConfigDocumentV1 { 464 let source = CONFIG 465 .replacen( 466 "4444444444444444444444444444444444444444444444444444444444444444", 467 &keys(2).public_key().to_hex(), 468 1, 469 ) 470 .replacen( 471 "3333333333333333333333333333333333333333333333333333333333333333", 472 &keys(4).public_key().to_hex(), 473 1, 474 ); 475 parse_myc_config_v1(source.as_bytes(), MycConfigProfile::RepoLocal).expect("configuration") 476 } 477 478 fn operation( 479 binding: &MycProviderBinding, 480 seed: u8, 481 input: MycProviderOperationInput, 482 ) -> MycProviderOperation { 483 MycProviderOperation::new( 484 binding, 485 MycProviderOperationId::from_bytes([seed; 32]), 486 MycProviderCorrelationId::from_bytes([seed.wrapping_add(1); 32]), 487 MycProviderDeadlineUnixMs::new(2_000_000_000_000).expect("deadline"), 488 input, 489 ) 490 .expect("operation") 491 } 492 493 #[test] 494 fn encrypted_executor_produces_independently_verified_results() { 495 let configuration = configuration(); 496 let binding = configuration 497 .provider_contract() 498 .binding(MycProviderRole::Transport) 499 .expect("transport binding"); 500 let identity = MycDecryptedIdentity::from_test_secret(secret(2)); 501 let peer = MycDecryptedIdentity::from_test_secret(secret(5)); 502 let observed = 503 MycProviderResponseObservedAtUnixMs::new(1_999_999_999_999).expect("observed time"); 504 505 for (seed, input) in [ 506 (1, MycProviderOperationInput::describe()), 507 (2, MycProviderOperationInput::public_identity()), 508 ( 509 3, 510 MycProviderOperationInput::nip04_encrypt( 511 peer.public_identity().clone(), 512 b"nip04 protected", 513 ) 514 .expect("NIP-04 input"), 515 ), 516 ( 517 4, 518 MycProviderOperationInput::nip44_encrypt( 519 peer.public_identity().clone(), 520 MycProviderNip44Version::V2, 521 b"nip44 protected", 522 ) 523 .expect("NIP-44 input"), 524 ), 525 ] { 526 let operation = operation(binding, seed, input); 527 let result = execute_encrypted(&identity, &operation).expect("encrypted result"); 528 let verified = 529 verify_encrypted_provider_response(binding, &operation, observed, result) 530 .expect("independent verification"); 531 assert!(verified.matches_operation(&operation)); 532 } 533 534 let discovery = configuration 535 .provider_contract() 536 .binding(MycProviderRole::Discovery) 537 .expect("discovery binding"); 538 let discovery_identity = MycDecryptedIdentity::from_test_secret(secret(4)); 539 let unsigned = nostr::UnsignedEvent::new( 540 discovery_identity 541 .public_identity() 542 .as_hex() 543 .parse() 544 .expect("public key"), 545 Timestamp::from_secs(1_725_000_000), 546 Kind::Custom(31_990), 547 Vec::<Tag>::new(), 548 "{}", 549 ); 550 let operation = operation( 551 discovery, 552 5, 553 MycProviderOperationInput::sign_event(unsigned.as_json().as_bytes()) 554 .expect("sign input"), 555 ); 556 let result = execute_encrypted(&discovery_identity, &operation).expect("signature"); 557 let verified = verify_encrypted_provider_response(discovery, &operation, observed, result) 558 .expect("verified signature"); 559 assert!(verified.matches_operation(&operation)); 560 } 561 562 #[test] 563 fn capability_matrix_and_diagnostics_are_closed() { 564 for capability in MycProviderCapability::ALL { 565 assert!(capability_allowed_for_role( 566 MycProviderRole::Transport, 567 capability 568 )); 569 assert!(capability_allowed_for_role( 570 MycProviderRole::User, 571 capability 572 )); 573 assert_eq!( 574 capability_allowed_for_role(MycProviderRole::Discovery, capability), 575 matches!( 576 capability, 577 MycProviderCapability::Describe 578 | MycProviderCapability::PublicIdentity 579 | MycProviderCapability::SignEvent 580 ) 581 ); 582 } 583 for kind in [ 584 MycProviderExecutionErrorKind::Binding, 585 MycProviderExecutionErrorKind::Open, 586 MycProviderExecutionErrorKind::Operation, 587 MycProviderExecutionErrorKind::Cancelled, 588 MycProviderExecutionErrorKind::Transport, 589 MycProviderExecutionErrorKind::Verification, 590 MycProviderExecutionErrorKind::UnsupportedPlatform, 591 ] { 592 let error = execution_error(kind); 593 assert_eq!(error.kind(), kind); 594 assert!(error.source().is_none()); 595 assert!(!format!("{error} {error:?}").contains("protected")); 596 } 597 } 598 599 #[test] 600 fn provider_probe_identifiers_are_domain_seed_index_and_role_bound() { 601 let seed = [7_u8; 32]; 602 let operation = probe_identifier(b"operation", &seed, 0, MycProviderRole::Transport); 603 assert_eq!( 604 operation, 605 probe_identifier(b"operation", &seed, 0, MycProviderRole::Transport) 606 ); 607 assert_ne!( 608 operation, 609 probe_identifier(b"correlation", &seed, 0, MycProviderRole::Transport) 610 ); 611 assert_ne!( 612 operation, 613 probe_identifier(b"operation", &[8_u8; 32], 0, MycProviderRole::Transport) 614 ); 615 assert_ne!( 616 operation, 617 probe_identifier(b"operation", &seed, 1, MycProviderRole::Transport) 618 ); 619 assert_ne!( 620 operation, 621 probe_identifier(b"operation", &seed, 0, MycProviderRole::User) 622 ); 623 } 624 625 #[cfg(any(target_os = "linux", target_os = "macos"))] 626 #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 627 async fn dropping_blocking_provider_work_drains_it_before_returning() { 628 use std::sync::{ 629 Arc, 630 atomic::{AtomicBool, Ordering}, 631 mpsc, 632 }; 633 634 let (entered_tx, entered_rx) = mpsc::channel(); 635 let (release_tx, release_rx) = mpsc::channel(); 636 let completed = Arc::new(AtomicBool::new(false)); 637 let worker_completed = Arc::clone(&completed); 638 let task = OwnedBlockingTask::spawn(move || { 639 entered_tx.send(()).expect("entered signal"); 640 release_rx.recv().expect("release signal"); 641 worker_completed.store(true, Ordering::SeqCst); 642 }); 643 entered_rx.recv().expect("worker entered"); 644 let releaser = std::thread::spawn(move || { 645 std::thread::sleep(core::time::Duration::from_millis(25)); 646 release_tx.send(()).expect("release worker"); 647 }); 648 649 drop(task); 650 651 releaser.join().expect("releaser joined"); 652 assert!(completed.load(Ordering::SeqCst)); 653 } 654 }