Siyao Meng created HDDS-17177:
---------------------------------

             Summary: OM renew delegation token apply can write a canceled 
token back and can lower a token's stored expiry
                 Key: HDDS-17177
                 URL: https://issues.apache.org/jira/browse/HDDS-17177
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-1-reject-renew-of-removed-token-and-keep-later-expiry.patch, 
TestBugMC1RenewAfterCancel.java, TestBugMC2RenewLowersExpiry.java

h3. Mechanism
{{OMRenewDelegationTokenRequest.preExecute}} validates the token on the leader 
({{OzoneDelegationTokenSecretManager.renewToken}}: the token is in the in 
memory token map, is not expired, and the caller is its renewer) and carries 
the computed expiry in the request. 
{{OMRenewDelegationTokenRequest.validateAndUpdateCache}} then, on every OM, 
calls {{OzoneDelegationTokenSecretManager.updateRenewToken}}, which puts the 
token into the in memory map unconditionally, then adds a {{dTokenTable}} cache 
entry with that expiry, and {{OMRenewDelegationTokenResponse}} writes the row 
to RocksDB. The apply neither checks that the token is still stored nor 
compares the new expiry with the stored one. Two consequences:
* If a cancel of the token was applied before the renew, the renew apply writes 
the token back into the map, the cache and RocksDB. 
{{OMCancelDelegationTokenRequest}} had removed it from all three and returned 
success.
* If a renew carrying an earlier expiry is applied after a renew carrying a 
later one, the stored expiry goes down to the earlier value, below what the 
first applied renew returned.

h3. Trigger
A renew whose validation on the leader passed is applied when the stored state 
is no longer the one it was validated against: the token's cancel has been 
applied since, or a renew with a later expiry has been applied since. 
Validation in preExecute and the apply are separate steps, and nothing at apply 
time checks the validated state again. The expiry in the request is computed by 
the leader, clients cannot set it.

h3. Impact
* *Canceled token written back.* {{retrievePassword}}, which the OM RPC server 
uses to authenticate token connections, accepts the canceled token again. The 
row is in RocksDB, and a secret manager started on the same DB, as after an OM 
restart, loads the token and accepts it. Every OM applies the same entries 
through the same code, so all of them hold it (argued, one OM was run). The 
token stays valid until its stored expiry and its renewer can renew it again up 
to its max date (7 days by default). The client that canceled it was told that 
the cancel succeeded. [^TestBugMC1RenewAfterCancel.java]
* *Stored expiry lowered.* Once the earlier expiry passes, {{retrievePassword}} 
rejects the token and {{renewToken}} refuses to renew it, although a later 
expiry was returned for it, and {{ExpiredTokenRemover}} deletes it on its next 
scan. This fails closed. The shortfall equals the gap between the leader's 
validations of the two renews. [^TestBugMC2RenewLowersExpiry.java]

h3. Reproduction
The two attached classes run in {{hadoop-ozone/ozone-manager}} (target path and 
Maven command are in each class comment) and pass on unmodified source while 
the bug is present. They use the real {{OzoneDelegationTokenSecretManager}}, 
the real {{validateAndUpdateCache}} of the cancel and renew requests and a real 
{{OmMetadataManagerImpl}} with RocksDB, and call them in sequence: the leader 
side validation ({{renewToken}}, {{cancelToken}}), then the applies in the 
stated sequence, each response committed to RocksDB as the double buffer does. 
No Ratis server and no second OM are involved.

h3. Patch
[^MC-1-reject-renew-of-removed-token-and-keep-later-expiry.patch], against 
b94df213f20ec0d675f079b57a4566e4e64e4060. It also applies to master at 
df42f7f60a2.
* {{OzoneDelegationTokenSecretManager}} records the identifiers removed by 
applied cancels ({{removeToken}}, whose only caller is the cancel apply) and 
drops each one once the token's max date has passed, since no renew can extend 
a token beyond it. The renew apply rejects a token found there with 
{{TOKEN_ERROR_OTHER}}, so no row is written and the map and cache are left 
alone. The check deliberately does not look at {{dTokenTable}} or the in memory 
map: {{ExpiredTokenRemover}} deletes from both outside Ratis on each OM's own 
clock (HDDS-13457), on a restarted OM even before it replays its log, so a 
check there could accept a renew on one OM and reject it on another, and lose a 
valid token on the OM that catches up.
* The renew apply stores {{min(maxDate, max(new expiry, stored expiry))}} when 
a row exists, otherwise the request's expiry as before. The cap keeps today's 
result when the stored expiry is beyond max date (renew interval longer than 
the max lifetime).

The record of applied cancels is kept in memory only. An OM that restarts, or 
installs a checkpoint, between applying a cancel and applying a later renew of 
the same token still writes that token back, as every OM does today. A stricter 
alternative is a persisted cancel tombstone in {{dTokenTable}} that is rebuilt 
into the record when tokens are loaded; it changes what a cancel writes, may 
need a layout feature, and still meets rows that {{ExpiredTokenRemover}} 
deletes outside Ratis.

Covered by four cases in the existing 
{{TestOzoneDelegationTokenSecretManager}}: 
{{testRenewAppliedAfterCancelDoesNotRestoreToken}}, 
{{testRenewAppliedWithEarlierExpiryKeepsStoredExpiry}}, 
{{testRenewAppliedAfterRemovalOutsideRatisIsApplied}} (a renew applied after 
the remover deleted the token must still be applied, as on the other OMs) and 
{{testRenewAppliedCapsExpiryAtMaxDate}}. Without the change the first two fail 
("expected: TOKEN_ERROR_OTHER but was: OK", and the earlier expiry stored 
instead of the later one). With it, 146 tests in the delegation token, request 
handler, state machine and double buffer suites pass, checkstyle is clean, and 
the two attached classes no longer show the bug. The Kerberos integration 
suites were not run with the change.

Found by TLA+ model checking and code review of the OM delegation token secret 
manager and request paths under HDDS-15926, on commit 
b94df213f20ec0d675f079b57a4566e4e64e4060. Checked against HDDS issues and 
apache/ozone pull requests for duplicates before filing. Related but not 
duplicates: HDDS-13457 (the expired token remover runs on every OM outside 
Ratis), HDDS-1973 and HDDS-1974 (moved renew and cancel to the cache and double 
buffer). The attached patch is a proposal for review. Generated with Specula 
(Claude Opus 5.5).



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to