Skip to content

fix(downgrade): block during protected array shrink - #2698

Closed
Eli Bosley (elibosley) wants to merge 11 commits into
masterfrom
codex/parity-protected-shrink-downgrade-guard
Closed

fix(downgrade): block during protected array shrink#2698
Eli Bosley (elibosley) wants to merge 11 commits into
masterfrom
codex/parity-protected-shrink-downgrade-guard

Conversation

@elibosley

@elibosley Eli Bosley (elibosley) commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

An OS downgrade can strand a parity-protected array shrink on a release that cannot recover it. This adds a shared OS-level interlock plus a retained downgrade marker and blocks downgrade staging while protected recovery is active, incomplete, or uncertain.

Resolution

  • Acquire the same exclusive /var/run/unraid-os-storage-interlock file lock used by Core before either side checks or publishes its intent/marker.
  • Create /var/run/unraid-os-downgrade exclusively while holding that lock, then inspect the two Core-owned production records:
    • parity-protected-shrink.json
    • parity-protected-shrink.recovery.json
  • Run checked /bin/sync -f /boot/config (syncfs(2)) while still holding the lock, clear PHP stat caches, and reread both records before deciding.
  • Permit downgrade only when both paths are real regular files below the trusted /boot/config directory chain and contain strict, identity-equivalent version-7 records whose literal stage is completed.
  • Fail closed on missing siblings, I/O/stat ambiguity, non-regular files, final or ancestor symlinks, malformed JSON, active or legacy records, torn writes, and identity disagreement.
  • Remove the transient marker and return HTTP 409/503 when protection remains active or durability cannot be proven; do not move boot artifacts.
  • Retain the marker after successful downgrade staging so Core rejects new protected storage work until reboot.

Reviewer considerations

  • The shared flock makes downgrade-marker publication and protected-shrink publication one cross-process decision.
  • The checked sync plus post-barrier reread closes the case where Core's v7 rename is visible but its own durability barrier returned an error before releasing the lock.
  • Recursive directory enumeration distinguishes proven absence from an unclassifiable lstat failure without following symlinks.
  • Rollout order is this WebGUI guard before Core PR 290. This PR does not add an unraidd dependency.

Verification

  • Current PR head: d74e79c44c28c3e3c5b7648faf365a4d750e7ecd.
  • Destructive VM acceptance head: 31cb4ce25b08919b53ea89b7325967a3eba89935; the current-head delta
    extracts the existing rejection boundary for direct testing and makes unknown
    decisions fail closed before boot mutation.
  • PHP syntax checks pass for the guard, downgrade endpoint, and both tests.
  • parity_protected_shrink_guard_test.php and
    downgrade_endpoint_decision_test.php pass.
  • Route coverage proves exact 503/409 mappings, unknown-state fail-closed behavior,
    rejection-side boot-mutation suppression, and ok-only continuation.
  • Guard coverage includes no state, active pairs, recovery-only state, matching v7 completion, post-barrier reread, sync failure, identity mismatch, numeric-looking strict identity, torn JSON, legacy additive completion, directory paths, direct and ancestor symlinks, injected stat ambiguity, marker cleanup, and lock handoff.
  • Core flow 460 passed in 8.7 minutes and the full three-checkpoint flow-465 hard-power matrix passed in 44.6 minutes against the destructive acceptance WebGUI head. The accepted environment was then restored and independently verified clean.

Risk

Low code volume but safety-critical ordering. The shared lock, checked durability barrier, strict two-copy validation, retained downgrade marker, and fail-closed path classification keep rollback decisions conservative.

Summary by CodeRabbit

  • New Features
    • Added parity-protected downgrade fencing with pre-checks for active/completed/recovery proof identity consistency before proceeding.
    • Downgrades are now blocked with translated, user-facing HTTP status/error guidance when safety/durability synchronization fails or proofs are missing/diverge.
    • Added coordinated downgrade marker handling and an exclusive interlock to prevent unsafe concurrent actions.
  • Tests
    • Added end-to-end and decision-mapping PHP tests covering active/completed/recovery scenarios, invalid/torn data, durability barrier handling, and interlock locking behavior.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The downgrade flow now checks interlock availability, concurrent downgrade markers, and parity-protected shrink activity before continuing. A reusable guard module handles filesystem coordination, with tests covering status transitions, marker cleanup, and lock handoff.

