[
https://issues.apache.org/jira/browse/HDDS-16956?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Siyao Meng updated HDDS-16956:
------------------------------
Attachment: TestBugMC2SecretKeyFileNotCrashSafe.java
> SCM secret key file is rewritten in place without sync: a crash can leave it
> empty or cut, and a new leader then drops the keys or cannot initialize
> ----------------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: HDDS-16956
> URL: https://issues.apache.org/jira/browse/HDDS-16956
> Project: Apache Ozone
> Issue Type: Bug
> Reporter: Siyao Meng
> Priority: Major
> Attachments: MC-2-write-secret-key-file-atomically-and-durably.patch,
> TestBugMC2SecretKeyFileNotCrashSafe.java
>
>
> h3. Mechanism
> {{LocalSecretKeyStore.save}} truncates the secret key file and rewrites it in
> place, with one write per key and no sync. SCM keeps its symmetric secret
> keys only in memory and in this file, while the applied index is persisted
> separately in scm.db. A crash can therefore leave the file empty or cut while
> scm.db already records an index at or past the key update.
> After a restart Ratis replays only the entries above the index in scm.db.
> {{LocalSecretKeyStore.load}} has a single caller,
> {{SecretKeyManager.checkAndInitialize}}, which only runs on a leader. So the
> restarted SCM is caught up but has no keys in memory until the next key
> update is applied to it ({{hdds.secret.key.rotate.duration}}, default 1d). If
> it becomes leader before that, {{checkAndInitialize}} trusts the file:
> * *Empty file.* {{load}} returns an empty list, as for a missing file. The
> leader generates one new key and replicates it through
> {{SecretKeyState.updateKeys}} as the complete key set. Every SCM replaces its
> keys with it and rewrites its own file, so the committed, unexpired keys are
> gone from the whole cluster.
> * *Cut file.* {{load}} throws. Every initialization attempt fails,
> {{SecretKeyManagerService}} logs "Error occurred when updating SecretKeys."
> at each rotation check, and the leader has no current key for as long as it
> leads.
> h3. Trigger
> The key file must end up empty or cut on an SCM whose scm.db is at or past
> the key update, and that SCM must become leader (election or {{ozone admin
> scm transfer}}) before the next key update reaches it. Two ways to get such a
> file:
> # *Power loss or kernel crash after the unsynced save.* Shown with a device
> image probe (see Reproduction), and only for the first save of the key file
> on XFS: the image held the index and a 0 byte key file at some point within
> about a minute after the save. It was not seen for later saves (rotations) or
> on ext4, and no image held a cut file. By the code, the first save on an SCM
> is its first applied key update or its first snapshot install (new cluster,
> newly added SCM).
> # *Process kill or write error inside the save, on a path that is not
> replayed.* A SIGKILL during {{save}} leaves empty and cut files (attached
> test). On the apply path this heals, because the entry is applied again after
> the restart. It does not heal in a snapshot install, where
> {{SCMStateMachine.reinitialize}} replaces scm.db before it saves the keys
> (HDDS-16923 describes that window with a missing or stale file). The kill
> inside a snapshot install was not run here.
> h3. Impact
> Run on a secure mini cluster with 3 SCMs, 1 OM and 1 datanode, default
> rotation and expiry:
> * *Empty file.* After the leadership transfer all three SCMs hold a single
> new key and {{SecretKeyProtocol.getSecretKey}} returns null for the committed
> key, which would not expire for 9 more days. OM still signs with it, and the
> datanode side check ({{BlockTokenVerifier}}) rejects a block token that OM
> issued after the event: "Can't find the signing secret key". How long that
> lasts was not measured (same code paths as in HDDS-16923).
> * *Cut file.* The new leader logs one ERROR per rotation check and
> {{SCMSecurityProtocolServer.getCurrentSecretKey}} throws "Secret key
> initialization is not finished yet." (called on the server object, not over
> RPC). The other SCMs still hold the key in memory, and after leadership was
> transferred to one of them the secret key RPC returned it again.
> Both fail closed: no token is accepted that should be rejected.
> h3. Reproduction
> [^TestBugMC2SecretKeyFileNotCrashSafe.java] (path and Maven command in the
> class comment) passes on unmodified source, which means the defect is
> present. Its first test kills a child JVM that runs {{save}} in a loop with
> 10 keys: of 40 kills, 8 left an empty file and 4 a cut one (this shows that
> both states exist, not how often they occur). The two cluster tests replace
> the key file of a stopped follower by an empty or a cut one, restart it,
> assert that it catches up with no keys in memory (this part is not injected),
> transfer leadership to it and check the outcomes above.
> The power loss route was checked with a device image probe (not attached) in
> a Linux VM: kernel 7.0.14, XFS and ext4 on a loop device, default mount
> options, {{dirty_writeback_centisecs}} 1500 (the kernel default is 500, so
> the window is longer here). It runs {{save}}, then an unsynced RocksDB batch
> that stands for the applied index, and copies the device image while the
> writer is alive. On XFS, for the first save, the image held the index and a 0
> byte key file from right after a WAL sync (as in a DB checkpoint) to the last
> image at 46 seconds, and without a sync (WAL dirty for 25 seconds before the
> save) at about 51 and 65 seconds. Each scenario ran once and no image was fed
> to a cluster.
> h3. Patch
> [^MC-2-write-secret-key-file-atomically-and-durably.patch], against
> 025266b52a6dcaf39508607cad736799c40907e8. It also applies to master at
> a65a32e7c4b.
> {{LocalSecretKeyStore.save}} now writes the keys to a new temporary file next
> to the key file, syncs it, renames it over the key file and syncs the
> directory, using the Ratis {{AtomicFileOutputStream}} as {{YamlUtils.dump}}
> does. A failed write aborts the stream, so an incomplete file is never
> renamed. The temporary file is created owner only, as in
> {{KeyStorage.storeKey}}, and a leftover one is deleted, not reused. The
> content is byte for byte what the old code wrote, so the format is unchanged
> for existing key files and for a downgrade. The file is replaced on each
> save: a symlink at the key path would become a regular file, and hard links,
> ACLs or a non default owner are not carried over.
> The cost is two syncs per key update on the Ratis apply thread, inside the
> key state write lock (once per scheduled or forced rotation and per snapshot
> install). A save that fails, now including a failed sync of the file, stops
> the SCM as a failed write did before, and leaves the previous file in place.
> {{TestLocalKeyStore}} gets three cases that fail without the change: the file
> is replaced and not rewritten in place, a failed write keeps the previous
> keys, and a leftover temporary file is not reused. With it the symmetric key
> unit suites, {{TestSecretKeySnapshot}} and {{TestSecretKeysApi}} pass and
> checkstyle is clean. On the test machine {{TestSecretKeysApi}} often failed
> in cluster startup with the race in HDDS-12174, with and without the patch.
> With the patch no kill in the reproduction leaves a damaged file.
> The patch only stops new damage. What a leader does with a missing, empty or
> unreadable key file is unchanged (the two cluster tests still pass with it):
> a file that is already empty or cut from an earlier crash is not repaired, a
> file lost or damaged by other means still leads to the outcomes above, and
> the snapshot install ordering of HDDS-16923 still leaves a stale or missing
> file, with the first outcome above. Whether a leader without keys should ask
> its peers, as a snapshot install does with {{getSecretKeysFromLeader}}, or
> load its file at startup so that a problem shows before it leads, is a larger
> change and is left open here. This patch and the one attached to HDDS-16923
> touch different files and apply together.
> Found by TLA+ model checking and code review of the SCM secret key rotation
> and distribution paths under HDDS-15926, on commit
> 025266b52a6dcaf39508607cad736799c40907e8. Checked against HDDS issues and
> apache/ozone pull requests for duplicates before filing. Related but not a
> duplicate: HDDS-16923 (open) covers the snapshot install ordering and names
> the in place, unsynced save as the gap its patch leaves open. 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]