Skip to content

fix(server): retract corrupt PCH pairs instead of trusting them - #517

Merged
16bit-ykiko merged 3 commits into
mainfrom
fix/pch-quality-gate
Jul 17, 2026
Merged

16bit-ykiko merged 3 commits into
mainfrom
fix/pch-quality-gate

Conversation

@16bit-ykiko

@16bit-ykiko 16bit-ykiko commented Jul 17, 2026 •

Copy link
Copy Markdown
Member

Background

Compilation artifacts (the PCH and its paired index blob) are validated purely through dependency snapshots: as long as the source files they were built from are unchanged, the blobs are trusted. Nothing ever checks that the bytes on disk are still readable, and a consumption failure was never written back as invalidation. Three consequences:

  • A .pch corrupted on disk (bit rot, partial write, external interference) was trusted for the life of the cache store. The stateful compile failed before parsing, the master treated the incomplete reply as success — published empty diagnostics as current, cleared the dirty flag, recorded an empty deps snapshot that nothing could ever invalidate — and the file went silently dead. Restarts did not heal it.
  • CompileResult had no success signal at all, so any non-result (setup failure, interrupted parse) was settled as a valid product.
  • A corrupt .pch.idx was detected when first opened, but only the in-memory path was cleared. The on-disk pair kept posing as complete, so every later session re-adopted it from the cache metadata and silently served degraded results (no preamble overlay, no preamble links) forever.

Investigating the repro also surfaced a third failure shape: corruption past the validation-covered region can abort the AST reader outright (report_fatal_error in the bitstream reader), killing the worker process. And notably, sparse flips in semantically dead bytes are consumed without any error at all — PCH blobs carry no whole-file checksum — which shaped the test vectors.

Changes

  • CompileResult now carries CompileStatus (Done / Cancelled / SetupFail) plus a pch_suspect flag: the parse failed and its diagnostics blame the consumed PCH — they name the blob's path, or they are AST-deserialization errors (matched by clang's diagnostic category via the new DiagnosticID::is_deserialization_error(), since that family's messages do not reliably carry the path, e.g. malformed or corrupted precompiled file: 'Blob ends too soon') that name no other prebuilt input. A bare setup failure without that attribution deliberately does not blame the PCH: retracting a healthy shared pair over a broken module input or bad invocation would rebuild it on every request.
  • run_compile gains an artifact quality gate: on a pch_suspect reply it retracts the pair (store + in-memory cache) and reruns the round once — ensure_pch misses and rebuilds both halves, so real diagnostics recover within the same request. A worker crash while consuming a PCH retracts the pair too (deep corruption aborts the reader before any diagnostic can anchor); recovery lands on the next request, and a genuinely poisonous document still pays its own quarantine budget.
  • Non-Done replies are no longer settled: the dirty flag stays set, deps/index are not recorded, and the gap is published as versionless empty diagnostics instead of a stale list posing as current.
  • Workspace::preamble_state() is now the single consumption gate for the index blob: when it turns out unreadable, the on-disk pair is retracted as well, so the next ensure_pch (this session or any later one) treats the key as a miss and rebuilds the pair.

Testing

  • Unit: Compiler.CorruptPCHAttributable pins the attribution contract — whole-file garbage and truncation must yield a setup failure or fatal error whose diagnostics blame the blob (path or deserialization category), never a completed parse. Compiler.StopCompilation now also pins the Cancelled status mapping.
  • Integration: test_corrupt_pch_rebuilt_on_restart (parametrized: whole-file garbage → attributable failure healed inline; mid-file flip → may kill the worker, healed on the next request; flips that land in semantically dead bytes are consumed harmlessly and skip), test_corrupt_pch_idx_retracted (pair retracted and rebuilt, valid index blob back on disk), test_setup_fail_keeps_dirty (a non-result with no PCH to blame publishes an honest empty gap and recompiles on the next request instead of settling).
  • All three integration tests verified red on the pre-fix binary, green after the fix. Full suites pass: 1016 unit, 292 integration, 3/3 smoke.

Known residuals

  • The worker-crash retraction path has no deterministic test: whether a given mid-file flip aborts the reader depends on which record it lands in. The integration middle vector exercises it on this LLVM build; the unit test deliberately excludes shapes that would abort the test process.
  • On the non-Done path the previous buffer's file index is retained rather than reset; it is unreachable while the dirty flag is set (all consumers gate on it), matching the existing dispatch-crash path.
  • Index-blob-only corruption now costs a full pair rebuild instead of a degraded-but-served compile; the pair is only usable complete, and the rebuild is a one-time cost at detection.

