Skip to content

serviceability: closed access passes and bans leave accounts that can never be cleaned up #4191

Description

@elitegreg

Parent: #4193

Two independent defects found while tracing the recycled-IP scenario. Both create states that cannot be cleaned up by any existing instruction. Neither depends on the rest of the parent issue, and both should be fixed regardless of which direction the reclaim work takes.

1. A closed access pass makes its user account permanently undeletable

process_delete_user hard-requires the access pass account to exist:

// .../processors/user/delete.rs:108-111
if accesspass_account.data_is_empty() {
    return Err(DoubleZeroError::AccessPassNotFound.into());
}

The body below it already handles the missing-pass case correctly, guarded by if !accesspass_account.data_is_empty(). The early return makes that branch unreachable. The SDK mirrors the requirement and fails with "You have no Access Pass" before it can even build the instruction (smartcontract/sdk/rs/src/commands/user/delete.rs:60-67).

So if a pass is ever closed while its user account still exists, that account can never be deleted. RequestBan does not close the account either, it only marks it Banned — which still occupies the PDA and still blocks every future connect at that IP. Banning a stale account does not free the address.

CloseAccessPass refuses when connection_count != 0, which usually prevents this ordering, but that counter drifts (see below) and uses saturating_sub, so it is a reachable state rather than a theoretical one.

Fix: drop the early return and let the existing optional branch handle it, relaxing the PDA assertion accordingly. Consider whether RequestBan should close the account rather than leaving a permanent squatter on the PDA.

2. RequestBan never decrements the access pass, so the pass can never be closed

process_request_ban_user does not take the access pass account at all (.../processors/user/requestban.rs:57-80). It deallocates resources and moves the user to Banned, but accesspass.connection_count keeps counting a connection that no longer exists.

process_close_access_pass refuses to close a pass with a nonzero count:

// .../processors/accesspass/close.rs
if accesspass.connection_count != 0 {
    msg!("AccessPass has {} active connections, cannot close", accesspass.connection_count);
    return Err(DoubleZeroError::AccessPassInUse.into());
}

Every ban therefore leaves an access pass that can never be closed, and inflates the count that seat accounting and the Connected / Disconnected status both read.

Fix: have RequestBan take the access pass account and run the same release path DeleteUser does — decrement connection_count, remove_user, release_feed_seats, recompute status. Decide separately whether existing drifted passes need a one-off repair instruction or an admin path to force a close.

Testing verification

  • Delete a user whose access pass has been closed; it succeeds and settles device counters.
  • Ban a user, then close its access pass; the close succeeds and seat counts are correct.
  • Ban then delete still settles counters exactly once, with no double decrement.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions