NextBSD: executing the releng/15.1 bump & drm-kmod 6.12

The per-repo runbook for issues #336 (base 15.0→15.1) and #337 (graphics kexts 6.6→6.12 + firmware) — from a six-agent scope of the whole build chain, June 2026.

1. The four headline findings (verified during scoping)

2. The build chain & dispatch topology

Six repos, wired by repository_dispatch. The bump must propagate in dependency order; each arrow is an automatic cross-repo dispatch (on a real upstream change, not PRs). Your manual checkpoint sits right after the sync.

freebsd-src (fork)        sync-fork.yml  ──upstream-updated──▶  toolchain
   releng/15.1                                                     │
        ▲                                                          ├──toolchain-updated──▶  nextbsd-kernel
   [YOU VERIFY HERE]                                               └──toolchain-updated──▶  nextbsd-freebsd-compat
                                                                          │                        │
   nextbsd-kernel ──kernel-updated──▶ nextbsd-kernel-modules             │                        │
        │  publishes kernel-obj + nextbsd-kernel to  ◀── continuous ──┐  │                        │
        ▼                                                             │  ▼                        ▼
   (continuous: kernel-obj-*.tar.gz, nextbsd-kernel-*.tar.gz)         (continuous: graphics-kexts.tar.gz)   (continuous: nextbsd-base-*.tar.gz)
        └──────────────────────────────┬───────────────────────────────────────────┬───────────────────────┘
                                        ▼  base-updated / latest-continuous pull
                                   nextbsd (ISO assembler)  ── boot-test ──▶  image

Note the toolchain fans out to both nextbsd-kernel and nextbsd-freebsd-compat in parallel. The ISO repo pulls all three upstream artifacts (kernel, modules, compat) from their rolling continuous tags — no pins — so 15.1 artifacts flow in automatically once published.

3. Per-repo scope

Pills mark which ticket each edit serves: #336 base bump, #337 drm 6.12. “Decoy” = a FREEBSD_BRANCH env that looks like the pin but is never dereferenced (the real source version arrives baked in the toolchain image).

3.1  nextbsd-redux/freebsd-src — the sync fork #336

Pure upstream mirror, no NextBSD-local commits on the releng branches (confirmed: fork releng/15.0 is byte-identical to upstream; this upholds the “never touch freebsd-src” rule). releng/15.1 already exists in the fork but is stale — sitting at 15.1-RC2, ~18 commits behind upstream releng/15.1 HEAD (96841ea0, 2026-06-12). The sync is fast-forward-only (GitHub merge-upstream API), so it cannot lose work or force-push.

File:lineNowEdit
sync-fork.yml:20ref: releng/15.015.1
sync-fork.yml:28gh repo sync --branch releng/15.015.1
sync-fork.yml:35git fetch origin releng/15.015.1
sync-fork.yml:36git rev-parse origin/releng/15.015.1
sync-fork.yml:50client_payload[branch]=releng/15.015.1 (dispatch to toolchain)
sync-fork.yml:9comment “operates exclusively on releng/15.0”15.1 (cosmetic)

A global releng/15.0→releng/15.1 replace in this one file is correct (0 other refs anywhere in the repo). Decision for you: replace 15.0 entirely (clean bump, the issue’s intent) vs. sync both branches through a transition window.

3.2  nextbsd-redux/nextbsd-kernel-toolchain — the container #336

Clones the fork at ${FREEBSD_BRANCH} into /usr/src and bakes a kernel-toolchain stage; the GHCR image is the source artifact downstream builds inside. This is the first repo to actually change after the fork. Clang/LLVM stays 19 for 15.1 (both cut from stable/15) — do not bump it. __FreeBSD_version is not baked here; it rides in the cloned tree.

File:lineEdit
build-toolchain.yml:19FREEBSD_BRANCH: releng/15.0 → 15.1 (the value CI uses; flows to build-args at :88)
Dockerfile.amd64:5ARG FREEBSD_BRANCH=releng/15.0 → 15.1
Dockerfile.arm64:5ARG FREEBSD_BRANCH=releng/15.0 → 15.1
README.md:7 / Dockerfile.*:12doc/comment “15.0”/“Clang 19” wording (cosmetic)

3.3  nextbsd-redux/nextbsd-kernel — patches + config + overlay #336 #337

The small patch repo (not a source fork). Corrected headline (§1.1, §7.5): 5 of 7 patches are clean on 15.1; 0001 and 0004 needed a mechanical rebase. The FREEBSD_BRANCH env here is a decoy (never read — the branch lives in the toolchain Dockerfiles).

PatchTarget(s)15.1 risk
0001 widen lkmnosys bandsyscalls.masterDRIFTED — 15.1 added base syscalls 599–602; band shifted 599–646 → 603–650 (PR #47); make sysent regenerates companions
0002 newbus quiesce hooksubr_bus.c, eventhandler.hVery low — both identical
0003 reserve EVFILT_MACHPORTsys/event.hNone — 15.1 still SYSCOUNT 15; clean
0004 firmware_path raw searchsubr_firmware.cDRIFTED — 15.1 fixed the inverted warn bool at :284; removed-line context updated to (flags & FIRMWARE_GET_NOWARN) == 0 (PR #47); 91-line refactor hunk already clean
0005 coredumps offkern_ucoredump.cNone — one-liner
0006 unionfs rename dir targetunion_vnops.cLow — hand-authored zero-index hunk; identical tree
0007 unionfs tagged fileidunion_vnops.c, union.hLow — large; disjoint hunks from 0006; order matters (0006→0007)

Edits: build.yml:15 FREEBSD_BRANCH→15.1 (decoy, cosmetic) and build.yml:323 vmactions release: '15.0'→'15.1' (real — the smoke-test VM that md-mounts/injects the kernel; keep aligned for KBI hygiene). The build also appends mach_knote_enqueue to stock kern_event.c at build.yml:135 — depends on knote_enqueue staying static-callable (kqueue unchanged on 15.1, low risk). Verify config(8) accepts every config/NEXTBSD device name (em, ig4, ietp, iichid, the h* HID leaves) against 15.1’s GENERIC on the first build.

If 6.12 had needed a new baked dep (it does not — §1.2): config-level deps go next to the graphics block at config/NEXTBSD:41-42; an option-less helper module mirrors files.linuxkpi_video (new src-overlay/conf/files.<mod> + a cat >> conf/files wire-in step). Kept here for the record.

3.4  nextbsd-redux/nextbsd-kernel-modules — where both tickets land #336 #337

The repo that builds the graphics kexts. The DRM version lives in exactly one literal; the 15.1-ness arrives via the ingested kernel-obj, not a local pin.

File:lineEditTicket
build.yml:28FREEBSD_BRANCH: releng/15.0 → 15.1 — the load-bearing pin; drives the release-notes string#336
build.yml:225--branch 6.6-lts → 6.12-lts — the single DRM literal (no submodule/var)#337
build.yml:454, 621, 674FW_TAG=20260519 → newer linux-firmware tag — three un-shared copies, edit all (boot-test sample / iwlwifi kext / graphics kexts) or they desync#337
build.yml:223, 186-187step name + comments “6.6-lts … drm-66-kmod … releng/15.0” (cosmetic)both
build.yml:530vmactions release: '15.0' (host VM for UFS inject) → '15.1' optional/availability only#336
tools/gen-i915-personalities.sh:47REWRITE the ID regex — see below#337

Firmware size: bundling is whole-vendor cp -RL (build.yml:687), no per-GPU subsetting; amdgpu alone is “hundreds of MB” (the workflow says so at :663) and a newer FW_TAG only grows it, inflating graphics-kexts.tar.gz and the ISO. Subsetting to matched ASICs, or splitting firmware into a non-ISO asset, is a design change — out of scope for the one-line flip but flagged.

3.5  nextbsd-redux/nextbsd-freebsd-compat — the curated base #336

The FREEBSD_BRANCH: releng/15.0 at build.yml:18 is vestigial (never dereferenced; /usr/src comes baked in the toolchain image). So there is no load-bearing pin here — but this repo carries the migration’s sharpest runtime-not-CI risks:

Edits: build.yml:1815.1 (documentary) + comment refresh (build.yml:130,223, srclist.txt:53,185,240,242). The substance is the post-build verification list above, gated before the continuous cascade to the ISO.

3.6  nextbsd-redux/nextbsd — the ISO assembler #336

Net: near-zero substantive change. Pure “latest continuous” pass-through — base, kernel, and kexts are pulled by version-agnostic patterns, so 15.1 artifacts flow in the moment upstream publishes them. PKG_ABI is FreeBSD:${MAJOR}:${ARCH} (build.sh:31) → FreeBSD:15:amd64 for all 15.x — must not be hardcoded to a minor. The image’s displayed identity is a build timestamp, not a FreeBSD version. IGNORE_OSVERSION=yes everywhere means a 15.0 build host can run a 15.1 assembly — but see the correction in §7.1: the build VM also gap-fills the rootfs with its own binaries, so its release does end up in the shipped image. The matrix bump below is required, not optional.

File:lineVerdict
build.sh:31 PKG_ABI=FreeBSD:15:amd64KEEP — ABI-stable, derived by stripping minor
build.sh:652 grep 'releng/15.0' (non-fatal diagnostic)Bump to 15.1 or generalise to releng/15 — cosmetic, else the print goes silent
build.yml:100-102,280-281,398-399 matrix freebsd:'15.0' + variant:'15.0-RELEASE'REQUIRED (not optional — correction, §7.1) — the VM gap-fills the rootfs, so it must be 15.1. 15.1-RELEASE is on vmactions/the mirror. Bump both; also drop the version from the job name: labels (PR #341)
tests/boot-test.shRecommend ADD a sysctl kern.osreldate1500509 assertion — the acceptance floor is gated nowhere today

3.7  Deep dive: the i915 6.12 personality generator fix #337

An empty match table is a non-shipper, so this gets its own fully-scoped fix. The breakage, the trap, and the implementation:

What broke. tools/gen-i915-personalities.sh:48-52 extracts IDs with grep -oE 'INTEL_VGA_DEVICE\(0x[0-9A-Fa-f]+' over i915_pciids.h. On 6.12 that header has zero such literal rows — IDs moved into MACRO__(0x….) entries inside per-platform INTEL_<PLAT>_IDS(MACRO__, …) macros — so IDS is empty and the script trips its own [ -n "$IDS" ] guard. The output contract it must preserve: an IOKitPersonalities dict whose IOPCIPrimaryMatch is space-separated 0x<device><vendor> words (e.g. 0x59168086), vendor hardcoded 8086, IOProbeScore=10000. Consumed verbatim by ko2kext.sh -p into the kext Info.plist.

The fix — let the C preprocessor expand the driver’s own list. Redefine the INTEL_VGA_DEVICE callback to emit a tagged id, replay i915_pci.c’s INTEL_<PLAT>_IDS(INTEL_VGA_DEVICE, …) rows, and scrape. This resolves all nesting (MTL→ARL) for free, auto-excludes anything not in pciidlist, and tracks future drm-kmod bumps. The generator takes a new i915_pci.c arg; only the extraction block changes, output stays byte-identical:

REFS=$(grep -E 'INTEL_[A-Z0-9_]+_IDS[[:space:]]*\(INTEL_VGA_DEVICE' "$PCI_C" | sed -E 's@/\*.*@@')
IDS=$(
  {
    printf '#include "%s"\n'              "$SRC"
    printf '#undef INTEL_VGA_DEVICE\n'
    printf '#define INTEL_VGA_DEVICE(id, info) @@id@@\n'
    printf 'int nbkm_i915_ids[] = {\n%s\n};\n' "$REFS"
  } | "${CC:-cc}" -E -P -xc - 2>/dev/null \
    | grep -oE '@@0x[0-9A-Fa-f]+@@' | grep -oE '0x[0-9A-Fa-f]+' \
    | tr 'A-F' 'a-f' | sort -u
)
[ -n "$IDS" ] || { echo "i915 pciidlist expansion produced no ids" >&2; exit 1; }

The host runner (drm-kmod job, build.yml:271) has cc/clang; the header is self-contained (no -I needed). The build step gains a locator and passes it — and tighten the find to the moved path:

I915H=$(find /tmp/drm-kmod -path '*drm/intel/i915_pciids.h' | head -1)
I915C=$(find /tmp/drm-kmod -path '*gpu/drm/i915/i915_pci.c'   | head -1)
"$T/gen-i915-personalities.sh" "$I915H" "$I915C" org.nextbsd.kext.intelgraphics IntelGraphics > pers/IntelGraphics.iokitpersonalities

CI gate — close the hole that hid this. The current selftest (tests/personalities-selftest.sh) feeds the generator a synthetic old-form INTEL_VGA_DEVICE(0x….) fixture, so it would have stayed green against the real 6.12 break. Three layers:

  1. Update the fixture to 6.12 shape (a MACRO__(0x…) header + a tiny i915_pci.c stub), asserting MTL→ARL expansion lands and an xe-only id does not.
  2. Real-source count band after generation: N=$(grep -oE '0x[0-9a-f]{8}' pers/IntelGraphics.iokitpersonalities | sort -u | wc -l); [ "$N" -ge 300 ] && [ "$N" -le 450 ] (~364 expected). This alone would have caught the empty table.
  3. Driver cross-check in the FreeBSD VM (build.yml:528+): kldxref/pnpinfo the built i915kms.ko (its LKPI_PNP_INFO table), normalise to 0x<dev>8086, and assert set-equality with the generated match words — ground-truth parity that fails loudly on any future header drift.

Fallback if a compiler is ever absent: a pure-shell recursive macro-expansion seeded from the pciidlist macro names (must replicate the preprocessor’s fixpoint nesting — more fragile, kept only as a backstop). When NextBSD later adds an xe kext, LNL/BMG/standalone-ARL get their own generator off xe’s pciidlist — not this one.

4. Ordered runbook

Follow the dependency chain. Each step has a gate — do not cascade until it’s green.

Step 0 — Sync the fork to 15.1 #336

Edit sync-fork.yml (§3.1, five literals), merge to main, run via workflow_dispatch. First run fast-forwards releng/15.1 from RC2 to upstream HEAD and will cascade a full rebuild — expected.

Step 1 — [YOUR MANUAL CHECKPOINT] Verify the 15.1 source is current

gh api "repos/nextbsd-redux/freebsd-src/compare/freebsd:releng%2F15.1...nextbsd-redux:releng%2F15.1" \
  --jq '{status, ahead_by, behind_by}'      # expect {"status":"identical","ahead_by":0,"behind_by":0}

gh api repos/nextbsd-redux/freebsd-src/contents/sys/sys/param.h?ref=releng/15.1 \
  --jq '.content' | base64 -d | grep __FreeBSD_version   # expect 1501000  (≥ 1500509)

You asked to do this yourself — this is the gate before anything downstream builds on 15.1.

Step 2 — Rebuild the toolchain image #336

Edit the 4 FREEBSD_BRANCH pins (§3.2), merge. Gate: a PR build first (PRs never publish) to smoke-test the 15.1 clone + kernel-toolchain stage before it pushes :latest and dispatches downstream. Confirm clang stays 19.

Step 3 — Rebuild the kernel on 15.1 #336

Edit build.yml:15 (decoy) + :323 (smoke-test VM). Gate: git apply of all 7 patches succeeds (expected clean per §1.1); config(8) accepts every NEXTBSD device; buildkernel green; boot-smoke-test passes. The kernel publishes kernel-obj-* + nextbsd-kernel-* to continuous and dispatches modules.

Step 4 — Rebuild the curated base on 15.1 #336

(Runs in parallel with Step 3 — toolchain dispatches both.) After build: verify Lever B (strings ld-elf.so.1 | grep /System/Library/Libraries), confirm krb5 soname/flags, diff /stage libs vs Apple inventory for clobbers, reconcile the setuid guard. Do not let continuous cascade to the ISO until these pass.

Step 5 — Bump graphics kexts to 6.12 + firmware #337

Now on a 15.1 kernel-obj: flip build.yml:225 to 6.12-lts, bump all three FW_TAG copies, and rewrite gen-i915-personalities.sh per the implementation-ready diff in §3.7 (preprocessor-driven, 364 IDs). Gates: required .ko set still produced (i915kms/amdgpu/radeonkms); i915 ID count is non-zero and matches kldxref; OSBundleLibraries chain still valid (no new drm.ko dep, per §1.2). Watch graphics-kexts.tar.gz size.

Step 6 — Assemble & boot-test the ISO #336 #337

15.1 artifacts flow in automatically. Optional: bump build.sh:652 diagnostic; add the kern.osreldate assertion to boot-test.sh. Final acceptance: boot the image, sysctl kern.osreldate1500509 (expect 1501000), launchd/kexts/networking up, a supported GPU brings up KMS against the 6.12 drm.ko, pkg install still works (FreeBSD:15:amd64).

5. Risk register (ranked)

#RiskWhereSurfacesMitigation
1i915 generator extracts 0 IDs on 6.12 (old regex form gone)modules gen-i915-*.sh:48-52Build — generate step exit 1s on the empty set (blocks #337; selftest stays green on its synthetic fixture)§3.7: preprocessor rewrite off pciidlist (364 IDs); count-band + kldxref parity gate
2Lever B silently drops /System/Library/Librariescompat build.yml:101-104Runtime (desktop can’t load libs)strings ld-elf.so.1 | grep post-build
3New 6.12 MODULE_DEPEND not bakedkernel config / modulesKext load on real HWResolved — 6.12 drm.ko adds no new dep (§1.2)
4krb5 closure fails to compile on 15.1compat build.yml:128-164BuildRe-check soname/MK_*; adjust KRBFLAGS
5Toolchain tag skew (no 15.x marker)toolchain GHCR tagsSilent 15.0 carry-overConfirm downstream reads dispatch payload, not literal tags
6Firmware bloat inflates ISOmodules build.yml:687ISO sizePer-ASIC subset or split FW asset (design change)
7Graphics kexts not load-tested in CImodules smoke-testBoot onlyAdd graphics-kext load test
8No osreldate floor gate in CInextbsd boot-test.shAdd sysctl kern.osreldate assertion
9Build VM gap-fill ships stale 15.0 userland binaries (init, uname, freebsd-version) onto a 15.1 kernelnextbsd build.sh:~202 + matrix freebsd:'15.0'Runtime — CI green, but booted image shows uname -U 15.0 vs uname -K 15.1Bump matrix VM to 15.1 (§7.1, PR #341); verify uname -U == uname -K == 1501000 on a booted image

6. Acceptance — mapped to the tickets

7. Execution addendum — corrections & extra steps from the live bump (20 June 2026)

The bump was executed end-to-end on 20 June 2026 (kernel PR #47, compat PR #27, ISO-matrix PR #341). Four findings corrected or extended the scoping above — including a wrong “zero patch drift” prediction (§7.5) — recorded here so the next minor bump (15.1→15.2) is mechanical.

7.1  The build VM is a load-bearing source — the matrix bump is REQUIRED, not optional

§3.6 originally rated the matrix.freebsd:'15.0' pin “optional, build-host only.” That was wrong. build.sh (~line 202) gap-fills the rootfs from the VM’s /bin /sbin /usr/bin with cp -RpPn (no-clobber) for everything the curated from-source base omits — which includes init, uname, and freebsd-version (the last removed from the base in #300). So a 15.0 VM bakes 15.0 userland binaries into a 15.1 image.

Fix: bump matrix.freebsd 15.0→15.1 and variant 15.0-RELEASE→15.1-RELEASE in all three matrix blocks (PR #341). The plan’s “only if vmactions offers a 15.1 image” conditional is resolved: 15.1-RELEASE is present on the mirror (.../releases/amd64/15.1-RELEASE/base.txz → HTTP 200) and accepted by vmactions/freebsd-vm. The job name: labels were also made version-agnostic (build (${{ matrix.arch }})) so they stop drifting each bump — variant still drives the distfiles cache key + FREEBSD_VARIANT.

7.2  workflow_dispatch builds but never publishes/cascades — use repository_dispatch

Every repo gates its publish-to-continuous and downstream cascade on github.event_name == 'push' || 'repository_dispatch'never workflow_dispatch. So a manual run (or a PR merge in a repo with no push trigger) builds and validates but leaves continuous stale. This bit the bump three times before it was understood.

RepoPublishes onTo actually publish/cascade after a manual change
nextbsd-kernelpush to main publishes continuous; module cascade only on repository_dispatchmerge cascades publish; for the full chain, dispatch the toolchain
nextbsd-freebsd-compatpublish gates only on ref==main (so workflow_dispatch on main does publish); no push trigger; ISO cascade only on repository_dispatchworkflow_dispatch on main to publish; then base-updated for the ISO
nextbsd (ISO)release job excludes workflow_dispatch; no push trigger on the buildgh api repos/nextbsd-redux/nextbsd/dispatches -f event_type=base-updated

Rule of thumb: a manual dispatch is for validation; a repository_dispatch (or the toolchain re-cascade) is for shipping.

7.3  Cache purge — optional hygiene, not a fix

No cache caused any version skew. ccache is content-addressed (a 15.1 source compile yields 15.1 objects every time — it cannot emit a stale-version binary); the distfiles cache is keyed on variant (build.yml:121), so it auto-invalidates when variant flips to 15.1-RELEASE. The stale binaries came from the VM image, which we don’t cache. For a clean-slate rebuild guarantee only:

for r in nextbsd-kernel-toolchain nextbsd-kernel nextbsd-kernel-modules nextbsd-freebsd-compat nextbsd; do
  gh cache delete --all -R nextbsd-redux/$r
done
# then re-cascade the whole chain from the top:
gh workflow run "Build Toolchain Containers" -R nextbsd-redux/nextbsd-kernel-toolchain --ref main

7.4  Verification on a booted image

7.5  The patch stack did drift — headline §1.1 was wrong

The scoping’s biggest claimed de-risk — “all 7 patch targets byte-identical, zero rebase” — did not hold. It was caught by the live Apply patches CI step failing, then confirmed against a clean releng/15.1 tree. Two of seven patches drifted (PR #47); the other five applied unchanged.

PatchWhat changed in 15.1Rebase
0001 lkmnosys band15.1 added base syscalls 599 kexec_load, 600 pdrfork, 601 pdwait, 602 renameat2, colliding with the band’s old 599–646 rangeShifted the 48-slot band to 603–650 (first free slot after 602). Safe — mach_syscall_wire.c auto-allocates via NO_SYSCALL; nothing hardcodes the base. make sysent regenerates companions.
0004 firmware_path15.1 fixed an inverted warn bool at subr_firmware.c:284 — the same bug 0004 already corrects (semantic convergence)Updated the removed-line context to 15.1’s (flags & FIRMWARE_GET_NOWARN) == 0; the 91-line refactor hunk was already clean.

Lesson: “same last-touch SHA on 15.0” does not imply identical on 15.1 — a releng minor can touch any file. The reliable gate is the Apply patches CI step itself (strict + cumulative, in order), not a pre-scan of file SHAs. Both edits were mechanical (no new logic), so the “minimal patch authoring” spirit held even though “zero” did not.

Sources & method

Scope produced by six parallel read-only agents over the NextBSD build chain (June 2026), each reporting file:line edits, rebase risk, and runtime-vs-CI failure surfaces. Cross-repo facts verified directly against GitHub: