ZOOKEEPER-4689: Fix ACL cache recovery from fuzzy snapshots#2425
Open
adamyi wants to merge 1 commit into
Open
Conversation
ZooKeeper serializes the ACL cache before the data tree while taking a fuzzy snapshot. A node can therefore be serialized with an ACL id which is absent from the serialized cache. Transaction replay repairs these references using the ACL value carried by the transaction (ZOOKEEPER-3306 and ZOOKEEPER-4846). Three issues remained: * On a fresh restore, aclIndex accounted only for ids in the serialized cache. Replay could reissue an id referenced by a snapshot node to a different ACL. A delete of a node still carrying that stale id could then decrement and remove an unrelated live ACL entry. Advance aclIndex past every id referenced by the loaded tree, including ids absent from the cache. * The existing-node create path incremented the new ACL's reference count without releasing the node's previous reference. Transfer the reference under the node lock so each node contributes exactly one count. * ZOOKEEPER-4799 made createNode, deleteNode, and setData resolve ACLs before triggering watches. During restore the watch tables are normally empty, but a dangling ACL still threw before the watch manager was reached and aborted replay. Use a non-throwing lookup only for watch filtering. Represent an unknown ACL with a no-permissions ACL so any unexpected delivery fails closed; null and empty ACLs are treated as unrestricted by checkACL. Client ACL lookups remain strict. The change requires no snapshot format change and does not require ACL ids to remain equal across servers or restarts.
Contributor
Author
|
(From https://ci-hadoop.apache.org/job/zookeeper-precommit-github-pr/job/PR-2425/1/consoleFull , looks like the build actually succeeded but there's some problem with the hadoop16 node that marked it failing?) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #1997.
Background
ZooKeeper snapshots are fuzzy: the ACL cache is serialized first, and the data tree is serialized afterward while transactions continue to modify both. A snapshot can therefore contain a node whose ACL id is absent from the serialized ACL cache:
This is recoverable because the transaction which introduced the ACL is newer than the snapshot zxid and is replayed after the snapshot is loaded.
ZOOKEEPER-3306 made create replay intern the transaction's ACL even when the node already existed in the snapshot. ZOOKEEPER-4846 completed the basic repair by moving that existing node to the locally interned id. Using the ACL value carried by the transaction is important: numeric ACL ids are local cache implementation details and need not remain equal across servers or restarts.
Three issues remain on current master. They are fixed together deliberately: reserving snapshot-referenced ids (fix 1) keeps those ids unresolved until their repairing txns replay, which is exactly the state the watch-time ACL lookup (fix 3) must tolerate. None of this changes the snapshot format, and ACL ids are never required to match across servers.
1. An id referenced by the snapshot can be reissued to another ACL
On a fresh restore,
ReferenceCountedACLCache.deserializeadvancesaclIndexonly from ids present in the serialized cache. It does not account for ids which appear only in the nodes serialized later.For example, suppose the serialized cache ends at id 2, while fuzzy snapshot nodes refer to ids 4 and 5. Replay can allocate id 4 to a different ACL before all snapshot nodes have been repaired. A replayed delete of a node still carrying stale id 4 then decrements the new entry's reference count. That entry can be garbage-collected while an unrelated live node still points to it, leaving that node with a dangling ACL after its repairing transaction has already replayed.
This patch advances
aclIndexfor every id referenced by a deserialized node, including ids absent from the cache. Such ids are never reissued; replay moves the nodes to locally interned ids derived from the ACL values in their transactions.The invariant is local—this does not try to keep ACL ids equal across ensemble members:
2. Create replay leaves reference counts inflated
In the ZOOKEEPER-4846 path,
convertAclsincrements the new entry's reference count before the existing node is updated, but the node's previous reference is not released. This leaks a reference even when the old and new ids are equal, and prevents unused ACL entries from being collected until the next reload rebuilds the counts.The existing-node path now transfers the reference under the node lock:
When both ids are equal, the increment and decrement balance. When they differ, the node contributes exactly one reference to its new entry.
3. ACL lookup for watch filtering can abort replay
ZOOKEEPER-4799 added eager ACL lookup in
createNode,deleteNode, andsetDataso watch events can be filtered by READ permission. During normal restore and learner synchronization, transactions are replayed before the server starts serving clients, so the watch tables are normally empty. Nevertheless, these methods resolve the ACL before calling the watch manager.One example is a node deleted and re-created in the fuzzy range. The snapshot can contain the new incarnation with an ACL absent from the serialized cache, while replay begins with
setDataordeletetransactions for the old incarnation. The eager lookup throwsFailed to fetch acls for ...and aborts recovery before the empty watch manager is consulted; the later create transaction never gets a chance to repair the reference.This patch uses a non-throwing lookup only for ACLs passed to watch delivery. A missing entry is represented by a no-permissions ACL:
checkACLtreats both as unrestricted and that would reintroduce CVE-2024-23944 / ZOOKEEPER-4799 for these nodes;getDataandgetACLretain their strict lookup behavior.On the valid recovery path this does not suppress any client notification: local restore and learner synchronization replay transactions before the server starts serving clients, so the no-permissions ACL is passed to an empty watch manager. The fallback is observable only if a dangling reference has already survived into live state—for example, in a snapshot written by an older version after the repairing transaction fell outside the replay range. That member no longer has enough information to determine who can read the node, so failing closed is the only safe delivery policy.
Tests
Failing on current master, fixed by this patch:
FileTxnSnapLogTest.testAclIdsReferencedBySnapshotAreNotReissuedDuringReplay— fix 1; fails withFailed to fetch acls for ...when resolving a node whose own txns replayed normally, i.e. the id-reuse corruption.DataTreeTest.testCreateTxnReplayOnExistingNodeKeepsAclRefCountExact— fix 2; fails with a leaked cache entry after the only node using the ACL is deleted.FileTxnSnapLogTest.testDeleteTxnReplayOnDanglingAclDoesNotCrash,testSetDataTxnReplayOnDanglingAclDoesNotCrash,testCreateTxnReplayUnderDanglingAclParentDoesNotCrash— fix 3; fail withFailed to fetch acls for ...fromdeleteNode/setData/createNode(the last is the same line as the stack trace on the ZOOKEEPER-4846 JIRA).Security behavior of fix 3:
FileTxnSnapLogTest.testWatchEventsForDanglingAclFailClosed— current master aborts at the strict ACL lookup. With the non-throwing lookup in place, the test additionally asserts that the ACL passed to watch delivery grants no READ; it fails against a variant which passes null, sincecheckACLtreats null as unrestricted.Guards against the known-bad #1997 approach (pass on current master and with this patch):
FileTxnSnapLogTest.testAclRefCountDuringFuzzySnapshotSync— catches under-counting when multiple snapshot nodes share the same missing ACL.FileTxnSnapLogTest.testAclValuesMatchAfterFuzzySnapshotSyncReplay— catches repair keyed by the stale snapshot id instead of the ACL value carried by the transaction.The fuzzy-snapshot tests share small helpers (
FuzzySnapshot,ReplayTxn), and the pre-existingtestACLCreatedDuringFuzzySnapshotSyncis rewritten on top of them with its assertions preserved, except one correction: it used to resolve aDataNodetaken from the leader tree against the restored tree's ACL cache. ACL ids are server-local, so it now resolves the restored tree's own node.Testing performed:
We have carried the
aclIndexreservation part of this fix in Jane Street's internal ZooKeeper 3.5-based fork since 2023.