Summary by CodeRabbit

  • Bug Fixes

    • Improved corrupted precompiled header and index handling: unreadable artifacts are invalidated/retracted and rebuilt, including on restart.
    • Prevented stale or misleading diagnostic settling after setup failures or interrupted compilation.
    • Added stronger retry/quality-gate logic for suspect precompiled artifacts and improved attribution for deserialization-related diagnostics.
  • Tests

    • Expanded integration and unit coverage for PCH/PCH-index corruption recovery, setup-failure “dirty” behavior, cancellation status mapping, and diagnostic blame attribution.

A corrupt .pch or .pch.idx on disk was trusted for the life of the
cache store: deps snapshots are the only invalidation channel, so a
blob the frontend could not read was never written back as invalid.
The document went silently dead (empty diagnostics published as
current, dirty flag cleared) and no restart healed it.

- CompileResult now carries CompileStatus (Done/Cancelled/SetupFail)
  plus pch_suspect: the parse failed and its diagnostics blame the
  consumed PCH — naming its path, or an AST-deserialization error
  (category-based; that family's messages do not reliably carry the
  path) naming no other prebuilt input.
- run_compile gains an artifact quality gate: a pch_suspect reply
  retracts the pair (store + cache) and reruns the round once, so the
  pair is rebuilt and real diagnostics recover within one request. A
  worker crash while consuming a PCH retracts the pair too — deep
  corruption aborts the AST reader before any diagnostic can anchor —
  with recovery on the next request; a genuinely poisonous document
  still pays its own quarantine budget.
- Non-Done replies are no longer settled as success: the dirty flag
  stays set, deps/index are not recorded, and the gap is published as
  versionless empty diagnostics.
- Workspace::preamble_state is now the single consumption gate for
  .pch.idx blobs: an unreadable blob retracts the on-disk pair so
  later sessions rebuild instead of silently degrading forever.
@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0bf4b338-f500-4d77-b416-6733a0641425

📥 Commits

Reviewing files that changed from the base of the PR and between 4d76263 and 1368459.

📒 Files selected for processing (4)
  • src/server/compiler/compiler.cpp
  • src/server/worker/stateful_worker.cpp
  • tests/integration/compilation/test_persistent_cache.py
  • tests/integration/compilation/test_staleness.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/integration/compilation/test_staleness.py
  • src/server/compiler/compiler.cpp
  • tests/integration/compilation/test_persistent_cache.py

📝 Walkthrough

Walkthrough

The change adds deserialization-error classification, explicit compile statuses, centralized PCH state loading, corrupted-artifact invalidation and retry handling, plus unit and integration coverage for PCH corruption and setup failures.

Changes

PCH diagnostics and compile result handling

Layer / File(s) Summary
Diagnostic and compile result contracts
src/compile/diagnostic.*, src/server/protocol/worker.h, src/server/worker/stateful_worker.cpp, tests/unit/compile/compilation_tests.cpp
Deserialization diagnostics are classified by clang category, compile results distinguish completed, cancelled, and setup-failed states, and incomplete PCH compilations can mark the artifact suspect. Cancellation and corrupted-PCH attribution are tested.

PCH cache consumption and recovery

Layer / File(s) Summary
Centralized PCH state consumption
src/server/state/workspace.*, src/server/compiler/compiler.cpp, src/server/service/feature_router.cpp, src/server/service/query.cpp
Preamble state loading is centralized through Workspace::preamble_state; unreadable index blobs retract their corresponding stored PCH pair, and consumers use the accessor.
PCH invalidation and retry orchestration
src/server/compiler/compiler.*
Compiler handling invalidates PCH entries after crashes or suspect results, clears session keys, retries a suspect artifact once, and handles non-completed compile results without normal settlement.

Recovery validation

Layer / File(s) Summary
Persistent-cache and staleness tests
tests/integration/compilation/test_persistent_cache.py, tests/tools/workspace.py, tests/integration/compilation/test_staleness.py
Integration coverage corrupts PCH and index files, verifies rebuilding after restart, and checks that setup failures preserve the recompilation path.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • clice-io/clice#479: Both changes modify compiler PCH lifecycle and worker compilation flow.
  • clice-io/clice#501: Both changes use the paired PCH and PreambleState infrastructure in compiler and workspace code.