Changes

Downgrade protection

Layer / File(s) Summary
Parity-protected shrink guard
emhttp/plugins/dynamix.plugin.manager/include/ParityProtectedShrinkGuard.php
Defines protected-shrink proof paths, detects active files, and provides shared interlock acquisition and release helpers.
Downgrade runtime gating
emhttp/plugins/dynamix.plugin.manager/include/Downgrade.php, emhttp/plugins/dynamix.plugin.manager/include/ParityProtectedShrinkGuard.php
Creates the downgrade marker under an interlock, blocks pending or active operations, and returns HTTP 503 or 409 responses as appropriate.
Guard validation
emhttp/plugins/dynamix.plugin.manager/tests/parity_protected_shrink_guard_test.php
Tests proof-file states, marker publication and removal, status codes, and interlock handoff behavior.

Estimated code review effort: 3 (Moderate) | ~15–30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Downgrade
  participant ShrinkGuard
  participant Filesystem
  Downgrade->>ShrinkGuard: begin downgrade with proof paths
  ShrinkGuard->>Filesystem: acquire interlock and create marker
  Filesystem-->>ShrinkGuard: return marker and lock status
  ShrinkGuard->>Filesystem: inspect protected shrink files
  Filesystem-->>ShrinkGuard: return active or idle state
  ShrinkGuard-->>Downgrade: return guard status
Loading

Poem

A rabbit guards the downgrade door,
While proof files hop across the floor.
Locks say “wait,” markers say “not yet,”
Safe paths keep the system set.
Tests twitch noses, green and bright—
The guarded flow now runs just right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: blocking downgrades during protected array shrink operations.
✨ 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 codex/parity-protected-shrink-downgrade-guard

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.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

🔧 PR Test Plugin Available

A test plugin has been generated for this PR that includes the modified files.

Version: 2026.07.19.0557
Build: View Workflow Run

📥 Installation Instructions:

Install via Unraid Web UI:

  1. Go to Plugins → Install Plugin
  2. Copy and paste this URL:
https://preview.dl.unraid.net/pr-plugins/pr-2698/webgui-pr-2698.plg
  1. Click Install

Alternative: Direct Download

⚠️ Important Notes:

  • Testing only: This plugin is for testing PR changes
  • Backup included: Original files are automatically backed up
  • Easy removal: Files are restored when plugin is removed
  • Conflicts: Remove this plugin before installing production updates
  • Post-merge behavior: This preview stays available after merge until preview storage expires or it is manually cleaned up

📝 Modified Files:

Click to expand file list
emhttp/plugins/dynamix.plugin.manager/include/Downgrade.php
emhttp/plugins/dynamix.plugin.manager/include/ParityProtectedShrinkGuard.php
emhttp/plugins/dynamix.plugin.manager/tests/parity_protected_shrink_guard_test.php

🔄 To Remove:

Navigate to Plugins → Installed Plugins and remove webgui-pr-2698, or run:

plugin remove webgui-pr-2698

🤖 This comment is automatically generated and will be updated with each new push to this PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
emhttp/plugins/dynamix.plugin.manager/tests/parity_protected_shrink_guard_test.php (1)

28-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use double quotes for newline escape sequences.

The string 'stage=prepared\n' uses single quotes, which means \n is interpreted as literal characters \ and n rather than a newline character. While this doesn't impact the is_file() check in this test, it's best to use double quotes if a true newline was intended.

🧽 Proposed nitpick
-file_put_contents($testFiles[2], 'stage=prepared\n');
+file_put_contents($testFiles[2], "stage=prepared\n");
🤖 Prompt for 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.

In
`@emhttp/plugins/dynamix.plugin.manager/tests/parity_protected_shrink_guard_test.php`
at line 28, Update the file_put_contents call in
parity_protected_shrink_guard_test to use a double-quoted string so \n is
written as an actual newline, preserving the existing content and test behavior.
🤖 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.

Nitpick comments:
In
`@emhttp/plugins/dynamix.plugin.manager/tests/parity_protected_shrink_guard_test.php`:
- Line 28: Update the file_put_contents call in
parity_protected_shrink_guard_test to use a double-quoted string so \n is
written as an actual newline, preserving the existing content and test behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8e288f21-1229-4663-b6f8-e20811c64a73

