feat: build system hardening — collision detection, validation gates, init path enforcement
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.
This commit is contained in:
@@ -144,6 +144,51 @@ make all
|
|||||||
- **Syscall ABI**: Unstable intentionally. Stability via `libredox` and `relibc`
|
- **Syscall ABI**: Unstable intentionally. Stability via `libredox` and `relibc`
|
||||||
- **Drivers**: ALL userspace daemons via scheme system. No kernel-space drivers (except serio)
|
- **Drivers**: ALL userspace daemons via scheme system. No kernel-space drivers (except serio)
|
||||||
|
|
||||||
|
## INSTALLER FILE LAYERING
|
||||||
|
|
||||||
|
The installer creates filesystem images in four layers. Understanding this ordering is critical
|
||||||
|
to avoid silent file overwrites.
|
||||||
|
|
||||||
|
### 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). This is the bug class
|
||||||
|
that caused the D-Bus regression: config overrides at `/usr/lib/init.d/` were overwritten by
|
||||||
|
the `base` package staging the same paths.
|
||||||
|
- **Layer 3 overwrites Layer 2** (intentional — postinstall overrides).
|
||||||
|
- For init services, config overrides **MUST** use `/etc/init.d/` so they survive Layer 2.
|
||||||
|
|
||||||
|
### Init Service File Ownership
|
||||||
|
|
||||||
|
- **Packages own `/usr/lib/init.d/`** — 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**
|
||||||
|
- Run `make lint-config` to detect violations
|
||||||
|
|
||||||
|
### Collision Detection
|
||||||
|
|
||||||
|
The installer now includes a `CollisionTracker` (in `collision.rs`) that detects when package
|
||||||
|
staging overwrites config pre-install files. Init service collisions always error. Other
|
||||||
|
collisions warn by default, error in strict mode (`REDBEAR_STRICT_COLLISION=1`).
|
||||||
|
|
||||||
|
### Validation Gates
|
||||||
|
|
||||||
|
After building an image, run `make validate` to verify:
|
||||||
|
- Init service path violations (via `lint-config`)
|
||||||
|
- Override effectiveness and scheme binary existence (via `validate-init-services.sh`)
|
||||||
|
- File ownership conflicts (via `validate-file-ownership.sh`)
|
||||||
|
|
||||||
|
See `local/docs/BUILD-SYSTEM-HARDENING-PLAN.md` for the full plan.
|
||||||
|
|
||||||
## ANTI-PATTERNS (THIS PROJECT)
|
## ANTI-PATTERNS (THIS PROJECT)
|
||||||
|
|
||||||
- **DO NOT** suppress errors with `as any` / `@ts-ignore` — use proper `Result` handling
|
- **DO NOT** suppress errors with `as any` / `@ts-ignore` — use proper `Result` handling
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ include mk/config.mk
|
|||||||
# Build system dependencies
|
# Build system dependencies
|
||||||
include mk/depends.mk
|
include mk/depends.mk
|
||||||
|
|
||||||
all: $(BUILD)/harddrive.img
|
all: lint-config $(BUILD)/harddrive.img
|
||||||
|
|
||||||
# ── Red Bear OS Build Cache (OBLIGATORY) ─────────────────────────────────
|
# ── Red Bear OS Build Cache (OBLIGATORY) ─────────────────────────────────
|
||||||
# Cache sync is a mandatory part of every successful build.
|
# Cache sync is a mandatory part of every successful build.
|
||||||
@@ -229,3 +229,5 @@ wireshark: FORCE
|
|||||||
wireshark $(BUILD)/network.pcap
|
wireshark $(BUILD)/network.pcap
|
||||||
packages-sync: ; @bash local/scripts/sync-packages.sh
|
packages-sync: ; @bash local/scripts/sync-packages.sh
|
||||||
packages-list: ; @ls -la Packages/*.pkgar 2>/dev/null | wc -l && echo "pkgar files in Packages/"
|
packages-list: ; @ls -la Packages/*.pkgar 2>/dev/null | wc -l && echo "pkgar files in Packages/"
|
||||||
|
validate-patches:
|
||||||
|
@bash local/scripts/validate-patches.sh
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ pattern = "i915/adlp_dmc_ver2_16.bin"
|
|||||||
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
|
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
|
||||||
"""
|
"""
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/12_boot-late.target"
|
path = "/etc/init.d/12_boot-late.target"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Late boot services target"
|
description = "Late boot services target"
|
||||||
@@ -508,7 +508,7 @@ pattern = "i915/adlp_dmc_ver2_16.bin"
|
|||||||
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
|
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
|
||||||
"""
|
"""
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/00_driver-manager.service"
|
path = "/etc/init.d/00_driver-manager.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "PCI driver spawner"
|
description = "PCI driver spawner"
|
||||||
@@ -584,7 +584,7 @@ pattern = "i915/adlp_dmc_ver2_16.bin"
|
|||||||
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
|
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
|
||||||
"""
|
"""
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/10_evdevd.service"
|
path = "/etc/init.d/10_evdevd.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Evdev input daemon"
|
description = "Evdev input daemon"
|
||||||
@@ -661,7 +661,7 @@ pattern = "i915/adlp_dmc_ver2_16.bin"
|
|||||||
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
|
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
|
||||||
"""
|
"""
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/15_cpufreqd.service"
|
path = "/etc/init.d/15_cpufreqd.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "CPU frequency scaling daemon"
|
description = "CPU frequency scaling daemon"
|
||||||
@@ -701,7 +701,7 @@ pattern = "i915/adlp_dmc_ver2_16.bin"
|
|||||||
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
|
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
|
||||||
"""
|
"""
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/15_thermald.service"
|
path = "/etc/init.d/15_thermald.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Thermal management daemon"
|
description = "Thermal management daemon"
|
||||||
@@ -741,7 +741,7 @@ pattern = "i915/adlp_dmc_ver2_16.bin"
|
|||||||
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
|
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
|
||||||
"""
|
"""
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/15_hwrngd.service"
|
path = "/etc/init.d/15_hwrngd.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Hardware RNG entropy daemon"
|
description = "Hardware RNG entropy daemon"
|
||||||
@@ -781,7 +781,7 @@ pattern = "i915/adlp_dmc_ver2_16.bin"
|
|||||||
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
|
chain = ["i915/adlp_dmc_ver2_14.bin", "i915/adlp_dmc_ver2_12.bin"]
|
||||||
"""
|
"""
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/13_driver-params.service"
|
path = "/etc/init.d/13_driver-params.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Driver parameter scheme"
|
description = "Driver parameter scheme"
|
||||||
@@ -793,7 +793,7 @@ type = { scheme = "driver-params" }
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/16_redbear-acmd.service"
|
path = "/etc/init.d/16_redbear-acmd.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "USB CDC ACM serial daemon"
|
description = "USB CDC ACM serial daemon"
|
||||||
@@ -805,7 +805,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/16_redbear-ecmd.service"
|
path = "/etc/init.d/16_redbear-ecmd.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "USB CDC ECM/NCM ethernet daemon"
|
description = "USB CDC ECM/NCM ethernet daemon"
|
||||||
@@ -817,7 +817,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/16_redbear-usbaudiod.service"
|
path = "/etc/init.d/16_redbear-usbaudiod.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "USB Audio Class daemon"
|
description = "USB Audio Class daemon"
|
||||||
|
|||||||
+20
-19
@@ -159,7 +159,7 @@ data = "/usr/share/fonts"
|
|||||||
symlink = true
|
symlink = true
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/05_boot-essential.target"
|
path = "/etc/init.d/05_boot-essential.target"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Boot essential services target"
|
description = "Boot essential services target"
|
||||||
@@ -169,7 +169,7 @@ requires_weak = [
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/13_iommu.service"
|
path = "/etc/init.d/13_iommu.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "IOMMU DMA remapping daemon"
|
description = "IOMMU DMA remapping daemon"
|
||||||
@@ -184,7 +184,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/12_dbus.service"
|
path = "/etc/init.d/12_dbus.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "D-Bus system bus"
|
description = "D-Bus system bus"
|
||||||
@@ -200,7 +200,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/13_redbear-sessiond.service"
|
path = "/etc/init.d/13_redbear-sessiond.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Red Bear session broker (org.freedesktop.login1)"
|
description = "Red Bear session broker (org.freedesktop.login1)"
|
||||||
@@ -214,7 +214,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/13_seatd.service"
|
path = "/etc/init.d/13_seatd.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "seatd seat management daemon"
|
description = "seatd seat management daemon"
|
||||||
@@ -230,7 +230,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/13_redbear-keymapd.service"
|
path = "/etc/init.d/13_redbear-keymapd.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Runtime keymap daemon"
|
description = "Runtime keymap daemon"
|
||||||
@@ -244,7 +244,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/13_redbear-ime.service"
|
path = "/etc/init.d/13_redbear-ime.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Input method engine daemon"
|
description = "Input method engine daemon"
|
||||||
@@ -258,7 +258,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/13_redbear-accessibility.service"
|
path = "/etc/init.d/13_redbear-accessibility.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Accessibility input filter daemon (sticky/slow/bounce keys)"
|
description = "Accessibility input filter daemon (sticky/slow/bounce keys)"
|
||||||
@@ -272,7 +272,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/14_redbear-upower.service"
|
path = "/etc/init.d/14_redbear-upower.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "UPower D-Bus service (org.freedesktop.UPower)"
|
description = "UPower D-Bus service (org.freedesktop.UPower)"
|
||||||
@@ -286,7 +286,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/14_redbear-udisks.service"
|
path = "/etc/init.d/14_redbear-udisks.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "UDisks2 D-Bus service (org.freedesktop.UDisks2)"
|
description = "UDisks2 D-Bus service (org.freedesktop.UDisks2)"
|
||||||
@@ -300,7 +300,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/14_redbear-polkit.service"
|
path = "/etc/init.d/14_redbear-polkit.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "PolicyKit1 D-Bus service (org.freedesktop.PolicyKit1)"
|
description = "PolicyKit1 D-Bus service (org.freedesktop.PolicyKit1)"
|
||||||
@@ -314,7 +314,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/19_redbear-authd.service"
|
path = "/etc/init.d/19_redbear-authd.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Red Bear authentication daemon"
|
description = "Red Bear authentication daemon"
|
||||||
@@ -329,7 +329,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/20_display.service"
|
path = "/etc/init.d/20_display.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "KDE session assembly helper"
|
description = "KDE session assembly helper"
|
||||||
@@ -349,7 +349,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/20_greeter.service"
|
path = "/etc/init.d/20_greeter.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Red Bear greeter service"
|
description = "Red Bear greeter service"
|
||||||
@@ -369,7 +369,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/29_activate_console.service"
|
path = "/etc/init.d/29_activate_console.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Activate fallback console VT"
|
description = "Activate fallback console VT"
|
||||||
@@ -384,7 +384,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/30_console.service"
|
path = "/etc/init.d/30_console.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Console terminals"
|
description = "Console terminals"
|
||||||
@@ -399,7 +399,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/31_debug_console.service"
|
path = "/etc/init.d/31_debug_console.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Debug console on serial port"
|
description = "Debug console on serial port"
|
||||||
@@ -409,12 +409,13 @@ requires_weak = [
|
|||||||
|
|
||||||
[service]
|
[service]
|
||||||
cmd = "getty"
|
cmd = "getty"
|
||||||
args = ["/scheme/debug", "-J"]
|
args = ["/scheme/debug/no-preserve", "-J"]
|
||||||
type = "oneshot_async"
|
type = "oneshot_async"
|
||||||
|
respawn = true
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/99_diag_serial.service"
|
path = "/etc/init.d/99_diag_serial.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Serial diagnostic marker"
|
description = "Serial diagnostic marker"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# This fragment is intended to be included by the active desktop/graphics target.
|
# This fragment is intended to be included by the active desktop/graphics target.
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/05_boot-essential.target"
|
path = "/etc/init.d/05_boot-essential.target"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Boot essential services target"
|
description = "Boot essential services target"
|
||||||
@@ -30,7 +30,7 @@ redbear-session-launch = {}
|
|||||||
redbear-greeter = {}
|
redbear-greeter = {}
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/19_redbear-authd.service"
|
path = "/etc/init.d/19_redbear-authd.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Red Bear authentication daemon"
|
description = "Red Bear authentication daemon"
|
||||||
@@ -44,7 +44,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/20_display.service"
|
path = "/etc/init.d/20_display.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Compositor proof (Phase 2: KWin virtual + Qt6 smoke + 60s survival)"
|
description = "Compositor proof (Phase 2: KWin virtual + Qt6 smoke + 60s survival)"
|
||||||
@@ -61,7 +61,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/20_greeter.service"
|
path = "/etc/init.d/20_greeter.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Red Bear greeter service (experimental — Phase 3 user session bring-up)"
|
description = "Red Bear greeter service (experimental — Phase 3 user session bring-up)"
|
||||||
@@ -80,7 +80,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/30_console.service"
|
path = "/etc/init.d/30_console.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Console terminals"
|
description = "Console terminals"
|
||||||
@@ -96,7 +96,7 @@ respawn = true
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/29_activate_console.service"
|
path = "/etc/init.d/29_activate_console.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Activate fallback console VT"
|
description = "Activate fallback console VT"
|
||||||
@@ -111,7 +111,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/31_debug_console.service"
|
path = "/etc/init.d/31_debug_console.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Debug console"
|
description = "Debug console"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
zsh = {}
|
zsh = {}
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/00_base.service"
|
path = "/etc/init.d/00_base.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Base environment setup (tmpdir)"
|
description = "Base environment setup (tmpdir)"
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
# the active redbear-full config provides its own display/console/greeter services.
|
# the active redbear-full config provides its own display/console/greeter services.
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/20_display.service"
|
path = "/etc/init.d/20_display.service"
|
||||||
data = ""
|
data = ""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/29_activate_console.service"
|
path = "/etc/init.d/29_activate_console.service"
|
||||||
data = ""
|
data = ""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/30_console.service"
|
path = "/etc/init.d/30_console.service"
|
||||||
data = ""
|
data = ""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/31_debug_console.service"
|
path = "/etc/init.d/31_debug_console.service"
|
||||||
data = ""
|
data = ""
|
||||||
|
|||||||
+15
-11
@@ -296,7 +296,7 @@ type = { scheme = "ucsi" }
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/12_boot-late.target"
|
path = "/etc/init.d/12_boot-late.target"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Late boot services target"
|
description = "Late boot services target"
|
||||||
@@ -306,7 +306,7 @@ requires_weak = [
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/11_udev.service"
|
path = "/etc/init.d/11_udev.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "udev compatibility shim"
|
description = "udev compatibility shim"
|
||||||
@@ -317,11 +317,11 @@ requires_weak = [
|
|||||||
|
|
||||||
[service]
|
[service]
|
||||||
cmd = "udev-shim"
|
cmd = "udev-shim"
|
||||||
type = { scheme = "udev" }
|
type = "oneshot_async"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/10_evdevd.service"
|
path = "/etc/init.d/10_evdevd.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Evdev input daemon"
|
description = "Evdev input daemon"
|
||||||
@@ -336,7 +336,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/11_wifictl.service"
|
path = "/etc/init.d/11_wifictl.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Wi-Fi control daemon"
|
description = "Wi-Fi control daemon"
|
||||||
@@ -351,7 +351,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/12_dbus.service"
|
path = "/etc/init.d/12_dbus.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "D-Bus system bus"
|
description = "D-Bus system bus"
|
||||||
@@ -366,7 +366,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/13_redbear-sessiond.service"
|
path = "/etc/init.d/13_redbear-sessiond.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Red Bear session broker (org.freedesktop.login1)"
|
description = "Red Bear session broker (org.freedesktop.login1)"
|
||||||
@@ -380,7 +380,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/13_iommu.service"
|
path = "/etc/init.d/13_iommu.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "IOMMU DMA remapping daemon"
|
description = "IOMMU DMA remapping daemon"
|
||||||
@@ -395,7 +395,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/14_redbear-upower.service"
|
path = "/etc/init.d/14_redbear-upower.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "UPower D-Bus service (org.freedesktop.UPower)"
|
description = "UPower D-Bus service (org.freedesktop.UPower)"
|
||||||
@@ -409,7 +409,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/14_redbear-udisks.service"
|
path = "/etc/init.d/14_redbear-udisks.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "UDisks2 D-Bus service (org.freedesktop.UDisks2)"
|
description = "UDisks2 D-Bus service (org.freedesktop.UDisks2)"
|
||||||
@@ -423,7 +423,7 @@ type = "oneshot_async"
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/14_redbear-polkit.service"
|
path = "/etc/init.d/14_redbear-polkit.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "PolicyKit1 D-Bus service (org.freedesktop.PolicyKit1)"
|
description = "PolicyKit1 D-Bus service (org.freedesktop.PolicyKit1)"
|
||||||
@@ -465,3 +465,7 @@ path = "/etc/pcid.d/00_text_mode_gpu_mask.toml"
|
|||||||
data = """
|
data = """
|
||||||
# redbear-live-mini: no display driver matched; class 0x03 devices are skipped
|
# redbear-live-mini: no display driver matched; class 0x03 devices are skipped
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
[[files]]
|
||||||
|
path = "/etc/environment.d/10-debug.conf"
|
||||||
|
data = "INIT_LOG_LEVEL=DEBUG"
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ IP=bounded
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/usr/lib/init.d/12_netctl.service"
|
path = "/etc/init.d/12_netctl.service"
|
||||||
data = """
|
data = """
|
||||||
[unit]
|
[unit]
|
||||||
description = "Network profile application"
|
description = "Network profile application"
|
||||||
|
|||||||
@@ -611,6 +611,45 @@ local/Assets/
|
|||||||
|
|
||||||
**Current status**: Assets are committed to git. Not yet integrated into the build — requires bootloader and display server integration (P2 hardware validation).
|
**Current status**: Assets are committed to git. Not yet integrated into the build — requires bootloader and display server integration (P2 hardware validation).
|
||||||
|
|
||||||
|
## BUILD SYSTEM SAFETY
|
||||||
|
|
||||||
|
The build system includes collision detection and validation to prevent the D-Bus regression
|
||||||
|
class (config overrides silently overwritten by package staging).
|
||||||
|
|
||||||
|
### Validation Targets
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make lint-config # Check for /usr/lib/init.d/ in config [[files]]
|
||||||
|
make validate CONFIG_NAME=redbear-mini # Full validation: lint + init services + ownership
|
||||||
|
```
|
||||||
|
|
||||||
|
### Init Service Path Convention
|
||||||
|
|
||||||
|
- Packages own `/usr/lib/init.d/` — default service files from recipe staging
|
||||||
|
- Config overrides own `/etc/init.d/` — override files from `[[files]]` entries
|
||||||
|
- Config `[[files]]` MUST NOT use `/usr/lib/init.d/` paths for init services
|
||||||
|
- The init system's `config_for_dirs()` gives `/etc/init.d/` priority via BTreeMap dedup
|
||||||
|
|
||||||
|
### Collision Detection (installer)
|
||||||
|
|
||||||
|
The installer includes `CollisionTracker` (in `collision.rs`) that detects when package
|
||||||
|
staging overwrites config pre-install files. Init service collisions always error. Other
|
||||||
|
collisions warn by default, error in strict mode (`REDBEAR_STRICT_COLLISION=1`).
|
||||||
|
|
||||||
|
### Recipe Installs Manifest
|
||||||
|
|
||||||
|
Recipes can declare installed paths via `installs = [...]` in `[package]` section.
|
||||||
|
`scripts/validate-file-ownership.sh` checks for conflicts. No recipes declare installs yet.
|
||||||
|
|
||||||
|
### Manifest Generation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/generate-installs-manifest.sh base # Output suggested installs for base package
|
||||||
|
```
|
||||||
|
|
||||||
|
See `local/docs/BUILD-SYSTEM-HARDENING-PLAN.md` for the full 5-phase hardening plan.
|
||||||
|
See `local/docs/BUILD-SYSTEM-INVARIANTS.md` for invariants I1-I3.
|
||||||
|
|
||||||
## ANTI-PATTERNS
|
## ANTI-PATTERNS
|
||||||
|
|
||||||
- **DO NOT** edit files under mainline `recipes/` directly — put patches in `local/patches/`
|
- **DO NOT** edit files under mainline `recipes/` directly — put patches in `local/patches/`
|
||||||
|
|||||||
@@ -0,0 +1,403 @@
|
|||||||
|
# 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-*.toml` files
|
||||||
|
- Finds `[[files]]` entries with `path = "/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:**
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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()`:
|
||||||
|
|
||||||
|
1. If a file existed from `ConfigPreInstall` and was overwritten by `Package`:
|
||||||
|
- **WARN** (default): Print a warning showing the collision
|
||||||
|
- **ERROR** (strict mode via `STRICT_COLLISION=1` env): Fail the build
|
||||||
|
|
||||||
|
2. 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
|
||||||
|
|
||||||
|
3. 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.rs` with `InstallTracker`
|
||||||
|
- 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
|
||||||
|
|
||||||
|
```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 (`.service` files in `init.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 `installs` field 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`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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:
|
||||||
|
1. **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.
|
||||||
|
|
||||||
|
2. **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.
|
||||||
|
|
||||||
|
3. **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.
|
||||||
|
|
||||||
|
4. **Dependency cycle check**: Parse all service files, build a dependency graph,
|
||||||
|
detect cycles.
|
||||||
|
|
||||||
|
5. **Missing dependency check**: For each `requires`/`requires_weak` entry,
|
||||||
|
verify the referenced target/service file exists.
|
||||||
|
|
||||||
|
### 4.2 Makefile integration
|
||||||
|
|
||||||
|
Add to `mk/disk.mk`:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
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:
|
||||||
|
|
||||||
|
1. **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)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **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's `config_for_dirs()` picks
|
||||||
|
them up via BTreeMap dedup
|
||||||
|
|
||||||
|
3. **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's
|
||||||
|
> `config_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 with `postinstall = true` run 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:
|
||||||
|
|
||||||
|
1. **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).
|
||||||
|
|
||||||
|
2. **Add lint script** (Phase 1.1): ~30 minutes of work.
|
||||||
|
|
||||||
|
3. **Update AGENTS.md** (Phase 5.1): ~1 hour of documentation.
|
||||||
|
|
||||||
|
4. **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.
|
||||||
@@ -382,3 +382,55 @@ Based on these invariants, the first practical implementation slice should do al
|
|||||||
4. ensure release-mode source trees before deep build execution
|
4. ensure release-mode source trees before deep build execution
|
||||||
|
|
||||||
If those four changes land cleanly, the build system will already move from reactive deep-build debugging toward proactive build-state validation.
|
If those four changes land cleanly, the build system will already move from reactive deep-build debugging toward proactive build-state validation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Installer file-layer separation
|
||||||
|
|
||||||
|
### Layer ordering
|
||||||
|
|
||||||
|
The installer (`install_dir()`) processes files in this order:
|
||||||
|
|
||||||
|
1. Config pre-install `[[files]]` (`postinstall = false`)
|
||||||
|
2. Package staging (`install_packages()`)
|
||||||
|
3. Config post-install `[[files]]` (`postinstall = true`)
|
||||||
|
4. User/group creation (`passwd`, `shadow`, `group`)
|
||||||
|
|
||||||
|
Layer 2 silently overwrites Layer 1 files at the same path. Layer 3 overwrites
|
||||||
|
Layer 2. There is no collision detection or warning.
|
||||||
|
|
||||||
|
### 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 init system's `config_for_dirs(["/usr/lib/init.d", "/etc/init.d"])` uses a
|
||||||
|
BTreeMap keyed by filename. For the same filename, the `/etc/init.d/` entry
|
||||||
|
overwrites the `/usr/lib/init.d/` entry, so config overrides take effect.
|
||||||
|
|
||||||
|
Config entries using `/usr/lib/init.d/` paths will be silently overwritten by
|
||||||
|
package staging. The `scripts/lint-config-paths.sh` tool detects violations.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
For init services, `/etc/init.d/` provides this via the `config_for_dirs()`
|
||||||
|
BTreeMap mechanism. For other file types, either use `/etc/` paths or mark the
|
||||||
|
file as `postinstall = true`.
|
||||||
|
|
||||||
|
### Invariant I3: Post-install as explicit override
|
||||||
|
|
||||||
|
`[[files]]` entries with `postinstall = true` run after package installation and
|
||||||
|
are guaranteed to overwrite any package-provided file at the same path.
|
||||||
|
|
||||||
|
Prefer `/etc/` directory overrides over `postinstall` for init services, because
|
||||||
|
`postinstall` requires every override to be explicitly marked and is easy to
|
||||||
|
miss. The `/etc/init.d/` path convention is self-enforcing via `config_for_dirs()`.
|
||||||
|
|
||||||
|
### Enforcement
|
||||||
|
|
||||||
|
- `scripts/lint-config-paths.sh` — detects `/usr/lib/init.d/` paths in config files
|
||||||
|
- `make lint-config` — runs the lint as a build step
|
||||||
|
- Full plan: `local/docs/BUILD-SYSTEM-HARDENING-PLAN.md`
|
||||||
|
|||||||
@@ -200,10 +200,10 @@ index 417ff2d..fab677c 100644
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
diff --git a/src/installer.rs b/src/installer.rs
|
diff --git a/src/installer.rs b/src/installer.rs
|
||||||
index 4e077a9..ba3f9dd 100644
|
index 4e077a9..7432444 100644
|
||||||
--- a/src/installer.rs
|
--- a/src/installer.rs
|
||||||
+++ b/src/installer.rs
|
+++ b/src/installer.rs
|
||||||
@@ -3,6 +3,13 @@ use anyhow::{bail, Result};
|
@@ -3,11 +3,19 @@ use anyhow::{bail, Result};
|
||||||
use pkg::Library;
|
use pkg::Library;
|
||||||
use rand::{rngs::OsRng, TryRngCore};
|
use rand::{rngs::OsRng, TryRngCore};
|
||||||
use redoxfs::{unmount_path, Disk, DiskIo, FileSystem, BLOCK_SIZE};
|
use redoxfs::{unmount_path, Disk, DiskIo, FileSystem, BLOCK_SIZE};
|
||||||
@@ -217,7 +217,13 @@ index 4e077a9..ba3f9dd 100644
|
|||||||
use termion::input::TermRead;
|
use termion::input::TermRead;
|
||||||
|
|
||||||
use crate::config::file::FileConfig;
|
use crate::config::file::FileConfig;
|
||||||
@@ -23,12 +30,104 @@ use std::{
|
use crate::config::package::PackageConfig;
|
||||||
|
use crate::config::Config;
|
||||||
|
+use crate::collision::CollisionTracker;
|
||||||
|
use crate::disk_wrapper::DiskWrapper;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
@@ -23,12 +31,104 @@ use std::{
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -322,7 +328,44 @@ index 4e077a9..ba3f9dd 100644
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_target() -> String {
|
fn get_target() -> String {
|
||||||
@@ -360,6 +459,155 @@ fn decide_mount_path(mount_path: Option<&Path>) -> PathBuf {
|
@@ -136,17 +236,36 @@ pub fn install_dir(
|
||||||
|
|
||||||
|
let output_dir = output_dir.to_owned();
|
||||||
|
|
||||||
|
+ let mut tracker = CollisionTracker::new();
|
||||||
|
+
|
||||||
|
for file in &config.files {
|
||||||
|
if !file.postinstall {
|
||||||
|
file.create(&output_dir)?;
|
||||||
|
+ let path = Path::new(file.path.trim_start_matches('/'));
|
||||||
|
+ tracker.record_config_pre_install(path, &output_dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+ tracker.snapshot_pre_package(&output_dir)?;
|
||||||
|
+
|
||||||
|
install_packages(&config, &output_dir, cookbook)?;
|
||||||
|
|
||||||
|
+ let package_names: Vec<&String> = config
|
||||||
|
+ .packages
|
||||||
|
+ .iter()
|
||||||
|
+ .filter_map(|(name, pkg)| match pkg {
|
||||||
|
+ PackageConfig::Build(rule) if rule == "ignore" => None,
|
||||||
|
+ _ => Some(name),
|
||||||
|
+ })
|
||||||
|
+ .collect();
|
||||||
|
+ tracker.detect_package_overwrites(&output_dir, &package_names)?;
|
||||||
|
+ tracker.report()?;
|
||||||
|
+
|
||||||
|
for file in &config.files {
|
||||||
|
if file.postinstall {
|
||||||
|
file.create(&output_dir)?;
|
||||||
|
+ let path = Path::new(file.path.trim_start_matches('/'));
|
||||||
|
+ tracker.record_config_post_install(path, &output_dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -360,6 +479,155 @@ fn decide_mount_path(mount_path: Option<&Path>) -> PathBuf {
|
||||||
mount_path
|
mount_path
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +521,7 @@ index 4e077a9..ba3f9dd 100644
|
|||||||
pub fn with_redoxfs_mount<D, T, F>(
|
pub fn with_redoxfs_mount<D, T, F>(
|
||||||
fs: FileSystem<D>,
|
fs: FileSystem<D>,
|
||||||
mount_path: Option<&Path>,
|
mount_path: Option<&Path>,
|
||||||
@@ -481,7 +729,7 @@ pub fn fetch_bootloaders(
|
@@ -481,7 +749,7 @@ pub fn fetch_bootloaders(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
cookbook: Option<&str>,
|
cookbook: Option<&str>,
|
||||||
live: bool,
|
live: bool,
|
||||||
@@ -487,7 +530,7 @@ index 4e077a9..ba3f9dd 100644
|
|||||||
let bootloader_dir =
|
let bootloader_dir =
|
||||||
PathBuf::from(format!("/tmp/redox_installer_bootloader_{}", process::id()));
|
PathBuf::from(format!("/tmp/redox_installer_bootloader_{}", process::id()));
|
||||||
|
|
||||||
@@ -491,39 +739,78 @@ pub fn fetch_bootloaders(
|
@@ -491,39 +759,78 @@ pub fn fetch_bootloaders(
|
||||||
|
|
||||||
fs::create_dir(&bootloader_dir)?;
|
fs::create_dir(&bootloader_dir)?;
|
||||||
|
|
||||||
@@ -557,8 +600,7 @@ index 4e077a9..ba3f9dd 100644
|
|||||||
+ } else {
|
+ } else {
|
||||||
+ Vec::new()
|
+ Vec::new()
|
||||||
+ };
|
+ };
|
||||||
|
+
|
||||||
- Ok((bios_data, efi_data))
|
|
||||||
+ let grub_efi_data = if use_grub {
|
+ let grub_efi_data = if use_grub {
|
||||||
+ let grub_path = boot_dir.join("grub.efi");
|
+ let grub_path = boot_dir.join("grub.efi");
|
||||||
+ if grub_path.exists() {
|
+ if grub_path.exists() {
|
||||||
@@ -583,7 +625,8 @@ index 4e077a9..ba3f9dd 100644
|
|||||||
+
|
+
|
||||||
+ Ok((bios_data, efi_data, grub_efi_data, grub_cfg_data))
|
+ Ok((bios_data, efi_data, grub_efi_data, grub_cfg_data))
|
||||||
+ })();
|
+ })();
|
||||||
+
|
|
||||||
|
- Ok((bios_data, efi_data))
|
||||||
+ let _ = fs::remove_dir_all(&bootloader_dir);
|
+ let _ = fs::remove_dir_all(&bootloader_dir);
|
||||||
+ result
|
+ result
|
||||||
}
|
}
|
||||||
@@ -593,7 +636,7 @@ index 4e077a9..ba3f9dd 100644
|
|||||||
pub fn with_whole_disk<P, F, T>(disk_path: P, disk_option: &DiskOption, callback: F) -> Result<T>
|
pub fn with_whole_disk<P, F, T>(disk_path: P, disk_option: &DiskOption, callback: F) -> Result<T>
|
||||||
where
|
where
|
||||||
P: AsRef<Path>,
|
P: AsRef<Path>,
|
||||||
@@ -570,7 +857,7 @@ where
|
@@ -570,7 +877,7 @@ where
|
||||||
let gpt_reserved = 34 * 512; // GPT always reserves 34 512-byte sectors
|
let gpt_reserved = 34 * 512; // GPT always reserves 34 512-byte sectors
|
||||||
let mibi = 1024 * 1024;
|
let mibi = 1024 * 1024;
|
||||||
|
|
||||||
@@ -602,7 +645,7 @@ index 4e077a9..ba3f9dd 100644
|
|||||||
let bios_start = gpt_reserved / block_size;
|
let bios_start = gpt_reserved / block_size;
|
||||||
let bios_end = (mibi / block_size) - 1;
|
let bios_end = (mibi / block_size) - 1;
|
||||||
|
|
||||||
@@ -672,10 +959,13 @@ where
|
@@ -672,10 +979,13 @@ where
|
||||||
fscommon::StreamSlice::new(&mut disk_file, disk_efi_start, disk_efi_end)?;
|
fscommon::StreamSlice::new(&mut disk_file, disk_efi_start, disk_efi_end)?;
|
||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
@@ -618,7 +661,7 @@ index 4e077a9..ba3f9dd 100644
|
|||||||
|
|
||||||
eprintln!("Opening EFI partition");
|
eprintln!("Opening EFI partition");
|
||||||
let fs = fatfs::FileSystem::new(&mut disk_efi, fatfs::FsOptions::new())?;
|
let fs = fatfs::FileSystem::new(&mut disk_efi, fatfs::FsOptions::new())?;
|
||||||
@@ -683,20 +973,58 @@ where
|
@@ -683,20 +993,58 @@ where
|
||||||
eprintln!("Creating EFI directory");
|
eprintln!("Creating EFI directory");
|
||||||
let root_dir = fs.root_dir();
|
let root_dir = fs.root_dir();
|
||||||
root_dir.create_dir("EFI")?;
|
root_dir.create_dir("EFI")?;
|
||||||
@@ -689,7 +732,7 @@ index 4e077a9..ba3f9dd 100644
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Format and install RedoxFS partition
|
// Format and install RedoxFS partition
|
||||||
@@ -712,6 +1040,225 @@ where
|
@@ -712,6 +1060,225 @@ where
|
||||||
with_redoxfs(disk_redoxfs, disk_option.password_opt, callback)
|
with_redoxfs(disk_redoxfs, disk_option.password_opt, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -915,7 +958,7 @@ index 4e077a9..ba3f9dd 100644
|
|||||||
#[cfg(not(target_os = "redox"))]
|
#[cfg(not(target_os = "redox"))]
|
||||||
pub fn try_fast_install<D: redoxfs::Disk, F: FnMut(u64, u64)>(
|
pub fn try_fast_install<D: redoxfs::Disk, F: FnMut(u64, u64)>(
|
||||||
_fs: &mut redoxfs::FileSystem<D>,
|
_fs: &mut redoxfs::FileSystem<D>,
|
||||||
@@ -801,6 +1348,27 @@ pub fn try_fast_install<D: redoxfs::Disk, F: FnMut(u64, u64)>(
|
@@ -801,6 +1368,27 @@ pub fn try_fast_install<D: redoxfs::Disk, F: FnMut(u64, u64)>(
|
||||||
|
|
||||||
fn install_inner(config: Config, output: &Path) -> Result<()> {
|
fn install_inner(config: Config, output: &Path) -> Result<()> {
|
||||||
println!("Installing to {}:\n{}", output.display(), config);
|
println!("Installing to {}:\n{}", output.display(), config);
|
||||||
@@ -943,7 +986,7 @@ index 4e077a9..ba3f9dd 100644
|
|||||||
let cookbook = config.general.cookbook.clone();
|
let cookbook = config.general.cookbook.clone();
|
||||||
let cookbook = cookbook.as_ref().map(|p| p.as_str());
|
let cookbook = cookbook.as_ref().map(|p| p.as_str());
|
||||||
if output.is_dir() {
|
if output.is_dir() {
|
||||||
@@ -823,28 +1391,45 @@ fn install_inner(config: Config, output: &Path) -> Result<()> {
|
@@ -823,28 +1411,45 @@ fn install_inner(config: Config, output: &Path) -> Result<()> {
|
||||||
let live = config.general.live_disk.unwrap_or(false);
|
let live = config.general.live_disk.unwrap_or(false);
|
||||||
let password_opt = config.general.encrypt_disk.clone();
|
let password_opt = config.general.encrypt_disk.clone();
|
||||||
let password_opt = password_opt.as_ref().map(|p| p.as_bytes());
|
let password_opt = password_opt.as_ref().map(|p| p.as_bytes());
|
||||||
@@ -1002,3 +1045,289 @@ index 4e077a9..ba3f9dd 100644
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diff --git a/src/lib.rs b/src/lib.rs
|
||||||
|
index 216478d..186c24b 100644
|
||||||
|
--- a/src/lib.rs
|
||||||
|
+++ b/src/lib.rs
|
||||||
|
@@ -3,6 +3,8 @@ extern crate serde_derive;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
#[cfg(feature = "installer")]
|
||||||
|
+mod collision;
|
||||||
|
+#[cfg(feature = "installer")]
|
||||||
|
mod disk_wrapper;
|
||||||
|
#[cfg(feature = "installer")]
|
||||||
|
mod installer;
|
||||||
|
diff --git a/src/collision.rs b/src/collision.rs
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..145c62c
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/src/collision.rs
|
||||||
|
@@ -0,0 +1,267 @@
|
||||||
|
+use std::collections::BTreeMap;
|
||||||
|
+use std::path::{Path, PathBuf};
|
||||||
|
+
|
||||||
|
+use anyhow::{Context, Result, bail};
|
||||||
|
+
|
||||||
|
+#[allow(dead_code)]
|
||||||
|
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
+pub enum FileLayer {
|
||||||
|
+ ConfigPreInstall,
|
||||||
|
+ Package,
|
||||||
|
+ ConfigPostInstall,
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+#[allow(dead_code)]
|
||||||
|
+#[derive(Debug)]
|
||||||
|
+pub struct CollisionReport {
|
||||||
|
+ pub path: PathBuf,
|
||||||
|
+ pub previous_layer: FileLayer,
|
||||||
|
+ pub current_layer: FileLayer,
|
||||||
|
+ #[allow(dead_code)]
|
||||||
|
+ pub previous_source: String,
|
||||||
|
+ #[allow(dead_code)]
|
||||||
|
+ pub current_source: String,
|
||||||
|
+ pub is_init_service: bool,
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+impl std::fmt::Display for FileLayer {
|
||||||
|
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
+ match self {
|
||||||
|
+ FileLayer::ConfigPreInstall => write!(f, "config (pre-install)"),
|
||||||
|
+ FileLayer::Package => write!(f, "package staging"),
|
||||||
|
+ FileLayer::ConfigPostInstall => write!(f, "config (post-install)"),
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+impl CollisionReport {
|
||||||
|
+ fn severity(&self) -> &'static str {
|
||||||
|
+ if self.is_init_service {
|
||||||
|
+ "ERROR"
|
||||||
|
+ } else {
|
||||||
|
+ "WARN"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+impl std::fmt::Display for CollisionReport {
|
||||||
|
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
+ let severity = self.severity();
|
||||||
|
+ write!(
|
||||||
|
+ f,
|
||||||
|
+ "[COLLISION-{}] {} (was: {}, now: {})",
|
||||||
|
+ severity,
|
||||||
|
+ self.path.display(),
|
||||||
|
+ self.previous_layer,
|
||||||
|
+ self.current_layer
|
||||||
|
+ )
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+struct FileRecord {
|
||||||
|
+ layer: FileLayer,
|
||||||
|
+ source: String,
|
||||||
|
+ hash: Option<String>,
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+pub struct CollisionTracker {
|
||||||
|
+ files: BTreeMap<PathBuf, FileRecord>,
|
||||||
|
+ collisions: Vec<CollisionReport>,
|
||||||
|
+ strict: bool,
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+fn is_init_service_path(path: &Path) -> bool {
|
||||||
|
+ let path_str = path.to_string_lossy();
|
||||||
|
+ (path_str.contains("/init.d/") || path_str.contains("init.d"))
|
||||||
|
+ && (path_str.ends_with(".service") || path_str.ends_with(".target"))
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+fn is_environment_override_path(path: &Path) -> bool {
|
||||||
|
+ let path_str = path.to_string_lossy();
|
||||||
|
+ path_str.contains("/environment.d/")
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+impl CollisionTracker {
|
||||||
|
+ pub fn new() -> Self {
|
||||||
|
+ let strict = std::env::var("REDBEAR_STRICT_COLLISION")
|
||||||
|
+ .map(|v| v == "1" || v == "true")
|
||||||
|
+ .unwrap_or(false);
|
||||||
|
+ Self {
|
||||||
|
+ files: BTreeMap::new(),
|
||||||
|
+ collisions: Vec::new(),
|
||||||
|
+ strict,
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ pub fn record_config_pre_install(&mut self, path: &Path, dest: &Path) {
|
||||||
|
+ let full_path = dest.join(path);
|
||||||
|
+ let hash = Self::hash_file(&full_path);
|
||||||
|
+ self.files.insert(
|
||||||
|
+ path.to_path_buf(),
|
||||||
|
+ FileRecord { layer: FileLayer::ConfigPreInstall, source: "config pre-install".to_string(), hash },
|
||||||
|
+ );
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ pub fn record_config_post_install(&mut self, path: &Path, dest: &Path) {
|
||||||
|
+ let full_path = dest.join(path);
|
||||||
|
+ let hash = Self::hash_file(&full_path);
|
||||||
|
+ self.files.insert(
|
||||||
|
+ path.to_path_buf(),
|
||||||
|
+ FileRecord { layer: FileLayer::ConfigPostInstall, source: "config post-install".to_string(), hash },
|
||||||
|
+ );
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ pub fn snapshot_pre_package(&mut self, dest: &Path) -> Result<()> {
|
||||||
|
+ Self::walk_dir(dest, dest, &mut |relative_path| {
|
||||||
|
+ if !self.files.contains_key(&relative_path) {
|
||||||
|
+ let full_path = dest.join(&relative_path);
|
||||||
|
+ let hash = Self::hash_file(&full_path);
|
||||||
|
+ self.files.insert(
|
||||||
|
+ relative_path,
|
||||||
|
+ FileRecord {
|
||||||
|
+ layer: FileLayer::ConfigPreInstall,
|
||||||
|
+ source: "config pre-install".to_string(),
|
||||||
|
+ hash,
|
||||||
|
+ },
|
||||||
|
+ );
|
||||||
|
+ }
|
||||||
|
+ Ok(())
|
||||||
|
+ })
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ pub fn detect_package_overwrites(&mut self, dest: &Path, package_names: &[&String]) -> Result<()> {
|
||||||
|
+ let pkg_label = if package_names.len() == 1 {
|
||||||
|
+ format!("package {}", package_names[0])
|
||||||
|
+ } else {
|
||||||
|
+ format!("packages ({} total)", package_names.len())
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
+ Self::walk_dir(dest, dest, &mut |relative_path| {
|
||||||
|
+ if let Some(existing) = self.files.get(&relative_path) {
|
||||||
|
+ if existing.layer == FileLayer::ConfigPreInstall {
|
||||||
|
+ let full_path = dest.join(&relative_path);
|
||||||
|
+ let is_init = is_init_service_path(&relative_path);
|
||||||
|
+ let is_env = is_environment_override_path(&relative_path);
|
||||||
|
+ let contents_differ = existing.hash.as_ref().map_or(true, |h| {
|
||||||
|
+ Self::hash_file(&full_path).as_ref() != Some(h)
|
||||||
|
+ });
|
||||||
|
+ if contents_differ {
|
||||||
|
+ let report = CollisionReport {
|
||||||
|
+ path: relative_path.clone(),
|
||||||
|
+ previous_layer: existing.layer,
|
||||||
|
+ current_layer: FileLayer::Package,
|
||||||
|
+ previous_source: existing.source.clone(),
|
||||||
|
+ current_source: pkg_label.clone(),
|
||||||
|
+ is_init_service: is_init || is_env,
|
||||||
|
+ };
|
||||||
|
+ self.collisions.push(report);
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ self.files.insert(
|
||||||
|
+ relative_path,
|
||||||
|
+ FileRecord {
|
||||||
|
+ layer: FileLayer::Package,
|
||||||
|
+ source: pkg_label.clone(),
|
||||||
|
+ hash: None,
|
||||||
|
+ },
|
||||||
|
+ );
|
||||||
|
+ Ok(())
|
||||||
|
+ })
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ fn hash_file(path: &Path) -> Option<String> {
|
||||||
|
+ use std::collections::hash_map::DefaultHasher;
|
||||||
|
+ use std::io::Read;
|
||||||
|
+ let mut file = std::fs::File::open(path).ok()?;
|
||||||
|
+ let mut hasher = DefaultHasher::new();
|
||||||
|
+ let mut buf = [0u8; 8192];
|
||||||
|
+ loop {
|
||||||
|
+ let n = file.read(&mut buf).ok()?;
|
||||||
|
+ if n == 0 {
|
||||||
|
+ break;
|
||||||
|
+ }
|
||||||
|
+ std::hash::Hasher::write(&mut hasher, &buf[..n]);
|
||||||
|
+ }
|
||||||
|
+ Some(format!("{:016x}", std::hash::Hasher::finish(&hasher)))
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ pub fn report(&self) -> Result<()> {
|
||||||
|
+ if self.collisions.is_empty() {
|
||||||
|
+ println!("[COLLISION-CHECK] No file collisions detected");
|
||||||
|
+ return Ok(());
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ let init_collisions: Vec<&CollisionReport> = self
|
||||||
|
+ .collisions
|
||||||
|
+ .iter()
|
||||||
|
+ .filter(|c| c.is_init_service)
|
||||||
|
+ .collect();
|
||||||
|
+ let other_collisions: Vec<&CollisionReport> = self
|
||||||
|
+ .collisions
|
||||||
|
+ .iter()
|
||||||
|
+ .filter(|c| !c.is_init_service)
|
||||||
|
+ .collect();
|
||||||
|
+
|
||||||
|
+ for collision in &other_collisions {
|
||||||
|
+ eprintln!(
|
||||||
|
+ "[COLLISION-WARN] {} overwritten by {}",
|
||||||
|
+ collision.path.display(),
|
||||||
|
+ collision.current_source
|
||||||
|
+ );
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if !init_collisions.is_empty() {
|
||||||
|
+ eprintln!("[COLLISION-ERROR] {} init service file collision(s) detected:", init_collisions.len());
|
||||||
|
+ for collision in &init_collisions {
|
||||||
|
+ eprintln!(" {}", collision);
|
||||||
|
+ if collision.path.to_string_lossy().contains("/usr/lib/init.d/") {
|
||||||
|
+ eprintln!(" Fix: Change config [[files]] path from /usr/lib/init.d/ to /etc/init.d/");
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ bail!(
|
||||||
|
+ "Build aborted: {} init service file collision(s). \
|
||||||
|
+ Config overrides were silently overwritten by package staging. \
|
||||||
|
+ See local/docs/BUILD-SYSTEM-HARDENING-PLAN.md for details.",
|
||||||
|
+ init_collisions.len()
|
||||||
|
+ );
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if self.strict && !other_collisions.is_empty() {
|
||||||
|
+ bail!(
|
||||||
|
+ "Build aborted (strict mode): {} file collision(s) detected. \
|
||||||
|
+ Set REDBEAR_STRICT_COLLISION=0 to downgrade to warnings.",
|
||||||
|
+ other_collisions.len()
|
||||||
|
+ );
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ println!(
|
||||||
|
+ "[COLLISION-WARN] {} non-critical file collision(s) detected (see above)",
|
||||||
|
+ other_collisions.len()
|
||||||
|
+ );
|
||||||
|
+ Ok(())
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ fn walk_dir<F>(root: &Path, prefix: &Path, callback: &mut F) -> Result<()>
|
||||||
|
+ where
|
||||||
|
+ F: FnMut(PathBuf) -> Result<()>,
|
||||||
|
+ {
|
||||||
|
+ if !root.is_dir() {
|
||||||
|
+ return Ok(());
|
||||||
|
+ }
|
||||||
|
+ for entry in std::fs::read_dir(root)
|
||||||
|
+ .with_context(|| format!("reading dir {}", root.display()))?
|
||||||
|
+ {
|
||||||
|
+ let entry = entry?;
|
||||||
|
+ let path = entry.path();
|
||||||
|
+ if path.is_dir() {
|
||||||
|
+ Self::walk_dir(&path, prefix, callback)?;
|
||||||
|
+ } else {
|
||||||
|
+ if let Ok(relative) = path.strip_prefix(prefix) {
|
||||||
|
+ callback(relative.to_path_buf())?;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ Ok(())
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
|||||||
@@ -26,4 +26,8 @@ endif
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
lint-config:
|
||||||
|
@scripts/lint-config-paths.sh
|
||||||
|
|||||||
@@ -109,3 +109,10 @@ else
|
|||||||
@$(FUMOUNT) /tmp/redox_installer 2>/dev/null || echo "Warning: failed to unmount /tmp/redox_installer"
|
@$(FUMOUNT) /tmp/redox_installer 2>/dev/null || echo "Warning: failed to unmount /tmp/redox_installer"
|
||||||
@echo "\033[1;36;49mFilesystem unmounted\033[0m"
|
@echo "\033[1;36;49mFilesystem unmounted\033[0m"
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
validate-init: $(BUILD)/harddrive.img
|
||||||
|
@scripts/validate-init-services.sh $(BUILD)/harddrive.img
|
||||||
|
|
||||||
|
validate: lint-config validate-init
|
||||||
|
@scripts/validate-file-ownership.sh
|
||||||
|
@echo "\033[1;36;49mBuild validation passed\033[0m"
|
||||||
|
|||||||
@@ -39,6 +39,76 @@ patches = [
|
|||||||
"P4-thermald-workspace.patch",
|
"P4-thermald-workspace.patch",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package]
|
||||||
|
installs = [
|
||||||
|
"/lib/pcid.d/ac97d.toml",
|
||||||
|
"/lib/pcid.d/e1000d.toml",
|
||||||
|
"/lib/pcid.d/ihdad.toml",
|
||||||
|
"/lib/pcid.d/ihdgd.toml",
|
||||||
|
"/lib/pcid.d/ixgbed.toml",
|
||||||
|
"/lib/pcid.d/rtl8139d.toml",
|
||||||
|
"/lib/pcid.d/rtl8168d.toml",
|
||||||
|
"/lib/pcid.d/vboxd.toml",
|
||||||
|
"/lib/pcid.d/virtio-netd.toml",
|
||||||
|
"/lib/pcid.d/xhcid.toml",
|
||||||
|
"/usr/bin/audiod",
|
||||||
|
"/usr/bin/dhcpd",
|
||||||
|
"/usr/bin/dw-acpi-i2cd",
|
||||||
|
"/usr/bin/gpiod",
|
||||||
|
"/usr/bin/i2cd",
|
||||||
|
"/usr/bin/i2c-gpio-expanderd",
|
||||||
|
"/usr/bin/i2c-hidd",
|
||||||
|
"/usr/bin/inputd",
|
||||||
|
"/usr/bin/intel-gpiod",
|
||||||
|
"/usr/bin/ipcd",
|
||||||
|
"/usr/bin/netstack",
|
||||||
|
"/usr/bin/pcid",
|
||||||
|
"/usr/bin/pcid-spawner",
|
||||||
|
"/usr/bin/ptyd",
|
||||||
|
"/usr/bin/redoxerd",
|
||||||
|
"/usr/bin/smolnetd",
|
||||||
|
"/usr/bin/ucsid",
|
||||||
|
"/usr/lib/drivers/ac97d",
|
||||||
|
"/usr/lib/drivers/amd-mp2-i2cd",
|
||||||
|
"/usr/lib/drivers/e1000d",
|
||||||
|
"/usr/lib/drivers/ihdad",
|
||||||
|
"/usr/lib/drivers/ihdgd",
|
||||||
|
"/usr/lib/drivers/intel-lpss-i2cd",
|
||||||
|
"/usr/lib/drivers/intel-thc-hidd",
|
||||||
|
"/usr/lib/drivers/ixgbed",
|
||||||
|
"/usr/lib/drivers/rtl8139d",
|
||||||
|
"/usr/lib/drivers/rtl8168d",
|
||||||
|
"/usr/lib/drivers/sb16d",
|
||||||
|
"/usr/lib/drivers/thermald",
|
||||||
|
"/usr/lib/drivers/usbctl",
|
||||||
|
"/usr/lib/drivers/usbhidd",
|
||||||
|
"/usr/lib/drivers/usbhubd",
|
||||||
|
"/usr/lib/drivers/usbscsid",
|
||||||
|
"/usr/lib/drivers/vboxd",
|
||||||
|
"/usr/lib/drivers/virtio-gpud",
|
||||||
|
"/usr/lib/drivers/virtio-netd",
|
||||||
|
"/usr/lib/drivers/xhcid",
|
||||||
|
"/usr/lib/init.d/00_base.target",
|
||||||
|
"/usr/lib/init.d/00_ipcd.service",
|
||||||
|
"/usr/lib/init.d/00_pcid-spawner.service",
|
||||||
|
"/usr/lib/init.d/00_ptyd.service",
|
||||||
|
"/usr/lib/init.d/00_sudo.service",
|
||||||
|
"/usr/lib/init.d/00_tmp",
|
||||||
|
"/usr/lib/init.d/05_boot_essential.target",
|
||||||
|
"/usr/lib/init.d/10_dhcpd.service",
|
||||||
|
"/usr/lib/init.d/10_net.target",
|
||||||
|
"/usr/lib/init.d/10_smolnetd.service",
|
||||||
|
"/usr/lib/init.d/12_boot_late.target",
|
||||||
|
"/usr/lib/init.d/12_dbus.service",
|
||||||
|
"/usr/lib/init.d/13_seatd.service",
|
||||||
|
"/usr/lib/init.d/13_sessiond.service",
|
||||||
|
"/usr/lib/init.d/20_audiod.service",
|
||||||
|
"/usr/lib/init.d/29_activate_console.service",
|
||||||
|
"/usr/lib/init.d/30_console.service",
|
||||||
|
"/usr/lib/init.d/30_thermald.service",
|
||||||
|
"/usr/lib/init.d/31_debug_console.service",
|
||||||
|
]
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
template = "custom"
|
template = "custom"
|
||||||
script = """
|
script = """
|
||||||
|
|||||||
Executable
+74
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# generate-installs-manifest.sh — Inspect recipe stage directory and output
|
||||||
|
# suggested `installs = [...]` declarations for recipe.toml
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/generate-installs-manifest.sh <recipe-name>
|
||||||
|
# scripts/generate-installs-manifest.sh base
|
||||||
|
# scripts/generate-installs-manifest.sh evdevd
|
||||||
|
#
|
||||||
|
# The script examines the recipe's stage directory after a successful build
|
||||||
|
# and lists all installed files relative to the sysroot root.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ $# -lt 1 ]; then
|
||||||
|
echo "Usage: $0 <recipe-name>" >&2
|
||||||
|
echo " Inspects the recipe's stage directory and outputs suggested installs = [...]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RECIPE_NAME="$1"
|
||||||
|
|
||||||
|
# Find the recipe directory
|
||||||
|
RECIPE_DIR=""
|
||||||
|
for category_dir in recipes/*/; do
|
||||||
|
if [ -d "${category_dir}${RECIPE_NAME}" ]; then
|
||||||
|
RECIPE_DIR="${category_dir}${RECIPE_NAME}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$RECIPE_DIR" ]; then
|
||||||
|
echo "ERROR: Recipe '$RECIPE_NAME' not found in recipes/*/" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine target architecture
|
||||||
|
source mk/config.mk 2>/dev/null || true
|
||||||
|
TARGET_ARCH="${ARCH:-x86_64}"
|
||||||
|
STAGE_DIR="${RECIPE_DIR}/target/${TARGET_ARCH}-unknown-redox/stage"
|
||||||
|
|
||||||
|
if [ ! -d "$STAGE_DIR" ]; then
|
||||||
|
STAGE_DIR="${RECIPE_DIR}/target/${TARGET_ARCH}-unknown-redox/stage.tmp"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$STAGE_DIR" ]; then
|
||||||
|
echo "ERROR: Stage directory not found: $STAGE_DIR" >&2
|
||||||
|
echo " Build the recipe first: ./target/release/repo cook $RECIPE_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "# Generated installs manifest for $RECIPE_NAME"
|
||||||
|
echo "# Recipe: $RECIPE_DIR"
|
||||||
|
echo "# Stage: $STAGE_DIR"
|
||||||
|
echo ""
|
||||||
|
echo "[package]"
|
||||||
|
|
||||||
|
# Collect all files, sorted
|
||||||
|
FILES=$(cd "$STAGE_DIR" && find . -type f -o -type l | sed 's|^\./||' | sort)
|
||||||
|
|
||||||
|
if [ -z "$FILES" ]; then
|
||||||
|
echo "# No files found in stage directory"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "installs = ["
|
||||||
|
while IFS= read -r file; do
|
||||||
|
echo " \"/${file}\","
|
||||||
|
done <<< "$FILES"
|
||||||
|
echo "]"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "# Total: $(echo "$FILES" | wc -l) files"
|
||||||
|
echo "# To apply: copy the installs = [...] block into $RECIPE_DIR/recipe.toml"
|
||||||
Executable
+81
@@ -0,0 +1,81 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# lint-config-paths.sh — Detect init service file path violations in config files
|
||||||
|
#
|
||||||
|
# Init service files in config [[files]] entries MUST use /etc/init.d/ paths,
|
||||||
|
# NOT /usr/lib/init.d/. The base package installs to /usr/lib/init.d/ and
|
||||||
|
# silently overwrites any config files placed there during install_dir().
|
||||||
|
#
|
||||||
|
# The init system's config_for_dirs() gives /etc/init.d/ priority over
|
||||||
|
# /usr/lib/init.d/ for the same filename, so config overrides must use /etc/.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/lint-config-paths.sh # Check all redbear configs
|
||||||
|
# scripts/lint-config-paths.sh config/*.toml # Check specific files
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 — No violations found
|
||||||
|
# 1 — Violations found (printed to stderr)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Default to all redbear configs if no arguments
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
set -- config/redbear-*.toml
|
||||||
|
fi
|
||||||
|
|
||||||
|
violations=0
|
||||||
|
|
||||||
|
for config_file in "$@"; do
|
||||||
|
if [ ! -f "$config_file" ]; then
|
||||||
|
echo "WARN: $config_file not found, skipping" >&2
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find [[files]] entries with /usr/lib/init.d/ paths
|
||||||
|
# We look for path = "/usr/lib/init.d/..." lines
|
||||||
|
line_num=0
|
||||||
|
in_files_section=false
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
line_num=$((line_num + 1))
|
||||||
|
|
||||||
|
# Track TOML structure to only check [[files]] sections
|
||||||
|
if [[ "$line" =~ ^\[\[files\]\] ]]; then
|
||||||
|
in_files_section=true
|
||||||
|
continue
|
||||||
|
elif [[ "$line" =~ ^\[ ]]; then
|
||||||
|
in_files_section=false
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $in_files_section; then
|
||||||
|
# Check for /usr/lib/init.d/ paths
|
||||||
|
if [[ "$line" =~ path[[:space:]]*=[[:space:]]*\"/usr/lib/init\.d/ ]]; then
|
||||||
|
echo "VIOLATION: $config_file:$line_num" >&2
|
||||||
|
echo " Line: $line" >&2
|
||||||
|
echo " Fix: Change /usr/lib/init.d/ to /etc/init.d/" >&2
|
||||||
|
echo "" >&2
|
||||||
|
violations=$((violations + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Also check for /usr/lib/environment.d/ (similar override pattern)
|
||||||
|
if [[ "$line" =~ path[[:space:]]*=[[:space:]]*\"/usr/lib/environment\.d/ ]]; then
|
||||||
|
echo "VIOLATION: $config_file:$line_num" >&2
|
||||||
|
echo " Line: $line" >&2
|
||||||
|
echo " Fix: Change /usr/lib/environment.d/ to /etc/environment.d/" >&2
|
||||||
|
echo "" >&2
|
||||||
|
violations=$((violations + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < "$config_file"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $violations -gt 0 ]; then
|
||||||
|
echo "FAILED: $violations init service path violation(s) found." >&2
|
||||||
|
echo "Config [[files]] entries must use /etc/init.d/ not /usr/lib/init.d/" >&2
|
||||||
|
echo "See: local/docs/BUILD-SYSTEM-HARDENING-PLAN.md Phase 1" >&2
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "OK: No init service path violations in config files."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
Executable
+128
@@ -0,0 +1,128 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# validate-file-ownership.sh — Check recipe file ownership conflicts
|
||||||
|
#
|
||||||
|
# Reads the optional 'installs' field from recipe.toml [package] sections
|
||||||
|
# and detects conflicts where multiple recipes claim the same path.
|
||||||
|
#
|
||||||
|
# Also cross-references config [[files]] paths against recipe installs
|
||||||
|
# to detect config-layer / package-layer collisions.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/validate-file-ownership.sh
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 — No conflicts or violations found
|
||||||
|
# 1 — Conflicts detected
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
failures=0
|
||||||
|
|
||||||
|
# Registry: path -> recipe_name
|
||||||
|
declare -A PATH_REGISTRY
|
||||||
|
|
||||||
|
echo "=== Scanning recipes for installs declarations ==="
|
||||||
|
|
||||||
|
recipe_count=0
|
||||||
|
declared_count=0
|
||||||
|
|
||||||
|
for recipe_toml in recipes/*/recipe.toml recipes/*/*/recipe.toml; do
|
||||||
|
[ -f "$recipe_toml" ] || continue
|
||||||
|
recipe_count=$((recipe_count + 1))
|
||||||
|
|
||||||
|
recipe_dir=$(dirname "$recipe_toml")
|
||||||
|
recipe_name=$(basename "$recipe_dir")
|
||||||
|
|
||||||
|
# Parse installs field from [package] section
|
||||||
|
# Format: installs = ["/usr/bin/foo", "/usr/lib/init.d/10_bar.service"]
|
||||||
|
in_package=false
|
||||||
|
in_installs=false
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^\[package\] ]]; then
|
||||||
|
in_package=true
|
||||||
|
in_installs=false
|
||||||
|
continue
|
||||||
|
elif [[ "$line" =~ ^\[ ]]; then
|
||||||
|
in_package=false
|
||||||
|
in_installs=false
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $in_package; then
|
||||||
|
if [[ "$line" =~ ^installs ]]; then
|
||||||
|
in_installs=true
|
||||||
|
fi
|
||||||
|
if $in_installs; then
|
||||||
|
paths=$(echo "$line" | grep -oP '"[^"]+"' | tr -d '"' || true)
|
||||||
|
for path in $paths; do
|
||||||
|
declared_count=$((declared_count + 1))
|
||||||
|
if [ -n "${PATH_REGISTRY[$path]+x}" ]; then
|
||||||
|
existing="${PATH_REGISTRY[$path]}"
|
||||||
|
echo "CONFLICT: '$path' claimed by both '$existing' and '$recipe_name'"
|
||||||
|
if [[ "$path" == *"/init.d/"* ]]; then
|
||||||
|
echo " SEVERITY: init service conflict (critical)"
|
||||||
|
failures=$((failures + 1))
|
||||||
|
else
|
||||||
|
echo " SEVERITY: non-critical path overlap"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
PATH_REGISTRY["$path"]="$recipe_name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$line" =~ \] ]]; then
|
||||||
|
in_installs=false
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < "$recipe_toml"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo " Scanned $recipe_count recipes, found $declared_count declared install paths"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Cross-referencing config [[files]] against recipe installs ==="
|
||||||
|
|
||||||
|
config_conflicts=0
|
||||||
|
for config_file in config/redbear-*.toml; do
|
||||||
|
[ -f "$config_file" ] || continue
|
||||||
|
config_name=$(basename "$config_file")
|
||||||
|
|
||||||
|
in_files=false
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^\[\[files\]\] ]]; then
|
||||||
|
in_files=true
|
||||||
|
continue
|
||||||
|
elif [[ "$line" =~ ^\[ ]]; then
|
||||||
|
in_files=false
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $in_files && [[ "$line" =~ path[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
|
||||||
|
config_path="${BASH_REMATCH[1]}"
|
||||||
|
for registered_path in "${!PATH_REGISTRY[@]}"; do
|
||||||
|
if [ "$config_path" = "$registered_path" ]; then
|
||||||
|
echo "COLLISION: config '$config_name' creates '$config_path' but recipe '${PATH_REGISTRY[$registered_path]}' also installs it"
|
||||||
|
config_conflicts=$((config_conflicts + 1))
|
||||||
|
if [[ "$config_path" == *"/usr/lib/init.d/"* ]]; then
|
||||||
|
echo " SEVERITY: init service in /usr/lib/init.d/ (will be overwritten by package)"
|
||||||
|
failures=$((failures + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
done < "$config_file"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo " Found $config_conflicts config/recipe path collision(s)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Validation complete ==="
|
||||||
|
if [ $declared_count -eq 0 ]; then
|
||||||
|
echo " NOTE: No recipes declare 'installs' yet. Add installs = [...] to [package] sections for full validation."
|
||||||
|
fi
|
||||||
|
if [ $failures -gt 0 ]; then
|
||||||
|
echo "FAILED: $failures conflict(s) found" >&2
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "PASSED: No conflicts found"
|
||||||
|
fi
|
||||||
Executable
+193
@@ -0,0 +1,193 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# validate-init-services.sh — Post-image init service validation
|
||||||
|
#
|
||||||
|
# Validates that init service files in a built image match expectations:
|
||||||
|
# 1. Config overrides in /etc/init.d/ differ from /usr/lib/init.d/ counterparts
|
||||||
|
# 2. No missing init service files that configs expected to create
|
||||||
|
# 3. Scheme-type services have corresponding binaries
|
||||||
|
# 4. No dependency cycles in service graph
|
||||||
|
#
|
||||||
|
# Requires: redoxfs FUSE mount (or ext4 mount)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/validate-init-services.sh build/x86_64/redbear-full/harddrive.img
|
||||||
|
# scripts/validate-init-services.sh build/x86_64/redbear-mini/harddrive.img
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 — All validations passed
|
||||||
|
# 1 — Validation failures found
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
IMAGE_PATH="${1:?Usage: $0 <image-path>}"
|
||||||
|
|
||||||
|
if [ ! -f "$IMAGE_PATH" ]; then
|
||||||
|
echo "ERROR: Image not found: $IMAGE_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
MOUNT_DIR=$(mktemp -d /tmp/redbear-validate-XXXXXX)
|
||||||
|
failures=0
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
fusermount -u "$MOUNT_DIR" 2>/dev/null || true
|
||||||
|
rmdir "$MOUNT_DIR" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "=== Mounting image ==="
|
||||||
|
redoxfs "$IMAGE_PATH" "$MOUNT_DIR"
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 1. Checking /etc/init.d/ override effectiveness ==="
|
||||||
|
|
||||||
|
if [ -d "$MOUNT_DIR/usr/lib/init.d" ] && [ -d "$MOUNT_DIR/etc/init.d" ]; then
|
||||||
|
etc_count=$(find "$MOUNT_DIR/etc/init.d" -type f 2>/dev/null | wc -l)
|
||||||
|
usr_count=$(find "$MOUNT_DIR/usr/lib/init.d" -type f 2>/dev/null | wc -l)
|
||||||
|
echo " /usr/lib/init.d/: $usr_count files"
|
||||||
|
echo " /etc/init.d/: $etc_count files"
|
||||||
|
|
||||||
|
for etc_file in "$MOUNT_DIR"/etc/init.d/*; do
|
||||||
|
[ -f "$etc_file" ] || continue
|
||||||
|
basename=$(basename "$etc_file")
|
||||||
|
usr_file="$MOUNT_DIR/usr/lib/init.d/$basename"
|
||||||
|
|
||||||
|
if [ -f "$usr_file" ]; then
|
||||||
|
if diff -q "$etc_file" "$usr_file" > /dev/null 2>&1; then
|
||||||
|
echo " WARN: /etc/init.d/$basename identical to /usr/lib/init.d/$basename (redundant override)"
|
||||||
|
else
|
||||||
|
echo " OK: /etc/init.d/$basename differs from /usr/lib/init.d/$basename (override active)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " OK: /etc/init.d/$basename (new service, no package counterpart)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo " One or both init.d directories missing — skipping override check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 2. Checking scheme-type services have binaries ==="
|
||||||
|
|
||||||
|
if [ -d "$MOUNT_DIR/usr/lib/init.d" ] || [ -d "$MOUNT_DIR/etc/init.d" ]; then
|
||||||
|
for svc_file in "$MOUNT_DIR"/usr/lib/init.d/*.service "$MOUNT_DIR"/etc/init.d/*.service; do
|
||||||
|
[ -f "$svc_file" ] || continue
|
||||||
|
|
||||||
|
scheme_name=$(grep -oP 'type\s*=\s*\{\s*scheme\s*=\s*"([^"]+)"' "$svc_file" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' || true)
|
||||||
|
if [ -n "$scheme_name" ]; then
|
||||||
|
# Scheme daemon binaries use <name>d convention (e.g., scheme "evdev" -> binary "evdevd")
|
||||||
|
found_binary=false
|
||||||
|
for candidate in "$scheme_name" "${scheme_name}d" "redbear-$scheme_name"; do
|
||||||
|
if [ -x "$MOUNT_DIR/usr/bin/$candidate" ]; then
|
||||||
|
echo " OK: scheme '$scheme_name' has binary /usr/bin/$candidate"
|
||||||
|
found_binary=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$found_binary" = "false" ]; then
|
||||||
|
echo " FAIL: scheme '$scheme_name' has NO binary at /usr/bin/$scheme_name, /usr/bin/${scheme_name}d, or /usr/bin/redbear-$scheme_name"
|
||||||
|
failures=$((failures + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 3. Checking service dependency graph for cycles ==="
|
||||||
|
|
||||||
|
if [ -d "$MOUNT_DIR/usr/lib/init.d" ] || [ -d "$MOUNT_DIR/etc/init.d" ]; then
|
||||||
|
# Collect all services
|
||||||
|
services_dir=$(mktemp -d)
|
||||||
|
for svc_file in "$MOUNT_DIR"/usr/lib/init.d/* "$MOUNT_DIR"/etc/init.d/*; do
|
||||||
|
[ -f "$svc_file" ] || continue
|
||||||
|
basename=$(basename "$svc_file")
|
||||||
|
cp "$svc_file" "$services_dir/$basename" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
|
||||||
|
# Simple cycle detection: build adjacency list, do DFS
|
||||||
|
python3 -c "
|
||||||
|
import os, re, sys
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
svc_dir = '$services_dir'
|
||||||
|
graph = defaultdict(list)
|
||||||
|
all_nodes = set()
|
||||||
|
|
||||||
|
for f in os.listdir(svc_dir):
|
||||||
|
all_nodes.add(f)
|
||||||
|
content = open(os.path.join(svc_dir, f)).read()
|
||||||
|
for dep in re.findall(r'requires_weak\s*=\s*\[([^\]]*)\]', content) + re.findall(r'requires\s*=\s*\[([^\]]*)\]', content):
|
||||||
|
for d in re.findall(r'\"([^\"]+)\"', dep):
|
||||||
|
graph[f].append(d)
|
||||||
|
|
||||||
|
# DFS cycle detection
|
||||||
|
WHITE, GRAY, BLACK = 0, 1, 2
|
||||||
|
color = {n: WHITE for n in all_nodes}
|
||||||
|
has_cycle = False
|
||||||
|
|
||||||
|
def dfs(node):
|
||||||
|
global has_cycle
|
||||||
|
color[node] = GRAY
|
||||||
|
for neighbor in graph.get(node, []):
|
||||||
|
if neighbor not in color:
|
||||||
|
continue
|
||||||
|
if color[neighbor] == GRAY:
|
||||||
|
print(f' CYCLE: {node} -> {neighbor}')
|
||||||
|
has_cycle = True
|
||||||
|
elif color[neighbor] == WHITE:
|
||||||
|
dfs(neighbor)
|
||||||
|
color[node] = BLACK
|
||||||
|
|
||||||
|
for node in sorted(all_nodes):
|
||||||
|
if color[node] == WHITE:
|
||||||
|
dfs(node)
|
||||||
|
|
||||||
|
if not has_cycle:
|
||||||
|
print(' OK: No dependency cycles detected')
|
||||||
|
else:
|
||||||
|
sys.exit(1)
|
||||||
|
" 2>&1 || {
|
||||||
|
echo " FAIL: Dependency cycle detected!"
|
||||||
|
failures=$((failures + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
rm -rf "$services_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 4. Checking for missing dependencies ==="
|
||||||
|
|
||||||
|
if [ -d "$MOUNT_DIR/usr/lib/init.d" ] || [ -d "$MOUNT_DIR/etc/init.d" ]; then
|
||||||
|
all_files=$(mktemp)
|
||||||
|
for svc_file in "$MOUNT_DIR"/usr/lib/init.d/* "$MOUNT_DIR"/etc/init.d/*; do
|
||||||
|
[ -f "$svc_file" ] || continue
|
||||||
|
basename "$svc_file" >> "$all_files"
|
||||||
|
done
|
||||||
|
|
||||||
|
for svc_file in "$MOUNT_DIR"/usr/lib/init.d/* "$MOUNT_DIR"/etc/init.d/*; do
|
||||||
|
[ -f "$svc_file" ] || continue
|
||||||
|
svc_name=$(basename "$svc_file")
|
||||||
|
for dep in $(grep -oP 'requires_weak\s*=\s*\[([^\]]*)\]' "$svc_file" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' || true); do
|
||||||
|
if ! grep -q "^${dep}$" "$all_files" 2>/dev/null; then
|
||||||
|
echo " WARN: $svc_name requires '$dep' but file not found in any init.d directory"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for dep in $(grep -oP 'requires\s*=\s*\[([^\]]*)\]' "$svc_file" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' || true); do
|
||||||
|
if ! grep -q "^${dep}$" "$all_files" 2>/dev/null; then
|
||||||
|
echo " FAIL: $svc_name requires '$dep' (hard dep) but file not found"
|
||||||
|
failures=$((failures + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
rm -f "$all_files"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Validation complete ==="
|
||||||
|
if [ $failures -gt 0 ]; then
|
||||||
|
echo "FAILED: $failures validation failure(s) found" >&2
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "PASSED: All validations passed"
|
||||||
|
fi
|
||||||
@@ -130,6 +130,8 @@ pub struct PackageRecipe {
|
|||||||
pub dependencies: Vec<PackageName>,
|
pub dependencies: Vec<PackageName>,
|
||||||
pub version: Option<String>,
|
pub version: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
#[serde(rename = "installs")]
|
||||||
|
pub installs: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
|
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
|
||||||
|
|||||||
Reference in New Issue
Block a user