fix: Reject credentials on pseudo-accounts and unpin Vault/LoanBroker (FN-36) - #7877
fix: Reject credentials on pseudo-accounts and unpin Vault/LoanBroker (FN-36)#7877tyalymov wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
| // owning object) can be deleted. Other objects are left untouched: the | ||
| // caller's own checks decide whether the remaining directory blocks | ||
| // deletion. | ||
| return cleanupOnAccountDelete( |
There was a problem hiding this comment.
DoS risk: unbounded deletion with no maxNodesToDelete limit. Unlike deleteAMMTrustLines (512 cap) and AccountDelete (1000 cap), an attacker can pin credentials to a pseudo-account and force one transaction to walk and delete an arbitrarily large directory. Add maxNodesToDelete parameter (e.g., 512), propagate tecINCOMPLETE to callers, and allow retryable cleanup instead of one-pass enumeration.
Example pattern to follow:
- VaultDelete, LoanBrokerDelete, deleteAMMAccount should treat tecINCOMPLETE as retryable, allowing multiple transactions to make cleanup progress
- Mirror the implementation in deleteAMMTrustLines (512 limit, returns tecINCOMPLETE on partial progress)
There was a problem hiding this comment.
Good catch, clanker!
@tyalymov once maxNodesToDelete is added, let's add a test analogous to the existing kMaxDeletableAmmTrustLines overflow tests (AMM_test.cpp:5214, :7192) that pins more than the cap's worth of credentials to a pseudo-account pre-amendment and verifies tecINCOMPLETE + eventual multi-transaction cleanup.
- Codecov flags one uncovered line each in AMMHelpers.cpp, LoanBrokerDelete.cpp, and VaultDelete.cpp — all the !isTesSuccess(ter) early-return branches after calling deletePseudoAccountCredentials. Once the function can return tecINCOMPLETE (per the Must Fix above), these branches become reachable and should be covered by the new test.
There was a problem hiding this comment.
Thanks for catching this. I started implementing the bound and hit a snag I'd like to consult you on.
The plan was to delete up to N credentials per transaction and return tecINCOMPLETE so a follow-up transaction resumes the cleanup. That works for LoanBroker and AMM. For Vault it collides with ValidMPTIssuances: VaultDelete carries the DestroyMptIssuance privilege, and the invariant requires any applied VaultDelete to remove exactly one MPT issuance in the same transaction. An incremental VaultDelete that clears a batch of credentials and returns tecINCOMPLETE hasn't removed the share issuance yet, so it trips tecINVARIANT_FAILED.
Two directions I came up with:
- Relax the invariant to require the issuance removal only on a
tesSUCCESSVaultDelete, not on an in-progresstecINCOMPLETEone. It's a small change and a no-op before the amendment (VaultDeletecan't returntecINCOMPLETEwithout it), but it does touch a consensus invariant. - Refuse the delete when the pseudo-account holds more than the cap, like
AccountDelete'stefTOO_BIG, with no partial progress. Simpler, but a Vault pinned with more than the cap could then never be deleted, which doesn't fully cure the pre-amendment case.
LoanBroker and AMM I can bound incrementally regardless; it's the Vault path I'm unsure about. Any thoughts on that?
There was a problem hiding this comment.
The first direction sounds right, in the transactor we first attempt to delete the credentials. If we fail to do so because there are too many of them, then the user must submit the transaction again to delete the rest of the credentials and the account.
| return tesSUCCESS; | ||
| } | ||
|
|
||
| TER |
There was a problem hiding this comment.
Unbounded credential cleanup allows DoS: attacker can pre-load credentials before amendment activates, forcing unbounded work in one transaction. Add maxNodesToDelete bound and handle tecINCOMPLETE result like deleteAMMTrustLines:
| TER | |
| // Define a bounding constant | |
| static constexpr std::size_t kMaxDeletablePseudoAccountCredentials = 100; | |
| // Modify function to accept and pass maxNodesToDelete parameter, | |
| // returning tecINCOMPLETE if unable to complete in one transaction | |
| TER | |
| deletePseudoAccountCredentials( | |
| ApplyView& view, | |
| AccountID const& pseudoAcct, | |
| std::size_t maxNodesToDelete, | |
| beast::Journal j); |
Tapanito
left a comment
There was a problem hiding this comment.
Nicely done, some minor comments.
| TER | ||
| deletePseudoAccountCredentials(ApplyView& view, AccountID const& pseudoAcct, beast::Journal j) | ||
| { | ||
| // Pseudo-accounts can't sign, so any credential linked into their owner |
There was a problem hiding this comment.
While the method name suggests thta credentials are deleted for pseudoAccounts, in reality, any accountID can be passed.
It might be a good idea to check that the account passed is a pseudo-account.
| // Remove every credential linked into a pseudo-account's owner directory. | ||
| // Pseudo-accounts can't sign, so any such credential names the pseudo-account | ||
| // as subject and can never be accepted or deleted by it. Non-credential objects | ||
| // are left in place; the caller decides whether they block deletion. |
There was a problem hiding this comment.
nit: Please use doxygen style comment.
| // Remove every credential linked into a pseudo-account's owner directory. | ||
| // Pseudo-accounts can't sign, so any such credential names the pseudo-account | ||
| // as subject and can never be accepted or deleted by it. Non-credential objects | ||
| // are left in place; the caller decides whether they block deletion. |
There was a problem hiding this comment.
Since after this PR CredentialCreate would not allow creating credentials for a pseudo-account, this function, at least for now, will only be useful, until the amendment that introduced it, is retired. It would be great to document why this method is needed (in case there are any pseudo-accounts already locked up).
| // Add new amendments to the top of this list. | ||
| // Keep it sorted in reverse chronological order. | ||
|
|
||
| XRPL_FIX (CredentialPseudoAccount, Supported::Yes, VoteBehavior::DefaultNo) |
There was a problem hiding this comment.
Each amendment requires to be voted in. Since Cleanup3_4_0 is not yet released, let's add the fix under it, rather than a new amendment.
| // owning object) can be deleted. Other objects are left untouched: the | ||
| // caller's own checks decide whether the remaining directory blocks | ||
| // deletion. | ||
| return cleanupOnAccountDelete( |
There was a problem hiding this comment.
Good catch, clanker!
@tyalymov once maxNodesToDelete is added, let's add a test analogous to the existing kMaxDeletableAmmTrustLines overflow tests (AMM_test.cpp:5214, :7192) that pins more than the cap's worth of credentials to a pseudo-account pre-amendment and verifies tecINCOMPLETE + eventual multi-transaction cleanup.
- Codecov flags one uncovered line each in AMMHelpers.cpp, LoanBrokerDelete.cpp, and VaultDelete.cpp — all the !isTesSuccess(ter) early-return branches after calling deletePseudoAccountCredentials. Once the function can return tecINCOMPLETE (per the Must Fix above), these branches become reachable and should be covered by the new test.
9bb06fa to
ae41661
Compare
… (FN-36) CredentialCreate accepted any existing Subject, including Vault, LoanBroker, and AMM pseudo-accounts. A pseudo-account can't sign, so it can never accept or delete a credential issued to it; the unaccepted credential stays pinned in the pseudo-account's owner directory and blocks deletion of the owning object, locking it and its owner reserve indefinitely. Behind fixCleanup3_4_0: - CredentialCreate::preclaim rejects a pseudo-account Subject with tecPSEUDO_ACCOUNT. - VaultDelete and LoanBrokerDelete remove any credentials pinned to the pseudo-account before deleting it. - isOnlyLiquidityProvider ignores credential entries and deleteAMMAccount removes them, so an AMM pinned before the amendment can still be withdrawn and deleted. This clears objects pinned before the amendment and prevents new pins after it.
ae41661 to
02de7a5
Compare
High Level Overview of Change
CredentialCreatelet any existing account be theSubject, including Vault, LoanBroker, and AMM pseudo-accounts. A pseudo-account can't sign, so it can never accept or delete a credential issued to it; the credential stays pinned in the pseudo-account's owner directory and blocks the deletion of the owning object, pinning it and its owner reserve indefinitely. This rejects such credentials and clears any that already exist, behind a new amendment.Ref: FN-36.
Context of Change
CredentialCreate::preclaimonly checked that theSubjectaccount existed. When the subject differs from the issuer,doApplylinks the unaccepted credential into the subject's owner directory. For a pseudo-account subject that link is permanent: pseudo-accounts can't submitCredentialAcceptorCredentialDelete, and the object owner is neither issuer nor subject, so it can't delete a non-expired credential either.The stuck entry then blocks cleanup of the owning object:
VaultDeleteandLoanBrokerDeleterequire the pseudo-account's owner directory to be empty and fail withtecHAS_OBLIGATIONS.isOnlyLiquidityProvider, which walks the AMM account's owner directory and returnstecINTERNALon the unexpected credential entry, so the AMM can't be fully withdrawn or deleted.Behind the new
fixCredentialPseudoAccountamendment:CredentialCreate::preclaimrejects a pseudo-accountSubject(Vault / LoanBroker / AMM) withtecPSEUDO_ACCOUNT.VaultDeleteandLoanBrokerDeletedelete any credentials pinned to the pseudo-account before removing it. A non-credential object still yieldstecHAS_OBLIGATIONS.isOnlyLiquidityProviderskips credential entries so an AMM with a pre-amendment pin can still be withdrawn, anddeleteAMMAccountremoves the pinned credentials before deleting the trust lines.The cleanup is a shared helper
credentials::deletePseudoAccountCredentials, reusing the existingcleanupOnAccountDeletewalker andcredentials::deleteSLE. It only removes credentials and leaves any other object in place, so unpinned deletions are unaffected. Everything is gated on the amendment, so pins created before it activates are cured and no new ones can be created after.API Impact
libxrplchange (any change that may affectlibxrplor dependents oflibxrpl)New transaction outcomes for
CredentialCreate,VaultDelete,LoanBrokerDelete, and AMM withdrawal/deletion, all gated behind the newfixCredentialPseudoAccountamendment.Test Plan
Added
testCredentialPinsPseudoAccounttoVault_test,LoanBroker_test, andAMM_test. With the amendment off, each pins a credential to the pseudo-account and confirms the object can't be deleted (tecHAS_OBLIGATIONSfor Vault/LoanBroker,tecINTERNALfor AMM). It then enables the amendment and confirms a new pin is rejected withtecPSEUDO_ACCOUNTand the object deletes cleanly, with the credential removed and the issuer's owner count restored. Full unit-test and ctest suites are green.