Moving a board to a newer kernel
A board that compiles its own kernel is pinned to an exact tag, and that tag goes stale. This tutorial moves the Turing RK1 forward, in the two shapes that job takes:
- Within the track —
v7.2to a later7.2.ypoint release. The patch series already claims the version, so this is a re-pin and a rebuild. - Across a version boundary — 7.2 to 7.3. The series makes no claim about 7.3 yet, so the first move is measuring whether it would survive, without changing a pin.
The second shape is the interesting one, and the order it runs in is the point: find out, then adopt. Nothing mutates a lock or a series until the answer is in.
This is the build side. Upgrading the kernel on a board that is already running is a different job with a different mechanism — see Upgrading the kernel.
What a kernel bump touches
Four things, in different repos, and they move at different rates:
| Thing | Where | Moves when |
|---|---|---|
| The pinned tag | the recipe’s .lock | every bump |
The kernel definition (track, fragments, series) | kernels/<id>.toml | at a version boundary — a kernel definition is version-coupled |
| The series envelope and per-patch ranges | the patches repo | when a boundary is measured |
The [support] claim | the recipe | when the moved pins are re-earned on hardware |
The device’s supported_kernels list gates which definitions a board can resolve, so a
new definition is also one line there.
Shape 1: within the track
kernels/rk3588-mainline-7.2.toml tracks 7.2.y, and series/rk3588-accel.toml declares
applies_to_kernel = ">=7.1.5, <7.3". A later 7.2 point release is inside both, so if the
series holds, nothing in the config or the series changes — only the pin.
# 1. Re-pin. This is the only command that consults upstream.
boot2deb update turing-rk1/forky --kernel-ref v7.2.1
# 2. Does the series still apply to the new tree? (--kernel-src makes the fetch
# near-instant if you have a checkout; omit it and the tree is auto-fetched.)
boot2deb verify-patches turing-rk1/forky --kernel-src ../linux
# 3. Does the .config still generate cleanly on the patched tree?
boot2deb verify-config turing-rk1/forky
# 4. What will actually recompile? Offline, reads the lock and the build stamps.
boot2deb why-rebuild turing-rk1/forky
# 5. Build.
boot2deb build turing-rk1/forky
Step 1 says two things worth reading rather than scrolling past. When a recipe claims
validated, moving its pins retires the evidence that claim rested on, and update says
so:
warning: recipe 'turing-rk1/forky' claims support = "validated" as of 2026-07-16, but
this update moved its pins:
kernel v7.2 (8d3ae59288f1) -> v7.2.1 (155b42bec9cb)
that claim now describes a combination nothing has booted — re-validate on hardware and
update the date, or set status = "expected" until you do
note: this re-pin changes the generated support matrix — regenerate it:
boot2deb support-matrix --markdown > docs/src/reference/support-matrix.md
Both are advisory, since the lock is written either way. Both name work that belongs at the end of this tutorial, in Closing the loop.
Deciding whether the validated claim survives means knowing what the re-pin moved,
which is what diff is for. Keep the old lock before step 1 and compare against it
afterwards:
cp recipes/turing-rk1/forky.lock /tmp/old.lock # before step 1
boot2deb diff /tmp/old.lock turing-rk1/forky # after it
It names the kernel ref and commit that moved, and the kconfig symbols the fragment sets now request differently (with the fragment behind each). Where the patches commit moved too, it names the individual patch files that were added, removed, or rewritten. That is the evidence the claim is re-earned or retired on. See Comparing two build points.
If you would rather not touch the lock until you know the answer, step 2 can come first.
verify-patches turing-rk1/forky --kernel v7.2.1 --kernel-path ../linux measures a
version the lock does not name and leaves the lock alone. That is the candidate
path below, and it works inside the envelope as well
as outside it.
Step 2 can fail on a patch, and step 3 on a kconfig symbol renamed out from under a fragment. Either turns this shape into the next one. The series has hit a boundary, even inside its declared envelope.
Shape 2: across a version boundary
Step 1: get a tree at the candidate version
The lock pins no commit for a kernel it does not name, so the candidate has to come from a checkout you point at:
git -C ../linux fetch --tags origin
git -C ../linux checkout v7.3
A release candidate is a legitimate answer here — measuring v7.3-rc5 before 7.3 exists is
exactly what this path is for.
Step 2: measure, and change nothing
boot2deb verify-patches turing-rk1/forky \
--kernel v7.3 --kernel-path ../linux --keep-going
--kernel verifies against a kernel the lock does not pin and leaves the lock alone.
Three rules differ from the locked path, and each of them exists so the question can be
asked at all:
- The declared envelope does not gate the run. The series still says
<7.3— refusing the candidate on that basis would answer the question by assuming it. The run reports that the kernel is outside the envelope and measures it anyway, and whatgit amdoes is the answer. A clean result is the evidence for widening the envelope, not a claim that it already covers 7.3. - A release candidate is matched as its base release, because by semver
7.3.0-rc3satisfies neither<7.3nor>=7.3and a release-only range would reject every RC. The build path stays release-strict, and this path does not. --keep-goingreports every failure in one pass. One boundary usually spawns adjacent ones, because a reworked patch shifts the context every later patch applies against. Stopping at the first therefore turns the survey into serial discovery. Each failing patch is skipped so the rest still get measured, which makes the report a map of the damage rather than a final verdict.
Per-entry kernels ranges still narrow the series on this path. A patch already marked
obsolete at the candidate drops out instead of counting as a failure.
Step 3: act on the report
Every failure is one of three things, and each has its own encoding in the series manifest:
Upstreamed. The patch is in the new kernel already, and the failure is the code being there twice. Give the entry an upper bound while an older kernel in the envelope still needs it. Delete it once the floor passes the version that took it:
kernel = [
{ path = "media-accel/kernel/050-av1-iommu-v14-curated.patch", kernels = "<7.2" },
]
Read what upstream took, not just that it applied, and check that a clean apply still
builds. Three shapes get through the git am pass:
Partial absorption. The Verisilicon IOMMU’s driver, binding and DT node landed in 7.2
but its CONFIG_VSI_IOMMU=m defconfig line did not. Dropping the patch therefore stopped
building the driver, until a kconfig fragment picked the symbol up.
A conflicting fix in the same function. 7.2.5 added its own bounds check to
st_ref_pic_set_prediction(), returning void — inside a function media-accel/kernel/083
retypes to int. Every hunk applied strictly and the compile then failed on 'return' with no value. Only a build finds that one.
An equivalent fix beside yours. 7.2.7 bounds the HEVC and AV1 tile counts in the V4L2
core’s control validation. media-accel/kernel/073 and 074 made the same check in the
drivers. Both still applied strictly and still built, so every gate passed with each
bound checked twice. Only reading the incremental patch finds that one. Retiring the pair
then moved the context 083 applies against, so it was rebased in the same round.
Reworked. The patch is still needed but no longer applies. Rebase it, keep both versions, and give them complementary ranges. One list then builds both generations correctly from a single checkout, which a list mutated in place cannot do.
The RK3588 RGA device-tree wiring is the worked example. In 7.1 the device tree describes one RGA core, and in 7.2 it describes all three. The patch that points them at the out-of-tree driver therefore read differently on each, for as long as the envelope covered both:
kernel = [
{ path = "media-accel/kernel/072-rk3588-rga-dts-7.1.patch", kernels = "<7.2" },
{ path = "media-accel/kernel/072-rk3588-rga-dts-7.2.patch", kernels = ">=7.2" },
]
A rebase needs no pair when the floor has moved past the divergence. rocket/083 keeps
the IOMMU domain attached across jobs, and 7.2.4 rewrote the error handling around the
iommu_attach_group() call it replaces. With the floor past 7.2.4 that patch is rebased
onto the kernel’s goto err_put_pm, with no second variant to carry.
Regenerate a rebased patch with git format-patch rather than hand-editing the diff —
git am --3way needs the index lines a hand-written hunk does not carry.
Obsolete. Nothing needs it any more, at any version. Retire the entry and its file:
an old lock names an old patches commit, whose tree still contains both.
Verify with plain git am, not git am --3way, before believing a clean run. A shallow
build tree holds only the blobs of the commit it is at. A patch that needs a three-way
merge against a previous generation’s file therefore resolves in a full checkout and
hard-fails in a build. Adding --3way is what hides that difference.
Re-run step 2 after each round. When it comes back clean, and only then, widen the envelope:
applies_to_kernel = ">=7.1.5, <7.4"
That edit is what turns a measurement into a claim, which is why it comes last. Commit it in
the patches repo — and push it, because a pin naming an unpushed commit resolves against
your checkout and nowhere else:
git -C ../patches add -A && git -C ../patches commit -m "rk3588-accel: extend to 7.3"
git -C ../patches push
Step 4: write the kernel definition
A kernel definition owns everything version-coupled, so 7.3 is a new file rather than an
edit — kernels/rk3588-mainline-7.3.toml:
flavor = "mainline"
source = "linux-stable"
track = "7.3.y"
base_defconfig = "defconfig"
config_fragments = ["base/debian-arm64", "soc/rk3588", "accel/full"]
patch_series = ["rk3588-accel"]
patches_url = "https://github.com/gregordinary/patches.git"
supported_socs = ["rk3588"]
The series name is unchanged — series names are semantic, never version-suffixed, so the
kernel definitions referencing them stay stable. Then let the board resolve it, in
devices/turing-rk1.toml:
supported_kernels = ["rk3588-mainline-7.2", "rk3588-mainline-7.3"]
default_kernel = "rk3588-mainline-7.2" # still the one with board evidence
Check the point resolves before pinning anything — --kernel selects the definition for a
resolve without touching a file:
boot2deb resolve turing-rk1/forky --kernel rk3588-mainline-7.3
Step 5: adopt it in a recipe
update has no --kernel flag. Which definition a recipe pins is the recipe’s own
kernel field, not a per-run choice, so adopting 7.3 is a recipe edit. Do it in a new
leaf rather than in forky.toml, and the board keeps a validated recipe while the new
one is unproven. That leaf is recipes/turing-rk1/forky-7.3.toml:
device = "turing-rk1"
kernel = "rk3588-mainline-7.3"
suite = "forky"
features = []
layout = "combined"
[support]
status = "experimental" # nothing has booted this yet
date = "2026-08-21" # the day the claim was last assessed
Then run the same sequence as shape 1 against the new leaf, on the locked path this time.
There is no --kernel, because the lock now names 7.3 itself:
boot2deb update turing-rk1/forky-7.3 --kernel-ref v7.3
boot2deb verify-patches turing-rk1/forky-7.3 --kernel-src ../linux
boot2deb verify-config turing-rk1/forky-7.3
boot2deb build turing-rk1/forky-7.3
If you skipped ahead and pinned 7.3 before widening the envelope, nothing is broken. The
envelope check is pure metadata, so update reports the mismatch and keeps going, since
pinning is the first step of adopting. build refuses before cloning anything, and names
the verify-patches --kernel line to run instead. That is the cheap check telling you
the series makes no claim about your kernel. The expensive one tells you whether it
would have worked anyway.
Step 6: the kernel config
A version bump moves kconfig too: symbols get renamed, split, or absorbed, and a fragment
naming one that no longer exists silently stops setting anything. verify-config reports
the merge, and with a reference config it asserts byte-identical CONFIG_* parity:
boot2deb verify-config turing-rk1/forky-7.3 \
--reference-config build/turing-rk1/forky/linux/.config
Comparing the new kernel’s generated config against the old one’s is the fastest way to see what the bump changed on its own. Expect differences and read them. The ones that matter are options you asked for and did not get.
Step 7: boot it
Everything so far is a build-time claim. Flash the image, boot the board, and check the hardware the kernel is there for — for the RK1, see Turing RK1.
Closing the loop
A kernel bump is not finished when the image builds:
- Re-earn the claim. Set
status = "validated"and the date on which an image from these pins booted. A claim is per-pin: it cannot be re-dated for pins that moved, only re-earned. Until then,expectedis the honest status. - Regenerate the matrix, which is generated from the locks and therefore stale the
moment a pin moves:
boot2deb support-matrix --markdown > docs/src/reference/support-matrix.md - Check the pins are durable.
verify-sourcesgrades every pin in the lock — a tag is durable, a branch tip is ephemeral, a force-pushed branch is orphaned:boot2deb verify-sources turing-rk1/forky-7.3 - Retire the old definition once the new one is validated: delete the superseded
recipe leaf and kernel file, and drop the id from
supported_kernels. Old locks name old commits in thepatchesrepo, so retiring the config does not strand a build that was already pinned.
Keeping both kernel definitions is worth it only while a board genuinely supports both.