fix: retain Session recovery failures across real trust rejection

This commit is contained in:
2026-11-04 20:39:16 +09:00
parent 9fadd1bcaf
commit 6c67390567
4 changed files with 45 additions and 12 deletions
Generated
+1
View File
@@ -7580,6 +7580,7 @@ dependencies = [
"content-history", "content-history",
"content-history-adapters", "content-history-adapters",
"content-history-store", "content-history-store",
"error-contract",
"feature-storage", "feature-storage",
"flate2", "flate2",
"flow", "flow",
@@ -116,7 +116,7 @@ impl ProfileRuntimeWorkerFactory {
.await; .await;
let mut worker = worker_result.map_err(|error| { let mut worker = worker_result.map_err(|error| {
self.retain_construction_failure(request, &error); self.retain_construction_failure(request, &error);
cause(restore_worker_cause(&error)) RuntimeRestoreFailure::from_worker(RuntimeRestoreStage::Recovery, error)
})?; })?;
let tasks = worker.retain_startup_feature_cleanup(); let tasks = worker.retain_startup_feature_cleanup();
let sessions = worker.workdir_sessions(); let sessions = worker.workdir_sessions();
@@ -141,11 +141,13 @@ impl ProfileRuntimeWorkerFactory {
.map_err(|_| cause("recovery Backend Job binding failed"))?; .map_err(|_| cause("recovery Backend Job binding failed"))?;
} }
worker worker
.finalize_subjektiv_session_attribution( .finalize_subjektiv_session_attribution_retaining_startup_failure(
request.request.subjektiv_attached, request.request.subjektiv_attached,
SubjektivSessionAttributionLifecycle::RestoredSession, SubjektivSessionAttributionLifecycle::RestoredSession,
) )
.map_err(|error| cause(restore_worker_cause(&error)))?; .map_err(|error| {
RuntimeRestoreFailure::from_worker(RuntimeRestoreStage::Recovery, error)
})?;
if pending { if pending {
worker::WorkerController::recover_pending_head(&mut worker, &root) worker::WorkerController::recover_pending_head(&mut worker, &root)
.await .await
@@ -161,21 +163,38 @@ impl ProfileRuntimeWorkerFactory {
tasks tasks
.shutdown() .shutdown()
.await .await
.map_err(|_| cause("recovery Feature settlement failed"))?; .map_err(|_| RuntimeRestoreCleanupFailure::Feature)?;
sessions sessions
.close_all() .close_all()
.await .await
.map_err(|_| cause("recovery Workdir close failed"))?; .map_err(|_| RuntimeRestoreCleanupFailure::Workdir)?;
if let Some(allocation) = allocation { if let Some(allocation) = allocation {
allocation allocation
.release() .release()
.map_err(|_| cause("recovery allocation release failed"))?; .map_err(|_| RuntimeRestoreCleanupFailure::Allocation)?;
} }
Ok::<_, RuntimeRestoreFailure>(()) Ok::<_, RuntimeRestoreCleanupFailure>(())
} }
.await; .await;
result?; match (result, cleanup) {
cleanup?; (Err(mut primary), cleanup) => {
primary.cleanup = match cleanup {
Ok(()) => RuntimeRestoreCleanup::Confirmed,
Err(failure) => {
primary.cleanup_failure = Some(failure);
RuntimeRestoreCleanup::Failed(failure)
}
};
return Err(primary);
}
(Ok(()), Err(cleanup)) => {
let mut failure = cause("restore recovery cleanup failed");
failure.cleanup_failure = Some(cleanup);
failure.cleanup = RuntimeRestoreCleanup::Failed(cleanup);
return Err(failure);
}
(Ok(()), Ok(())) => {}
}
self.failed_restore_resources self.failed_restore_resources
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) .unwrap_or_else(std::sync::PoisonError::into_inner)
+1
View File
@@ -66,6 +66,7 @@ webauthn-rs = { workspace = true, features = ["conditional-ui"] }
webauthn-rs-proto = "0.5.5" webauthn-rs-proto = "0.5.5"
[dev-dependencies] [dev-dependencies]
error-contract.workspace = true
content-history.workspace = true content-history.workspace = true
content-history-store.workspace = true content-history-store.workspace = true
content-history-adapters.workspace = true content-history-adapters.workspace = true
@@ -2365,9 +2365,21 @@ async fn connected_explicit_repair(scenario: ConnectedRepair) {
assert_eq!(probe.activations.load(SeqCst), 0); assert_eq!(probe.activations.load(SeqCst), 0);
assert_eq!(probe.publications.load(SeqCst), 0); assert_eq!(probe.publications.load(SeqCst), 0);
assert_eq!(fixture.model.calls.load(SeqCst), model_before); assert_eq!(fixture.model.calls.load(SeqCst), model_before);
assert!( let saved_failure = metadata.read_by_name(&name).unwrap().unwrap();
matches!(metadata.read_by_name(&name).unwrap().unwrap().subjektiv_session_attribution, // Trust rejection occurs before the Session domain handler. An unsigned
Some(session_store::SubjektivSessionAttributionState::Failed { session_id: id, .. }) if id == session_id) // legacy 401 attests neither a Session refusal nor absence of earlier effects.
assert!(matches!(saved_failure.subjektiv_session_attribution,
Some(session_store::SubjektivSessionAttributionState::OutcomeUnknown { session_id: id, .. }) if id == session_id));
let session_store::worker_metadata::StartupFailureRead::Present(
error_contract::DecodeFailure::Known(cause),
) = saved_failure
.decode_startup_failure::<protocol::session_failure::SessionRegistrationCause>()
else {
panic!("recovery must retain its typed Session unknown cause")
};
assert_eq!(
*cause.facts().cause(),
protocol::session_failure::SessionRegistrationCause::outcome_unknown()
); );
let denied = record(); let denied = record();
assert_eq!( assert_eq!(