You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@ozone.apache.org by "Siyao Meng (Jira)" <ji...@apache.org> on 2026/10/21 07:22:00 UTC
[jira] [Created] (HDDS-17216) S3 secret cache cleanup can remove the entry of an unflushed transaction, so OMs can commit different S3SecretTable contents
Siyao Meng created HDDS-17216:
---------------------------------
Summary: S3 secret cache cleanup can remove the entry of an unflushed transaction, so OMs can commit different S3SecretTable contents
Key: HDDS-17216
URL: https://issues.apache.org/jira/browse/HDDS-17216
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
h3. Mechanism
Each write to an S3 secret puts an entry carrying its transaction index into {{S3InMemoryCache}}. After the double buffer has committed a batch, the flush thread calls {{S3InMemoryCache.clearCache}} with the batch's indexes, which removes the entries carrying those indexes, while the apply thread keeps applying later transactions. A read that misses the cache loads the committed row and caches it with index 0. The cache is consistent as long as every applied but unflushed write keeps its entry until its own index is flushed. Three places do not ensure that:
# {{S3RevokeSecretRequest}} replaces the entry with {{S3SecretValue.deleted()}}, which keeps the index of the entry it replaces instead of the revoke's index. When the revoke is applied after the batch holding the previous write to that access ID was committed and before that batch's cleanup, the cleanup removes the deleted entry although the revoke is not flushed.
# {{OMTenantRevokeUserAccessIdRequest}} calls {{invalidateCacheEntry}}, which only replaces an entry that is already cached ({{computeIfPresent}}), and calls it without the access ID's {{S3_SECRET_LOCK}}. If the access ID has no cache entry, the tenant revoke leaves none.
# {{clearCache}} maps each cached index to its access ID in a first pass and removes each mapped access ID in a second pass, without checking the entry's index again. {{S3SecretLockedManager.clearS3Cache}} holds the {{S3_SECRET_LOCK}} stripe of the key "cache", not of the access IDs, so a write to the access ID can be applied between the two passes, and its entry is removed.
In each case a read that misses the cache before the newer transaction is flushed caches the older row with index 0, which the cleanup of the newer transaction does not match. After that flush, the OM's cache entry for the access ID no longer matches its committed row.
Which OMs are affected depends on when each OM's double buffer flush runs relative to its apply thread. That timing is local to each OM and is not part of the Ratis log.
h3. Impact
* *OMs can commit different S3SecretTable contents.* Two OMs apply the same log create@1, revoke@2, create@3 for one access ID. On an OM that flushes all three in one batch, create@3 ({{GetS3Secret}}) returns OK and commits the new secret. On an OM that applies revoke@2 between the commit and the cleanup of batch [1], create@3 reads the older row, returns {{S3_SECRET_ALREADY_EXISTS}} and writes nothing, so after both OMs have flushed index 3 one holds the new secret and the other holds no row for the access ID. Nothing compares or repairs the tables of OMs at the same applied index, so the difference survives restarts. A later {{GetS3Secret}} or {{SetS3Secret}} for that access ID can likewise get a different result on an OM whose cache entry no longer matches its committed row than on the others.
* *Cache entry and committed row disagree.* After path 3 the DB holds the newer write, but the OM's cache entry for the access ID does not match its committed row until the OM restarts or the access ID is written again.
Affected versions: the deleted entries and the index based cleanup (paths 1 and 3) exist since HDDS-9444, path 2 since the tenant revoke request. The missing cleanup registration described under Patch is on master only, since HDDS-16643.
h3. Reproduction
There is no standalone reproduction attached. The regression tests are in the patch, in the existing {{TestS3GetSecretRequest}}: real {{OmMetadataManagerImpl}} (RocksDB), {{S3SecretLockedManager}} over {{S3SecretManagerImpl}} with the in-memory cache, requests applied through {{preExecute}} and {{validateAndUpdateCache}}, and each flush done in two steps by the test (commit the responses with {{checkAndUpdateDB}}, then {{clearS3Cache}} for the responses that registered the S3 secret table), so the interleavings above become a plain sequence of calls. All four fail without the change:
* {{testRevokeKeptInCacheWhenEarlierIndexIsCleanedUp}} (path 1): commit [1], apply revoke@2, clean up [1]; expects the deleted entry with index 2 to stay until the revoke is flushed.
* {{testCreateAfterRevokeWhenEarlierIndexIsCleanedUp}} (path 1, the diverging OM): the same, then {{GetS3Secret}}@3; fails with "expected: OK but was: S3_SECRET_ALREADY_EXISTS".
* {{testTenantRevokeOfUncachedSecretIsKeptInCache}} (path 2): the secret is committed and not cached, tenant revoke@1 is applied; expects a deleted entry until the revoke is flushed.
* {{testCacheCleanupKeepsEntryReplacedDuringCleanup}} (path 3): {{SetS3Secret}}@2 is applied at the moment {{clearCache([1])}} reads the index of the create's entry (a Mockito spy on that cache value); expects the set's entry to stay.
The two OM case and paths 1 and 3 were also run through the flush method of a real {{OzoneManagerDoubleBuffer}} (the one its daemon calls) on two OM stacks in one JVM, with the results described under Impact. These runs show that the interleavings exist, not how often they occur.
h3. Patch
[^MC-2-keep-s3-secret-cache-entries-until-own-index-flushed.patch], against df04bc0e309f625a7dcdcd60790a42f34e2743d9. It also applies to master at 2cb1b6dd4e9.
The patch keeps every cache entry until its own transaction is flushed:
* {{S3SecretCache.invalidate(id, transactionLogIndex)}} (new, with a {{S3SecretManager.invalidateCacheEntry}} counterpart) always puts a deleted entry carrying the revoking transaction's index, whether or not the access ID is cached. {{S3RevokeSecretRequest}} passes {{context.getIndex()}}. {{OMTenantRevokeUserAccessIdRequest}} passes its {{transactionLogIndex}} and now calls it under the access ID's lock through {{doUnderLock}}, as {{OMTenantAssignUserAccessIdRequest}} already does. The index is the Ratis index every OM applies the entry with, so no request or proto change is needed.
* {{S3InMemoryCache.clearCache}} removes an entry only if it still carries the flushed index ({{computeIfPresent}}).
* The new {{invalidateCacheEntry}} records the S3 secret table with {{TableCacheUpdateTracker}}, as {{updateCache}} does, so the double buffer runs the cleanup for the revoke's index. Since HDDS-16643 a revoke whose secret was already cached did not register that table.
For stores with batch support (the default RocksDB store), every unflushed write is then represented by an entry that no earlier cleanup removes, and reads and writes of an access ID are serialized by its lock, so a cache miss means the committed row is current and an entry loaded with index 0 holds it. On stores without batch support the store is written at flush time; that is addressed by HDDS-17214, which also edits {{S3RevokeSecretRequest}}, {{OMTenantRevokeUserAccessIdRequest}} and {{TestS3GetSecretRequest}}, so the two patches need a rebase whichever lands second. The existing one argument {{invalidate}} is kept unchanged for its remaining caller, the flush time {{S3SecretManagerImpl.revokeSecret}} of those stores.
The lock in {{OMTenantRevokeUserAccessIdRequest}} is argued from the code (a read that missed the cache could otherwise overwrite the new deleted entry with the committed row) and is not covered by a failing test.
The patch includes the {{S3SecretManagerImpl.getSecretString}} change proposed in HDDS-17200 so that it is complete on its own; drop that hunk if HDDS-17200 is committed first.
Without the races the behaviour is unchanged, except that a deleted entry is now removed when the revoke itself is flushed instead of when the replaced entry's index is flushed.
With the patch the four tests pass, the ozone-manager unit suites for S3 requests and responses, the double buffer, the delegation token secret manager and multi tenancy pass (315 tests), {{TestMultiTenantVolume}}, {{TestSecureOzoneCluster.testGetSetRevokeS3Secret}} and {{TestSecureOzoneRpcClient.testS3Auth}} pass on a mini cluster (7 tests), and checkstyle is clean.
Found by TLA+ model checking and code review of the OM S3 secret cache and external S3 secret store paths under HDDS-15926, on commit df04bc0e309f625a7dcdcd60790a42f34e2743d9. Checked against HDDS issues and apache/ozone pull requests for duplicates before filing. Related but not duplicates: HDDS-17200 (deleted cache entries in {{getSecretString}}), HDDS-9444 (introduced the index based cleanup and the deleted entries), HDDS-16643 (removed the static cleanup table declarations) and HDDS-5358 (an earlier invalidation fix). 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: issues-unsubscribe@ozone.apache.org
For additional commands, e-mail: issues-help@ozone.apache.org