fix(core): tear down shares on forget/block and fail-closed blocks

Cancel targeted protocol shares synchronously before await, and treat block
lookup errors as denied so store failures cannot admit blocked peers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-11 03:35:18 +02:00
parent 22c842acb1
commit e2cfad7fb3
6 changed files with 36 additions and 12 deletions

View File

@@ -183,7 +183,7 @@ impl ApprovalService {
.contacts()
.is_blocked(&remote_endpoint_id)
.await
.unwrap_or(false)
.unwrap_or(true)
{
// Indistinguishable from other refusals so probing cannot detect blocks.
return self

View File

@@ -129,10 +129,11 @@ impl DeviceRelationshipService {
}
pub(super) async fn is_blocked(&self, endpoint_id: &str) -> bool {
// Fail closed: a store error must not admit blocked traffic.
self.contacts()
.is_blocked(endpoint_id)
.await
.unwrap_or(false)
.unwrap_or(true)
}
/// Drop orphaned relationship grant secrets and disable rows whose secrets are gone.

View File

@@ -86,7 +86,7 @@ impl CoreInner {
.contacts()
.is_blocked(&peer_endpoint_id)
.await
.unwrap_or(false)
.unwrap_or(true)
{
return Ok(false);
}
@@ -103,7 +103,7 @@ impl CoreInner {
.device_relationships
.forget(peer_endpoint_id.clone())
.await?;
// Targeted transfers for this relationship only (ticket 10 fills in).
// Targeted transfers for this relationship only.
// Invitation-domain shares are deliberately not cancelled here.
self.cancel_targeted_transfers_for_peer(&peer_endpoint_id)
.await?;

View File

@@ -62,6 +62,17 @@ impl CoreInner {
.push(peer_endpoint_id.to_string());
}
self.targeted_offers.discard_from(peer_endpoint_id).await;
let protocol_ids = self
.targeted_store()
.protocol_ids_for_peer(peer_endpoint_id)
.await?;
// Signal active transfers synchronously before awaiting share teardown.
for protocol_transfer_id in &protocol_ids {
let _ = self.take_active_transfer(*protocol_transfer_id);
}
for protocol_transfer_id in &protocol_ids {
let _ = self.cancel_idle_or_share(*protocol_transfer_id).await;
}
self.targeted_store().cancel_by_peer(peer_endpoint_id).await
}

View File

@@ -187,10 +187,6 @@ impl TargetedOfferInbox {
}
}
#[allow(
dead_code,
reason = "called via cancel_targeted_transfers_for_peer for ticket 09"
)]
pub(crate) async fn discard_from(&self, endpoint_id: &str) {
let ids: Vec<String> = {
let pending = self.pending.lock().await;

View File

@@ -159,10 +159,6 @@ impl TargetedTransferStore {
rows.into_iter().map(row_to_transfer).collect()
}
#[allow(
dead_code,
reason = "called via cancel_targeted_transfers_for_peer for ticket 09"
)]
pub(crate) async fn cancel_by_peer(&self, peer_endpoint_id: &str) -> Result<u64, VnidropError> {
let now = now_ms();
let result = sqlx::query(
@@ -180,6 +176,26 @@ impl TargetedTransferStore {
.map_err(VnidropError::repository)?;
Ok(result.rows_affected())
}
pub(crate) async fn protocol_ids_for_peer(
&self,
peer_endpoint_id: &str,
) -> Result<Vec<u64>, VnidropError> {
let rows = sqlx::query(
r#"
SELECT protocol_transfer_id FROM targeted_transfers
WHERE sender_endpoint_id = ?1 OR receiver_endpoint_id = ?1
"#,
)
.bind(peer_endpoint_id)
.fetch_all(&self.pool)
.await
.map_err(VnidropError::repository)?;
Ok(rows
.into_iter()
.map(|row| row.get::<i64, _>(0) as u64)
.collect())
}
}
#[derive(Debug, Clone)]