Skip to content

Latest commit

 

History

History
392 lines (255 loc) · 12.3 KB

ARCHITECTURE.md

File metadata and controls

392 lines (255 loc) · 12.3 KB

Cleanup Bootstrap Root Architecture

Purpose

Cleanup Bootstrap Root performs one controlled filesystem transition:

Remove verified filesystem content from a retired Btrfs bootstrap root after the running system has permanently transitioned to a nested snapshot root.

It is a standalone post-transition utility. It does not perform the transition, create snapshots, select snapshots, modify boot configuration, or manage BootPrep.


System Model

The utility expects the following state:

Btrfs filesystem
├── <retired-root>
│   ├── retired bootstrap-root content
│   └── .snapshots
│       └── <number>/snapshot
│           └── current running root
└── other discovered subvolumes

The current root must match:

<retired-root>/.snapshots/<number>/snapshot

where <number> contains only decimal digits. The derived retired-root path must contain only safe components and cannot itself be nested beneath another .snapshots directory. The running snapshot must also be the Btrfs default subvolume.

The cleanup target is the derived <retired-root>.


Architectural Boundary

Cleanup Bootstrap Root owns:

  • Environment validation.
  • Retired-root derivation and temporary mounting by numeric subvolume ID.
  • Filesystem and subvolume discovery.
  • Comparison of the bootstrap root with the running snapshot.
  • Protection and retention policy.
  • Construction of typed deletion queues.
  • Optional execution of the verified cleanup plan.
  • Post-operation policy verification.
  • Temporary mount cleanup.

It does not own:

  • Snapshot creation or deletion.
  • Migration into a snapshot.
  • Selection of the default subvolume.
  • Snapper configuration.
  • GRUB or EFI configuration.
  • BootPrep installation or operation.
  • Cleanup inside separate Btrfs subvolumes.

Safety Invariants

The script establishes these invariants before analysis:

  1. The process is running as root.
  2. The root filesystem is Btrfs.
  3. The current filesystem root matches <retired-root>/.snapshots/<number>/snapshot.
  4. The derived retired-root path passes component and nesting validation.
  5. The running root's numeric Btrfs ID equals the default subvolume ID.
  6. The filesystem source resolves to a block device.
  7. The derived retired root resolves to exactly one numeric subvolume ID.
  8. The corresponding <retired-root>/.snapshots subvolume exists exactly once.
  9. Every temporary mount uses that numeric ID and resolves to the expected filesystem-root path.

Failure of any invariant terminates the program.

The operating mode must also be explicit:

--dryrun
--execute

No argument is treated as a usage error.


Runtime State

The script maintains discovered state in scalar variables, indexed arrays, and associative maps.

Environment state

  • DEVICE
  • ROOT_FSTYPE
  • CURRENT_SOURCE
  • CURRENT_SUBVOL
  • CURRENT_SUBVOL_ID
  • DEFAULT_SUBVOL_ID
  • BOOTSTRAP_ROOT
  • BOOTSTRAP_SUBVOL
  • BOOTSTRAP_SUBVOL_ID
  • MOUNTED_BOOTSTRAP_SUBVOL
  • SNAPSHOT_STORE_SUBVOL

Discovery maps

Map Purpose
SNAPSHOT_MAP O(1) lookup of objects in the running snapshot
BOOTSTRAP_MAP O(1) lookup of objects in the retired root
SUBVOLUME_PATH_MAP Actual subvolume paths nested beneath the derived root
SUBVOLUME_PARENT_MAP Parent directory entries required to reach nested subvolumes
RETAINED_MAP Bootstrap objects not proven safe for deletion

Policy groups

  • KEEP_BOOT
  • KEEP_SNAPSHOTS
  • KEEP_SUBVOLUME
  • RETAINED_OBJECTS
  • DELETE_CANDIDATE

Execution queues

  • DELETE_LINKS
  • DELETE_FILES
  • DELETE_SOCKETS
  • DELETE_DEVICES
  • DELETE_DIRS
  • DELETE_OTHER

Retired-Root Derivation

The script reads the current root from findmnt -no FSROOT /, removes a single leading slash, and accepts only the exact nested-snapshot form.

@/.snapshots/8/snapshot             -> @
@rootfs/.snapshots/17/snapshot      -> @rootfs
@btrfs/.snapshots/204/snapshot      -> @btrfs
systems/root/.snapshots/3/snapshot  -> systems/root

Empty names, nonnumeric snapshot identifiers, suffixes after snapshot, unsafe components, repeated separators, and snapshot-of-snapshot paths are rejected.

The derived path is resolved to one numeric Btrfs subvolume ID. Mounting by ID avoids ambiguity from mount-option formatting, and checking the mounted FSROOT proves that the intended retired root was selected.


Mount Lifecycle

The temporary mount point is created with:

/run/bootstrap-root.XXXXXX

The mount lifecycle begins in both modes with a read-only mount:

Create temporary directory
        |
        v
Mount derived retired root read-only by numeric ID
        |
        v
Discover, compare, classify, and build execution queues

From that point, the lifecycle depends on the selected mode:

  • In --dryrun, the retired root remains mounted read-only while queue processing and directory removal are simulated.
  • In --execute, it is unmounted, remounted read-write by the same numeric ID, and verified again before the queues are processed.

After queue processing, both modes mount the same numeric ID read-only for final policy verification and then rely on the exit trap to unmount it and remove the temporary directory.


Discovery Pipeline

Active snapshot discovery

The running snapshot is scanned with find -xdev.

The -xdev boundary prevents traversal into other mounted filesystems.

Every relative path is indexed in SNAPSHOT_MAP.

Bootstrap-root discovery

The retired root is mounted separately and scanned with find -xdev.

Every relative path is indexed in BOOTSTRAP_MAP.

Subvolume discovery

The script discovers and reports every path returned by btrfs subvolume list /.

Only subvolumes beneath the exact derived root prefix can appear within the retired bootstrap-root namespace. For each of those subvolumes, the script protects:

  • The subvolume path.
  • Every parent directory entry required to reach it.
  • Everything beneath the protected path during classification.

Sibling subvolumes outside the derived root are beyond the cleanup namespace and cannot be modified. This prevents cleanup from crossing any Btrfs subvolume boundary it can reach.


Comparison Model

Every bootstrap-root object is compared with the running snapshot.

An object is treated as matched when:

  • The same path exists in the running snapshot; or
  • The object is absent from the snapshot but its immediate parent still exists there.

Objects absent from the snapshot whose immediate parent is also absent are placed in the retained map. They represent abandoned or otherwise unproven directory trees and are not deleted automatically.

This comparison is intentionally conservative: absence alone is not sufficient proof that an entire tree should be removed.


Policy Classification

Classification applies ordered rules to every object in BOOTSTRAP_MAP.

Rule 1: Protect boot content

boot and boot/* are always retained.

Rule 2: Protect snapshot content

.snapshots and .snapshots/* are always retained.

Rule 3: Protect subvolumes

Every discovered subvolume within the derived retired-root namespace, its required parent directory entries, and everything beneath its protected path are retained.

Rule 4: Retain unproven objects

Objects in RETAINED_MAP are retained.

Rule 5: Create delete candidates

Only objects that pass all preceding protection and retention rules become delete candidates.

The order matters. A protected object can never fall through into the delete plan.


Execution Planning

Delete candidates are divided by filesystem object type:

Symbolic links
Regular files
Unix-domain sockets
Verified /dev device nodes
Directories
Other

Directories are sorted by path depth, deepest first.

The complete queue count must equal the delete-candidate count. A mismatch terminates the program.

Unix-domain sockets are explicitly recognized because retired roots can contain stale runtime sockets, including GnuPG agent sockets. They are removed with the same non-recursive single-object deletion primitive used for files and links.

Character and block device nodes are accepted only beneath dev/. The stored node and active /dev counterpart must resolve to the same node type and major/minor numbers. A live symlink such as /dev/ptmx is permitted only when it resolves to the matching device within /dev. The comparison is repeated immediately before Phase 1, so a changed or missing counterpart stops the run before deletion begins.

FIFOs, mismatched device nodes, device nodes outside dev/, and unknown special types remain in DELETE_OTHER. A nonempty DELETE_OTHER queue rejects the execution plan.

The verified execution order is:

  1. Symbolic links
  2. Regular files
  3. Unix-domain sockets
  4. Verified device nodes
  5. Empty directories
  6. Final verification

Execution Modes

Dry run

--dryrun performs the complete discovery, comparison, classification, planning, queue-processing, and validation pipeline.

The delete helper returns without calling rm. Directory removal is counted but not performed.

Execute

--execute removes queued symbolic links, regular files, Unix-domain sockets, and verified device nodes with rm --.

Queued directories are processed with rmdir, not recursive removal. A directory remains when it is not empty.

Failures removing links, regular files, sockets, or verified device nodes stop execution. Directory removal is best-effort because retained children may legitimately keep a parent directory nonempty.


Compatibility Model

The architecture depends on verified Btrfs state, not distribution identity.

Version 1.1.0 completed cleanup execution and final validation on Debian, Kubuntu, and TUXEDO OS for Debian/Ubuntu-based coverage and on Manjaro, CachyOS, and EndeavourOS for Arch-based coverage.

Additional @rootfs compatibility testing proved dynamic retired-root derivation and covered persistent character devices in the stored /dev. Dry run, execution, and the subsequent reboot passed with the guarded device-node queue.

Version 1.0.1 previously added an explicit Unix-domain socket queue after CachyOS exposed stale GnuPG sockets in the retired bootstrap root.


Final Verification

After queue processing, the script remounts the derived retired root read-only by numeric ID and builds FINAL_MAP.

In execution mode it verifies:

  • Non-directory delete candidates no longer exist.
  • Every protected boot, snapshot, and subvolume object still exists.
  • Every intentionally retained object still exists.

Directories are excluded from the delete-remaining failure count because they may legitimately remain as parents of protected or retained content.

In dry-run mode, the script reports the planned delete, protected, and retained totals and exits successfully without expecting candidates to be absent.


Failure Behavior

The script uses set -euo pipefail and explicit fail checks.

A failure exits immediately. The exit trap then attempts to:

  • Unmount the temporary bootstrap-root mount.
  • Remove the temporary mount directory.

The utility does not attempt automatic rollback of already completed deletions. Its safety strategy is prevention through explicit mode selection, environment refusal checks, conservative classification, typed queues, pre-execution device verification, and final verification.


Module Sequence

Module Responsibility
1 Environment Verification
2 Mount Bootstrap Root
3 Discover Btrfs Subvolumes
4 Discover Active Snapshot Tree
5 Build Snapshot Lookup Table
6 Build Bootstrap Lookup Table
7 Compare Bootstrap to Active Snapshot
8 Policy Classification
9 Cleanup Verification Report
10 Build Execution Plan
11 Execute Cleanup Plan
12 Final Validation

Each module consumes verified state from the preceding modules.


Project Structure

cleanup-bootstrap-root/
├── ARCHITECTURE.md
├── CHANGELOG.md
├── LICENSE
├── README.md
├── cleanup-bootstrap-root.sh
└── tests/
    ├── test_device_nodes.py
    └── test_root_detection.py

The script is intentionally standalone. No installed runtime, service, plugin, or configuration file is required.


License

The source file declares GPL-3.0-or-later.