Sequence Diagram(s)

sequenceDiagram
  participant StatefulWorker
  participant Compiler
  participant Workspace
  participant PCHStore
  StatefulWorker->>Compiler: Return pch_suspect or non-Done status
  Compiler->>Workspace: Invalidate consumed PCH key
  Workspace->>PCHStore: Retract PCH and index blobs
  Compiler->>StatefulWorker: Retry compilation once
  StatefulWorker-->>Compiler: Return rebuilt compilation result
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: corrupt PCH pairs are retracted rather than reused.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pch-quality-gate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server/compiler/compiler.cpp`:
- Around line 1025-1031: Capture the PCH key associated with params.pch.first
before send_stateful can suspend, rather than reading the mutable
session->pch_key afterward. In the worker-crashed branch and related
diagnostic-based invalidation, use the captured key to retract only the pair
consumed by this dispatch, while preserving the existing empty-key and reset
behavior.
- Around line 1064-1077: Update the suspect-PCH handling in the compile attempt
loop so a suspect pair is always invalidated and session->pch_key is cleared
whenever pch_suspect, a PCH key, and a nonempty params.pch.first are present.
Use artifact_retried only to gate the retry and --attempt/continue path; when
the retry budget is exhausted, fall through to the non-result handling path so
dirty state remains set.

In `@tests/integration/compilation/test_persistent_cache.py`:
- Around line 610-614: Before the pytest.skip branch in the persistent-cache
test, shut down the manually created c2 server and its associated I/O tasks
using the existing cleanup mechanism. Ensure cleanup runs before skipping the
semantically dead mid-file corruption case, while preserving the current skip
behavior.

In `@tests/integration/compilation/test_staleness.py`:
- Around line 637-640: Extend the retry verification around wait_for_recompile
to assert that the retried request publishes no diagnostics for uri, rather than
only confirming recompilation occurred. Preserve the existing client.diagnostics
cleanup and wait, and validate the empty result after the retry completes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a6882c71-4169-4d71-8fe0-cfb9dd65a72c

📥 Commits

Reviewing files that changed from the base of the PR and between 3f1dbde and 7b5bdcd.

📒 Files selected for processing (14)
  • src/compile/diagnostic.cpp
  • src/compile/diagnostic.h
  • src/server/compiler/compiler.cpp
  • src/server/compiler/compiler.h
  • src/server/protocol/worker.h
  • src/server/service/feature_router.cpp
  • src/server/service/query.cpp
  • src/server/state/workspace.cpp
  • src/server/state/workspace.h
  • src/server/worker/stateful_worker.cpp
  • tests/integration/compilation/test_persistent_cache.py
  • tests/integration/compilation/test_staleness.py
  • tests/tools/workspace.py
  • tests/unit/compile/compilation_tests.cpp

Comment thread src/server/compiler/compiler.cpp Outdated
Comment thread src/server/compiler/compiler.cpp Outdated
Comment thread tests/integration/compilation/test_persistent_cache.py
Comment thread tests/integration/compilation/test_staleness.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7b5bdcddd5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/server/compiler/compiler.cpp
The middle-corruption vector intentionally crashes a worker; Debug
builds abort the master on the resulting WorkerCrash anomaly unless
CLICE_ANOMALY_NO_TRAP is set, the same pattern test_crash_recovery.py
uses. Reproduced the Debug CI failure locally (master SIGABRT during
recovery), green after; full Debug integration suite passes.
- Snapshot the dispatched pch key per round: pch_key can be rewritten
  while the send is suspended, and blaming the current key could delete
  an unrelated pair. Reset the session key only when it still matches.
- Retract a blamed pair even after the retry budget is spent (a blamed
  pair never survives); the round then proceeds to publish its real
  fatal diagnostics or the honest empty gap.
- Drop unusable units (neither complete nor fatal) in the stateful
  worker after the reply is built: they can never serve a query but pin
  the consumed artifacts — on Windows a mapped PCH cannot be replaced,
  blocking the rebuild of a retracted pair.
- Tests: shut the second session down before pytest.skip in the
  dead-bytes case; assert the retried non-result stays an empty gap.
@16bit-ykiko
16bit-ykiko merged commit c16f4ac into main Jul 17, 2026
22 checks passed
@16bit-ykiko
16bit-ykiko deleted the fix/pch-quality-gate branch July 17, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant