5-phase hardening to prevent silent file-layer collisions (the D-Bus regression class): Phase 1: lint-config-paths.sh + make lint-config in depends.mk Phase 2: CollisionTracker in installer (content-hash comparison) Phase 3: installs manifests in recipe.toml + validate-file-ownership.sh Phase 4: validate-init-services.sh + make validate in disk.mk Phase 5: documentation (AGENTS.md, BUILD-SYSTEM-HARDENING-PLAN.md) Both redbear-mini and redbear-full build and validate clean. 66 declared install paths in base, zero conflicts.
15 KiB
Build System Hardening Plan
Date: 2026-05-03 Status: Implemented Scope: Installer file-layer collision detection, config-layer path enforcement, recipe file-ownership tracking, validation gates, and architectural documentation.
Triggering incident: 40 init service files in config/redbear-*.toml used
/usr/lib/init.d/ paths. The base package installs to the same directory.
Package staging silently overwrote config overrides. The init scheduler blocked
on scheme-type services that were supposed to be overridden to oneshot_async,
preventing D-Bus and 20+ services from ever starting.
Fix applied: Changed all config [[files]] init service paths from
/usr/lib/init.d/ to /etc/init.d/. The init system's config_for_dirs()
BTreeMap gives /etc/init.d/ priority over /usr/lib/init.d/ for the same
filename, so config overrides now survive package installation and take effect
at runtime.
Goal: Prevent this class of silent file collision from recurring by adding build-time detection, installer awareness, and architectural documentation.
Phase 1: Config-Layer Path Enforcement (1–2 days)
Objective: Ensure config [[files]] entries for init services always use
/etc/init.d/ paths. Detect violations at build time.
1.1 Add a build-time lint for init service path violations
Create scripts/lint-config-paths.sh that:
- Parses all
config/redbear-*.tomlfiles - Finds
[[files]]entries withpath = "/usr/lib/init.d/..." - Reports violations with file, line number, and path
- Returns non-zero if any violations found
- Can be integrated into the build as a pre-build step
Why a script, not Rust: Config parsing is already TOML-based and a shell
script with grep/awk is sufficient for this lint. Adding it to the cookbook
Rust tool would require rebuilding the tool for lint-only changes. A script is
cheaper to iterate on and can run without a Rust toolchain rebuild.
Acceptance:
scripts/lint-config-paths.sh # exits 0 when clean, 1 + report when violations found
1.2 Document the init service layer convention
Add to AGENTS.md (project root) a clear rule:
Init service file ownership:
- Packages own
/usr/lib/init.d/— the default service files installed by recipe staging- Config overrides own
/etc/init.d/— override files created by[[files]]entries- The init system's
config_for_dirs()gives/etc/init.d/priority via BTreeMap dedup- Config
[[files]]entries MUST NOT use/usr/lib/init.d/paths for init services
1.3 Add Makefile integration
In mk/config.mk or mk/depends.mk, add a pre-build lint step:
# Lint config files for init service path violations
lint-config:
@scripts/lint-config-paths.sh
# Hook into the build before repo cook
repo: lint-config
Phase 2: Installer Collision Detection (2–3 days)
Objective: The installer detects when a config [[files]] entry would be
silently overwritten by package staging, and warns or errors accordingly.
2.1 Track file provenance during install_dir()
Modify install_dir() in installer.rs to track which layer created each file:
struct InstallTracker {
/// Map from destination path to the layer that created it
files: BTreeMap<PathBuf, FileProvenance>,
}
enum FileProvenance {
ConfigPreInstall, // Created by [[files]] with postinstall=false
Package, // Created by install_packages()
ConfigPostInstall, // Created by [[files]] with postinstall=true
}
Implementation points:
- Before
file.create(&output_dir), record the path and layer - Before
install_packages(), snapshot existing files - After
install_packages(), diff to find new/overwritten files - After postinstall
[[files]], record new files
2.2 Detect and report collisions
During the diff after install_packages():
-
If a file existed from
ConfigPreInstalland was overwritten byPackage:- WARN (default): Print a warning showing the collision
- ERROR (strict mode via
STRICT_COLLISION=1env): Fail the build
-
For init service files specifically (
/usr/lib/init.d/*.service,/etc/init.d/*.service):- Always ERROR: Init service collisions are never acceptable because they silently break the boot sequence
-
For other file types:
- WARN by default: Some collisions may be intentional (e.g., default configs that packages override with versioned copies)
2.3 Collision report format
[COLLISION] /usr/lib/init.d/10_evdevd.service
Created by: config redbear-mini.toml (pre-install)
Overwritten by: package base
Impact: init service override lost
Fix: Change config [[files]] path from /usr/lib/init.d/ to /etc/init.d/
2.4 Implementation location
Patch against recipes/core/installer/source/src/installer.rs:
- New module
src/tracker.rswithInstallTracker - Modify
install_dir()to use tracker - Patch stored in
local/patches/installer/
Acceptance:
- Build with a known collision (revert the /etc/init.d/ fix temporarily) should produce clear error output
- Build with current configs should produce zero collisions
Phase 3: Recipe File-Ownership Manifests (3–5 days)
Objective: Recipes declare what paths they install, enabling build-time conflict detection between packages and between packages and config layers.
3.1 Add optional installs field to recipe.toml
[package]
# Optional: declare what paths this recipe installs into the image
# Used for collision detection and build validation
installs = [
"/usr/lib/init.d/10_evdevd.service",
"/usr/lib/init.d/11_udev.service",
"/usr/bin/evdevd",
"/usr/lib/libevdev.so",
]
This is optional — existing recipes without installs work as before.
New recipes and frequently-updated recipes should declare their installs.
3.2 Build-time ownership registry
The repo cook command builds an in-memory registry:
path → recipe_name
When multiple recipes claim the same path:
- WARN for non-critical paths (shared headers, etc.)
- ERROR for init service paths (
.servicefiles ininit.d/)
3.3 Auto-generation tool
Create scripts/generate-installs-manifest.sh:
- Inspects recipe stage directory after build
- Lists all installed files relative to sysroot root
- Outputs suggested
installs = [...]for recipe.toml - Can be run as
make manifest.<recipe>
3.4 Implementation location
Patch against src/cook/package.rs and recipe parsing in src/:
- Parse
installsfield from[package]section - Build registry during
repo cook --with-package-deps - Check for conflicts before staging
Phase 4: Post-Image Validation Gates (2–3 days)
Objective: After the image is created, validate that init service files match expectations and no config overrides were silently lost.
4.1 Init service validation script
Create scripts/validate-init-services.sh:
# Mount image, inspect init.d directories, validate:
# 1. Every /etc/init.d/*.service file has different content from /usr/lib/init.d/ counterpart
# (if they exist in both — if identical, the override is redundant)
# 2. No /usr/lib/init.d/*.service file was supposed to be overridden but wasn't
# 3. All scheme-type services have corresponding scheme daemons in the image
# 4. Service dependency graph has no missing dependencies
# 5. Service dependency graph has no cycles
Validation checks:
-
Override verification: For each file in
/etc/init.d/, verify it differs from the corresponding/usr/lib/init.d/file (if any). If identical, warn about redundant override. -
Missing override detection: For each config
[[files]]entry targeting/etc/init.d/, verify the file actually exists in the mounted image and matches the config content. -
Scheme service audit: List all services with
type = { scheme = "..." }. For each, verify the scheme binary exists in/usr/bin/. Warn about scheme services that may block the scheduler if the daemon isn't guaranteed to start. -
Dependency cycle check: Parse all service files, build a dependency graph, detect cycles.
-
Missing dependency check: For each
requires/requires_weakentry, verify the referenced target/service file exists.
4.2 Makefile integration
Add to mk/disk.mk:
# Validate init services in the built image
validate-init: $(BUILD)/harddrive.img
@scripts/validate-init-services.sh $(BUILD)/harddrive.img
# Full validation gate
validate: validate-init
@echo "Build validation passed"
4.3 CI integration
No .gitlab-ci.yml exists in the repository yet. When CI is added, include:
validate:
stage: validate
script:
- make validate CONFIG_NAME=redbear-full
- make validate CONFIG_NAME=redbear-mini
The make validate target runs lint-config, validate-init, and validate-file-ownership
in sequence. It requires a built image (harddrive.img) to exist.
Phase 5: Architectural Documentation (1 day)
Objective: Document the file ownership hierarchy, installer ordering, and init system override mechanism so future contributors understand the constraints.
5.1 Update AGENTS.md (project root)
Add a section "Installer File Layering" covering:
-
Layer ordering during
install_dir():Layer 1: Config pre-install [[files]] (postinstall = false) Layer 2: Package staging (install_packages()) Layer 3: Config post-install [[files]] (postinstall = true) Layer 4: User/group creation (passwd, shadow, group) -
Collision implications:
- Layer 2 overwrites Layer 1 silently (same path → last writer wins)
- Layer 3 overwrites Layer 2 (intentional — postinstall overrides)
- For init services, config overrides MUST use
/etc/init.d/(Layer 1 path) so they survive Layer 2 and the init system'sconfig_for_dirs()picks them up via BTreeMap dedup
-
Init system override mechanism:
config_for_dirs(["/usr/lib/init.d", "/etc/init.d"])→ BTreeMap- Same filename:
/etc/init.d/entry overwrites/usr/lib/init.d/entry - This is the intended override path: packages own
/usr/lib/init.d/, configs own/etc/init.d/
5.2 Update BUILD-SYSTEM-INVARIANTS.md
Add new invariants:
Invariant I1: Init Service Path Separation
Config
[[files]]entries that create or override init service files MUST use/etc/init.d/paths. Package-owned service files go in/usr/lib/init.d/. The installer does not detect file collisions between layers.
Invariant I2: Config Override Survival
Any file created by config
[[files]]that must survive package installation MUST use a path that packages do not install to. The init system'sconfig_for_dirs()mechanism provides this for init services via the/etc/init.d/override directory.
Invariant I3: Post-Install is the Override Layer
[[files]]entries withpostinstall = truerun AFTER package installation and are guaranteed to overwrite any package-provided file. Use this for files that must always reflect the config's content regardless of package content. Prefer/etc/directory overrides over postinstall for init services, because postinstall requires all overrides to be explicitly marked and is easy to miss.
5.3 Update local/AGENTS.md
Add a "Build System Safety" section referencing this plan and the invariants.
Implementation Order
| Phase | Duration | Dependencies | Risk | Value |
|---|---|---|---|---|
| Phase 1 | 1–2 days | None | Low | Prevents recurrence immediately |
| Phase 5 | 1 day | None | Low | Knowledge preservation |
| Phase 2 | 2–3 days | Phase 1 | Medium | Catches future collisions |
| Phase 4 | 2–3 days | Phase 1 | Medium | Validates built images |
| Phase 3 | 3–5 days | Phase 2 | Higher | Full ownership tracking |
Recommended execution order: Phase 1 → Phase 5 → Phase 2 → Phase 4 → Phase 3
Phases 1 and 5 are documentation and linting — zero risk, immediate value. Phase 2 is the core installer improvement. Phase 4 adds validation on top. Phase 3 is the most ambitious and can be deferred.
Quick Wins (Do First)
These can be done immediately without any code changes:
-
The fix already applied: All config
[[files]]paths changed from/usr/lib/init.d/to/etc/init.d/— verified working (40 services, D-Bus operational). -
Add lint script (Phase 1.1): ~30 minutes of work.
-
Update AGENTS.md (Phase 5.1): ~1 hour of documentation.
-
Update BUILD-SYSTEM-INVARIANTS.md (Phase 5.2): ~30 minutes.
File Change Summary
| File | Change | Phase |
|---|---|---|
scripts/lint-config-paths.sh |
New — lint for /usr/lib/init.d/ in config files | 1 |
mk/depends.mk |
Add lint-config target | 1 |
AGENTS.md |
Add installer file layering section | 5 |
local/docs/BUILD-SYSTEM-INVARIANTS.md |
Add invariants I1–I3 | 5 |
local/patches/installer/collision-detection.patch |
New — installer collision detection | 2 |
recipes/core/installer/recipe.toml |
Wire collision detection patch | 2 |
scripts/validate-init-services.sh |
New — post-image init validation | 4 |
mk/disk.mk |
Add validate-init target | 4 |
src/cook/package.rs |
Parse installs field from recipe.toml | 3 |
src/recipe.rs (or equivalent) |
Add installs field to recipe struct | 3 |
Scope Boundaries
In scope:
- Init service file path enforcement and collision detection
- Installer file-layer collision detection
- Post-image validation for init services
- Recipe file-ownership manifests (optional field)
- Architectural documentation
Out of scope:
- Init system redesign (scheduler, service types, dependency resolution)
- Package manager changes (pkgar format, dependency resolution)
- Build system Makefile restructuring
- Runtime validation of service startup order
- General file-conflict detection across all filesystem paths (init service paths are the critical path; general detection is Phase 3)
Relationship to Existing Plans
- BUILD-SYSTEM-INVARIANTS.md: This plan adds invariants I1–I3 to the existing surface-ownership model. Phases 1–4 implement enforcement of these new invariants.
- PATCH-GOVERNANCE.md: Unchanged. Patch governance covers source-tree durability; this plan covers installer file-layer collisions — orthogonal concerns.
- CONSOLE-TO-KDE-DESKTOP-PLAN.md: This plan is infrastructure, not a desktop feature. It prevents build-system regressions that could block the desktop path.
- DBUS-INTEGRATION-PLAN.md: The triggering incident was a D-Bus regression caused by init service file collisions. This plan prevents recurrence of the root cause.