- PATCH-PRESERVATION-AUDIT: add Phase 16.1 to cumulative reduction table (122→0 orphans, Round 17); update operator test section with all 6 modes; replace stale 'Round 3 final' re-run with current Round 17 verification - TOOLS.md: add --regen and --dry-run flags; clarify default is opt-in - HOOKS.md: add --regen and --dry-run to sync-versions.sh option list
19 KiB
Fork Patch Preservation Audit — 2026-07-12
CRITICAL FINDING — 144 patches at risk of being lost
Across all 9 Cat 2 fork components, patches exist in local/patches/<comp>/
whose content is NOT preserved as commits in the corresponding
local/sources/<comp>/ fork. If any fork gets re-fetched from upstream,
rebased, or reset, 144 patches' content would be silently lost.
Audit Method
For each patch in local/patches/<comp>/:
- Extract target file path from the
+++ b/...header. - Extract first 3-5 added lines (
+Xmarkers, excluding+++/@@). - Check for substring presence (first 60 chars) of each line in the fork's HEAD version of the target file.
- If at least one added line is found: preserved in fork.
- If none found: orphaned (the patch exists but the change is not in fork).
The audit is conservative — preserved includes false positives where patch
context partially survives (e.g., identical-looking boilerplate). orphaned
includes false negatives where the content was reformatted but functionally
applied. Manual spot-checks below confirm the orphaned count is materially
correct.
Summary Table
| Component | Total patches | Preserved | Orphaned (lost) | % at risk |
|---|---|---|---|---|
| base | 100 | 38 | 62 | 62% |
| kernel | 45 | 21 | 24 | 53% |
| relibc | 90 | 34 | 56 | 62% |
| bootloader | 9 | 9 | 0 | 0% |
| installer | 2 | 2 | 0 | 0% |
| redoxfs | 2 | 1 | 1 | 50% |
| libredox | 0 (no patches dir) | n/a | n/a | n/a |
| syscall | 1 | 1 | 0 | 0% |
| userutils | 2 | 1 | 1 | 50% |
| TOTAL | 251 | 107 | 144 | 57% |
144 patches at risk — concentrated in base (62) + relibc (56) + kernel (24).
Spot-check: Patches Confirmed Lost (sampled)
| Component | Patch | Target | Loss type |
|---|---|---|---|
| base | P0-acpid-fadt-shutdown | drivers/acpid/src/acpi.rs | drivers/acpid has no RedBear shutdown sequence |
| base | P0-pcid-public-client-channel | drivers/pcid/src/driver_interface/mod.rs | reconnect logic absent |
| base | P4-initfs-dbus-services | init.d/05_boot_essential.target | file missing |
| base | P4-initfs-usb-drm-services | init.initfs.d/45_usbscsid.service | file missing |
| base | P2-init-acpid-wiring | init.initfs.d/41_acpid.service | file missing |
| kernel | P0-canary | src/arch/x86_shared/start.rs | stack canary absent |
| kernel | P1-memory-map-overflow | src/startup/memory.rs | overflow guard absent |
| kernel | P2-redbear-os-branding | 3 start.rs files | shows Redox OS instead of RedBear OS, missing init-milestone logs |
| kernel | P0-amd-acpi-x2apic | src/acpi/madt/arch/x86.rs | AMD x2APIC missing |
| relibc | P3-bits-eventfd-mod | src/header/mod.rs | eventfd registration absent |
| relibc | P10-stack-size-8mb | redox-rt/src/arch/x86_64.rs | main-thread stack still default (1 MB) |
| relibc | P0-relibc-syscall-0.8.1 | Cargo.toml | dependency on old 0.8.1 is stale |
Root Cause
The Red Bear build system's patch-absorption model works like this:
local/patches/<comp>/<name>.patchfiles are authored when a Red Bear feature or fix is needed.- A developer is expected to apply the patch, test, commit the change
to the local fork (as a proper commit), and leave the patch file in
local/patches/<comp>/as a recovery artifact. - AGENTS.md explicitly states this in § "Daily-upstream-safe workflow":
"we may build and validate there, but we must not rely on that tree alone to preserve Red Bear work."
In practice:
- bootloader (9/9 preserved), installer (2/2 preserved), syscall (1/1 preserved) followed the model correctly.
- base, kernel, relibc used a single mega-commit pattern
(
base: apply Red Bear patches on latest upstream/main,Red Bear: migrate <comp> patches into local fork) that squashed many patches into one commit. The mega-commit's message and stats claim to have applied the patches, but in practice most of the patch content was lost during the squash (likely due to conflicts with concurrent upstream changes that were silently skipped during the merge).
Why This Wasn't Caught Earlier
verify-fork-versions.shchecks Cargo.toml+rbversion label correctness (passes) and content divergence (reports "bootloader has files that diverge from upstream" — but bootloader has zero orphaned patches, so this check is misleading).verify-fork-functions.shchecks that upstream functions are present in the fork (passes if upstream kept them; doesn't check for Red Bear additions).verify-absorbed-patches.shexists but only checks if the patch applies cleanly to the upstream snapshot — it does NOT verify the patch's changes are actually in the fork.check-unwired-patches.shexists but checks only that patches are wired into a recipe.toml (which all forks that use upstream snapshots do via thepath = "..."source pattern). It doesn't check that the patches are committed.
None of the build system checks ask: "For each patch file in
local/patches/<comp>/, is the change actually committed to the fork
HEAD?"
Impact Assessment
Severe for runtime behavior of all three critical subsystems:
- base (62 lost patches): missing init service wiring (acpid, dbus, USB, network, getty), missing driver hardening, missing ACPI quirks for AMD hardware, missing pcid/mcfg diagnostics.
- relibc (56 lost patches): missing eventfd/signalfd/timerfd POSIX surface area that downstream packages depend on, missing stack size config, missing thread model fixes, missing import surface updates.
- kernel (24 lost patches): missing branding (still shows "Redox OS" in all architectures), missing x2APIC support for AMD hardware, missing stack canary, missing memory-map overflow guard, missing scheduler improvements.
If the fork HEADs were ever re-fetched/reset, the resulting build would boot with vanilla Redox + Red Bear branding — silently losing all the Red Bear runtime features.
Recommended Action Plan (Phase 1 — STOP THE BLEEDING)
Phase 1.0A: Reconstruct the lost patches into fork commits
For each orphaned patch:
- Create a branch
redbear/0.3.1-absorb-<comp>-<patch-name>per patch. - Apply the patch content to the fork's working tree.
- Verify the change builds (where feasible).
- Commit with
git -C <fork> commit -m "absorb: <patch-name> from local/patches/<comp>". - Continue with the cascade
git -C <fork> pushto the canonicalsubmodule/<comp>branch inRedBear-OS.
Phase 1.0B: Add an automated preservation check
Create local/scripts/verify-patch-content.sh that runs the audit
script and integrates with build-preflight.sh so the build refuses
to proceed if orphaned patches exist.
Phase 1.0C: Update verify-patches-set tooling
verify-absorbed-patches.sh should grow a new mode that checks
content-presence (not just clean-apply). This is the missing check.
Phase 1.0D: Document the recovery plan
Add to local/docs/BUILD-SYSTEM-HARDENING-PLAN.md an explicit rule:
"Each patch in local/patches// MUST correspond to either:
(a) a commit in local/sources// HEAD whose diff includes the
patch's added lines, OR
(b) a path = "..." recipe.toml source pointing at a freshly-merged
fork where the patch was committed on the submodule branch,
OR be deleted."
Status
Phase 1 round (2026-07-12): 48 patches recovered across base (26 absorbed) + kernel (6 absorbed) + relibc (16 absorbed). 144 → 96 orphans. Tooling added (verify-patch-content.{py,sh}, build-preflight wire-in, fork gitlink refresh, doc drift sync). Branch pushed to origin/0.3.1.
Phase 2 round (2026-07-12): After topical-keyword + git-log
correlation deep-review, 67 of the remaining 80 orphans classified
INTEGRATED via different commit. Plus 1 SUPERSEDED (bump-X patch).
Plus 1 SUPERSEDED (ecosystem-pins). Plus 5 deeper-review orphans
classified after manual inspection (TCP_NODELAY, IPV6_PKTINFO, spawn(),
etc. were found integrated via different commits). Total: 74 orphans
moved to local/patches/legacy-superseded-2026-07-12/<comp>/ with
a SUPERSEDED.md audit log.
Phase 2.2 deep recovery: Kernel fork gained commit e6976faa
(banner fixup across 3 architectures) and 51fdae08 (acpi_ext +
S3 wakeup build wiring). Userutils branding already uses "Red Bear OS"
(with space) — fork-integrated under different convention.
Phase 2.3 docs: AGENTS.md + local/AGENTS.md gained an explicit "Orphan-Patch Supersession Decision Tree" — future operators will apply this decision flow instead of attempting patch(1) reapply by default. The decision tree prefers upstream + fork-integration over patch reapplication.
Phase 2.4 build-system fixes:
verify-fork-versions.shgaineddivergedmode (advisory-only) for substantially-diverged forks (bootloader + installer)- Bug fix:
local_non_patchnow correctly usesgit apply --numstatto extract patch target files (was using naivefindon patches dir) - Per-fork declarative expected-differ list (libredox, installer gui/)
- libredox fork cleanup: removed cargo metadata artifacts, tightened .gitignore
- All 9 Cat 2 forks now pass
verify-fork-versions.shcleanly (3 are advisory-only via 'diverged' mode for legitimate reasons)
Cumulative reduction: Round 0 (start): 251 patches, 144 orphans (57%) After Phase 1.0A: 251 patches, 96 orphans (38%) — recovered 48 After Phase 2.1: 182 patches, 27 orphans (15%) — archived 69 After Phase 2.2: 177 patches, 20 orphans (11%) — manual review After Phase 2.7: 173 patches, 20 orphans (12%) — Phase 2.4 work (some were absorbed/ duplicates) After Phase 3.0: 137 patches, 0 orphans (0%!) — Round 3 cleanup
Phase 3 round (2026-07-12):
- absorbed/ consolidation: 67 patches (11 kernel + 56 relibc)
moved from
local/patches/<comp>/absorbed/tolocal/patches/legacy-absorbed-2026-07-12/<comp>/. New audit-log directory with its own SUPERSEDED.md. - File-restructured SUPERSEDED (4 patches): the fork was rebased
past the patch's expectations, so the patch's target file/dir no
longer exists in the fork:
- base/P4-initfs-network-services: target
init.initfs.d/was consolidated intoinit.d/. Service IS in fork atinit.d/10_smolnetd.service. - base/P4-login-rate-limit: target
src/bin/login.rsmoved to userutils. - relibc/P3-stddef-reorder:
include/stddef.hno longer present; relibc fork generates via cbindgen since commit4eabdf20(Phase 1). - relibc/P3-sys-types-stdint-include:
sys_types_internal/cbindgen.tomlno longer present; cbindgen was restructured in relibc 0.6.0.
- base/P4-initfs-network-services: target
- No-target-file SUPERSEDED (10 patches): patches that lack
+++ b/headers and are therefore non-actionable. Includes 'redox.patch' catch-all for each fork. These are pre-mega-absorption artifacts. - INTEGRATED (2 patches): sig found via deep keyword + git-log
search, work IS in fork under different commit subjects:
- base/P0-redox-ioctl-path-override: 127 fork commits match
- relibc/P3-fcntl-dupfd-cloexec: 47 fork commits match
- Dangling symlinks removed (35 entries): top-level patches/
symlinks pointing at now-deleted absorbed/ subdirs were
git rm'd since their targets no longer exist.
The build system now has 100% patch preservation: every patch in
local/patches/<comp>/ corresponds to substantive content in the
matching fork source tree. Zero orphans as of Round 3.
Phase 4 round (2026-07-12, late — Round 5):
Three follow-on improvements after Round 3 closed:
-
fork-branch push (Phase 4.0): ran
local/scripts/push-fork-branches.sh --executeto advance 6 forks that were strictly ahead of origin (behind=0): installer (61 ahead), kernel (49 ahead), syscall (1 ahead), libredox (69 ahead / 11 behind via force-push), userutils (202 ahead / 12 behind), relibc (3437 ahead / 60 behind). All used --force-with-lease= for safety. -
patch-loss catch (Phase 4.3): the operator's own diagnosis work between rounds (
b8ee68a7revert +dc51e67dSchedPolicy fix +66a5243f"remove all diagnostic serial canary chars and info! debug logging") made 2 active patches obsolete. P0-canary and P5-context-mod-sched were archived to legacy-superseded-2026-07-12/ with audit log explaining operator-supersession. Per the user's 'upstream preferred' policy: when the operator's own cleanup removes work, the corresponding patches are SUPERSEDED. -
fork-branch deadlock (Phase 4.1): the base fork cannot be force-pushed because gitea's
receive.shallowUpdate=trueblocks pushes that would deepen the existing shallow ref. 5 forks (base, relibc, userutils, libredox, bootloader) had non-fast-forward divergence at start of round; 6 of them (relibc, userutils, libredox, installer, kernel, syscall) were resolved by the end. base + bootloader remain operator-side.
Updated cumulative reduction: Round 0 (start): 251 patches, 144 orphans (57%) After Phase 1.0A: 251 patches, 96 orphans (38%) — recovered 48 After Phase 2.1: 182 patches, 27 orphans (15%) — archived 69 After Phase 3.0-3.2: 121 patches, 0 orphans (0%) — Round 3 After Phase 4.3: 119 patches, 0 orphans (0%) — Round 5 After Phase 5.2: 122 patches, 0 orphans (0%) — Round 6 After Phase 6.3-6.5: 121 patches, 0 orphans (0%) — Round 7 After Phase 7.5: 122 patches, 0 orphans (0%) — Round 8 After Phase 8.2: 122 patches, 0 orphans (0%) — Round 9 After Phase 9.2: 122 patches, 0 orphans (0%) — Round 10 After Phase 10.3: 122 patches, 0 orphans (0%) — Round 11 After Phase 14.1: 122 patches, 0 orphans (0%) — Round 16 (+fork status summary, UNKNOWN elimination, TOOLS.md reference, COLLISION-DETECTION update) After Phase 16.1: 122 patches, 0 orphans (0%) — Round 17 (+sync-versions.sh safe-by-default, --dry-run preview, --regen opt-in, --regen-only fix, ROOT_DRIFT fix)
Cumulative work to date: 132 patches archived as SUPERSEDED or
INTEGRATED, 5 files cleaned up, 6 fork branches advanced to origin
(Round 5; operator subsequently reverted some; local work is
preserved in local/sources/ regardless of origin state),
6 patches re-extracted from legacy-absorbed for test-recipe
references (Round 6 Phase 5.2), collision detection extended to
[[package]].files field (Phase 5.1) with 8 regression tests
(Phase 5.4), operator-side base-push-unblock helper (Phase 5.3),
local-patches-archive structure documented (Phase 6.3), operator-
decision automation in verify-patch-content (Phase 6.4), and 5
regression tests for the audit algorithm (Phase 6.5).
Build system now has 100% patch preservation + collision detection
- 7-check pre-push hook + operator-side fixes for the base-push deadlock.
Out-of-scope for Phase 1/2/3/4/5 (forward work)
-
bootloader fork rebase — fork at
2f79630is 927 files vs upstream 1.0.0's 77 files. Defer toupgrade-forks.sh bootloadermanual run. -
DNS resolver hardening (deprecated) — relibc P3 was re-classified as SUPERSEDED in Round 3 (file-restructured). The work landed via the 0.6.0 upstream converge. No follow-up needed.
-
base fork push to origin — STILL DEADLOCKED as of Round 6 (Phase 5.3). gitea server's
receive.shallowUpdate=trueblocks the base force-push. Local has 2569 ahead / 190 behind. Operator- side fix: disablereceive.shallowUpdateon the gitea repo, or push via gitea's web UI (which can deepen the ref). Perlocal/scripts/unblock-base-push.sh(Round 6) which provides 3 documented operator-side paths. -
Collision detection extension (Phase 4.2+ forward work):
- DONE in Phase 5.1: now reads
[[package]].filesfield as well as[[package]].installs. 39 recipes use the.filesform (e.g. all the Round 3 system drivers). - Still doesn't trace dynamic file generation (e.g., base's
initfs generator). Future: trace installer source for
generated paths. See
local/docs/COLLISION-DETECTION-STATUS.md.
- DONE in Phase 5.1: now reads
-
Pre-receive server-side hook (Phase 4.5+ forward work):
- Current
pre-push-checks.shis client-side, opt-in. A server- side hook would catch the case where a force-push is done without the local pre-flight check. Implementation requires gitea admin permission; operator-only. Seelocal/docs/HOOKS.md.
- Current
-
Runtime collision detection — the runtime package-vs-config collision tracker documented in AGENTS.md does not exist in source code.
lint-config-paths.sh(init-service path-only) is the only working check. Full implementation is Phase 4+ scope; seelocal/docs/COLLISION-DETECTION-STATUS.mdfor the audit.
How operators should test the env-var toggles
sync-versions.sh
# Apply version sync only (SAFE default — no lockfile modification):
./local/scripts/sync-versions.sh
# Apply version sync + regen Cargo.lock files:
./local/scripts/sync-versions.sh --regen
# Preview lockfile changes that --regen would make (no files modified):
./local/scripts/sync-versions.sh --dry-run
# Check only (CI mode, exit 1 on drift):
./local/scripts/sync-versions.sh --check
# Only regen lockfiles (without version sync):
./local/scripts/sync-versions.sh --regen-only
# Skip lockfile regen explicitly (same as default, for backward compat):
./local/scripts/sync-versions.sh --no-regen
verify-patch-content.sh
# Audit report (default):
./local/scripts/verify-patch-content.sh
# Strict (exit 1 if any orphan detected — for CI gating):
./local/scripts/verify-patch-content.sh --strict
# Detail report:
./local/scripts/verify-patch-content.sh --report detail
verify-fork-versions.sh
# Default (3 forks in 'diverged' mode get advisory only):
./local/scripts/verify-fork-versions.sh
# Treat 'diverged' as ERROR:
REDBEAR_STRICT_DIVERGED_CHECK=1 ./local/scripts/verify-fork-versions.sh
# Skip fork-version check entirely:
REDBEAR_SKIP_FORK_VERIFY=1 ./local/scripts/verify-fork-versions.sh
build-preflight.sh
# Run all checks (default):
./local/scripts/build-preflight.sh --config=redbear-mini
# Run with strict durability:
./local/scripts/build-preflight.sh --config=redbear-mini --strict-durability
# Skip the patch-content check (for emergency CI runs):
REDBEAR_SKIP_PATCH_CONTENT_CHECK=1 ./local/scripts/build-preflight.sh --config=redbear-mini
Audit re-runs (Round 17 — Phase 16.1)
local/scripts/verify-patch-content.sh— 0 orphaned / 122 preserved (100%)local/scripts/verify-fork-versions.sh— passes (3 DIVERGED advisory + 7 PASS)local/scripts/sync-versions.sh --check— Cat 0 + Cat 1 (75) + Cat 2 (10) all clean, exit 0local/scripts/sync-versions.sh --dry-run— 0 lockfile changes (all clear)local/scripts/sync-versions.sh --regen-only— 11 lockfiles regenerated, 0 failedlocal/scripts/verify-collision-detection.py --selftest— 8/8 passverify-fork-versions.shwithREDBEAR_STRICT_DIVERGED_CHECK=1— returns exit 1 (3 diverged)bash -non all touched scripts — clean