Repository navigation
CAS — consolidated static analysis audit findings (tracking) #2031
Description
CAS (metadata_type = content_addressed MergeTree backend) — consolidated audit tracking
This is a tracking issue for a static-analysis audit of the Content-Addressed Storage (CAS) MergeTree
disk backend.
2026-08-31 update. All 39 audits plus a distributed-systems audit and a usability audit were
redone from scratch against PR #2159 head after Filimonov's 2026-08-21 triage and the post-triage
fix series that landed before merge. CAS-### IDs are unchanged from the 2026-08-12 catalogue
that Filimonov triaged — they are not renumbered. Items that the code no longer has, or that
Filimonov already closed as by-design / not-a-bug / duplicate, are checked off below and dropped
from the open work list.
Important
Static/logical review only. Filimonov's 2026-08-21 per-id residuals are treated as the
authoritative narrowing: a real code shape with an invented consequence is not re-raised.
Locate by symbol; 2026-08-12 CA/... line numbers are stale.
📎 Audit reports
- Original (2026-07-09): https://gist.github.com/vzakaznikov/8b0506a495187ce3d634385544beebea
- Re-run vs PR [WIP] CAS draft (adopting to CI/CD, not for review / merge) #2073 (2026-07-30): https://gist.github.com/alsugiliazova/7fb1441688ff428cc0e0a18918077c26
- Re-run vs PR [experimental] CAS (Content-addressed storage) over shared object storage for antalya-26.6 #2159 (2026-08-12, code-only): https://gist.github.com/alsugiliazova/6dce01834f93cdb7cdbb2fc70d1efc5f
- Re-run vs PR [experimental] CAS (Content-addressed storage) over shared object storage for antalya-26.6 #2159 head (2026-08-31): https://gist.github.com/alsugiliazova/f1b74c378277ef4b6842178d89333d2d — all 39 per-audit reports +
distributed-systems.md+usability.md+RECONCILIATION-2031.md. - Code pin:
ceee42c51a06cb05e2c9a2d811ef7e1726825552(cas: admit worker renewals only over an Active keeper). Merge commit onantalya-26.6:a49d9ed. No further CAS product commits onantalya-26.6after the merge. - PR: #2159 (merged 2026-08-26).
Finding the full detail for a CAS-###
The checklist line is a one-line residual. For the full write-up, open the 2026-08-31 gist and
search for the id or the audit name (e.g. CAS-007 → interleaving.md / security.md /
distributed-systems.md; CAS-020 → read-protocol.md / idisk-contract.md).
RECONCILIATION-2031.md maps buckets. Historical write-ups stay in the 2026-08-12 gist.
Filimonov's per-id reasoning is in this comment (2026-08-21).
How to triage
- Check the box once it is triaged (resolved, dismissed, or filed as its own issue).
- Replace
resolution:inline with a verdict. - Add reasoning as a comment referencing the
CAS-###id.
carried from prev CAS-### provenance is unreliable (Filimonov). Do not use it.
Closed on #2159 after the 2026-08-12 catalogue (do not re-open)
These were the four P1s plus the write-protocol rewrite. Verified gone at ceee42c.
- CAS-001 Shadow/
FREEZEnamespace is nowserverPrefix()-scoped ·DATA-LOSS— resolution: ✅ fixed (335802a, closes #2212) - CAS-040 Manifest entry path is escaped in both the record line and the banner; orphan sweep skips an undecodable manifest instead of aborting the pool ·
INTEGRITY— resolution: ✅ fixed (2649bce) - CAS-058
freezeRemoteclones into a content-addressed disk in one transaction ·FEATURE-GAP— resolution: ✅ fixed (84b30f6, closes #2173) - CAS-106 CAS settings live under the
cas_config-key prefix; thenon_cas_keysskip-list is gone ·CONFIG— resolution: ✅ fixed (917600b, closes #2243) - CAS-010 Empty conditional token /
putOverwriteon blob create — blob publish is no longer conditional · — resolution: ✅ obsolete (940b168) - CAS-031 Multipart
If-None-Matchon blob CREATE —putIfAbsentStream/promoteStaged/conditionalCreateControlled/copyObjectConditionalare gone · — resolution: ✅ obsolete (940b168) - CAS-088 Unconditional
resurrectreturning a token it did not write —resurrectis gone;publishBlobis tokenless by design · — resolution: ✅ obsolete (940b168; Filimonov already had this as partly / comments-only) - CAS-103 Dedup counters incremented before admit — counters now fire only on a successful adopt · — resolution: ✅ fixed (
940b168) - CAS-070
remount_runninglatch / lost wakeup — closed by the mount-lease rewrite (7f932d3,ceee42c, closes #2244) - CAS-093 Text-index temp ref leak — already ✅ fixed in the 2026-08-21 triage
Also landed in the same series (not originally P1, but they change the model): per-row catalog
proof for the writer path (83c03e2 — GC ref-cleanup still uses whole-catalog stillness,
CAS-079), GCS generation isolated to CAS requests (b69051a), detached-work drain (205af29).
Still open — Filimonov-accepted residuals that HEAD still has
Priority is Filimonov's 2026-08-21 P2/P3, not the original audit High. Headlines are his residual,
re-verified at ceee42c.
P2
- CAS-007 Nested
server_root_id(a/b) passesvalidateServerRootIdwhile decommission ownership issridorsrid + "/"prefix, soSYSTEM CAS DROP POOL MEMBERonadeletes live membera/b's namespaces and objects ·DATA-LOSS— resolution: 🔴 still-present (Filimonov confirmed, P2) ·CasServerRoot.hvalidateServerRootId;CasDecommission.cppvictim_srid + "/" - CAS-020
getStorageObjectsstill returnsStoredObject(location.key, path, location.length)and drops the envelope offset. Same-description server-side copy (CAS S3 → another S3 disk on the same endpoint, including same-host BACKUP-to-S3) copies envelope bytes as file content. Cross-type MOVE to local usesreadFile/prepareReadand is safe. Inline files still fail loud (empty key) ·INTEGRITY— resolution: 🔴 still-present (Filimonov confirmed, P2) ·ContentAddressedMetadataStorage.cppgetStorageObjects - CAS-004 residual: server-side
SYSTEM CAS GC REBUILDis safe (holds the live GC lease, condemns nothing). Offlineclickhouse-disks cas-gc-rebuildstill has no mount-lease interlock against a live writer ·INTEGRITY— resolution: 🔴 still-present (Filimonov partly, P2) - CAS-014 Placement classifier is still a closed suffix allowlist and still misses
primary.cidx,.mrk4/.cmrk4, and secondary-index files. Not corruption (1 MiB cap + blob spill); cost is whole-file RAM buffer + a double write ·CORRECTNESS— resolution: 🔴 still-present (Filimonov partly, P2) - CAS-015 / CAS-049 Single-flight / recovery / SQL
SYSTEM CAS FSCK/GC RUNhave no query cancellation. Each inner I/O is still budgeted; what remains isKILL QUERY/max_execution_timedo not terminate the verb, and SQL FSCK holdslifecycle_mutexfor the whole scan ·LIVENESS— resolution: 🔴 still-present (Filimonov partly, P2) - CAS-022 Orphan-manifest sweep still nominates a manifest whose namespace has no catalog row (body is written before the catalog row). Window is narrow; outcome is loud, not silent data loss ·
DATA-LOSS— resolution: 🔴 still-present (Filimonov partly, P2) - CAS-032 Pool identity is still not bound to endpoint/bucket. Harm still requires writing into a CRR destination / bidirectional replication over the prefix ·
INTEGRITY— resolution: 🔴 still-present (Filimonov partly, P2) · docs residual - CAS-034 Per-round ref-cleanup / janitor budgets can sit below the creation rate. Consequence is deferred reclamation, not data loss ·
LEAK— resolution: 🔴 still-present (Filimonov partly, P2) - CAS-035 A GC round still fully lists
cas/ns/stream/and retains every key in memory (also on rounds that then defer). Defaultgc_shards=1·PERF/SCALE— resolution: 🔴 still-present (Filimonov confirmed, P2) - CAS-044 Aggregate 16 MiB inline-data limit per manifest is still checked only in
stageManifestwith no reclassification to a blob. INSERT/merge fails loud and reproducibly ·FEATURE-GAP— resolution: 🔴 still-present (Filimonov confirmed, P2) - CAS-045 Part-folder cache entry weight is still always 256 bytes (
Resolved::manifest_sizehardcoded 0).part_folder_cache_bytesand the oversized-bypass threshold are inoperative ·PERF/SCALE— resolution: 🔴 still-present (Filimonov confirmed, P2) - CAS-046 Local scratch is still the part's full bytes, unreserved, unaccounted, deleted only at transaction end, no startup cleanup. Failures are loud (
ENOSPC) ·PERF/SCALE— resolution: 🔴 still-present (Filimonov confirmed, P2) - CAS-050
CasGcScheduler::stopstill joins thread objects outside the mutex that guards them (reachable viaDROP POOL MEMBERconcurrent with GC STOP/shutdown) ·CONCURRENCY— resolution: 🔴 still-present (Filimonov partly, P2) - CAS-055 Carry-forward
createHardLinkstill ForceFresh-resolves the source per file. Defaultpart_folder_validate=always⇒ one manifest HEAD per file. Fix is still transaction-level memoization as inunlinkFile·PERF/SCALE— resolution: 🔴 still-present (Filimonov confirmed, P2) - CAS-061 Only
gc/statehas a rebuild path; catalog /_ckpt/_pool_metafail closed. All CA tools still open via_pool_meta·OBSERV/DAY2— resolution: 🔴 still-present (Filimonov partly, P2) - CAS-065 Native
If-None-Matchruns in CAS-over-S3 CI; still missing a required GCS-generation lane and a Native row in the backend contract suite ·TEST-GAP— resolution: 🔴 still-present (Filimonov partly, P2) - CAS-074 Generation prune still advances
snap_pruned_throughpast a referenced generation; the compensating hand-off is one-shot and lost undersuppress_destructive.runFsckstill never listsgc/, so the promised fsck backstop does not exist ·LEAK— resolution: 🔴 still-present (Filimonov partly, P2) - CAS-079 GC
cleanupRefObjectsstill requires the pool-global catalog token to be unchanged for the whole remaining pass and bails out of the phase on any CREATE/DROP.83c03e2fixed this class only on the ref-writer path ·LEAK— resolution: 🔴 still-present (Filimonov confirmed, P2) ·CasGc.cppcleanupRefObjects - CAS-081 S3 staging objects are still retained on abort (deliberate resurrect/promote source). Residuals: no periodic cleanup during a mount, no introspection.
DROP POOL MEMBERstill drains a dead member ·LEAK— resolution: 🔴 still-present (Filimonov partly, P2) - CAS-085
always_use_copy_instead_of_hardlinks=1still makes mutations and same-disk clones throwNOT_IMPLEMENTED; the setting is never rejected. Fail-closed, no corruption ·FEATURE-GAP— resolution: 🔴 still-present (Filimonov partly, P2) - CAS-092 residual:
CLOCK_BOOTTIMEstill has no non-Linux fallback (Darwin). The "different clocks for fence and request" half stays refuted ·CORRECTNESS— resolution: 🔴 still-present (Filimonov partly, P2) - CAS-098
last_success_age_seconds=0still means both "never led" and "succeeded just now";is_leader=0does not separate a stopped GC from a follower ·OBSERV/DAY2— resolution: 🔴 still-present (Filimonov partly, P2) - CAS-107
ContentAddressedMetadataStoragestill does not overrideapplyNewSettings.SYSTEM RELOAD CONFIGapplies nocas_*setting and does not log that fact. A disk removed from config keeps renewing its mount lease until restart ·CONFIG— resolution: 🔴 still-present (Filimonov confirmed, P2) - CAS-111 64 MiB encoded snapshot / removal cap still hard-limits a table at ~0.6–0.9 M refs. Loud fail-closed before any object is created ·
PERF/SCALE— resolution: 🔴 still-present (Filimonov confirmed, P2) - CAS-112 Every committed positive ref chunk still does a fresh uncached GET + full decode + linear scan of the pool-global
cas/ref_catalog·PERF/SCALE— resolution: 🔴 still-present (Filimonov partly, P2) - CAS-114 Recovery still closes every skipped writer epoch with its own durable pair. First touch of a long-idle table is O(mount count) sequential writes with no cap ·
PERF/SCALE— resolution: 🔴 still-present (Filimonov confirmed, P2)
P3 residuals still confirmed (keep only the residual, not the original headline)
- CAS-005 residual: no multi-ref atomicity; no rollback of a committed-ref repoint;
dropRefIfMatchesis still best-effortnoexcept. Ordinary INSERT uses unique part names and does not take the repoint branch ·DATA-LOSS— resolution: 🔴 still-present (Filimonov partly, P3) - CAS-006 residual: cross-namespace
RENAME/moveDirectoryis still a journal-less per-ref walk. Nothing is physically deleted; only non-Atomic (deprecated Ordinary) databases reach a true table rename ·DATA-LOSS— resolution: 🔴 still-present (Filimonov partly, P3) - CAS-009 residual: upload still never re-hashes local scratch; a length-preserving scratch divergence can publish under the wrong digest. Presence-only admit of an already-stored body stays 📐 by-design (CAS-008) ·
INTEGRITY— resolution: 🔴 still-present (Filimonov partly, P2/P3) - CAS-017 residual:
dropNamespacestill latches admission closed before the durable work; a failed recovery empty-catch can leave the latch closed until remount. Not a permanently-broken table on every transient error ·LIVENESS— resolution: 🔴 still-present (Filimonov partly, P3) - CAS-029 / CAS-030 Versioning is still queried only on the GCS generation dialect; ETag mounts rely on the delete-marker probe.
skip_access_checkstill skips the probe on ETag writable mounts ·CONFIG— resolution: 🔴 still-present (Filimonov partly, P3) - CAS-037 residual:
location.offset + location.lengthcan still wrap and collapse the read window to EOF; GC generation listing still usesstd::stoull(-1→max_gen + 1 == 0). Central "wrapping defeats every decoder gate" stays untenable ·DECODE/DoS— resolution: 🔴 still-present (Filimonov partly, P3) - CAS-048 Empty covering-part publish still runs under
DataPartsLock(DROP/REPLACE PARTITION). Same class as an ordinary object-storage disk writing a part under that lock ·LIVENESS— resolution: 🔴 still-present (Filimonov partly, P3) - CAS-086 residual: a repeated
FREEZE WITH NAMEsilently merges into the existing shadow ref instead ofDIRECTORY_ALREADY_EXISTS·CORRECTNESS— resolution: 🔴 still-present (Filimonov partly, P3) · this is also the "audit-missed" item Filimonov named on 2026-08-21 - CAS-090 residual: SSE-C +
staging_backend=s3still uses server-side copy without copy-source customer-key headers. Fail-closed mount probe falls back to local staging; combination is optional ·SECURITY— resolution: 🔴 still-present (Filimonov partly, P3) - CAS-094 A refused / CAS-losing rebuild still leaves run objects + a fold seal. Next-round-adopts-it and leaks-forever stay false ·
INTEGRITY— resolution: 🔴 still-present (Filimonov partly, P3) - CAS-095
cas-gc-dryrunis still silentlypreview_deletes=0on a missinggc/state·OBSERV/DAY2— resolution: 🔴 still-present (Filimonov partly, P3) - CAS-097
cas-inspectstill cannot decode pool meta / catalog / owner / epoch / GC outcomes ·OBSERV/DAY2— resolution: 🔴 still-present (Filimonov partly, P3) - CAS-124 Empty content under cityhash128 still aliases fsck's unparsable-key sentinel. Confined to one fsck classification line ·
INTEGRITY— resolution: 🔴 still-present (Filimonov partly, P3) - CAS-128 Staged-source inline
createHardLink/moveFileinto a dest with no build still fails closed at commit (LOGICAL_ERROR).writeFileitself now callsbuildFor·CORRECTNESS— resolution: 🔴 still-present (Filimonov partly, P3)
New this round (not in the 2026-08-12 catalogue)
- CAS-136
SYSTEM CAS GC RUNwith no disk name fans out to both the raw CAS disk and itsDiskObjectStorageCachewrapper and runs two sequential rounds on the same metadata storage ·OBSERV/DAY2— resolution: 🆕 new ·InterpreterSystemQuery.cpp+DiskObjectStorageCache.cpp - CAS-137 Production UniqueKey (
MergeTreeBitmapStore→DeleteBitmapFileOps::writeBitmapToStorage) andSSTIndexWriterstill dowriteFile(.tmp)+replaceFileon the part storage. On CAS thatreplaceFileopens a new transaction and throwsLOGICAL_ERROR("moveFile source not staged"). Filimonov marked CAS-057 🚫 not-a-bug on the claim thatwriteBitmapToStoragehas no production caller — that caller exists (MergeTreeBitmapStore.cpp). UniqueKey-on-CAS is still fail-loud, not silent corruption ·FEATURE-GAP— resolution: 🆕 / re-open residual of CAS-057
Adjudicated — no action (Filimonov 2026-08-21, still true on HEAD)
Do not re-raise these as defects. Docs gaps (lifecycle / trust boundary / crypto-shred) stay documentation, not code work, unless called out above.
- CAS-002
adoptEvidence/ §4 manifest-trust · 📐 by-design - CAS-008 Default cityhash128, no re-hash on read · 📐 by-design
- CAS-012 Lifecycle / Object Lock / Glacier fail-open · 📐 by-design + docs (Glacier still has no restore-and-retry; keep as docs under CAS-029/032 if wanted)
- CAS-013 Algorithm admission raises
min_reader_generation· 📐 latent; format floor already equalsG_BUILD(now 10) - CAS-024 Two CAS disks, one pool, one
server_root_id· 🚫 not-a-bug (second mountABORTED) - CAS-025 REBUILD resets the condemn universe · 📐 by-design (R4 retention)
- CAS-026 Relink identity is not just
pool_uuid· 📐 by-design - CAS-027 Bucket credential is the trust boundary · 📐 by-design
- CAS-028 Unsalted content-hash keys · 📐 by-design (CAS dedup)
- CAS-033 Pool-wide
suppress_destructive· 📐 by-design (fail-closed) - CAS-042 / CAS-122 One global generation; producerless
!key · 📐 by-design / duplicate (recreate-only policy; pool format is now generation 10) - CAS-047 Process-wide 16-thread upload pool, blocking enqueue · 📐 by-design (backpressure)
- CAS-052
shared_from_thison pool · 🚫 not-a-bug - CAS-057
moveFile/replaceFileLOGICAL_ERROR on unstaged source · 🚫 not-a-bug (fail-loud stub) — see CAS-137 if UniqueKey-on-CAS is in scope - CAS-059 / CAS-060 Encrypted disk over CAS · 📐 out-of-scope; fails loud at first blob write. Docs list it (
8779adc, CAS: type=encrypted wrapping a CAS disk — CREATE succeeds, INSERT fails with Autocommit writes are not supported #2213). Missing fail-fast gate is the only residual - CAS-064 No decoder fuzzer · 📐 by-design
- CAS-066 Emulated mode chosen by storage type · 📐 by-design
- CAS-068
putIfAbsentControlledswallows local failures as ambiguity · 📐 by-design - CAS-071 Mount-keeper races · 📐 by-design
- CAS-073 Condemn marker not incarnation-scoped · 📐 by-design
- CAS-076 Seal-before-
gc/state· 🚫 not-a-bug (prefix prune collects unaccepted seals) - CAS-080 Snapshot publish only on write · 🚫 not-a-bug (also on read)
- CAS-083 LWD does not free surviving-entry blob bytes · 📐 by-design
- CAS-087
detached/movingparser order · 📐 by-design (Ordinary only) - CAS-089 Envelope offset from pool meta · 📐 by-design
- CAS-110
allow_stalediscarded · 📐 by-design (resolve is always fresh) - CAS-125
Xxh3Streamernull deref · 🚫 not-a-bug - CAS-126 Fence pre-check only on S3 staging · 📐 by-design
- CAS-129 Epoch check only at
promoteentry · 🚫 not-a-bug - CAS-132 Path disclosure to unprivileged SQL · 🚫 not-a-bug (
GLOBALprivileges)
P3 "partly" items whose only leftover is cosmetic (dead comments, always-zero ProfileEvents,
debug/sanitizer asserts, emulated-mode mutex, readLine allocation, cache-hit log volume,
oc optional default, write-side line-cap on gc/state) are dropped from this body.
They remain in the 2026-08-12 gist if anyone wants them. Re-raise only if the residual
grows a production consequence.
Where to start (2026-08-31)
The four P1s are done. Next, in this order:
- CAS-007 — reject nested / prefix-overlapping
server_root_id(operator-reachable data loss onDROP POOL MEMBER). - CAS-020 — return a payload window from
getStorageObjects, or refuse server-side copy on a CA source (silent wrong bytes on same-endpoint S3 copy / same-host BACKUP-to-S3). - CAS-079 — revalidate the target catalog row in GC ref-cleanup, not the pool-global token (same class already removed from the writer).
- CAS-049 — give SQL FSCK a deadline and honor
KILL QUERY. - CAS-107 —
applyNewSettingsor a loud "restart required"; stop the keeper when the disk disappears from config. - CAS-086 — repeated
FREEZE WITH NAMEshould not silently merge.
Operator guardrails until those land: single-segment unique cas_server_root_id; never DROP POOL MEMBER on a prefix of a live id; do not BACKUP/MOVE CAS data onto another S3 disk with the same data-source description; use clickhouse-disks cas-fsck --timeout on large pools; restart after cas_* XML changes; do not wrap CAS in DiskEncrypted.
k-morozov (2026-08-18) on CAS-002 / CAS-003 / CAS-005 / CAS-006 is consistent with Filimonov's later verdicts (manifest-trust is architectural; GC dual-leader is extra work not split-brain; multi-ref / rename are known non-atomic). Those four stay in the residual list above, not as High.
Coverage of this re-run
| Batch | Result |
|---|---|
| 39 named audits | re-run from scratch at ceee42c; reports in the 2026-08-31 gist |
| Distributed-systems | 1 High (CAS-007), 6 Medium, 3 Low |
| Usability (operator) | overall ready with caveats (guardrails required) |
| Tier-1 happy path (INSERT / SELECT / merge / relink / GC / FSCK) | no confirmed blockers |
Happy path is clear. Remaining work is admin-verb safety (decommission prefix, FSCK cancel, reload),
one silent copy-path integrity bug (CAS-020), and the scale/ops residuals Filimonov already accepted.
Activity
[-]CAS (content_addressed MergeTree) — consolidated audit findings (tracking)[/-][+]CAS (content addressed storage for MergeTree) — consolidated audit findings (tracking)[/+][-]CAS (content addressed storage for MergeTree) — consolidated audit findings (tracking)[/-][+]CAS — consolidated audit findings (tracking)[/+][-]CAS — consolidated audit findings (tracking)[/-][+]CAS — consolidated static analysis audit findings (tracking)[/+]alsugiliazova commentedon Aug 13, 2026
CAS-021 split out: #2207
Ambiguous conditional-write outcomes are reported as definite ownership (content equality treated as authorship; lost ACK reported as foreign occupancy).
k-morozov commentedon Aug 18, 2026
CAS-002
The problem is architectural: adoptEvidence saves dependencies for promote to check before we've reached a durable precommit state. Throughout the whole transaction we don't have the same checks that observeAndAdmit has - it doesn't read the condemn marker, and it doesn't require our own edge to already be durable before trusting an existing blob.
Because of this, GC can fully complete its condemn - graduate - delete cycle before our reference even becomes visible to the protocol - and that decision is irreversible, it never rolls back. As a result, the new part gets published pointing at a blob that's already been deleted, and there's nothing to recover it from.
CAS-003
I agree that there is a scenario where a second GC can take the lease from the first one, while the first one keeps working - including deleting. It's important to say that true atomic deletes with a leadership check can't be built here, and the only real question is how to shrink the gap between checking leadership and deleting. The lease mechanism itself exists to avoid duplicate work, and if we trust that the algorithm proving something is safe to delete is correct, then the two-leader problem is just double work - not a critical issue. It can be handled by checking gc/state before deletes (for example, in phase 11) and by discussing whether the heartbeat should stay one-directional like it is now.
I wouldn't call the missing TTL a real problem - the lease mechanism already relies on updates to
gc/stateandgc/hb, and in my view that's enough.CAS-005
If we want to do 2 deletes inside one cas-transaction and we stop on the second one, with the current design we can't roll back the changes from the first one - because the ownership record in the ref-log for the old manifest has already been rewritten, and there's no operation to undo that. The blobs themselves aren't deleted yet at that point — they just become candidates for GC to delete later, asynchronously, as a separate process. So the user can end up seeing a partial state: the first part is already applied, the second one isn't, even though the whole transaction reports failure.
CAS-006
Known limitation. Seems very close to CAS-005.
filimonov commentedon Aug 21, 2026
All 135 findings were triaged one by one against current head, each verified in the code rather than against the report's own reasoning. Priorities were re-assigned from scratch. Verdict distribution:
CAS-###Priorities: 4 × P1, 38 × P2, 83 × P3, 10 need no action.
The four P1 items
FREEZEnamespace is pool-global, soUNFREEZEon one server deletes another server's frozen parts\n) leaves an undecodable orphan manifest, and every GC round in the pool then fails forever — reproduced livefreezeRemoteis the only one of the three clone paths without a CAS transaction, so cross-diskATTACH PARTITION FROMcannot worknon_cas_keysskip-set rejects legal S3 disk keys, so the server dies at startup withUNKNOWN_SETTINGThree of the four came in independently (two as their own issues, one from a field report). CAS-040 is the one the audit surfaced first — and its stated consequence was wrong in an instructive way: the INSERT fails closed, no data is lost and no unreadable part is committed, but the failed attempt wedges reclamation for the whole pool. Worth fixing exactly because the real effect is worse than the reported one in operational terms.
Two notes for whoever reads the findings next
The
carried from prev CAS-###provenance lines are unreliable. In every case that was checked closely, the claimed lineage did not match the content of either finding. Treat them as noise.The
CA/...anchors are stale — the code moved into subdirectories (Backend/,Pool/,Gc/,Parts/,Formats/,Primitives/,Tools/). Locate by symbol, not by path.Beyond the verdicts
The triage also produced work that was not in the audit:
UniversePolicy::kDefaulthad been flipped toAuthoritative, which a recorded hard constraint forbade until incarnation-keyed cursors landed. They landed two days before the flip, so the sequencing held; the constraint is now marked satisfied with the verification in place.FREEZE WITH NAMEsilently merges two snapshots into one frozen ref;fscknever enumeratesgc/, so the backstop the code promises for a stranded generation prefix does not exist; and the GC ref-cleanup still requires whole-catalog token stillness — the same over-sensitivity that was removed from the ref-writer path.Recurring failure modes, for calibration if the audit is re-run: a real code shape with an invented consequence; "reported nowhere" where a SQL row or CLI surface does report it; test-only fields and hooks treated as production data; invented arithmetic; cited callers with no production call site; claims about vendored third-party code that reading the source refutes; and privilege claims that ignore the
GLOBALprivileges guardingSYSTEM CAS *.Per-finding verdicts
FREEZEnamespace is pool-global, soUNFREEZEon one server deletes another server's frozen parts.adoptEvidenceis the deliberate §4 manifest-trust design (8fe6331a431); the claimed data-loss window is closed at every call site, leaving an accepted trade-off with an fsck backstop.SYSTEM CAS GC REBUILDis safe on a live disk (it condemns nothing, holds the GC lease) and the two entry points' opposed read-only postures are documented design; the only real gap is offlineclickhouse-disks cas-gc-rebuildhaving no mount-lease interlock against a live server.commit, exact rollback viaCommitOutcome); the tracked residuals are no multi-ref atomicity, no rollback of a committed-ref repoint, and immediate-class DDL.RENAMEsplits a table across namespaces, no reconciler) but data loss is heavily overstated: the move is idempotently redriven, nothing is physically deleted, and only non-Atomic (deprecated Ordinary) databases reach it.server_root_id(a/b) passes validation while decommission ownership is decided by path prefix, soSYSTEM CAS DROP POOL MEMBERonadeletes live membera/b's namespaces and objects.blob_hash= cityhash128 | xxh3-128 | sha256, default cityhash128, fixed at pool creation) and there is no re-hashing on read — a deliberate decision, not a bug.Tokenin Native mode passes every check and goes on the wire withoutIf-Match— but no call site has an empty token by construction: a missing fail-closed guard, not a proven data-loss path.CasRequestControlleris real and already tracked, but the fence-bypass claim is false (the fence generation is checked before every durable PUT/DELETE and is tested); 100 sleepless attempts are a live-lock slowdown, not a retry policy.docs/en/antalya/cas/, and a Glacier read fails with a rawS3Exceptionwithout restore-and-retry.min_reader_generationtoG_BUILDwithout writing a blob) but there is no harm today — the format floor already equalsG_BUILD, so old builds cannot read the pool anyway; the defect is latent.primary.cidx,.mrk4/.cmrk4and secondary-index files — but this is not corruption: there is a 1 MiB cap and blob spill, and the real cost is buffering the whole file in memory plus a double write.KILL QUERY/max_execution_time) and minutes of summed bounded work.attempt_timeout_msnever reaches the wire; payload reads take the ordinary object-storage path) but both are documented design; "no attempt timeout, no attempt limit, no classification" is false, and the real residual is no startup check of the budget against the disk'srequest_timeout_ms.Closed/Faultedterminality are confirmed, but the empty-catch, no-exit-from-Removingand permanently-broken-table claims are false; the residuals are a read answering "no ref" instead of retry-later inside the latch window, and oneFaultedbranch without auto-remount.noexceptcode and destructors under a memory limit hold, and "renewal fences the mount" is plainly wrong.ns+refwith no post-wait manifest-id check, but every issued view is internally consistent (one manifest) and single-flight runs only on stale-tolerantCachedForLoad, so the effect is a one-repoint lag, not two mixed manifests.getStorageObjectsloses the payload offset and has no source-side CA guard, so a server-side copy-object (MOVE out of CA, BACKUP to s3 on the same host) copies envelope bytes; but "without error" is overstated — inline files yield an empty key and the whole operation fails loudly.DATA-LOSSclass is not:gc_enabled=falseis a documented debug mode and the vanished-pool no-op success is a deliberate fail-close contract; only "every manual reclamation verb is refused" is false (FSCK andGC STOPwork).server_root_idnever reaches a write: the second disk fails the mount protocol withABORTED(live double-start), so there is noMOVE PARTITION TO DISKdata-loss path.unaccounted.pool_uuid: it is pool_uuid + server_root_id + namespace + ref + part + an exactManifestRefproven by publish-then-confirm at the source; the absent blob probe is deliberate §4 manifest-trust andcheck_consistency=falsematches the upstream byte path exactly.docs/en/antalya/cas/.system.cas_logneeds an explicit grant, but the absent crypto-shred primitive is confirmed and gets no line in the operator documentation.created_delete_marker), not by the GCS config check; three narrow residuals remain — the fail-open GCS check, enabling versioning after mount (LOGICAL_ERROR), andskip_access_check.putIfAbsentStream,promoteStaged) moveIf-None-MatchtoCompleteMultipartUploadfor large bodies while both probes exercise only the small single-operation path, so the battery certifies the wrong path; exploitable only on third-party S3-compatible stores.suppressedin phase metrics,pending_reclaim/wedged_namespace_count); granularity is already tracked.cas/ns/stream/holding every key in memory and materializes the entire new edge run as one in-memory string (pool-wide at the defaultgc_shards=1).object_capcheck and JSON key dedup stays Θ(k²); the attacker-declared read buffer and pre-check zstd resize are factually wrong, the "attacker" is the fully trusted bucket-credential holder, and every refusal is loud.readIntTextwrapper is a real shape with two real residuals (std::stoullaccepts-1in three GC-key parses, one yieldingmax_gen + 1 == 0;location.offset + location.lengthcan wrap and collapse the read window to EOF), but the central "wrapping defeats every decoder range gate" thesis is untenable.ocin the GC outcome log and the missing write-side line-cap check ongc/stateremain, and both are cosmetic.gc_shardshas no upper bound anywhere and a local XML value is silently superseded by the pool value) but the consequences are a loud fail-closed allocation refusal and a missing warning, not corruption; the "no comparison" claim is false — a durable-pair mismatch is caught and throws.\nin its name), but the consequence is misstated: no unreadable part is committed (INSERT fails fail-closed, no data is lost) — instead an orphaned manifest wedges every GC round pool-wide forever.CORRUPTED_DATAon a foreign field is unreachable today (the version gate fires earlier and louder); the residuals are a deadTolerantpolicy for this format plus a measured 27-63% of decode time and two extra payload copies.changePointsunread at decode) but this is the deliberate pre-release recreate-only policy; the silent-field-erasure andRosterconsequences are unreachable.CORRUPTED_DATAonly, while the version gate and the critical key raiseUNKNOWN_FORMAT_VERSION) but a generation skew inside one pool is impossible today — relink is offered only within one mounted pool — so only a one-line hardening remains.stageManifestand has no reclassification path to a blob, so INSERT/merge fails loudly and reproducibly — and it is reachable more widely than the finding describes.Resolved::manifest_sizeto zero, making thepart_folder_cache_bytesbyte budget and the oversized-bypass threshold inoperative.DataPartsLock— but the path is rare (only DROP/REPLACE PARTITION), the work small and bounded, the failure loud, and an ordinary object-storage disk already holds the same lock while writing a part.KILL QUERYand passes no deadline), while "shutdown waits for FSCK" is wrong and "unbounded scans" is imprecise — a round is work-budgeted.CasGcScheduler::stopis confirmed (join outsidemutex, reachable viaSYSTEM CAS DROP POOL MEMBERconcurrent with GC STOP/shutdown), but the "joinable-but-dead scheduler that reports itself running" half is practically unreproducible and harmless.pending_snapshot_publishescounter leak on dispatch failure was fixed back in829ad698ef6, so the forever-wait inquiesceRefTablesForRemount/dropNamespaceImplis unreachable; only the second half holds — background publish fan-out is not bounded pool-wide (one thread per namespace).Poolalways lives under ashared_ptr, all synchronous calls come fromPoolPtrowners, and the detached thread pins the pool with a copy ofself.admits) is stale — closed back in July; the snapshot every 256 transactions is a by-design background checkpoint; only a debug/sanitizer assert restoring O(K·N) on replay remains.createHardLinkdoes a ForceFresh resolve per file, which at the defaultpart_folder_validate = alwaysmeans a mandatory manifestHEADper file; "full view rebuild" is overstated (the decode comes from cache) and the fix is transaction-level memoization as inunlinkFile.LOGICAL_ERRORon a non-stagedmoveFile/replaceFileis confirmed and preserves the previously accepted fail-loud-stub position; the "fresh evidence" is false —DeleteBitmapFileOps::writeBitmapToStoragehas no production caller.freezeRemotereally is the only one of three clone paths without a CAS transaction and cross-diskATTACH PARTITION FROMfails on the very first part — issue #2173, already confirmed and reproduced on HEAD with a planned pre-release fix; only theREPLACE PARTITION FROMtrigger and the silent-partial-state hint are wrong (the failure is loud and the tmp part is cleaned up).DiskEncryptedtakes any delegate, overrides neitherisContentAddressednorsupportsAtomicFileWrites,use_fake_transactiondefaults to true) but CAS plus encryption is a settled out-of-scope position and the combination fails loudly withNOT_IMPLEMENTEDat the first part write; only a config fail-fast gate is missing.DiskEncrypteddoes not forwardisContentAddressed).gc/state; the catalog and_ckptfail closed with no recovery path; all CA tools open via_pool_meta) but it is already tracked, the mount-lease part is wrong (cas-drop-memberexists), and the absence of migration tooling is a deliberate pre-release decision.clean()is by design — the real residual is that those two counters are rendered on no surface at all./mountfilter inlistMountshold, but "a re-run cannot repair it" is factually wrong (a resume path exists in code and is pinned by tests); only the owner-only slot's invisibility insystem.content_addressed_mountsremains.If-None-Match/If-Matchpath runs in six CAS-over-S3 CI lanes (RustFS,Mode::Native) and passes a fail-closed capability battery on every writable pool open; only a repeatable GCS-generation-dialect lane and a native row in the contract suite are missing, and both are already filed.4f4f93c6bc6) but the claimed trigger (promoteStaged/resurrect→NOT_IMPLEMENTED) is unreachable in this lane and the outcome is a loud fail-closed wedge, not corruption; only lost diagnostics remain.SYSTEM CAS GC REBUILD), and on the undecodable branch it does not drop holds but finds the seal by listing and refuses withCORRUPTED_DATA.remount_runningis latched before the thread is created, so a failed spawn disables self-remount forever; "lost wakeup" and "no handler in the thread body" do not hold on HEAD.mount_keeperchanges only underPool::remount_mutexand its only rival is cut off by a config gate in code; the claimed fence/deadline gap againstmayMutate()is factually wrong, leaving one cosmetic residual (pool_uuidpublished outsidepointer_mutex).precommitAddon onePartWriteTxnis unreachable on every working path — a latent invariant with no executable guard.runFscknever listsgc/, so an orphaned generation prefix is both unreclaimed and invisible..meta" ordering and the absence of anyblobs/LIST outside fsck hold, but "reclaimed by no sweep at all and invisible" is half right: the class is already tracked as the by-design R4 residual and an orphaned body does show asunaccountedon all three fsck surfaces.gc/stateCAS) but the consequence is false on HEAD: the wholesalegc/gen/<g>/prefix prune is the one regular collector of all attempts, including unaccepted ones, and it is pinned by tests.min_active/writer_epochstop moving — but the finding names the mechanism wrongly (the lease does not vanish when a node is lost), "forever" holds only for a slot with no restart, successor or decommission, and the conservatism is deliberate control #9._ckptto a new snapshot.SYSTEM CAS DROP POOL MEMBERdrains a dead member and two disks with one srid are impossible thanks to the fail-closed mount claim; the residuals are no periodic cleanup during a mount and no introspection._probe/is excluded from the bootstrap scan) but the consequences are overstated: ordinary paths abort in the upstream buffer and probe debris arises only on a hard kill — a cosmetic, cost-only residual.BlobRef) but that is CAS dedup itself and exactly what ordinary MergeTree does on a lightweightDELETE; the "won't fix" position stands.always_use_copy_instead_of_hardlinks=1mutations and single-disk partition clones on a CA table fail with a loudNOT_IMPLEMENTEDand the setting is never rejected; butFREEZE, the BACKUP/RESTORE clone and implicit zero-copy do not reach this path, and the refusal is fail-closed with no data corruption.getHardlinkCount— zero-copy is off for CAS) or already closed (unlinkFileno longer stays silent), leaving one narrow residual: a repeatedFREEZE WITH NAMEsilently merges the shadow ref instead of raisingDIRECTORY_ALREADY_EXISTS.Ordinarydatabase: in the Atomic layout the uuid anchor always wins and an unclassified part dir as a table-level file is unreachable; only a missing fail-closed refusal on a reserved name remains.resurrectis deliberate design with three structural justifications; "fence-unchecked" is wrong (the check exists since1fe585ea078and is test-pinned) and "returns a token it never wrote" was closed today by9b887ac8886; only staleputOverwritecomments and test names remain.INSPECT) but an envelope-length divergence is structurally unreachable, identity is not stored in the envelope (only a diagnosticrefis truncated), and the version is gated pool-wide viamin_reader_generation; the INTEGRITY consequence does not hold and the rest is a format-freeze decision.staging_backend=s3, which has a fail-closed mount probe falling back to local staging; the MAC/digest half duplicates settled CAS-008, plaintext manifest metadata and re-key impossibility are already tracked, and the plaintext-small-file-bodies claim is wrong.Layout::checkNamespaceis the only one admitting./..segments, and the emulated-mode filesystem escape over local object storage is real — but there is no production reachability: every live namespace is prefixed by an already-validatedserver_root_id, UUIDs and names are escaped, and a FREEZE backup name goes throughescapeForFileName.CLOCK_BOOTTIME(Darwin build); "different clocks for fence and request" is refuted, and wall-clock in the decommission mint is by design and already documented.removeRecursive) but the leak was closed by a targeteddropRefIfPresentinmoveDirectory(B183,2027de8346a) and covered by a separate gtest (3a32e0259f8), so "the temp ref and its blobs survive" is false on HEAD.preview_deletes=0on a missinggc/state(and, more sharply, on a vanished adopted seal) is confirmed, but "unreadable gc/state" and "one bad run object" actually raise loudCORRUPTED_DATArather than staying silent; this is a read-only diagnostic tool.droppedHoldsis not counted in production at all: it is a test adapter.holdin the fold-seal dump are confirmed (10 of 17 live formats), but "raw keys are listed by nobody" and "the wedge is unnameable" are wrong, and a_files/mis-decode raises a loud error rather than substituting data.is_leader=0does not separate a stopped GC from a follower,pending_reclaimdoes not clear spared/replaced); the fifth (a clamp counter "firing every round") is stale in code and survives only in the ProfileEvent description text.~Pooldrains the ref lanes and writes a certified farewell, andSYSTEM CAS FORGET/GC STOPare the quiesce verbs; only a minor residual remains — the durablegc/statelease is not released on a clean stop, and it is already tracked.stale_edgehas a soak gate,--namespacewarns in the help); the only real residual is that the report has no machine-readable coverage flag, so "0 because unchecked" is indistinguishable from "0 because clean".objects_deleted/absent/replaced/spared) are computed from a budget-truncated outcome log and thatGcFoldBegin/GcFoldEndcarry the previous round's number — but the deletions themselves are counted exactly (entries_redeleted) andround = 0on phase rows is documented design (correlate byround_id), not a defect.CasNs::Serverrow is unreachable, server-root keys classify asGc) but this is a deliberate, documented and test-pinned state; "mount/lease volume cannot be measured" is wrong — the residual is cosmetic: 11 always-zero counters insystem.events.CASBlobBodyPutAvoided/CASBlobDeduplicationCacheHitare incremented beforeobserveAndAdmitand the condemned branch still uploads the body, so the request counter overcounts on a rare race — but nobody counts bytes and the branch is fail-closed for every code exceptABORTED.<cas_log>section removed the sink is still installed, so every event is built and discarded.validateCasRequestBudgetruns on every writable mount and is covered by tests;ref_table_cache_bytesduplicates the already-tracked CAS-053.non_cas_keysskip-set rejects almost every legal S3 disk key and the server fails to start withUNKNOWN_SETTING; the side claims (18 entries, unchecked numeric ranges) are inaccurate.ContentAddressedMetadataStoragedoes not overrideapplyNewSettings, so no CAS setting is applied onSYSTEM RELOAD CONFIGand nothing is logged about it; removing a disk from the config yields only a generic warning while the mount lease keeps renewing until restart.dbms,ShardReducer/manifestCleanupShardhave no production callers, a test seam drops a gate) but the racily-readable global hook is unreachable in production, "thegc_shards > 1path has no callers at all" is wrong, and both real residuals are already tracked.allow_stalereally is inert at both layers — but the consequence "callers get the strict path and lose the cheap best-effort one" is wrong: no strict/non-strict split exists, resolve is always fresh by contract, and only one stale comment remains.cas/ref_catalogwith a linear scan (≥2 GETs per part publish), but "deep copy and re-encode on every mutation" applies to catalog lifecycle mutations, not to ref append.kMaxManifestEncodedByteschecked afterencodePartManifest) and the consequence is overstated — pre-encode caps on entry count and inline bytes come first; the ref-lane claim is plainly wrong, since its admission budget is computed before encoding, incrementally and byte-exact._ckptCAS + an exact GET), the epoch is pool-wide and minted per mount while the seal chain is per namespace, so first touch of a long-idle table costs O(mount count) sequential writes with no upper bound; the per-epoch slot occupancy is forced by the protocol, so the step's cost must shrink rather than the step disappear.DedupWeighthalf is right (a constant 64 against a real ~176 bytes per entry, plusNO_MAX_COUNT); thePartManifestWeighthalf is overstated — the dominantinline_bytes.size()term is accounted honestly and there is a separate 16384-entry limit.moveDirectoryscans the destination nestedly — but the consequence is overstated: a few million short-string comparisons against one PUT per file on the same path..metasibling and a padded 256-byte envelope — but "wide part of small files" is wrong: small metadata is inlined into the manifest, and the packing/inline-by-size class is already tracked with measurements.HEADbefore probing the decode cache, aresolvebefore probing the view cache, every entry point parsing, routing and resolving itself) but the headline "no cache serves a read without a network round trip" is factually wrong —resolveRefis purely in-memory and a warmCachedForLoadhit never reachesreadManifestShared; the real residual is duplicated parse+route+getView per file open.jitter_factor = 0) and "the client pushes harder than throttling asked" is inverted (16 CAS attempts against the SDK's default 500); the residuals are missing jitter on upload fan-out and the lane's exclusion from disk-wide pacing.readFile/writeFileloop, with no relink fast path for a local move) but "re-writes every byte" is wrong — within one pool the publish is fully absorbed by the dedup gate, so the real cost is one-sided: a full sourceGETplus restaging.areBlobPathsRandom()==false") is factually wrong — part files have a precalculated checksum and it is verified before the FromRemotePath path.checkCompatibilitygate onvfires before the body and yieldsUNKNOWN_FORMAT_VERSION) and the producerless!key is a deliberately dormant compat plane of the pre-release recreate-only policy — exactly the ground already adjudicated as CAS-042.namespace_janitor_pending_bytescounter) and there is no reclaim-forecast surface;previewDeletesdoes mix physical and logical size in one column, but "counts entries it will not delete" is overstated — they are flagged in thereasoncolumn and there is no double counting.BlobRef{}) but the consequence is confined to one classification line infsckand does not affect data.XXH3_128bits_resetchecks forNULLand returnsXXH_ERROR, there is no dereference, and thevalid()guard is live and working.sink->finalize(), and the local path has no such effect (only a private scratch file) whose real write is fenced anyway.readLineallocating per record on decode paths.f7539af045c(the Inline branches ofcreateHardLink/moveFilecreate no destinationPartWriteTxn), but there are no production callers and the outcome is a loud fail-closedLOGICAL_ERRORbefore any publish, not data loss.requireAlive(with its epoch check) is called from seven call sites, not one, and the durable append itself fails closed onadmitted_fence_generation/checkFenceOrThrowplus an in-bandEpochSeal; the "90 s window" misreadsoperation_deadline_ms.seqis nowhere authoritative (liveness is judged by token stability) andwriter_epoch = 0requires a corruptedcas_epochobject and fails loudly at the first ref-transaction encode; two cheap hardening residuals remain.thread_idandquery_idtaken on the draining thread) is true on HEAD and was untracked — a real observability defect; skippingRefResolveon a warm cache hit is a documented volume decision (the hit shows inCASPartFolderViewHits), and the double-count/mixed-units claim about view-cache counters is factually wrong.SYSTEM CAS *verb is behind its own GLOBAL privilege, and inlinedisk()requires the user to bring their own bucket and credentials); literal object paths are a publicly documented layout, andsystem.cas_mountsexposes no more thansystem.clusters/system.disks.lifecyclecolumn is exactly what distinguishes a live pool from one not started or vanished; only a minor "LIST failed" versus "live pool with no slots" ambiguity remains.emu_mutexis the very mechanism that emulates conditional operations (without it exact-token semantics break) and serializing resurrects deliberately caps peak memory at a maximum rather than a sum;EmulatedSingleProcessis documented as tests and local development only, and the resurrect half is already tracked.ca: 2031-triage — skeleton document for per-finding adjudication of i…
cas: one undecodable manifest no longer stops reclamation for the who…
cas: give the CAS disk settings their own `cas_` config-key namespace
alsugiliazova commentedon Aug 31, 2026
Issue body was updated with new audit rerun after the merge of #2159.
docs(cas): Backlog.md, real creation dates, GitHub CAS reconciliation…
filimonov commentedon Oct 14, 2026
CAS-007 Nested server_root_id (a/b) - fix in #2566
filimonov commentedon Oct 14, 2026
CAS-022 Orphan-manifest sweep still nominates a manifest whose namespace has no catalog row - fix in a923533