📥 Commits

Reviewing files that changed from the base of the PR and between 369f0b2 and ff17ec2.

📒 Files selected for processing (3)
  • emhttp/plugins/dynamix.plugin.manager/include/Downgrade.php
  • emhttp/plugins/dynamix.plugin.manager/include/ParityProtectedShrinkGuard.php
  • emhttp/plugins/dynamix.plugin.manager/tests/parity_protected_shrink_guard_test.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
emhttp/plugins/dynamix.plugin.manager/tests/parity_protected_shrink_guard_test.php (2)

37-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test the downgrade_pending status.

The test correctly validates the success state but misses an opportunity to verify the downgrade_pending response that occurs when the marker already exists. Testing this state improves coverage of the guard's contract.

🧪 Proposed refactor to add the test case
 assert_same(true, is_file($testMarker), 'A successful downgrade decision must retain its marker.');
+assert_same(
+  'downgrade_pending',
+  parity_protected_shrink_begin_downgrade($testFiles, $testMarker, $testInterlock),
+  'A concurrent or subsequent downgrade attempt must report downgrade_pending.'
+);
 unlink($testMarker);
🤖 Prompt for 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.

In
`@emhttp/plugins/dynamix.plugin.manager/tests/parity_protected_shrink_guard_test.php`
around lines 37 - 38, Extend the test around the existing $testMarker assertions
to cover the downgrade_pending response when the marker already exists. After
establishing the marker, invoke the guard again and assert the returned status
is downgrade_pending before cleanup, while preserving the existing success-state
and marker-retention checks.

64-65: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clean up temporary directories to prevent resource leaks.

The test creates temporary nested directories ($testRoot/unraid/storage and $testRoot/storage) but only cleans up specific files at the end. This leaves dangling directories in the system temp directory after every test run.

Consider replacing the individual file unlink calls with a recursive removal of the entire test root to ensure all temporary artifacts are thoroughly cleaned up.

♻️ Proposed refactor for complete cleanup
-unlink($testFiles[2]);
-unlink($testInterlock);
+exec('rm -rf ' . escapeshellarg($testRoot));
🤖 Prompt for 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.

In
`@emhttp/plugins/dynamix.plugin.manager/tests/parity_protected_shrink_guard_test.php`
around lines 64 - 65, Update the cleanup at the end of the test to recursively
remove the entire temporary test root instead of only unlinking $testFiles[2]
and $testInterlock. Ensure the removal covers all nested directories and files
created under $testRoot, leaving no temporary artifacts after the test.
🤖 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.

Nitpick comments:
In
`@emhttp/plugins/dynamix.plugin.manager/tests/parity_protected_shrink_guard_test.php`:
- Around line 37-38: Extend the test around the existing $testMarker assertions
to cover the downgrade_pending response when the marker already exists. After
establishing the marker, invoke the guard again and assert the returned status
is downgrade_pending before cleanup, while preserving the existing success-state
and marker-retention checks.
- Around line 64-65: Update the cleanup at the end of the test to recursively
remove the entire temporary test root instead of only unlinking $testFiles[2]
and $testInterlock. Ensure the removal covers all nested directories and files
created under $testRoot, leaving no temporary artifacts after the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 43dc4d10-502c-47f3-9f75-f1bdc0fb15f2

📥 Commits

Reviewing files that changed from the base of the PR and between ff17ec2 and 59b2cc0.

📒 Files selected for processing (3)
  • emhttp/plugins/dynamix.plugin.manager/include/Downgrade.php
  • emhttp/plugins/dynamix.plugin.manager/include/ParityProtectedShrinkGuard.php
  • emhttp/plugins/dynamix.plugin.manager/tests/parity_protected_shrink_guard_test.php

@unraid Unraid (unraid) locked and limited conversation to collaborators Jul 20, 2026
@elibosley

Copy link
Copy Markdown
Member Author

Closing as obsolete: the new Core storage surface replaces the legacy WebGUI path, so this PHP downgrade guard is not part of the rollout and must not be shipped. Core owns storage and OS replacement coordination directly.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant