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() .contacts()
.is_blocked(&remote_endpoint_id) .is_blocked(&remote_endpoint_id)
.await .await
.unwrap_or(false) .unwrap_or(true)
{ {
// Indistinguishable from other refusals so probing cannot detect blocks. // Indistinguishable from other refusals so probing cannot detect blocks.
return self return self

View File

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

View File

@@ -86,7 +86,7 @@ impl CoreInner {
.contacts() .contacts()
.is_blocked(&peer_endpoint_id) .is_blocked(&peer_endpoint_id)
.await .await
.unwrap_or(false) .unwrap_or(true)
{ {
return Ok(false); return Ok(false);
} }
@@ -103,7 +103,7 @@ impl CoreInner {
.device_relationships .device_relationships
.forget(peer_endpoint_id.clone()) .forget(peer_endpoint_id.clone())
.await?; .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. // Invitation-domain shares are deliberately not cancelled here.
self.cancel_targeted_transfers_for_peer(&peer_endpoint_id) self.cancel_targeted_transfers_for_peer(&peer_endpoint_id)
.await?; .await?;

View File

@@ -62,6 +62,17 @@ impl CoreInner {
.push(peer_endpoint_id.to_string()); .push(peer_endpoint_id.to_string());
} }
self.targeted_offers.discard_from(peer_endpoint_id).await; 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 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) { pub(crate) async fn discard_from(&self, endpoint_id: &str) {
let ids: Vec<String> = { let ids: Vec<String> = {
let pending = self.pending.lock().await; let pending = self.pending.lock().await;

View File

@@ -159,10 +159,6 @@ impl TargetedTransferStore {
rows.into_iter().map(row_to_transfer).collect() 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> { pub(crate) async fn cancel_by_peer(&self, peer_endpoint_id: &str) -> Result<u64, VnidropError> {
let now = now_ms(); let now = now_ms();
let result = sqlx::query( let result = sqlx::query(
@@ -180,6 +176,26 @@ impl TargetedTransferStore {
.map_err(VnidropError::repository)?; .map_err(VnidropError::repository)?;
Ok(result.rows_affected()) 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)] #[derive(Debug, Clone)]