Skip to content

CAS — consolidated static analysis audit findings (tracking) #2031

Description

@vzakaznikov

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. It now consolidates 135 distinct findings from a fresh re-run in which all 39 audits were
redone from scratch
against the current PR, each with a unique CAS-### id.

The CAS-### ids are renumbered for this round and do NOT correspond to the previous numbering.
Nothing was carried forward; numeric coincidences with the old catalogue are meaningless. Where an item is
carried over from the previous triage, the old id is referenced inline as prev CAS-###.

Important

This is a static/logical review. Many items are expected to be by-design, not-a-bug, latent, or
already-handled.
The checklist is meant to be triaged item-by-item — please dismiss or resolve freely.

📎 Audit reports

Finding the full detail for a CAS-###

The checklist line is a one-line summary. For the complete write-up, open NEW-FINDINGS.md in the
2026-08-12 gist and search for the id: each entry lists the class, every code anchor, the impact and
the exact trigger, plus a Reported by: line naming the source audit findings (e.g. CAS-012 → ad6-6,
ad6-10). Those ids are prefixed with their report name, so ad6-* lives in
ad6-s3-lifecycle-cross-region.md in the same gist — search that file for the id to get the full
reasoning, the consequence chain and the code walk-through. NEW-FINDINGS.md also ends with a table
mapping each of the 39 reports to the ids it produced, for the reverse direction.

How to triage

