[
https://issues.apache.org/jira/browse/HDDS-16465?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
ASF GitHub Bot updated HDDS-16465:
----------------------------------
Labels: pull-request-available (was: )
> Non atomic snapshot chain update could misdirect the adjacency reclaim filter
> -----------------------------------------------------------------------------
>
> Key: HDDS-16465
> URL: https://issues.apache.org/jira/browse/HDDS-16465
> Project: Apache Ozone
> Issue Type: Bug
> Reporter: Siyao Meng
> Assignee: Andy Huang
> Priority: Critical
> Labels: pull-request-available
> Attachments: HDDS-16465.001.patch
>
>
> h3. Finding
> If the named masks were absent, the non-atomic dual-chain update / restart
> rebuild could give a surviving snapshot a different effective previous and
> misdirect the adjacency reclaim filter, physically reclaiming a still
> chain-visible key: externally observable data loss on snapshot reads with no
> automatic recovery. Reachable in principle from concurrent purge plus restart
> during snapshot GC. Currently masked by atomic single-batch purge
> persistence, the fail-closed corruption gate on restart rebuild, and
> reclaim-time re-validation (validatePreviousSnapshotId /
> validateExistingLastNSnapshotsInChain under SNAPSHOT_GC_LOCK plus the flush
> gate).
> h3. Classification
> * Verdict: MASKED
> * Severity: Critical
> * Source: Specula TLA+ model checking and confirmation debate, finding CR-4
> h3. Reproduce
> {noformat}
> Ozone commit: 9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb
> Specula: v1.1.0 (commit c6aa3dfa)
> Target: om-snapshot-gc
> Guidance:
> campaigns/ozone-9fbf9ee/targets/008-om-snapshot-gc/.prompt-extra.md
> {noformat}
> {code:none}
> specula run --agent=claude-code --effort=high --keep-original
> --max-parallel=2 \
> --enable-reviews --confirm-debate --tlc-memory-limit=28G
> --tlc-worker-limit=8 \
> "om-snapshot-gc|apache/ozone|Java|Use the target-specific .prompt-extra.md"
> {code}
> Discovered under HDDS-15927 (Specula TLA+ verification effort). The TLA+
> specification, counterexample, and confirmation debate live in the Specula
> run artifacts.
> Generated with Specula (Claude Opus 4.8).
> h3. Steps to reproduce
> 1. SnapshotChainManager keeps two in-memory chains that must agree: the
> global chain (all snapshots in creation order) and the per-path chain
> (snapshots for a given volume/bucket). GC and reclaim navigate them via
> previous/next-snapshot lookups.
> 2. A snapshot delete calls deleteSnapshot, which ran deleteSnapshotGlobal(id)
> && deleteSnapshotPath(path, id). Because of the short-circuit &&, if the
> global delete commits (returns true) but the path delete then returns false
> or throws, the global chain is mutated while the path chain is not. If the
> snapshot is missing from the global chain, the path delete is never attempted
> at all.
> 3. Either way the two chains diverge: one chain no longer contains the
> snapshot while the other still links through it. Subsequent
> previous/next-snapshot navigation returns inconsistent results, which can
> misdirect reclaim decisions.
> 4. addSnapshot had the mirror-image gap: it committed the global insertion
> before attempting the path insertion, so a failing path insertion left the
> snapshot stranded in the global chain.
> 5. Independently, latestGlobalSnapshotId and oldestGlobalSnapshotId were
> written only under the instance monitor but read by
> getLatestGlobalSnapshotId/getOldestGlobalSnapshotId (and the chain iterator)
> without holding it, so a concurrent reader could observe a stale value.
> h3. Root cause
> The dual-chain updates in addSnapshot and deleteSnapshot were not atomic and
> had no rollback, and deleteSnapshot's short-circuit && could skip or
> half-apply an update, so a failure or absence in one chain left the two
> chains inconsistent. The two global head/tail fields were also published
> without the memory visibility guarantee their unsynchronized getters rely on.
> h3. Suggested fix
> Make both chain mutations all-or-nothing. deleteSnapshot now checks
> membership in both chains first and refuses a partial delete (throws) when a
> snapshot is present in only one of them, and rolls back the global deletion
> (restoreSnapshotGlobal) if the path deletion fails. addSnapshot rolls back
> the global insertion if the path insertion fails. latestGlobalSnapshotId and
> oldestGlobalSnapshotId are made volatile so the unsynchronized getters
> observe the last committed value. Two invariant unit tests are added: a
> failing path insertion leaves both chains empty (add rollback), and a delete
> whose path does not match is refused and leaves the global chain unchanged.
> See [^HDDS-16465.001.patch].
> Note: the underlying trigger is a concurrency window (a failing or asymmetric
> chain update, or a reader racing a writer) that is timing dependent and not
> deterministically reproducible without a thread-scheduling seam that shipping
> code does not expose. The added tests pin the atomicity and partial-update
> invariants directly rather than reproducing a specific interleaving; the
> memory-visibility change is asserted by inspection.
> Patch is an AI-drafted proposal (Specula + Claude); pending human build, full
> test, and review before merge.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]