2e764746e7
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.
194 lines
6.5 KiB
Bash
Executable File
194 lines
6.5 KiB
Bash
Executable File
#!/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
|