For each item, when reviewed:

  1. Check the box once it is triaged (resolved, dismissed, or filed as its own issue).
  2. Replace resolution: inline with a verdict, e.g.
    ✅ fixed (#PR) · 🛠 will-fix · ❌ wontfix · 🚫 not-a-bug · 📐 by-design · 🟡 needs-repro · ↗ split-out (#NNN) · 🔴 still-present.
  3. Add reasoning as a comment referencing the CAS-### id.
  4. Inline verdicts below are from Filimonov's 2026-08-21 head triage
    (CAS — consolidated static analysis audit findings (tracking) #2031 (comment));
    that comment is the per-id reasoning. carried from prev CAS-### provenance is unreliable.
    Locate code by symbol — CA/... anchors are stale (the tree moved into Backend/, Pool/,
    Gc/, Parts/, Formats/, Primitives/, Tools/).

Severity is the highest assigned by any source audit. Class tags:
DATA-LOSS · LEAK · LIVENESS · CONCURRENCY · INTEGRITY · SECURITY · DECODE/DoS · COMPAT · FEATURE-GAP · PERF/SCALE · OBSERV/DAY2 · COMPLIANCE · CONFIG · TEST-GAP · CORRECTNESS.
Grading rule used this round: a fail-closed loud failure grades below silent corruption, and operability, cost
and scale gaps are not High on their own. The High / Medium / Low sections below keep that audit grading
(not reshuffled). Filimonov 2026-08-21 re-assigned priorities from scratch against current head:
4 × P1, 38 × P2, 83 × P3, 10 need no action. Verdicts: 87 partly · 21 by-design · 17 confirmed ·
8 not-a-bug · 1 already-fixed · 1 duplicate.

P1 (Filimonov 2026-08-21)

ID Substance Tracked as
CAS-001 The shadow/FREEZE namespace is pool-global, so UNFREEZE on one server deletes another server's frozen parts #2212
CAS-040 A newline in a part-file path (reachable from plain DDL: a projection named with \n) leaves an undecodable orphan manifest, and every GC round in the pool then fails forever 🛠 will-fix (pre-release)
CAS-058 freezeRemote is the only one of the three clone paths without a CAS transaction, so cross-disk ATTACH PARTITION FROM cannot work #2173
CAS-106 The enumerated non_cas_keys skip-set rejects legal S3 disk keys, so the server dies at startup with UNKNOWN_SETTING #2243

🔴 High

  • CAS-001 Shadow/FREEZE and backup namespaces are pool-global while exclusion primitives are per-server-root, so two servers share one unfenced shadow ref table and UNFREEZE on either deletes the other's frozen parts · DATA-LOSS — resolution: ↗ split-out (CAS: UNFREEZE on one replica deletes another replica's FREEZE (shadow namespace is missing server_root_id) #2212) (Filimonov 2026-08-21, P1) · The shadow/FREEZE namespace is pool-global, so UNFREEZE on one server deletes another server's frozen parts. — CA/ContentAddressedMetadataStorage.cpp:897-900
  • CAS-002 Manifest-trust adoptEvidence bypasses the durable condemn marker and EDGE-BEFORE-OBSERVE, so a committed manifest can name a blob GC has already deleted, with no source to re-upload from · DATA-LOSS — resolution: 📐 by-design (Filimonov 2026-08-21, P3) · The missing probe/condemn check in adoptEvidence is the deliberate §4 manifest-trust design (8fe6331); the claimed data-loss window is closed at every call site, leaving an accepted trade-off with an fsck backstop. — CA/Pool/CasPartWriteTxn.cpp:478-486
  • CAS-003 The GC lease has no TTL, is stealable on differential observation, and the destructive phases are never revalidated, so two GC actors can run destructive phases concurrently on one pool · CONCURRENCY — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The facts hold (no wall-clock TTL, theft by differential observation, a deposed leader learns only at the round-commit CAS) but this is design: catalog and ref phases revalidate the lease before every delete, so "destructive phases are never revalidated" is false; only liveness remains. — CA/Gc/CasGc.cpp:3155-3186
  • CAS-004 GC REBUILD has no writer/mount interlock and "read-only" does not gate writes, so a rebuild is accepted on a live writable disk with inserts in flight and the mount census result is discarded · INTEGRITY — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Server-side SYSTEM CAS GC REBUILD is 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 offline clickhouse-disks cas-gc-rebuild having no mount-lease interlock against a live server. — CA/Gc/CasGc.cpp:2725
  • CAS-005 A repointed committed ref is unrevertible and durable CAS mutations happen before commit() with a silent best-effort rollback, so readers can observe aborted and intermediate states · DATA-LOSS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The core finding was closed by the TXN-ONE-PIPELINE refactor (ref published only in commit, exact rollback via CommitOutcome); the tracked residuals are no multi-ref atomicity, no rollback of a committed-ref repoint, and immediate-class DDL. — CA/ContentAddressedTransaction.cpp:280-289
  • CAS-006 Cross-namespace RENAME/moveDirectory is a per-ref non-atomic migration ending in an unconditional source drop with no reconciler, so a crash mid-rename splits the refs and drops any added during the walk · DATA-LOSS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Mechanism confirmed (journal-less per-ref move, a crash mid-RENAME splits 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. — CA/ContentAddressedTransaction.cpp:846-874
  • CAS-007 Nested server_root_id is accepted and victim selection is prefix-based, so SYSTEM CAS DROP POOL MEMBER on srid=a erases the namespaces and control objects of a live member srid=a/b · DATA-LOSS — resolution: 🔴 still-present (Filimonov 2026-08-21, P2) · A nested server_root_id (a/b) passes validation while decommission ownership is decided by path prefix, so SYSTEM CAS DROP POOL MEMBER on a deletes live member a/b's namespaces and objects. — CA/Pool/CasServerRoot.h:104-134
  • CAS-008 Content addressing defaults to a non-cryptographic 128-bit hash and reads never re-verify, so a chosen collision silently substitutes data for every future reader of that content · SECURITY — resolution: 📐 by-design (Filimonov 2026-08-21) · Position holds on HEAD: the hash is selectable (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. — CA/ContentAddressedSettings.cpp:33
  • CAS-009 An occupied content address is admitted on existence alone, and no re-upload, resurrect or staged body is ever re-hashed — every admission is size-checked, never digest-checked · INTEGRITY — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Mechanics correct (presence-only admit, no re-hash of any re-upload or staged body) but the digest half is by design; the real corruption window narrows to a length-preserving divergence of local scratch plus the already-tracked non-atomic emulated-backend write. — CA/Pool/CasPartWriteTxn.cpp:250-305
  • CAS-010 An empty conditional token turns a fenced write into an unconditional clobber — putOverwrite validates only the token type, so an absent ETag plus an absent-HEAD sends no precondition at all · INTEGRITY — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Real mechanism — an empty Token in Native mode passes every check and goes on the wire without If-Match — but no call site has an empty token by construction: a missing fail-closed guard, not a proven data-loss path. — CA/Backend/CasObjectStorageBackend.cpp:165-173
  • CAS-011 Plain-object writes bypass the request controller and the margin-checked fence and never resolve their indeterminate outcomes, retrying one key up to 100 times with zero backoff · INTEGRITY — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Bypassing CasRequestController is 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. — CA/Pool/CasPlainObjects.cpp:21-41
  • CAS-012 Lifecycle rules, Object Lock and storage-class transitions are undetected and fail open, so an expiration rule silently deletes live blobs and Glacier reads have no restore-and-retry path · DATA-LOSS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The settled by-design-plus-docs position holds, but the docs half is closed only for versioning: lifecycle expiration, Object Lock/WORM and storage-class transitions are undocumented in docs/en/antalya/cas/, and a Glacier read fails with a raw S3Exception without restore-and-retry. — CA/Backend/CasObjectStorageBackend.cpp:272-282
  • CAS-013 One node admitting a hash algorithm rewrites the pool-wide reader floor to its own build number — mounting one disk with blob_hash_allow_new=1 locks every older build out of the entire pool · COMPAT — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Mechanics confirmed (admission CAS-raises the pool-global min_reader_generation to G_BUILD without writing a blob) but there is no harm today — the format floor already equals G_BUILD, so old builds cannot read the pool anyway; the defect is latent. — CA/Pool/CasPoolMeta.cpp:72
  • CAS-014 The file-placement classifier is a closed suffix allowlist that misses shipped MergeTree file names (primary.cidx, .mrk4, secondary-index data files), sending them down the fully-in-memory inline path · CORRECTNESS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · The classifier is indeed a closed allowlist unaware of primary.cidx, .mrk4/.cmrk4 and 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. — CA/ContentAddressedTransaction.cpp:65-73
  • CAS-015 Waits on CAS single-flight, leader and recovery paths have no deadline and no cancellation, so a stalled store hangs concurrent INSERTs, readers of a recovering namespace and DROP TABLE · LIVENESS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Waits on single-flight, leadership and recovery have no deadline and no query cancellation, but each sits behind time-bounded I/O (90 s/16 attempts, 120 s recovery), so nothing hangs forever; what remains is non-cancellability (KILL QUERY/max_execution_time) and minutes of summed bounded work. — CA/Pool/CasRefLedger.cpp:1457-1492
  • CAS-016 attempt_timeout_ms never reaches the wire and the blob payload read bypasses the CAS backend entirely, so reads run on the default 500-retry profile with no deadline and no classification · LIVENESS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Both facts are literally true (attempt_timeout_ms never 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's request_timeout_ms. — CA/Backend/CasRequestControl.h:84
  • CAS-017 Namespace removal latches read/write admission closed before anything is durable, and the ref lane has terminal states with no exit, so a transient backend error leaves the table permanently unusable · LIVENESS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Latch-before-durable ordering and Closed/Faulted terminality are confirmed, but the empty-catch, no-exit-from-Removing and permanently-broken-table claims are false; the residuals are a read answering "no ref" instead of retry-later inside the latch window, and one Faulted branch without auto-remount. — CA/Pool/CasRefLedger.cpp:3451-3458
  • CAS-018 Latches and leadership are set or released outside RAII and noexcept/destructor paths allocate, so a throw under a memory limit deadlocks the namespace forever or terminates the process · LIVENESS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The headline ref-queue leadership leak is already closed by a single exit point plus tests; of six anchors only theoretical allocations in noexcept code and destructors under a memory limit hold, and "renewal fences the mount" is plainly wrong. — CA/Pool/CasRefLedger.cpp:1519-1541
  • CAS-019 Part-folder single flight is keyed by ref only, collapsing different manifest ids onto one key, so a follower straddling a repoint receives the leader's view for a different manifest than it resolved · CORRECTNESS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · The single-flight key really is only ns+ref with no post-wait manifest-id check, but every issued view is internally consistent (one manifest) and single-flight runs only on stale-tolerant CachedForLoad, so the effect is a one-repoint lag, not two mixed manifests. — CA/Parts/PartFolderAccess.cpp:231-269
  • CAS-020 getStorageObjects returns objects that are not the file's bytes because the envelope offset is dropped, so MOVE PART/PARTITION TO DISK and TTL moves produce a corrupt destination part with no error · INTEGRITY — resolution: 🔴 still-present (Filimonov 2026-08-21, P2) · getStorageObjects loses the payload offset and has no source-side CA guard, so a server-side copy-object copies envelope bytes; "without error" is overstated — inline files yield an empty key and the operation fails loudly. — CA/ContentAddressedMetadataStorage.cpp:1336-1340
  • CAS-021 Ambiguous conditional-write outcomes are reported as definite ones: content equality is treated as proof of our own authorship, and a landed-then-timed-out write is reported as another writer's object · INTEGRITY — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · All six controller quotes are textually accurate on HEAD but every dangerous consequence is already neutralized; the residual is a stale in-process condemn-marker memo, which the user declined to fix by re-reading. — CA/Backend/CasRequestControl.cpp:427-435 CAS-021: Ambiguous conditional-write outcomes are reported as definite ownership #2207
  • CAS-022 The orphan-manifest sweep applies no protection at all to a manifest whose namespace has no catalog row — it deletes the body and emits BlobSourceRetirement for every entry during a namespace's first write · DATA-LOSS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · The "no catalog row" branch of the paged sweep planner checks neither watermark, coverage nor the §6 premise, and its rationale is wrong on HEAD (the manifest body is written before the row is created) — but the window is narrow and the outcome loud, not data loss. — CA/Gc/CasOrphanManifestSweep.cpp:546
  • CAS-023 Deletes are accepted and silently do nothing when GC is disabled or the pool has settled as vanished — with gc_enabled=false every manual reclamation verb is refused with BAD_ARGUMENTS · DATA-LOSS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Both behaviours are real code but the DATA-LOSS class is not: gc_enabled=false is 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 and GC STOP work). — CA/ContentAddressedMetadataStorage.cpp:611
  • CAS-024 Two CAS disks sharing a pool and a server_root_id resolve to one (namespace, ref), so a MOVE PARTITION TO DISK between them publishes then drops the same ref and the part is gone, silently · DATA-LOSS — resolution: 🚫 not-a-bug (Filimonov 2026-08-21, P3) · A config with two CAS disks on one pool sharing a server_root_id never reaches a write: the second disk fails the mount protocol with ABORTED (live double-start), so there is no MOVE PARTITION TO DISK data-loss path. — CA/ContentAddressedMetadataStorage.cpp:886-889

🟠 Medium

  • CAS-025 GC REBUILD discards the condemn universe and permanently orphans already-unreferenced blobs — prior_runs starts empty, the fold takes only +1 deltas, and the graduation guard is vacuous · LEAK — resolution: 📐 by-design (Filimonov 2026-08-21, P3) · Mechanics correct (rebuild starts from empty priors, edge-only fold, the condemn universe is reset, incremental fold never finds those blobs again) but this is a deliberate fail-closed trade-off already tracked as the REBUILD R4 residual: retention, not loss, visible as a non-draining fsck unaccounted. — CA/Gc/CasGc.cpp:2809-2824
  • CAS-026 Relink treats pool_uuid equality as proof of "same bucket" and publishes the adopted part unverified — no presence check on any blob dependency and check_consistency=false on publish · INTEGRITY — resolution: 📐 by-design (Filimonov 2026-08-21, P3) · Relink identity is not just pool_uuid: it is pool_uuid + server_root_id + namespace + ref + part + an exact ManifestRef proven by publish-then-confirm at the source; the absent blob probe is deliberate §4 manifest-trust and check_consistency=false matches the upstream byte path exactly. — src/Storages/MergeTree/DataPartsExchange.cpp:313-330
  • CAS-027 Any bucket-credential peer can permanently disable, fence or misdirect another member; there is no intra-pool authentication — one PUT to a peer's owner/mount retires it or steals its slot · SECURITY — resolution: 📐 by-design (Filimonov 2026-08-21, P3) · HEAD matches the settled position exactly (no intra-pool authentication; the bucket credential is the entire trust boundary); the only real residual is that this trust boundary is documented nowhere in docs/en/antalya/cas/. — CA/Pool/CasServerRoot.cpp:68-79
  • CAS-028 Blob keys are unsalted pool-global content hashes: no per-subject shred, guessable residue, and a dedup confirmation oracle observable from system.cas_log · SECURITY — resolution: 📐 by-design (Filimonov 2026-08-21, P3) · Blob keys really are unsalted pool-global content hashes — that is CAS dedup itself; the dedup oracle via system.cas_log needs an explicit grant, but the absent crypto-shred primitive is confirmed and gets no line in the operator documentation. — CA/Pool/CasPartWriteTxn.cpp:145
  • CAS-029 The provider dialect is declared by configuration and never detected, so the one bucket-versioning precondition runs only for GCS clients and fails open when it does run · DATA-LOSS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The central claim is false: the versioning precondition is enforced by a mandatory behavioural mount probe (created_delete_marker), not by the GCS config check; three narrow residuals remain — the fail-open GCS check, enabling versioning after mount (LOGICAL_ERROR), and skip_access_check. — CA/Backend/CasObjectStorageBackend.cpp:53-67
  • CAS-030 skip_access_check removes every bucket-configuration defense, is not recorded anywhere in the pool, and the decommission remount hard-codes it · CONFIG — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Mechanics correct (the probe is skipped entirely, decommission sets the flag unconditionally) but "removes every bucket-configuration defense" is overstated — the single-attempt gate and residual proof remain; the real residual is no operator signal at all plus a self-contradictory versioning message. — CA/Pool/CasPool.cpp:339-347
  • CAS-031 The conditional-write contract is validated only for single-PUT and then assumed for multipart, so on a store that ignores If-None-Match on CompleteMultipartUpload every large blob loses exclusivity · INTEGRITY — resolution: 🔴 still-present (Filimonov 2026-08-21, P2) · Both write-once CREATE primitives move If-None-Match to CompleteMultipartUpload for large bodies while both probes exercise only the small path, so the battery certifies the wrong path; exploitable only on third-party S3-compatible stores. — CA/Backend/CasProbe.cpp:42
  • CAS-032 Zero cross-region / replicated-bucket awareness — nothing binds a pool identity to an endpoint or region, so failover onto a CRR destination is indistinguishable from the primary · INTEGRITY — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Code shape confirmed — pool identity is never tied to endpoint or bucket — but harm requires an operator error (writing into a CRR destination, bidirectional replication over the prefix) that violates the implied but undocumented "the prefix belongs to CAS alone" requirement. — CA/Pool/CasPoolMeta.cpp:100-104
  • CAS-033 All reclamation is gated on a whole-pool "clean pass" predicate with no bound and no retention signal — one anomaly anywhere suppresses every destructive action pool-wide indefinitely · LEAK — resolution: 📐 by-design (Filimonov 2026-08-21, P2) · The gate really is pool-wide and unbounded — a deliberate fail-closed choice — but "no operator signal" is factually wrong (a WARNING with reasons, a ProfileEvent, suppressed in phase metrics, pending_reclaim/wedged_namespace_count); granularity is already tracked. — CA/Gc/CasGc.cpp:2063-2064
  • CAS-034 Per-round reclamation budgets sit below the steady-state creation rate — 5,000 ref objects per 60 s round and one 1,000-key janitor page, while every part commit creates two ref objects · LEAK — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Code shapes confirmed (5000 ref objects per round, one 1000-key janitor page) but the consequence is deferred reclamation and erasure latency, not data loss; the erasure SLA is settled by the author and the budget-vs-creation-rate arithmetic is real and already tracked. — CA/ContentAddressedSettings.cpp:46
  • CAS-035 The GC fold and its enumerations are O(total pool) every round, unbudgeted, with unbounded peak memory — the full ref LIST is retained with no cursor and is called even on deferred rounds · PERF/SCALE — resolution: 🔴 still-present (Filimonov 2026-08-21, P2) · Confirmed throughout, and the memory peak is worse than described: a GC round fully lists cas/ns/stream/ holding every key in memory and materializes the entire new edge run as one in-memory string (pool-wide at default gc_shards=1). — CA/Gc/CasBlobInDegree.cpp:484-555
  • CAS-036 Any bucket-sourced control object is materialized in memory unbounded and one planted object costs quadratic CPU — a multi-gigabyte _pool_meta or ref-log body OOMs the victim before any format check · DECODE/DoS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Of three claimed mechanics one and a half survive: the control-object body is materialized before the object_cap check 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. — CA/Backend/CasObjectStorageBackend.cpp:284-293
  • CAS-037 Every CAS numeric field silently wraps mod 2^64 and offset + length overflows on the read path, so a planted manifest sz produces a wrapped read window that defeats every decoder range gate · DECODE/DoS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The unchecked readIntText wrapper is a real shape with two real residuals (std::stoull accepts -1 in three GC-key parses, one yielding max_gen + 1 == 0; location.offset + location.length can wrap and collapse the read window to EOF), but the central "wrapping defeats every decoder range gate" thesis is untenable. — CA/Formats/CasTextFormat.cpp:193-223
  • CAS-038 Decoders make liveness- and safety-critical fields optional and default them to the least-safe value, so a truncated mount lease decodes as "expired, unfenced" and junk fold seals decode unvalidated · DECODE/DoS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Five of seven sub-claims either do not match HEAD or have a safe default; only the optional oc in the GC outcome log and the missing write-side line-cap check on gc/state remain, and both are cosmetic. — CA/Formats/CasServerRootFormats.cpp:147-169
  • CAS-039 gc_shards is adopted from bucket bytes with no upper bound and silently overrides the node's configured value while sizing vectors and loop bounds, on only a >= 1 check · DECODE/DoS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The shape is right (gc_shards has 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; a durable-pair mismatch is caught and throws. — CA/Formats/CasPoolMetaFormat.cpp:116
  • CAS-040 The part-manifest payload-zone banner is written raw and validated only on decode, so an entry path containing LF encodes successfully and produces a committed part that can never be decoded again · INTEGRITY — resolution: 🛠 will-fix (Filimonov 2026-08-21, P1) · Real and reachable through ordinary DDL (a projection named with a newline), but the consequence is misstated: INSERT fails closed and no unreadable part is committed — instead an orphaned manifest wedges every GC round pool-wide forever. — CA/Formats/CasPartManifestFormat.cpp:64-67
  • CAS-041 The manifest payload digest is recomputed by canonical re-encode of the decoded model, so any tolerated or foreign field reads as CORRUPTED_DATA — and the same implementation costs 2x work per decode · COMPAT — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Mechanics (digest = canonical re-encode) match HEAD exactly, but the claimed CORRUPTED_DATA on a foreign field is unreachable today (the version gate fires earlier and louder); the residuals are a dead Tolerant policy for this format plus a measured 27-63% of decode time and two extra payload copies. — CA/Formats/CasPartManifestFormat.cpp:263-267
  • CAS-042 One global build number is stamped as every object's minimum reader and the per-format change-point registry is populated but never consulted, so one generation bump invalidates every format · COMPAT — resolution: 📐 by-design (Filimonov 2026-08-21, P2) · Code shape correct (one global generation as the min-reader, changePoints unread at decode) but this is the deliberate pre-release recreate-only policy; the silent-field-erasure and Roster consequences are unreachable. — CA/Formats/CasFormat.cpp:82-93
  • CAS-043 The relink handshake negotiates a replication protocol number that says nothing about CAS generation, and the generation mismatch escapes the byte-fetch fallback, so a skewed fetch fails outright · COMPAT — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The narrow catch is confirmed (CORRUPTED_DATA only, while the version gate and the critical key raise UNKNOWN_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. — CA/ContentAddressedMetadataStorage.cpp:1610-1619
  • CAS-044 The 16 MiB per-part inline budget is enforced only at commit with no fallback, so a legitimately wide part fails its INSERT permanently and reproducibly with no re-classification to blob placement · FEATURE-GAP — resolution: 🔴 still-present (Filimonov 2026-08-21, P2) · The aggregate 16 MiB inline-data limit per manifest is checked only in stageManifest and has no reclassification path to a blob, so INSERT/merge fails loudly and reproducibly — reachable more widely than the finding describes. — CA/Pool/CasPartWriteTxn.cpp:54
  • CAS-045 The part-folder view cache accounts every retained manifest as 256 bytes because both producers hardwire .manifest_size = 0, so part_folder_cache_bytes is inoperative and nothing is excluded as oversized · PERF/SCALE — resolution: 🔴 still-present (Filimonov 2026-08-21, P2) · The part-folder cache entry weight is always 256 bytes because both producers hardcode Resolved::manifest_size to zero, making the part_folder_cache_bytes byte budget and the oversized-bypass threshold inoperative. — CA/Parts/PartFolderAccess.cpp:128-131
  • CAS-046 Local scratch staging is unreserved, unaccounted, uncapped, held for the whole transaction and never swept at startup, while the CAS disk reports no free space at all · PERF/SCALE — resolution: 🔴 still-present (Filimonov 2026-08-21, P2) · Local scratch = the part's full bytes, no reservation, accounting, quota or limit, deleted only at transaction end, no startup cleanup; already twice-tracked DESIRABLE class — failures are loud (ENOSPC), no silent corruption. — CA/ContentAddressedTransaction.cpp:1223-1235
  • CAS-047 The blob upload pool is process-global, 16 threads with a 16-slot queue, and enqueue blocks — one wide part exceeds the queue by itself and all CAS uploads for every disk serialize behind it · PERF/SCALE — resolution: 📐 by-design (Filimonov 2026-08-21, P3) · Code shape correct (one process-wide pool, 16 threads, queue_size == max_threads, blocking enqueue) but the attached consequence is false: blocking on enqueue is ordinary backpressure, not serialization or deadlock; the size is a server setting and the tuning is already tracked. — CA/Pool/CasBlobUploadPool.cpp:45-49
  • CAS-048 A CAS part publish runs object-store I/O while DataPartsLock is held, so a throttling or 5xx bucket stalls the table's parts lock and blocks every SELECT, merge scheduling and part-set mutation · LIVENESS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Shape confirmed — the empty covering part is published under 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. — src/Storages/MergeTree/MergeTreeData.cpp:5918-5922
  • CAS-049 GC STOP, shutdown and FSCK serialize behind whole in-flight unbounded scans, so an operator cannot stop GC, forget a disk or shut down while a round or scan is in flight against a slow bucket · LIVENESS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · The serialization is confirmed and mostly deliberate; the real residual is the absence of cooperative cancellation (a GC round and FSCK cannot be interrupted, SQL FSCK ignores KILL QUERY and passes no deadline), while "shutdown waits for FSCK" is wrong and "unbounded scans" is imprecise — a round is work-budgeted. — CA/Gc/CasGcScheduler.cpp:213
  • CAS-050 The GC scheduler joins its thread objects outside the mutex that guards them and its threads self-exit independently, leaving a joinable-but-dead scheduler that reports itself as running · CONCURRENCY — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · The data race on thread objects in CasGcScheduler::stop is confirmed (join outside mutex, reachable via SYSTEM CAS DROP POOL MEMBER concurrent with GC STOP/shutdown), but the "joinable-but-dead scheduler that reports itself running" half is practically unreproducible and harmless. — CA/Gc/CasGcScheduler.cpp:67-79
  • CAS-051 Snapshot-publish dispatch can leak its pending count, hanging quiesceRefTablesForRemount and dropNamespaceImpl forever, and its fan-out is unbounded pool-wide · LIVENESS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · The pending_snapshot_publishes counter leak on dispatch failure was fixed in 829ad69, so the forever-wait in quiesceRefTablesForRemount/dropNamespaceImpl is unreachable; only the second half holds — background publish fan-out is not bounded pool-wide (one thread per namespace). — CA/Pool/CasRefLedger.cpp:2754-2783
  • CAS-052 Anomaly reporting calls shared_from_this() on a possibly expiring pool, throwing bad_weak_ptr out of an unguarded region or handing a detached thread a pool being destroyed · CONCURRENCY — resolution: 🚫 not-a-bug (Filimonov 2026-08-21) · Code shape correct but the consequence is unreachable: Pool always lives under a shared_ptr, all synchronous calls come from PoolPtr owners, and the detached thread pins the pool with a copy of self. — CA/Pool/CasPool.cpp:972-1029
  • CAS-053 The ref-table runtime cache budget is enforced only at recovery, cannot evict a table being written, and its arithmetic can underflow and evict every evictable table at once · PERF/SCALE — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The single call site and the unguarded subtraction hold, but "once per namespace and never again" is imprecise (the pass runs on every cold admission) and the effects are only extra recovery I/O and a soft rather than hard memory ceiling; correctness is unaffected. — CA/Pool/CasRefLedger.cpp:1149-1210
  • CAS-054 Ref publication re-encodes the whole namespace every 256 transactions and obtains row byte counts by re-serializing rows; debug and sanitizer builds turn every txn apply into a further O(R) re-encode · PERF/SCALE — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The main charge (O(R) re-encode in 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. — CA/Pool/CasRefLedger.cpp:2741-2745
  • CAS-055 Every hardlinked file re-reads the source part manifest from the object store because part_folder_validate ships as always, costing hundreds to thousands of round trips per mutation · PERF/SCALE — resolution: 🔴 still-present (Filimonov 2026-08-21, P2) · The carry-forward branch of createHardLink does a ForceFresh resolve per file, which at default part_folder_validate=always means a mandatory manifest HEAD per file; "full view rebuild" is overstated (decode comes from cache); fix is transaction-level memoization as in unlinkFile. — CA/ContentAddressedTransaction.cpp:816
  • CAS-056 A single-file write or unlink on a committed part republishes the whole manifest twice and emits one adopt event per blob entry inside the ref-log CAS lambda · PERF/SCALE — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Real shape — a standalone write into a committed part does pay a second (draft) manifest PUT plus one adopt event per carried blob list — but "two full manifest encodes" and "inside the CAS retry closure" are inaccurate; correctness is unaffected. — CA/ContentAddressedTransaction.cpp:256-290
  • CAS-057 moveFile/replaceFile on a committed part file always throws LOGICAL_ERROR, so the standard "write .tmp then replaceFile" pattern is unusable and unique-key delete bitmaps fail on CAS · FEATURE-GAP — resolution: 🚫 not-a-bug (Filimonov 2026-08-21, P3) · The LOGICAL_ERROR on a non-staged moveFile/replaceFile is confirmed and preserves the previously accepted fail-loud-stub position; the "fresh evidence" is false — DeleteBitmapFileOps::writeBitmapToStorage has no production caller. — CA/ContentAddressedTransaction.cpp:1030-1055
  • CAS-058 Cross-disk ATTACH/REPLACE PARTITION FROM into a CAS disk is unimplemented and fails part-way through the part, leaving partial state rather than being rejected up front · FEATURE-GAP — resolution: ↗ split-out (CAS: ATTACH PARTITION FROM local disk to CAS disk fails with ref 'tmp_replace_from_1_1_1_0' already names a different committed manifest #2173) (Filimonov 2026-08-21, P1) · freezeRemote is the only one of three clone paths without a CAS transaction and cross-disk ATTACH PARTITION FROM fails on the first part; already confirmed on HEAD with a planned pre-release fix. REPLACE PARTITION FROM trigger and silent-partial-state hint are wrong (failure is loud; tmp part is cleaned up). — src/Storages/MergeTree/DataPartStorageOnDiskBase.cpp:593-621
  • CAS-059 An encrypted disk can be layered over CAS with no capability check and reports "not content-addressed" while still routing writes into CAS, disabling every CAS-aware MergeTree hook · CONFIG — resolution: 📐 by-design (Filimonov 2026-08-21, P3) · All code descriptions hold (DiskEncrypted takes any delegate, overrides neither isContentAddressed nor supportsAtomicFileWrites, use_fake_transaction defaults to true) but CAS plus encryption is a settled out-of-scope position and the combination fails loudly with NOT_IMPLEMENTED at the first part write; only a config fail-fast gate is missing. — src/Disks/DiskEncrypted.cpp:190-208
  • CAS-060 Per-file random IV makes every file a unique blob, silently destroying dedup — two replicas merging identical rows produce entirely distinct blobs and nothing detects it · CORRECTNESS — resolution: 📐 by-design (Filimonov 2026-08-21, P3) · Code shape correct (a random IV per rewrite plus CAS hashing whatever it is handed means no dedup at all) but CAS × encryption is an accepted out-of-scope position; there is no silent corruption and the combination is never actually wired (DiskEncrypted does not forward isContentAddressed). — src/Disks/DiskEncryptedTransaction.cpp:105-112
  • CAS-061 Only gc/state has a rebuild path; every other control object has no DR path, and because the tools open the pool through _pool_meta first, damage to that one object disables the DR tools themselves · OBSERV/DAY2 — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Core confirmed (the only rebuild verb is gc/state; the catalog and _ckpt fail 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-member exists), and the absence of migration tooling is a deliberate pre-release decision. — CA/Pool/CasPool.cpp:293-368
  • CAS-062 SYSTEM CAS FSCK is counts-only and no repair path exists anywhere — the SQL path can say the pool is corrupt but never which keys, with no timeout and no scoping · OBSERV/DAY2 — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The SQL FSCK path is indeed counts-only with no deadline and no scope, but that is already tracked as CAS-049; "no repair path anywhere" is wrong and excluding the meta/body counters from clean() is by design — the real residual is that those two counters are rendered on no surface at all. — src/Interpreters/InterpreterSystemQuery.cpp:2534
  • CAS-063 The only way to clear a dead member's mount slot is a verb that first erases that member's data, and a half-decommissioned member is invisible to cas_mounts and not repairable by a re-run · OBSERV/DAY2 — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The "drop namespaces then release the slot" ordering and the /mount filter in listMounts hold, 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 in system.content_addressed_mounts remains. — CA/Tools/CasDecommission.cpp:137-183
  • CAS-064 No CAS decoder is fuzzed, no property-based tests exist, and three live format classes skip the shared failure-mode battery, although every decoder consumes bucket-sourced input · TEST-GAP — resolution: 📐 by-design (Filimonov 2026-08-21, P3) · The absence of a fuzzer and property-based tests is factual but a settled position (S3 is trusted, decoders fail closed); the real residual is three live format classes not registered in the shared battery — a drift risk, not a coverage hole. — src/Disks/tests/cas_format_test_battery.h
  • CAS-065 No CI lane exercises a native or GCS conditional-write dialect — the exclusivity guarantee CAS is built on is only ever tested against the emulated in-process backend · TEST-GAP — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · The central claim is false — the native If-None-Match/If-Match path 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. — src/Disks/tests/gtest_cas_backend_contract.cpp:250-258
  • CAS-066 Emulated single-process mode is chosen by storage type alone with no override and only an INFO-level warning, so two servers over one shared local path both get in-process-only exclusivity · CONFIG — resolution: 📐 by-design (Filimonov 2026-08-21, P2) · Code shape confirmed (the mode is chosen by storage type, there is no knob, the log is at INFO) — a deliberate design decision with recorded rationale; the "two servers on one local path" risk is already tracked as doc debt, and the read-only-mount sub-claim has no consequence. — CA/ContentAddressedMetadataStorage.cpp:509-520
  • CAS-067 The emulated conditional-write token is a filesystem mtime, so coarse mtime granularity validates a stale token, and its state pruning stalls permanently on clock skew · INTEGRITY — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The first half (coarse mtime granularity yields a valid stale token) was closed by an mtime-quantum disambiguator on 2026-07-18 and is covered by a test; the second (clock skew breaks token-state expiry) is real but only a memory leak in emulated mode with no correctness impact. — CA/Backend/CasObjectStorageBackend.cpp:347-362
  • CAS-068 putIfAbsentControlled swallows deterministic local failures and reports them as ambiguity, so a LOGICAL_ERROR or NOT_IMPLEMENTED that provably never landed wedges the ref lane into recovery · INTEGRITY — resolution: 📐 by-design (Filimonov 2026-08-21, P3) · The shape is real and deliberate (the divergence from four siblings is recorded in code and in commit 4f4f93c) 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. — CA/Backend/CasRequestControl.cpp:271-281
  • CAS-069 Empty catches reclassify transient read failures as corruption — a MEMORY_LIMIT_EXCEEDED on a large gc/state is indistinguishable from genuine corruption and drives a full baseline rebuild · INTEGRITY — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The empty catches exist and do lose the cause, but "indistinguishable from corruption → full rebuild → permanent orphans per CAS-025" does not follow: rebuild is manual only (SYSTEM CAS GC REBUILD), and on the undecodable branch it does not drop holds but finds the seal by listing and refuses with CORRUPTED_DATA. — CA/Gc/CasGc.cpp:2633-2648
  • CAS-070 Remount self-healing is permanently disabled by a lost wakeup, a latched flag, or one unhandled throw — after a fence-out the mount stays fenced closed until process restart · LIVENESS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Only one of three claimed mechanisms is real — remount_running is 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. — CA/Pool/CasMountRuntime.cpp:341-369
  • CAS-071 Mount and pool state is read and written outside the mutex that guards it — keeper replacement races readers, fence/deadline state is torn, and pool identity is published under a TSA suppression · CONCURRENCY — resolution: 📐 by-design (Filimonov 2026-08-21, P3) · The prior verdict still holds — mount_keeper changes only under Pool::remount_mutex and its only rival is cut off by a config gate in code; the claimed fence/deadline gap against mayMutate() is factually wrong, leaving one cosmetic residual (pool_uuid published outside pointer_mutex). — CA/Pool/CasMountRuntime.h:164
  • CAS-072 Staged-manifest debris cleanup tracks only one precommit binding and can delete a body a live precommit still owns, leaving a durable reference to a missing object · DATA-LOSS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Real shape (a single precommit slot, overwritten without a check) but a second precommitAdd on one PartWriteTxn is unreachable on every working path — a latent invariant with no executable guard. — CA/Pool/CasPartWriteTxn.cpp:866-884
  • CAS-073 The condemn marker is not incarnation-scoped, is accepted as its own proof, and is never cleared, so a marker written for a previous incarnation licenses deleting the new one · INTEGRITY — resolution: 📐 by-design (Filimonov 2026-08-21, P3) · All three shapes are correct but the consequence is invented: the marker is by definition not the deletion authority — an exact-token delete using the token from the retired row does the deleting, and resurrect rotates the incarnation tag, so a previous incarnation's marker authorizes nothing. — CA/Formats/CasBlobMetaFormat.h:14-22
  • CAS-074 Generation prune advances a monotone cursor past still-referenced generations and the compensating hand-off is one-shot, so nothing ever revisits snap_pruned_through and those objects leak · LEAK — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Code shape correct and long documented by the code itself (monotonic cursor, single-use hand-off) and the leak is tracked in three places, but an untracked residual is confirmed: the promised fsck backstop does not exist — runFsck never lists gc/, so an orphaned generation prefix is both unreclaimed and invisible. — CA/Gc/CasGc.cpp:2456-2500
  • CAS-075 The blob body is durable before its meta marker and no GC phase enumerates bodies, so a crash in the window leaves a body reclaimed by no sweep at all and excluded from the clean() verdict · LEAK — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The "durable body then .meta" ordering and the absence of any blobs/ 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 as unaccounted on all three fsck surfaces. — CA/Pool/CasPartWriteTxn.cpp:423-429
  • CAS-076 GC seals a generation before committing gc/state, so repeated crashes accumulate complete-looking seals for generations no state references and nothing prunes them · LEAK — resolution: 🚫 not-a-bug (Filimonov 2026-08-21) · Code shape correct (the fold seal is written before the single gc/state CAS) but the consequence is false on HEAD: the wholesale gc/gen// prefix prune is the one regular collector of all attempts, including unaccepted ones, and it is pinned by tests. — CA/Gc/CasGc.cpp:2254
  • CAS-077 A permanently lost node pins its own manifest debris as unreclaimable — the sweep needs the owning mount lease for a watermark floor, and the only verb that removes the lease also erases its data · LEAK — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The retention is real: only the dead node's own mount floor grants deletion rights, and an unreclaimed slot's min_active/writer_epoch stop 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 Update README.md #9. — CA/Gc/CasOrphanManifestSweep.cpp:373-387
  • CAS-078 The namespace janitor rewinds its durable cursor to the beginning on a transient LIST failure, so one 5xx discards all prior progress on a janitor already limited to one page per round · LEAK — resolution: 🔴 still-present (Filimonov 2026-08-21, P3) · The namespace janitor cursor really does rewind on any LIST error and it is pinned by a test, but this is only delayed reclamation (loud, and counted by fsck), not data loss. — CA/Gc/CasNamespaceJanitor.cpp:22-31
  • CAS-079 Ref-object trimming is starved by any concurrent catalog mutation anywhere in the pool — one shared catalog object means any CREATE/DROP aborts all ref-log and snapshot trimming for the round · LEAK — resolution: 🔴 still-present (Filimonov 2026-08-21, P2) · GC ref-cleanup revalidation really does require token stillness of the whole pool-global catalog and bails out of the entire phase on failure — a live remnant of the class already removed for the ref writer in commit 684161d. — CA/Gc/CasGc.cpp:2320-2336
  • CAS-080 A snapshot published without a checkpoint advance is not re-driven on a quiescent namespace, so the namespace pays full log replay on every recovery until the table is written again · CORRECTNESS — resolution: 🚫 not-a-bug (Filimonov 2026-08-21) · The finding's key claim (the publish driver is called only from the write path) is false on HEAD: the publish trigger also sits on every read path, and after recovery the tail counters are above the threshold again, so the very first read or write drives _ckpt to a new snapshot. — CA/Pool/CasRefLedger.cpp:2937-3035
  • CAS-081 S3 staging residue is retained on abort and swept only for one's own server_root_id, so whole part-file plaintext persists after any killed INSERT and no shipped tool ever lists it · LEAK — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Code shape correct and holding the staging object on abort is deliberate design (it is the resurrect source for promote), but the attributed consequences are partly false: SYSTEM CAS DROP POOL MEMBER drains 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. — CA/ContentAddressedTransaction.cpp:148-172
  • CAS-082 There is no multipart-upload hygiene anywhere in CAS and capability-probe debris is explicitly excluded from the residual scan — incomplete uploads are billed, invisible to fsck and never aborted · LEAK — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Code shape correct (CAS keeps no multipart accounting of its own, _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. — CA/Backend/CasProbe.cpp:20-32
  • CAS-083 Lightweight DELETE and mutations free the deleted rows' bytes only for rewritten files — surviving entries are republished against the same blobs, so deleted row bytes stay in the pool · LEAK — resolution: 📐 by-design (Filimonov 2026-08-21) · Code shape confirmed on HEAD (hardlink/repoint carry the same BlobRef) but that is CAS dedup itself and exactly what ordinary MergeTree does on a lightweight DELETE; the "won't fix" position stands. — CA/ContentAddressedTransaction.cpp:782-829
  • CAS-084 Reclaimed blobs are never evicted from the node-local filesystem cache, so after GC deletes a blob its bytes stay on local disk: capacity is held and deleted content is still readable · LEAK — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Real mechanism — CA deletions bypass the caching object storage, so file-cache entries for GC-reclaimed blobs are not invalidated — but "capacity held forever" and "deleted content is readable" are overstated: the cache is necessarily bounded and LRU-evicted, and content-addressed keys mean a stale entry physically cannot return foreign or wrong bytes. — src/Disks/DiskObjectStorage/DiskObjectStorageCache.cpp:21-23
  • CAS-085 always_use_copy_instead_of_hardlinks=1 makes every CAS clone and mutation throw NOT_IMPLEMENTED and nothing rejects the setting, breaking mutations, FREEZE and ATTACH/REPLACE PARTITION · FEATURE-GAP — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Core correct and reachable: with always_use_copy_instead_of_hardlinks=1 mutations and single-disk partition clones on a CA table fail with a loud NOT_IMPLEMENTED and the setting is never rejected; but FREEZE, the BACKUP/RESTORE clone and implicit zero-copy do not reach this path, and the refusal is fail-closed with no data corruption. — CA/ContentAddressedTransaction.cpp:363-366
  • CAS-086 IDisk directory and metadata queries deviate from the contract their generic callers rely on, so removeDirectory's non-empty guard never fires and a populated part directory is dropped without error · CORRECTNESS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · All five shapes exist and nearly all are the deliberate, tested CA contract (a part dir is one atomic ref); the claimed consequences are either unreachable (getHardlinkCount — zero-copy is off for CAS) or already closed (unlinkFile no longer stays silent), leaving one narrow residual: a repeated FREEZE WITH NAME silently merges the shadow ref instead of raising DIRECTORY_ALREADY_EXISTS. — CA/ContentAddressedMetadataStorage.cpp:1293-1305
  • CAS-087 The part-path parser lets a component named detached/moving outrank part-dir detection, and silently reinterprets unclassified part dirs as table-level files, so misclassification surfaces only on read · CORRECTNESS — resolution: 📐 by-design (Filimonov 2026-08-21, P3) · Both parsing orders are real but this is an accepted, test-pinned limitation of a purely string-based parser, reachable only for a deprecated Ordinary database: in the Atomic layout the uuid anchor always wins; only a missing fail-closed refusal on a reserved name remains. — CA/Parts/PartPathParser.cpp:140-162
  • CAS-088 resurrect is an unconditional, budget-free, fence-unchecked overwrite that returns a token it did not write, so two writers on one condemned blob can each believe they own the object · INTEGRITY — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The unconditional resurrect is deliberate design with three structural justifications; "fence-unchecked" is wrong (the check exists since 1fe585e and is test-pinned) and "returns a token it never wrote" was closed by 9b887ac; only stale putOverwrite comments and test names remain. — CA/Backend/CasObjectStorageBackend.cpp:814-856
  • CAS-089 The blob envelope is written but never read back: the offset comes from pool meta and its identity field is silently truncatable, so any divergence in envelope length goes undetected · INTEGRITY — resolution: 📐 by-design (Filimonov 2026-08-21, P3) · Code shape correct (offset from pool meta, the envelope decoder only calls INSPECT) but an envelope-length divergence is structurally unreachable, identity is not stored in the envelope, and the version is gated pool-wide via min_reader_generation; the INTEGRITY consequence does not hold. — CA/Pool/CasManifestReader.cpp:137-144
  • CAS-090 Encryption interactions: SSE-C breaks staging promotion, CAS manifest metadata stays plaintext, AES-CTR carries no MAC, and immutable shared blobs make re-keying a pool impossible · SECURITY — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · All four code shapes are real but none of the consequences is silent corruption: SSE-C breaks only the optional 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. — src/IO/S3/Client.cpp:1273-1287
  • CAS-091 Layout::checkNamespace accepts . and .. segments unlike every other CAS path validator, so under the auto-selected emulated mode over local object storage the traversal is real on the filesystem · SECURITY — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Real validator divergence — Layout::checkNamespace is 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-validated server_root_id, UUIDs and names are escaped, and a FREEZE backup name goes through escapeForFileName. — CA/Formats/CasLayout.cpp:295-319
  • CAS-092 The write fence and the request it admits are on different clocks, and one cross-node wall-clock liveness gate survives on the decommission epoch mint; both CLOCK_BOOTTIME reads are Linux-only · CORRECTNESS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Only one of three sub-items is real — the non-portable 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. — CA/Pool/CasServerRoot.cpp:196-209
  • CAS-093 The temp text-index directory lives inside the part, publishes a ref before the part exists, and its removeRecursive cleanup is one of the silent no-ops, so the temp ref and its blobs survive · CORRECTNESS — resolution: ✅ fixed (Filimonov 2026-08-21) · Code shape correct but the leak was closed by a targeted dropRefIfPresent in moveDirectory (B183, 2027de8) and covered by a separate gtest (3a32e02), so "the temp ref and its blobs survive" is false on HEAD. — src/Storages/MergeTree/TextIndexUtils.cpp:601-609
  • CAS-094 GC REBUILD refusals and failures are not side-effect free — a refused rebuild reports performed=0 while leaving complete-looking run objects and a fold seal a later round can adopt or reject · INTEGRITY — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Code shape correct — a refusal on a lost CAS does leave a full set of run objects and a valid fold seal — but "the next round will pick it up" and "leaks forever" are wrong; what remains is a false comment, unreported residue and attempt numbering that is not lease-derived. — CA/Gc/CasGc.cpp:2811-2824
  • CAS-095 cas-gc-dryrun is not a preview of the next round and is silently empty in exactly the disaster state it exists for — a damaged gc/state yields preview_deletes=0, as if nothing needed deleting · OBSERV/DAY2 — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The silent preview_deletes=0 on a missing gc/state (and, more sharply, on a vanished adopted seal) is confirmed, but "unreadable gc/state" and "one bad run object" actually raise loud CORRUPTED_DATA rather than staying silent; this is a read-only diagnostic tool. — CA/Gc/CasGc.cpp:3017-3080
  • CAS-096 The rebuild reports almost nothing about the quality of the baseline it blesses — a rebuild that silently drops a parent row and its hold reports performed=1 and nothing else · OBSERV/DAY2 — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Shape correct — REBUILD does not surface dropped plan-row counters — but "reports only performed=1" is false (11 fields, 13 columns) and droppedHolds is not counted in production at all: it is a test adapter. — CA/Gc/CasGc.h:48-63
  • CAS-097 No surface names namespaces or raw keys, and cas-inspect cannot decode 8 of the 18 CAS formats, mis-decodes _files/ names and requires a raw key no shipped command can enumerate · OBSERV/DAY2 — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Format coverage and the loss of hold in 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. — CA/Tools/CasInspect.cpp:517-576
  • CAS-098 GC health is process-local and ephemeral, GC STOP is node-local and unobservable, and last_success_age_seconds=0 means both "never led" and "succeeded just now" · OBSERV/DAY2 — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Four of five claims hold on HEAD (0 means both "never led" and "just succeeded", is_leader=0 does not separate a stopped GC from a follower, pending_reclaim does not clear spared/replaced); the fifth (a clamp counter "firing every round") is stale in code and survives only in the ProfileEvent description text. — CA/Gc/CasGcScheduler.cpp:312-327
  • CAS-099 Rolling restart and planned node removal have no quiesce, drain or leadership-handoff verb — the only shutdown is "stop and join", so a rolling restart drops GC leadership abruptly · OBSERV/DAY2 — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The headline ("the only shutdown is stop-and-join", no ordered node-removal path) is factually wrong: ~Pool drains the ref lanes and writes a certified farewell, and SYSTEM CAS FORGET/GC STOP are the quiesce verbs; only a minor residual remains — the durable gc/state lease is not released on a clean stop, and it is already tracked. — CA/Gc/CasGcScheduler.cpp:67-79
  • CAS-100 fsck skips whole check families yet still reports a clean, non-partial result — source-edge checksums are never verified on a healthy pool and the stale-edge finding is unreachable from SQL · OBSERV/DAY2 — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · All four code shapes hold but the consequences are overstated: the skipped check families are declared cost decisions with compensating mechanisms; the only real residual is that the report has no machine-readable coverage flag, so "0 because unchecked" is indistinguishable from "0 because clean". — CA/Tools/CasFsck.cpp:654
  • CAS-101 GC round counters are derived from budget-truncated logs and phase observability reports constants and the wrong round, so after a large DROP the round undercounts real deletes · OBSERV/DAY2 — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Confirmed that the round counters are computed from a budget-truncated outcome log and that GcFoldBegin/GcFoldEnd carry the previous round's number — but the deletions themselves are counted exactly (entries_redeleted) and round=0 on phase rows is documented design (correlate by round_id), not a defect. — CA/Gc/CasGc.cpp:652-690
  • CAS-102 11 of the 156 CAS ProfileEvents can never fire and server-root I/O is counted as GC I/O, so lease and mount request volume cannot be measured or alerted on · OBSERV/DAY2 — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Both facts hold (the CasNs::Server row is unreachable, server-root keys classify as Gc) 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 in system.events. — CA/Backend/CasInstrumentedBackend.cpp:109-122
  • CAS-103 Savings and outcome counters are incremented before the outcome they claim is decided, so dedup-savings metrics count bytes that were in fact uploaded, over-reporting the headline value of the feature · OBSERV/DAY2 — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Code shape correct — CASBlobBodyPutAvoided/CASBlobDeduplicationCacheHit are incremented before observeAndAdmit and 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 except ABORTED. — CA/Pool/CasPartWriteTxn.cpp:155-175
  • CAS-104 Audit-event dispatch funnels read and write hot paths through one mutex and the shipped config enables the sink by default, so every ref resolve and manifest read serializes on that mutex · PERF/SCALE — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Code shape correct (one mutex-guarded dispatcher per pool, the sink enabled by shipping config) but the claimed "every resolve and every manifest read serializes on one mutex" does not hold; the single residual is that with the <cas_log> section removed the sink is still installed, so every event is built and discarded. — CA/Pool/CasEventDispatcher.cpp:17-44
  • CAS-105 The whole mount/lease/request budget and seven pool-level caps are unreachable from configuration, so none of the scaling problems in this report can be tuned and the validator that exists is dead code · CONFIG — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The missing config surface for the lease and request budget and some pool caps is confirmed, but "the validator is dead code that never fires" is wrong: validateCasRequestBudget runs on every writable mount and is covered by tests; ref_table_cache_bytes duplicates the already-tracked CAS-053. — CA/ContentAddressedSettings.cpp:29-58
  • CAS-106 The non-CAS config key allowlist is a fixed 18-entry set, so ordinary object-storage keys (connect_timeout_ms, max_connections, SSE keys) abort disk registration rather than being passed through · CONFIG — resolution: ↗ split-out (CAS: object-store connections exhaust the container's ephemeral ports, fencing out the mount lease #2243) (Filimonov 2026-08-21, P1) · The enumerated non_cas_keys skip-set rejects almost every legal S3 disk key and the server fails to start with UNKNOWN_SETTING; side claims (18 entries, unchecked numeric ranges) are inaccurate. — CA/ContentAddressedSettings.cpp:23-27
  • CAS-107 No CAS setting can be changed by config reload, the ignore is silent, and a removed CAS disk keeps its mount lease held so the slot cannot be reclaimed by anyone else · CONFIG — resolution: 🔴 still-present (Filimonov 2026-08-21, P2) · ContentAddressedMetadataStorage does not override applyNewSettings, so no CAS setting is applied on SYSTEM RELOAD CONFIG and nothing is logged; removing a disk from the config yields only a generic warning while the mount lease keeps renewing until restart. — src/Disks/DiskObjectStorage/MetadataStorages/IMetadataStorage.h:340-343
  • CAS-108 Dead code and test-only seams are compiled into the production binary — fault-injection surfaces link into clickhouse-server and a process-global unsynchronized test hook is read racily on every part commit · TEST-GAP — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The code shapes are real (a test-only backend links into dbms, ShardReducer/manifestCleanupShard have no production callers, a test seam drops a gate) but the racily-readable global hook is unreachable in production, "the gc_shards > 1 path has no callers at all" is wrong, and both real residuals are already tracked. — CA/Backend/CasInMemoryBackend.{h,cpp}
  • CAS-109 There is no deterministic crash-at-step-N harness and settings validation has one fail-closed test for the whole surface, so every crash-consistency window in this round is uncovered · TEST-GAP — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The missing deterministic crash-at-step-N harness half is correct and already tracked; the "validation of 29 settings is practically uncovered" half is factually wrong — the coverage exists, it is just spread across three files. — src/Disks/tests/gtest_cas_settings.cpp
  • CAS-110 resolveRef's allow_stale is plumbed through two layers and silently discarded, so callers that explicitly ask for a stale-tolerant resolve get the strict path and can block on recovery or throw · CORRECTNESS — resolution: 📐 by-design (Filimonov 2026-08-21, P3) · Code shape correct — allow_stale really 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. — CA/Pool/CasRefLedger.cpp:214-215
  • CAS-111 The per-namespace 64 MiB ref-table admission cap fails writes permanently and non-retryably at roughly 610k refs, reached by ordinary part accumulation on a wide-partitioned table · PERF/SCALE — resolution: 🔴 still-present (Filimonov 2026-08-21, P2) · The 64 MiB ceiling on an encoded snapshot or removal transaction imposes a hard per-table ref limit (~0.6-0.9 M) with growing writes refused; it is a loud fail-closed refusal before any object is created, and it is already tracked. — CA/Pool/CasRefLedger.cpp:2161-2169
  • CAS-112 Every ref append re-reads and linearly rescans the pool-global ref catalog — at least two full catalog GETs plus linear scans per part commit, scaling in the number of namespaces in the pool · PERF/SCALE — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P2) · Every committed positive ref chunk really does a fresh uncached GET plus a full decode of the pool-global cas/ref_catalog with 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. — CA/Pool/CasRefCatalog.cpp:109
  • CAS-113 Encoded-size caps are validated only after the oversized buffer has been built, so hitting a 256 MiB object cap requires first materializing 256 MiB — a memory spike rather than an early rejection · PERF/SCALE — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The code shape holds only for the manifest (kMaxManifestEncodedBytes checked after encodePartManifest) 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. — CA/Pool/CasPartWriteTxn.cpp:541-544
  • CAS-114 Recovery must seal every skipped writer epoch one at a time, so first touch of an idle table costs O(mount generations) sequential durable write pairs with no cap · PERF/SCALE — resolution: 🔴 still-present (Filimonov 2026-08-21, P2) · Recovery closes every skipped writer epoch with its own durable pair; 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. — CA/Pool/CasRefLedger.cpp:740-824
  • CAS-115 Cache weight functions under-account their entries — PartManifestWeight misses ~2x of the per-entry footprint and DedupWeight returns a constant 64 (~3.1x under), so both byte budgets over-admit · PERF/SCALE — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The DedupWeight half is right (a constant 64 against a real ~176 bytes per entry, plus NO_MAX_COUNT); the PartManifestWeight half is overstated — the dominant inline_bytes.size() term is accounted honestly and there is a separate 16384-entry limit. — CA/Pool/CasManifestReader.h:49-58
  • CAS-116 Staging is quadratic in the number of files in a part — every staging mutation rescans all prior staged entries and moveDirectory re-scans the destination set once per source entry · PERF/SCALE — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Shape confirmed — staging is a vector with no path index, every mutation rescans it, and moveDirectory scans the destination nestedly — but the consequence is overstated: a few million short-string comparisons against one PUT per file on the same path. — CA/ContentAddressedTransaction.cpp:510
  • CAS-117 CAS stores one object plus one meta object plus a 256-byte envelope per part file, so a wide part of small files is dominated by per-object overhead in request count and stored bytes · PERF/SCALE — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Mechanics correct — every blob file really carries a body, a .meta sibling 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. — CA/Pool/CasManifestReader.cpp:133-144
  • CAS-118 No cache can serve a read without a network round trip and one logical read resolves the ref several times — head() runs before the cache probe and resolve() before the view-cache probe · PERF/SCALE — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The code shapes are real (a mandatory HEAD before probing the decode cache, a resolve before 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 — resolveRef is purely in-memory and a warm CachedForLoad hit never reaches readManifestShared; the real residual is duplicated parse+route+getView per file open. — CA/Pool/CasManifestReader.cpp:56-78
  • CAS-119 Throttling amplification: conditional writes use a single-attempt client, so retry moves from the jittered SDK to CAS's un-jittered fixed backoff up to 16 times, each adding a resolution GET · PERF/SCALE — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The single-attempt client for conditional writes and the 16 attempts are confirmed deliberate design with a fail-closed mount gate, but "fixed backoff" is wrong (capped exponential 200 ms→5 s), "SDK jittered" is wrong (default jitter_factor = 0) and "the client pushes harder than throttling asked" is inverted; the residuals are missing jitter on upload fan-out and the lane's exclusion from disk-wide pacing. — CA/Backend/CasObjectStorageBackend.cpp:628-639
  • CAS-120 There is no relink fast path for local moves inside one CAS pool and the CAS copy is serial — every CAS destination goes through a plain recursive readFile/writeFile loop, even CAS-to-CAS on one pool · PERF/SCALE — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Code shape confirmed (a CA destination always goes through a byte-by-byte sequential readFile/writeFile loop, 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 source GET plus restaging. — src/Storages/MergeTree/DataPartStorageOnDiskBase.cpp:702-718
  • CAS-121 A CAS table outside an Atomic database cannot be backed up at all, and every CAS backup re-reads every byte because areBlobPathsRandom() returns false and forces checksums FromReading · FEATURE-GAP — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The BACKUP refusal via temporary hardlinks is confirmed but is a deliberate fail-closed gate reachable only for a deprecated Ordinary database; the second half ("every BACKUP re-reads all bytes because areBlobPathsRandom()==false") is factually wrong — part files have a precalculated checksum and it is verified before the FromRemotePath path. — src/Storages/MergeTree/DataPartStorageOnDiskBase.cpp:417-422
  • CAS-122 The !-prefixed critical-key escape hatch has no producer and strict formats report an additive field as data corruption, routing an operator to fsck/rebuild instead of a rollback · COMPAT — resolution: ❌ wontfix (Filimonov 2026-08-21, P3) · Duplicate of CAS-042. Both quotes hold on HEAD, but "an additive field reads as CORRUPTED_DATA" is unreachable (checkCompatibility on v fires first) and the producerless ! key is a deliberately dormant compat plane of the pre-release recreate-only policy. — CA/Formats/CasTextFormat.cpp:240-242

🟡 Low / hardening

  • CAS-123 There is no byte accounting outside blobs/ and no reclaim-forecast surface, so a bucket-versus-table gap cannot be attributed to manifests, ref logs, snapshots, staging or generations · OBSERV/DAY2 — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Byte accounting really is limited to blob bodies (plus one off-topic namespace_janitor_pending_bytes counter) and there is no reclaim-forecast surface; previewDeletes does mix physical and logical size in one column, but "counts entries it will not delete" is overstated — they are flagged in the reason column and there is no double counting. — CA/Tools/CasFsck.cpp:578-596
  • CAS-124 Empty content hashes to the all-zero digest, which is also the sentinel fsck substitutes for an unparsable key, so a legitimate zero-length blob and a parse failure are indistinguishable · INTEGRITY — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Real shape (empty content under cityhash128 yields a zero digest that collides with BlobRef{}) but the consequence is confined to one classification line in fsck and does not affect data. — CA/Primitives/CasBlobDigest.h:41
  • CAS-125 Xxh3Streamer dereferences a null state in its constructor, so the allocation-failure guard is dead and blob_hash=xxh3-128 under memory pressure faults instead of raising CANNOT_ALLOCATE_MEMORY · CORRECTNESS — resolution: 🚫 not-a-bug (Filimonov 2026-08-21) · False claim: XXH3_128bits_reset checks for NULL and returns XXH_ERROR, there is no dereference, and the valid() guard is live and working. — CA/Primitives/CasXxh3Streamer.h:17
  • CAS-126 The write-fence pre-check exists only on the S3 staging path, so on the default local backend a writer whose fence was lost mid-write streams the whole body and discovers the loss only at commit · INTEGRITY — resolution: 📐 by-design (Filimonov 2026-08-21) · The asymmetry is real but it is not a pre-check: the callback gates a durable effect before sink->finalize(), and the local path has no such effect (only a private scratch file) whose real write is fenced anyway. — CA/ContentAddressedTransaction.cpp:607-622
  • CAS-127 Avoidable per-byte and per-line copying and allocation on hot paths — three full copies per byte on blob writes, byte-at-a-time readLine with no reserve, and repeated substring searches per request · PERF/SCALE — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The code shapes are real but the consequences are overstated: "three copies per byte" holds only for xxh3/sha256 (the default cityhash128 does two) and five substring searches per request are negligible beside the network RTT; the only open residual is a byte-wise readLine allocating per record on decode paths. — CA/ContentAddressedTransaction.cpp:1220-1235
  • CAS-128 Inline entries staged into a destination part that never gets a build fail the whole commit closed — writing a small metadata file into part A then moving it into an unwritten part aborts the transaction · CORRECTNESS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · A real shape of the same class as the already-fixed f7539af (the Inline branches of createHardLink/moveFile create no destination PartWriteTxn), but there are no production callers and the outcome is a loud fail-closed LOGICAL_ERROR before any publish, not data loss. — CA/ContentAddressedTransaction.cpp:800-812
  • CAS-129 The writer-epoch fence on a build is checked only at entry to promote, not at the durable append, so a build admitted under epoch E1 can append on a fresh runtime after a self-remount to E2 · CONCURRENCY — resolution: 🚫 not-a-bug (Filimonov 2026-08-21) · The finding's key facts are wrong on HEAD: requireAlive (with its epoch check) is called from seven call sites, not one, and the durable append itself fails closed on admitted_fence_generation/checkFenceOrThrow plus an in-band EpochSeal; the "90 s window" misreads operation_deadline_ms. — CA/Pool/CasPartWriteTxn.cpp:125-128
  • CAS-130 Mount-lease and epoch identity fields are written but not enforced — doStart writes a literal seq = 1 over the slot claimMount just bumped, and allocateWriterEpoch can return 0 · CORRECTNESS — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · All three code shapes are real but none of the attributed consequences occurs: the mount lease seq is nowhere authoritative (liveness is judged by token stability) and writer_epoch = 0 requires a corrupted cas_epoch object and fails loudly at the first ref-transaction encode; two cheap hardening residuals remain. — CA/Pool/CasServerRoot.cpp:1021
  • CAS-131 Audit-event and cache-counter attribution defects: timestamps and thread_id/query_id are taken on the draining thread, and cache-hit resolves are missing from the log · OBSERV/DAY2 — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The first claim (timestamp, thread_id and query_id taken on the draining thread) is true on HEAD and was untracked — a real observability defect; skipping RefResolve on a warm cache hit is a documented volume decision (the hit shows in CASPartFolderViewHits), and the double-count/mixed-units claim about view-cache counters is factually wrong. — CA/ContentAddressedMetadataStorage.cpp:431-456
  • CAS-132 Bucket layout, hostnames, PIDs and server UUIDs are disclosed in errors reachable by unprivileged SQL users and via system.cas_mounts, which is readable with SELECT on system · SECURITY — resolution: 🚫 not-a-bug (Filimonov 2026-08-21) · An unprivileged SQL user never reaches these messages (the only throw site is the mount path, every SYSTEM CAS * verb is behind its own GLOBAL privilege, and inline disk() requires the user to bring their own bucket and credentials); literal object paths are a publicly documented layout. — CA/Pool/CasServerRoot.cpp:368-386
  • CAS-133 cas_mounts renders a transient LIST failure identically to a non-existent pool or a not-yet-started disk, so one throttled LIST makes a healthy pool look absent · OBSERV/DAY2 — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · Real shape (a LIST failure collapses into a synthetic row) but the consequence is inverted: the lifecycle column 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. — src/Storages/System/StorageSystemContentAddressedMounts.cpp:146-156
  • CAS-134 A receiver with two CAS pools in one policy advertises only the first, so fetches into the second pool silently transfer every byte instead of relinking, with no diagnostic · FEATURE-GAP — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The code shape holds only for the "no disk passed" branch (which takes the policy's first CA disk), but "silently ... with no diagnostic" is factually wrong — a pool mismatch after reservation is logged at LOG_INFO with both UUIDs, and it degrades to an ordinary byte fetch onto the CA disk with no loss of correctness. — src/Storages/MergeTree/DataPartsExchange.cpp:586-604
  • CAS-135 Emulated mode holds one mutex across multiple round trips per operation and a process-wide mutex across a whole blob body, serializing all CAS-over-local reads, writes and resurrects · PERF/SCALE — resolution: 🔴 still-present (partly) (Filimonov 2026-08-21, P3) · The code shapes are confirmed but the consequence is inverted: emu_mutex is 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; EmulatedSingleProcess is documented as tests and local development only, and the resurrect half is already tracked. — CA/Backend/CasObjectStorageBackend.cpp:491-507

Where to start (Filimonov 2026-08-21)

The four P1 items: CAS-001 (#2212), CAS-040 (🛠 will-fix, pre-release — a projection named with a
newline wedges every GC round pool-wide), CAS-058 (#2173), CAS-106 (#2243). Per-finding reasoning:
#2031 (comment)

carried from prev CAS-### provenance is unreliable. CA/... anchors are stale — locate by symbol.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions