* Merge Luckfox Pico build support into seedsigner-os
Fold the experimental seedsigner-luckfox-pico build system into this repo as a
coexisting subsystem, so a single repo/workflow can build both the Raspberry Pi
/ La Frite images (Buildroot) and the Luckfox Pico images (Rockchip vendor SDK).
- Add opt/luckfox/ with the full Luckfox build tree (os-build.sh, build.sh,
build-local.sh, Dockerfile, configs/luckfox_pico_defconfig, files/, patches/,
scripts/, blkenvflash).
- Add .github/workflows/build-luckfox.yml (matrix over hardware x boot medium),
adapted to a single-repo checkout: drops the seedsigner-os self-clone and the
override overlay, and consumes the in-repo opt/external-packages. Auto-triggers
are path-scoped so the expensive matrix does not run on unrelated pushes. The
existing build.yml (Pi/La Frite) is untouched.
- Converge external-packages to a single toolchain-aware set instead of per-arch
copies: ccid-sec1210, python-pgpy, python-pysatochip and python-pyscard now
branch on BR2_TOOLCHAIN_USES_UCLIBC (pycryptodome backend + embedded/uclibc
options for Luckfox; upstream glibc behavior unchanged for Pi/La Frite).
- Rewire os-build.sh / build.sh / build-local.sh to read the converged packages
locally (mounted into the container) rather than cloning seedsigner-os.
- Add a repo-root build.sh dispatcher routing --pi*/--lafrite to docker compose
and --luckfox to opt/luckfox/build.sh.
- Add docs/luckfox/, README "Hardware targets" section, and .gitignore entries.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Luckfox: seedsigner-os hostname + microSD mount; add OS identity/provenance marker
Part B of the platform-detection/error-handling work (pairs with the seedsigner
app branch generalized-platform-detection).
Luckfox runtime fixes:
- luckfox_pico_defconfig: BR2_TARGET_GENERIC_HOSTNAME=seedsigner-os (was
"seedsigner luckfox pico"), so the app's hostname fallback recognizes the OS.
- start-seedsigner.sh: set `hostname seedsigner-os` at launch (sethostname, no
disk write) so detection is reliable regardless of SDK init.
- fat-fsck-hotplug: after fsck, actually mount the FAT card at /mnt/microsd and
notify the app via /tmp/mdev_fifo; unmount + signal on removal. mdev.conf runs
the handler on both add and remove (@ -> *). The SDK does not auto-mount
removable media, so this mirrors the Raspberry Pi mdev handler.
OS identity + build provenance (/etc/seedsigner-os-release):
- Add opt/gen-os-release.sh: emits an os-release-style marker that positively
identifies a SeedSigner OS build and records repo/branch/commit/date for both
seedsigner-os and the seedsigner app (missing fields -> "unknown").
- Pi/La Frite: opt/build.sh generates it into the rootfs overlay (app git from the
clone, OS git from env); docker-compose.yml forwards SEEDSIGNER_OS_* env;
build.yml captures OS repo/branch/commit/date. Generated file is gitignored.
- Luckfox: build-luckfox.yml, os-build.sh (via a mount added in build.sh) and
build-local.sh generate it into the SDK rootfs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* build-luckfox: make reusable (workflow_call) for cross-repo use
Add a workflow_call trigger so other repos (the seedsigner app) can reuse the
authoritative Luckfox build. Inputs mirror workflow_dispatch, plus
seedsigner_os_repo/ref for a parameterized checkout. Existing push/dispatch
behavior is preserved:
- github.event.inputs.* -> inputs.* (works for dispatch and call; empty on push).
- matrix builds a single combo when specific hardware/boot inputs are provided
(dispatch or call), else the full 5-combo matrix (push).
- checkout defaults to this repo/ref, overridable by a caller.
- build_rust_from_source comparison made boolean-correct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Luckfox: add missing SeedSigner packages + rk-reboot (Loader mode) helper; document package set
Package parity (non-NFC-reader, non-OpenCT): enable python-ndeflib, python-keycard-py,
python-specter-card, python-pygp, python-smbus2 and libraqm on the Luckfox build. Each
needs both the injected menu "SeedSigner" source line (in all three build implementations)
and a BR2_PACKAGE_*=y in luckfox_pico_defconfig. The five pure-Python packages only select
deps already enabled; libraqm additionally pulls harfbuzz+libfribidi (native, complex-text
shaping for Pillow) — watch the Mini SPI-NAND 99MB rootfs and gate to Max/Pi if it overflows.
rk-reboot: add opt/luckfox/files/rk-reboot (installed to /usr/bin/rk-reboot). busybox `reboot`
ignores its mode argument, so `reboot loader` is a no-op; this helper issues the real
reboot(2) RESTART2 syscall via python3. `rk-reboot loader` maps to the DT mode-loader entry
and enters rockusb Loader mode, so SocToolKit/rkdeveloptool can re-flash without the BOOT
button (no firmware change needed). maskrom would need a DT mode-maskrom patch (deferred).
AGENTS.md: document the canonical per-platform SeedSigner package set (core/smartcard/
optional/platform-specific/excluded) and the two-place rule for custom external-packages,
so future platform builds don't drift.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs/luckfox: add build & dev overview page incl. reboot-to-flash (Loader) commands
Add docs/luckfox/README.md describing the merged-repo Luckfox build/dev process
(targets, GitHub Actions + local Docker/no-Docker builds, the SDK clone/patch/
package-injection/provenance flow, the three build implementations) and a
"Flashing & recovery — Loader / Maskrom mode" section documenting rk-reboot,
the inline python3 syscall for images without the helper, and the devmem
fallback. Point the README "Building for Luckfox Pico" link at it, and add a
banner to the legacy OS-build-instructions.md noting it reflects the original
standalone layout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: install app to /opt (Pi-parity layout) + compile translations
The Luckfox build installed only src/ -> /seedsigner, so app features that
resolve resources relative to the source tree looked for them beside src/ and
came up empty:
- javacard-cap (bundled applets, smartcard_views _internal_cap_dir) and
gpg_keys (release-signing keys, gpg_views) sit at the repo root; under
/seedsigner they resolved to /javacard-cap and /gpg_keys (absent) -> GPG
verify and JavaCard flashing silently failed.
- RestartView execs /opt/src/main.py on SeedSigner OS (hostname is set to
seedsigner-os), which did not exist under /seedsigner -> Restart was broken.
- translations were cloned but never compiled -> multi-language was dead.
Match the Raspberry Pi SeedSigner-OS layout: copy the whole app repo to /opt
(so /opt/src runs the app and /opt/javacard-cap, /opt/gpg_keys resolve), prune
dev/build cruft (git, docs, enclosures, electronics, tests, etc.), and point the
launcher (cd /opt/src) and the Mini settings.json patch at /opt/src.
Add opt/luckfox/compile-translations.sh (mirrors opt/build.sh
compile_translations_and_fonts): builds .po -> .mo via the fork's
setup.py compile_catalog overlay-merge and slims the CJK fonts. It runs after
the --recurse-submodules clone in all three build flows (build-luckfox.yml,
os-build.sh, build-local.sh) and degrades gracefully to English-only if the
python toolchain is unavailable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: add dev / non-dev build variant (default non-dev, hardened)
Adds a `build_variant` input (choice: non-dev default, dev) to build-luckfox.yml
(both workflow_dispatch and workflow_call blocks), mirrored locally via the
SEEDSIGNER_BUILD_VARIANT env in os-build.sh / build-local.sh.
Unlike the Pi/La Frite profiles (parallel -dev/non-dev directories), Luckfox is
the Rockchip SDK with one defconfig + an SDK-provided rootfs, so non-dev is a set
of build-time hardening steps gated on the flag. When non-dev, the build:
- strips the serial console bootargs (forces the existing UART2 lever on),
- disables the serial login: getty off in the defconfig + comments console/tty
getty/login/shell respawn lines in the rootfs /etc/inittab,
- disables USB ADB + RNDIS gadget: removes adbd/usbdevice binaries and
/etc/init.d/S*usb*, comments gadget/adb invocations in RkLunch.sh,
- removes syslogd/klogd autostart,
- drops python-pip, wget, libcurl/curl from the target.
The rootfs surgery lives in the new opt/luckfox/harden-nondev.sh <ROOTFS_DIR>,
called from all three build flows. SDK rootfs layout varies by version, so every
step is guarded/no-op-if-absent and logs each file it touched — a green build
does NOT prove the vectors are closed; verify on hardware/serial. reboot-to-Loader
(rk-reboot) is retained in both variants. Documents the variant + AGENTS.md
leak-vector mapping in docs/luckfox/README.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: CI (push/PR) builds the dev variant explicitly
Automatic push/PR builds are for validating the full, debuggable stack, so they
should produce dev images. They already did — but only as a side effect: on
push/pull_request events the `inputs` context is empty, so `inputs.build_variant`
was '' (never the 'non-dev' default, which only applies to dispatch/call), and the
hardening steps were skipped. The serial console, however, was stripped (its shell
fallback defaulted true), so those images weren't fully "dev".
Make it explicit and correct:
- Resolve the effective variant as `inputs.build_variant || 'dev'` everywhere, so
push/PR CI is dev by design while manual dispatch / workflow_call keep their
non-dev default.
- Serial console now follows the variant: `disable_uart2_console_debug` defaults to
'auto' (non-dev strips it, dev keeps it); 'true'/'false' still force it. So dev CI
images retain the console for validation.
Docs updated. No change to os-build.sh/build-local.sh (local build.sh) defaults.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: non-dev-only size/boot optimizations
Adds production-image optimizations gated on the non-dev variant (dev keeps
SDK-default behavior). All are guarded/no-op when the SDK target isn't present
and log what they touched.
- defconfig: BR2_OPTIMIZE_3 -> BR2_OPTIMIZE_S (size) in the non-dev sed (all 3 flows).
- new opt/luckfox/optimize-nondev.sh (runs after harden-nondev.sh): prune package
tests/ + *.dist-info/*.egg-info under site-packages and /opt/src, remove
/opt/tools; prune camera iqfiles keeping the board's sensor (IQFILES_KEEP);
drop /etc/seedsigner-nondev marker.
- start-seedsigner.sh: when /etc/seedsigner-nondev exists (non-dev only),
background the ~4s camera-graph bootstrap so the UI comes up first. Dev has no
marker -> unchanged SDK-default ordering.
- build-luckfox.yml: append `quiet loglevel=3` to the DTS bootargs (non-dev);
new guarded step zeroing any non-zero U-Boot bootdelay (non-dev).
Dropped as no-ops after investigation: pcscd-defer (already backgrounded) and
rootfs-fsck-skip (UBIFS isn't fsck'd). bash + ncurses kept (pinentry-ncurses
needs ncurses; Pi non-dev profiles ship bash).
iqfiles pruning and the UI-first camera reorder MUST be verified on hardware
(scan a QR) — a green build proves nothing about the camera; both are trivially
revertible. Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: fix non-dev boot break (blank screen) from hardening
The non-dev image booted to a blank screen: harden-nondev.sh (a) deleted the
usbdevice/adbd binaries and (b) commented an inittab console-shell respawn line.
On the real SDK the USB gadget is started by RkLunch.sh, which lives on the /oem
stage (not under the rootfs at build time, so it couldn't be patched). With the
binary gone, RkLunch's usbdevice call errored and aborted boot; and commenting a
bare "-/bin/sh" console line can also break the SDK's app boot path.
Fix, keeping ADB disabled:
- Neutralize usbdevice/adbd with a no-op '#!/bin/sh; exit 0' stub instead of
deleting them, so the RkLunch caller succeeds but no gadget/adb is configured.
- Narrow the inittab surgery to real login gettys (getty/login/sulogin) and leave
bare console shells intact. The login getty is already removed by the defconfig
(BR2_TARGET_GENERIC_GETTY off), so serial login stays hardened.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: recovery & robustness — ADB debug, camera-reorder off, watchdog→loader
Non-dev images were booting blank and were hard to recover (no serial, ADB off,
app menu unreachable). Add debuggability + automatic recovery:
- harden-nondev.sh: gate USB adb/gadget neutralization behind HARDEN_DISABLE_ADB
(default 0 = keep ADB). Non-dev now retains `adb shell` for debugging and no
longer risks orphaning RkLunch.sh's usbdevice call. Re-harden with =1.
- optimize-nondev.sh: the UI-first camera reorder is now opt-in
(OPTIMIZE_UI_FIRST_CAMERA=1, default off) — backgrounding the camera-graph
bootstrap can race SPI display init and blank the screen. SDK-default ordering
by default; other size optimizations unchanged.
- start-seedsigner.sh: fall into Loader mode instead of dying — on retry
exhaustion (rk-reboot loader) and via a ~120s boot watchdog if the app never
writes /tmp/seedsigner-ready (covers the app-hang case).
- build-luckfox.yml: add panic=5 to non-dev bootargs (panic reboots, not hangs).
- defconfig: add u-boot-tools (fw_setenv) for the app's bootcount reset.
- docs: document the recovery/failover flow; the U-Boot bootcount is a follow-up
needing the device env-partition offset (now readable via ADB).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs/luckfox: record verified U-Boot env layout for the bootcount follow-up
Confirmed from a live device via ADB: env is /dev/mtd0, offset 0x0, size 0x40000
(single copy), erase 0x20000 — a real U-Boot env. fw_env.config recorded; the
bootcount stays deferred until the SDK U-Boot bootcount support + altbootcmd are
verified (ship fw_env.config together with that change).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: U-Boot bootcount → auto-loader failover (non-dev)
Adds a boot-button/ADB/app-free recovery: after N failed boots the U-Boot boot
counter auto-enters rockusb Loader mode for re-flashing. For kernel/init failures
the userspace start-seedsigner watchdog can't catch.
- build-luckfox.yml: non-dev U-Boot step appends CONFIG_BOOTCOUNT_LIMIT=y +
CONFIG_BOOTCOUNT_ENV=y to the Luckfox U-Boot defconfigs (SDK U-Boot ships the
bootcount drivers). Installs /etc/fw_env.config on non-dev.
- opt/luckfox/files/fw_env.config: /dev/mtd0 0x0 0x40000 0x20000 — verified from a
live device (standard CRC32+data env in the dedicated env partition, single copy).
- start-seedsigner.sh: sets (once) bootlimit=5 and
altbootcmd='setenv bootcount 0; saveenv; mw.l 0xff020200 0x5242c301; reset' —
reusing the proven reboot-mode Loader entry (same 0x5242C301 magic as
rk-reboot loader) and clearing the counter so it's never a dead-end.
- app already resets bootcount on reaching Home (fw_setenv bootcount 0).
Env writes touch flash and the boot flow is safety-critical, so this MUST be
hardware-verified (steps + rollback in docs/luckfox/README.md). Non-dev only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: fix U-Boot boot-failover — memory-backed bootcount (GRF OS_REG)
The env-backed bootcount never fired: this Rockchip U-Boot 2017.09 fork's
drivers/bootcount/Kconfig defines only CONFIG_BOOTCOUNT/_EXT — there is no
CONFIG_BOOTCOUNT_LIMIT/_ENV symbol, so the defconfig lines were silently
dropped and bootcount was never compiled (confirmed by dumping the live
U-Boot: zero bootcount/altbootcmd strings).
Switch to the always-built generic backend (drivers/bootcount/bootcount.c)
via a board-header patch to include/configs/rv1106_common.h:
#define CONFIG_BOOTCOUNT_LIMIT
#define CONFIG_SYS_BOOTCOUNT_SINGLEWORD
#define CONFIG_SYS_BOOTCOUNT_ADDR 0xFF020218
The counter lives in a free GRF OS_REG scratch register (0xFF020218) —
hardware-verified on a live Pico Pro Max: the reg reads 0 (unused),
survives a warm reset (so kernel-panic reboot loops keep counting), and
clears on a cold power-cycle. No per-boot NAND wear. autoboot.c's
bootdelay_process() runs the increment + altbootcmd check every boot.
- build-luckfox.yml: replace the dead defconfig append with the
board-header patch (idempotent awk after the CONFIG_PREBOOT line).
- start-seedsigner.sh: clear the counter via `devmem 0xFF020218 32 0`
once userspace is up; altbootcmd now `mw.l 0xFF020218 0; mw.l
0xff020200 0x5242c301; reset` (zero counter, enter Loader).
- docs: rewrite the boot-counter section with the corrected mechanism
and a precise hardware-test (devmem 0xFF020218 32 0xB0010006; reboot).
Also verified on-device that U-Boot honours the mtd0 env fw_setenv writes
and that `preboot` does NOT execute on this build (so bootdelay_process,
not preboot, is the hook).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: bake bootlimit+altbootcmd into U-Boot default env (ENV_IS_NOWHERE)
Hardware testing the previous commit revealed the real reason the failover
never fired: this U-Boot is built ENV_IS_NOWHERE (no CONFIG_ENV_IS_* in any
Luckfox defconfig), so it uses ONLY its compiled-in default environment and
never reads the mtd0 env that fw_setenv writes.
Proven on a live Pico Pro Max with the memory-backed counter from the prior
commit:
- a boot-counter probe showed U-Boot read the forced counter (6) and stored
7, so the register + autoboot bootcount code work correctly;
- but with mtd0 bootlimit=5 set via fw_setenv and bootcount=7, U-Boot booted
normally — it used its BUILT-IN default bootlimit (10, from autoboot.c),
ignoring the mtd0 value entirely.
Fix: the build-luckfox.yml recovery step now also injects bootlimit=5 and
altbootcmd into CONFIG_EXTRA_ENV_SETTINGS in rv1106_common.h, so both live in
the compiled default env. altbootcmd = `mw.l 0xFF020218 0; mw.l 0xff020200
0x5242c301; reset` (mw + reset both confirmed present in the binary; the magic
is the same proven reboot-mode Loader entry rk-reboot uses).
- start-seedsigner.sh: drop the now-dead fw_setenv bootlimit/altbootcmd block
(U-Boot ignores the mtd0 env); keep only the devmem counter-clear.
- docs: correct the mechanism — default-env baking, not fw_env; note the
policy can only be disabled by rebuild now.
The memory-backed counter, its register semantics, and the mw/reset/altbootcmd
command availability are all binary- and hardware-verified; the final baked-env
divert can only be validated after this rebuild (forcing it on the current
firmware would risk a hang loop since its default env has no altbootcmd).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: disable ADB on non-dev by default (disable_adb toggle + escape hatch)
Now that the non-dev Pico Pro Max image is validated on hardware (app, camera,
and the full Loader-recovery stack — KEY3, userspace watchdog, and the U-Boot
bootcount failover — all proven), ship it air-gapped: turn the USB adb/RNDIS
gadget off by default on non-dev.
The mechanism already exists (harden-nondev.sh disables the gadget via safe
no-op stubs when HARDEN_DISABLE_ADB=1); this only wires how the flag is set:
- build-luckfox.yml: new `disable_adb` dispatch input (auto/true/false, default
auto), mirroring disable_uart2_console_debug. auto -> non-dev disables ADB,
dev keeps it; disable_adb=false keeps ADB on a non-dev debug image. The
non-dev harden step resolves it and exports HARDEN_DISABLE_ADB.
- os-build.sh / build-local.sh: default HARDEN_DISABLE_ADB=1 for non-dev
(respecting an explicit override) for parity with CI.
- Also fix a stale fw_env.config comment (the failover is ENV_IS_NOWHERE /
baked-default-env now, not fw_setenv-based) and update the ADB docs.
Recovery is unaffected: Loader is a U-Boot/maskrom USB mode independent of the
Linux adbd gadget, so KEY3->Loader and the bootcount failover still enumerate
the device for re-flashing with ADB off.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: switch USB to host mode instead of disabling adb via rootfs hardening
The disable_adb rootfs-stub approach (6106733) broke boot: the SDK's
RkLunch.sh lives on the /oem partition (un-editable at build time) and still
invoked the stubbed usbdevice, hanging boot — the device then failed over to
rockusb Loader on its own (a real-world proof of the U-Boot bootcount failover).
Replace it with a cleaner mechanism: switch the RV1106 USB role from device
gadget to host via a device-tree override. Host mode has no USB device gadget,
so there is no adb/RNDIS (air-gapped on the USB axis) AND the Pico can drive
external USB peripherals (camera, smartcard reader). Verified the knob on the
live device: /proc/device-tree/usbdrd/usb@ffb00000/dr_mode = "peripheral".
- build-luckfox.yml: new `usb_mode` dispatch input (auto/gadget/host, default
auto -> non-dev=host, dev=gadget), replacing `disable_adb`. New "Configure USB
mode in DTS" step appends a last-wins `&usbdrd_dwc3 { dr_mode = "host"; }`
override to the per-board DTS when host. Reverted the HARDEN_DISABLE_ADB
wiring in the harden step (kept the fw_env comment fix).
- os-build.sh / build-local.sh: reverted the HARDEN_DISABLE_ADB default.
- docs: describe usb_mode as the adb switch; host needs board VBUS for
bus-powered peripherals.
harden-nondev.sh keeps its serial-login/logging/dev-tool hardening; its dormant
HARDEN_DISABLE_ADB path is no longer wired (host mode supersedes it). Loader
recovery is unaffected (a U-Boot USB mode, independent of the Linux gadget).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: add usb_mode=otg (dual-role) + restore 10M userdata partition (Pro Max)
Two changes to unblock USB host work and lay persistence groundwork:
usb_mode=otg: new dual-role option. Boots as a gadget (adb works) but the dwc3
allows a runtime role switch to host (echo host > /sys/kernel/debug/usb/
ffb00000.usb/mode) — so host mode + peripherals can be tested live over adb
before committing to a no-adb dr_mode=host image. The DTS step now appends the
resolved dr_mode (host or otg) generically. (Confirmed on hardware that with
dr_mode=peripheral the controller is locked device-only and won't switch at
runtime — hence OTG for testing.)
userdata partition: restore a 10M userdata partition on Pro Max SPI-NAND,
placed BEFORE rootfs at a fixed offset (…oem 20M | userdata 10M | rootfs 217M).
rootfs stays the last partition so it can grow later without moving userdata,
and userdata survives a rootfs reflash. This is the SDK's own default layout
(SeedSigner had stripped it); rootfs ubi.mtd auto-derives from partition
position (project/build.sh -> ubi.mtd=6), and /userdata auto-mounts (UBIFS via
the retained userdata@/userdata@ubifs fs config). Same 252M total; rootfs is
only 31% used (60M/196M) so 217M is ample. Foundation for persistent
settings.json + a boot log that survives reflash.
Follow-ups (after the OTG test pinpoints the host boot cause over adb):
S50usbdevice host-aware skip, /userdata boot log, settings.json -> /userdata.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: gate the boot.img UART-strip assertion on console being disabled
The "Package firmware" step unconditionally failed the build if boot.img still
contained console=ttyFIQ0 / earlycon / user_debug tokens. That's only correct
when the serial console was meant to be stripped (non-dev / disable_uart2=true);
dev builds keep the console by design, so any dev build failed here (surfaced by
a dev+otg build). Resolve the same auto/true/false logic used by the DTS steps
and only assert the tokens are gone when the console was actually disabled.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: make S50usbdevice host-aware (skip USB gadget when dr_mode=host)
The SDK's /etc/init.d/S50usbdevice unconditionally configures the USB device
gadget (adb/RNDIS) and runs before S99seedsigner in rcS. In host mode there is
no UDC to bind, so it hangs the boot before the app starts — the host/otg
no-boot we hit. S99usb0config is already host-aware; S50usbdevice was not.
Patch it in the rootfs (Install SeedSigner application step) to check the live
dr_mode and, when host, bring up loopback and exit early — skipping all gadget
setup. Runtime check via /proc/device-tree/usbdrd/usb@ffb00000/dr_mode, so it's
a no-op on gadget/peripheral builds. Idempotent awk insert after `ifconfig lo
up`; validated to produce valid shell.
Debuggable now: telnet-over-Ethernet (S40network -> S50telnet, before the hang
point; root/luckfox) is a USB-independent debug + `rk-reboot loader` recovery
channel, so a host build that still misbehaves can be inspected and recovered
without USB or the BOOT button.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: include userdata.img in the NAND flash bundle when built
The Pro Max build creates userdata.img (and packs it into update.img), but the
individual-files NAND bundle omitted it, so per-partition flashing had no
userdata.img. Copy it into the bundle when present (optional — Mini/Pi have no
userdata partition). update.img already contains it, so full-image flashing was
never affected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: mount configfs in the S50usbdevice host-mode skip (fixes display)
The host-mode S50usbdevice skip (previous commit) let boot reach the app, but
the app then crashed opening the SPI display: /dev/spidev0.0 did not exist.
Root cause (diagnosed live over telnet-on-Ethernet): the SDK's S50usbdevice is
the only thing that mounts configfs (via configfs_init), and the Luckfox
device-tree overlay system — luckfox-config load, run by S99luckfoxconfigload —
needs configfs to enable SPI0 for the ST7789 display. My host-mode early-exit
skipped configfs_init, so configfs was unmounted, luckfox-config could not
create /sys/kernel/config/device-tree/overlays/*, SPI0 stayed disabled, and the
ST7789 driver failed with "Opening SPI device: No such file or directory".
Fix: in the host-mode branch, mount configfs before exiting (still skipping the
USB gadget). Verified live: after `mount -t configfs none /sys/kernel/config`,
luckfox-config load enables SPI0, /dev/spidev0.0 appears, and the app starts and
renders the display in host mode.
(Camera was unaffected because CSI is enabled in the base DTB, not via an
overlay. __symbols__ absence in /proc/device-tree is a red herring — luckfox
uses its own symbol DTB copy.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: add debug_network toggle (telnet/ssh over Ethernet), off for non-dev
The Ethernet telnet/ssh root shell is the SDK default and has been the debug
channel for host-mode work, but it's a remote-root-shell vector that must not
ship on an air-gapped image. Add a debug_network dispatch input (auto/on/off):
auto follows the variant (non-dev=off, dev=on). On non-dev with off (the
default), the harden step removes the telnet/ssh/dropbear init scripts so no
remote shell listens on the LAN. Build non-dev with debug_network=on for a
debuggable image (telnet in over Ethernet, root/luckfox).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* external-packages: one version per package on all toolchains (pysatochip 0.6a, PGPy 0.6.0)
pysatochip and PGPy were the only two external packages with per-toolchain
version splits (uClibc pinned pycryptodome-backed forks to dodge the Rust
python-cryptography dep). That split shipped pysatochip 0.5-alpha on
Luckfox while glibc platforms got 0.6a — so the B11 NDEF feature
(card_get_ndef/card_set_ndef, added in the Tools_View refactor) crashed
with AttributeError only on Luckfox.
python-cryptography now builds on uClibc (Tier-3 Rust target patch), so
the reason for the splits is gone. Pin pysatochip 0.6a and upstream
PGPy 0.6.0 + python-cryptography everywhere; 0.6a's cert validation
already falls back OpenSSL->pycryptodomex inside the package. Hash for
the 0.6a tarball re-verified against the current tag (d77e311).
Document the single-version rule in AGENTS.md and SMARTCARD_PACKAGES.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: close non-dev networking, strip adb userspace, sync CI + local builds
Audit of the non-dev hardening found two open vectors and a CI/local split:
1. Networking was live on shipped images. The SDK kernel keeps CONFIG_INET
(unlike the Pi/La Frite non-dev kernels), and nothing touched S40network
or udhcpc — so eth0 came up and broadcast DHCP (MAC + hostname) on any
LAN, with "hardening" only removing the listening daemons in CI.
harden-nondev.sh gains section 4 (HARDEN_DISABLE_NETWORK, default 1):
S*network* stubbed to loopback-only, udhcpc default.script removed,
/etc/network/interfaces reduced to lo, telnet/ssh/dropbear init scripts
removed. debug_network=on maps to 0 and keeps Ethernet+telnet as the
debug channel.
2. adbd/usbdevice binaries stayed in the rootfs (HARDEN_DISABLE_ADB was
never set). Default it to 1: stub the binaries, blank the gadget config,
remove S*adb* — but NEVER S*usb* init scripts: S50usbdevice mounts
configfs, which luckfox-config needs to enable SPI0 for the display
(removing it was part of the original disable_adb breakage). dr_mode=host
remains the primary adb removal; this is defence in depth.
3. Local Docker builds (os-build.sh / build-local.sh) were missing the USB
dr_mode switch, S50usbdevice host patch, U-Boot bootcount failover,
debug_network handling, and fw_env.config — a locally built "non-dev"
image still shipped gadget mode with adb and a telnet root shell.
Extract those CI steps verbatim into shared scripts
(configure-usb-mode.sh, uboot-recovery-config.sh, patch-s50usbdevice.sh)
and call them from all three builds; new SEEDSIGNER_USB_MODE /
SEEDSIGNER_DEBUG_NETWORK env knobs mirror the dispatch inputs.
All scripts tested against fake rootfs/SDK trees (correct + idempotent;
S50usbdevice and S99usb0config survive hardening; debug_network=on retains
telnet + DHCP). AGENTS.md gets a Luckfox leak-vector table (the generic one
described the Pi/La Frite kernel-level closure, which does not apply);
docs/luckfox/README.md hardening section rewritten to match reality and
document debug_network.
Verify on hardware: eth0 down, no DHCP on the LAN, :22/:23 closed,
adb absent, display + camera + app still work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: strip networking + WiFi from the non-dev kernel
The threat model is root-level code execution: the SeedSigner Python app
runs as root, so the userspace hardening added in d1141ef is not a control
on its own — root can just run `ifconfig eth0 up; udhcpc`, or insmod a
driver. Only removing the capability from the kernel actually holds.
An audit of the SDK kernel defconfig found networking is compiled in
(CONFIG_NET/PACKET/UNIX/INET=y, NETDEVICES=y, STMMAC_ETH=y, RK630_PHY=y),
and that the build ships a complete 802.11 stack as loadable modules on
the oem partition: cfg80211, mac80211, ipv6 and 8 vendor WiFi drivers
(8188fu, 8189fs, aic8800_*, atbm*, r8723bs, ssv6*) are packaged to
/oem/usr/ko, which no rootfs hardening touches. Several are USB drivers,
and non-dev runs USB in host mode.
Add strip-kernel-network.sh (shared by CI and both local builds):
Group A, gated on debug_network=off — INET, PACKET, IPV6, NETDEVICES,
STMMAC_ETH, RK630_PHY, USB_CONFIGFS_RNDIS off. Root cannot create an
interface or open an AF_INET socket.
Group B, always on non-dev — WL_ROCKCHIP (the umbrella that selects
CFG80211/MAC80211 and sources every vendor WiFi Kconfig) and RTL8723BS
off, so those modules are never built.
Three symbols are deliberately kept, and the script refuses to run if any
is off: NET/UNIX (pcscd uses an AF_UNIX socket — dropping NET kills
smartcards), MODULES (camera drivers are =m), and USB_GADGET, which is the
sole provider of configfs here (USB_CONFIGFS -> USB_LIBCOMPOSITE -> select
CONFIGFS_FS); without configfs luckfox-config cannot create the overlays
that enable SPI0 and the display dies. ADB stays blocked by dr_mode=host
instead: host-only dwc3 leaves /sys/class/udc empty, so a configfs gadget
has nothing to bind to.
Add assert-kernel-network.sh, run after build.sh kernel and again after
build.sh firmware. It checks the GENERATED kernel .config, not the
defconfig — Kconfig silently drops lines whose symbol or deps don't
resolve, which is exactly how the first bootcount attempt shipped green
with no bootcount code. It also asserts CONFIGFS_FS=y as a display canary
and that no wireless .ko reached /oem/usr/ko.
Both scripts tested against the real defconfig (correct, idempotent) and
the assert verified to catch each failure mode: INET left on, CONFIGFS_FS
dropped, stray wifi .ko in oem, and a missing .config (errors rather than
passing silently).
Also: optimize-nondev.sh takes an optional OEM_DIR. Its iqfiles prune has
never actually run — the oem partition is assembled by __PACKAGE_OEM
inside build.sh firmware, after this script — now documented as a known
limitation with the proper fix (the SDK's __RUN_PRE_BUILD_OEM_SCRIPT hook)
noted. Size-only, no security impact.
Hardware verification still required: display, camera and smartcards must
work, and as root eth0 must be absent, `ifconfig eth0 up` must fail, and
adb/udc must be empty.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: run the oem iqfiles prune from the SDK's pre-build-OEM hook
The iqfiles prune in optimize-nondev.sh had never executed in any build.
optimize-nondev.sh runs during the rootfs/app install step, but the oem
partition is assembled by the SDK's __PACKAGE_OEM, which is called only
from build_firmware() — so the iqfiles directory did not exist yet and the
prune took its "not found" branch every time (confirmed in the CI log for
run 30716732363). Passing a different path could not fix it; the problem
was purely sequencing.
Move the prune to opt/luckfox/prune-oem-iqfiles.sh and invoke it from the
SDK's __RUN_PRE_BUILD_OEM_SCRIPT hook, which fires after __PACKAGE_OEM but
before build_mkimg creates oem.img — the one window where the staged oem
tree exists and is still editable. This is the SDK's own pattern: the
vendor's luckfox-buildroot-oem-pre.sh prunes unused libs there, and
__RUN_POST_CLEAN_FILES drops unused NPU/audio models at the same point.
patch-oem-pre-hook.sh installs the call, APPENDING to whatever script
RK_PRE_BUILD_OEM_SCRIPT names rather than replacing it — all five Luckfox
board configs we build already point at the vendor script, and replacing
it would silently drop the vendor's prunes. If a board has no hook
configured, one is created and wired into the board config. Idempotent via
a marker.
The prune itself is hardened relative to the old code:
* two passes — decide, then delete. The old ordering meant a bad
IQFILES_KEEP destroyed the tuning data before any check noticed.
* aborts, having deleted nothing, if IQFILES_KEEP matches no entry,
rather than shipping a camera with no ISP tuning data.
* handles per-sensor subdirectories, not just files: the old
`for f in "$IQDIR"/*` with a -f test would silently prune nothing on
that layout.
* defaults OEM_DIR to $RK_PROJECT_PACKAGE_OEM_DIR, which the SDK exports,
so the hook needs no arguments.
Verified against a fake SDK tree with the real vendor hook and board
config: the hook patch is idempotent, the SDK-equivalent invocation runs
both the vendor prune and ours, sensor subdirectories and filenames with
spaces are handled, and the abort path leaves every file in place.
Size optimization only, no security impact. Camera must be verified on
hardware — removing the wrong sensor tuning file breaks it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: disable RK_ENABLE_WIFI with the kernel WiFi strip (fixes non-dev SPI_NAND build)
Regression from 7d06f67. Disabling CONFIG_WL_ROCKCHIP removes the in-kernel
cfg80211/mac80211, but the SDK ALSO builds out-of-tree wifi drivers from
sysdrv/drv_ko/wifi/* via the sysdrv Makefile's build-usb target whenever
RK_ENABLE_WIFI=y — entirely independently of the kernel defconfig. Those
drivers then failed modpost with hundreds of `"cfg80211_*" undefined!`
errors, killing build_rootfs:
ERROR: modpost: "cfg80211_get_bss" [.../wifi/rtl8188ftv/8188fu.ko] undefined!
[build.sh:error] Running build_rootfs failed!
That is exactly why non-dev SPI_NAND failed on Pro Max (#53, #57) and Mini
(#54, #58) while Pico Pi EMMC (#55, #59) succeeded — the Pi board config has
`#export RK_ENABLE_WIFI=y` commented out, the other two set it to y.
strip-kernel-network.sh now takes the board config as a 4th argument and, as
part of the WiFi strip, sets RK_ENABLE_WIFI=n and drops
overlay-luckfox-wifibt-firmware from RK_POST_OVERLAY. This is also the better
security outcome: those out-of-tree drivers are precisely the
8188fu/8189fs/aic8800/atbm/ssv6 modules that were being packaged to
/oem/usr/ko and left loadable by root — now they are never built.
assert-kernel-network.sh gains a check for built .ko under sysdrv/drv_ko/wifi,
so an incoherent strip (kernel symbols off but RK_ENABLE_WIFI still on) is
reported as a clear assertion failure instead of a wall of modpost errors.
Verified against the real Pro Max and Pico Pi board configs: the Pro Max
config flips to RK_ENABLE_WIFI=n with the firmware overlay removed, the Pi
config is correctly detected as already-disabled, and both the board config
and the defconfig edits are idempotent (an earlier revision re-appended its
comment line on every run — fixed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: persistent boot log, early splash + startup-failure on screen
A non-dev image has no console, no adb and no network, so when the app fails
to start the device is completely mute: dark screen, then a reboot into Loader
90s later. The one artifact that explains it — the app's traceback — was never
captured anywhere, and /tmp/startup.log evaporated on the reboot. That is what
made the current Pro Max failure undiagnosable.
Three changes, all best-effort and non-blocking:
1. Capture the app's own output. `python main.py &` inherited the console,
which non-dev strips, so tracebacks went nowhere. Redirect to
/tmp/seedsigner-app.log and copy its tail into the log on failure. A plain
redirect rather than `| tee` deliberately: a pipe would make $! the tee PID
and break `wait "$APP_PID"` and the camera helper that keys off it.
2. Persist the startup log to /userdata/seedsigner-boot.log — a separate
partition that survives reboot AND reflash, which is what it was restored
for. Rotated one generation so a crash loop cannot bury the first (usually
informative) failure, and capped at 128 KiB so it cannot fill the partition.
Entirely guarded: /userdata is absent on Mini and Pico Pi, and will be
read-only once the rootfs hardening lands.
3. show-screen-message.py draws directly to the panel, outside the app:
* an early "Loading..." splash, so the device is not dark for ~20s — and,
more usefully, it splits the two failure classes at a glance: splash
then nothing means the display/SPI chain is fine and the app is at
fault; a screen that never lights means suspect the display chain.
* a "STARTUP FAILED" message naming the last exception before the Loader
reboot, so a bricked-looking device is not also a silent one.
It draws and then immediately releases SPI/GPIO via the driver's cleanup(),
so it cannot block the app from opening the panel afterwards.
Deliberately NOT wired into the boot-watchdog Loader path: that fires while
the app is still running and probably holds /dev/spidev0.0, so a second
process opening the panel could contend for SPI and wedge the boot itself.
Known limitation, documented in the script: if the app died *because* the
display could not be opened, this cannot draw either. That is why a dark
screen is itself a signal.
Also fixes a false failure in assert-kernel-network.sh: ipv6.ko was in the
wireless pattern list and checked unconditionally, so a debug_network=on build
failed over a module it correctly retains. Wireless and networking modules are
now checked against their own expectations. A false failure on an intended
configuration is as bad as a missed real one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: delete the persistent boot log once the boot succeeds
A log that outlives a good boot is a liability, not a diagnostic: an app
traceback can quote paths, arguments or other material we would rather not
leave sitting in flash on a signing device. The log now exists only inside the
diagnostic window.
* clear_persistent_log() removes both the current and the rotated copy, and
is called when the app reports ready (the boot succeeded) and on a clean
app exit.
* persist_line() additionally skips once $READY_FILE exists, so nothing is
written after readiness even though the watchdog that observes it runs in
a subshell and cannot change the parent's state.
* A FAILED boot still keeps the log — that is the whole point — and a later
successful boot clears the stale copy, so a one-off failure does not leave
a record behind forever.
Verified the full lifecycle against a stubbed tree: persisted during boot,
stops writing after ready, cleared on success, kept on failure, rotation
preserves the previous boot's log, and the size cap holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: add harden_adb build input as a display-regression bisect lever
The Pro Max display worked on non-dev builds up to #46 (b665ac9) and has been
blank since. The correlation with d1141ef is exact: that commit flipped
HARDEN_DISABLE_ADB from 0 to 1, so the adb-stripping block in harden-nondev.sh
began running for the first time in any build. It stubs /usr/bin/usbdevice and
blanks the gadget config, and S50usbdevice — the script that mounts configfs
for the SPI0 device-tree overlays the display depends on — lives in that same
area.
Hypothesis, not yet proven, so make it directly testable rather than guessed
at: harden_adb=off skips the rootfs surgery while changing nothing else.
Note this does NOT re-enable adb — the transport is removed by dr_mode=host
regardless — so an 'off' build is still adb-free and directly comparable.
Default 'on' preserves current behaviour; builds already in flight are
unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* luckfox: fix the early splash probe crashing on show_image()
show-screen-message.py called driver.show_image(image), but ST7735/ST7789
declare show_image(Image, Xstart, Ystart) with no defaults (the other four
drivers default both to 0). Every invocation therefore raised TypeError and
was swallowed by the surrounding try/except, logging only:
show-screen-message: draw failed (ST7789.show_image() missing 2 required
positional arguments: 'Xstart' and 'Ystart')
So the app-independent display probe - added specifically to diagnose the
blank-screen reports - has never actually drawn anything, on any build.
Confirmed present in both a working and a blank Pro Max boot log.
Passing the origin explicitly binds against all six driver signatures. With
this fixed the console self-labels each boot, which is what an intermittent
display fault needs:
drew 'loading' message + black screen -> panel died after a good write
could not open display (...) -> SPI/overlay chain
no line at all -> the splash never ran
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: fix Mini display ENOMEM; keep userdata on Mini and Pi
Three related changes, all driven by a Mini boot log in which the pre-app splash
drew correctly and the app then failed to open the same display:
python: page allocation failure: order:6 ... spidev_open+0x65/0x94
free:697 (~2.8MB) Normal: ... 3*128kB 0*256kB 0*512kB
SPIError: [Errno 12] Opening SPI device: Cannot allocate memory
The panel, overlays and wiring are fine. spidev_open() kmallocs its tx and rx
buffers at `bufsiz` EACH, physically contiguous, and the vendor default makes
that an order-6 (256 KB) allocation. On the 64 MB Mini, with the largest free
block down to 128 KB, it fails.
1. Pin spidev.bufsiz=8192 on the kernel command line (all boards). 8 KB is
ample: ST7789._chunked_transfer() caps a single transfer at CHUNK_SIZE=4096
and halves on EMSGSIZE, so nothing ever asks for more. That makes it an
order-1 allocation which succeeds under pressure. It must go on the cmdline
because spidev's bufsiz is module_param(..., S_IRUGO) - read-only at runtime.
2. Make rkipc actually die. `killall rkipc` plus a fixed sleep does not work
when the encoder never came up: rkipc sits in a tight retry loop emitting
thousands of "RK_MPI_VENC_GetStream timeout" per second and is slow or
unwilling to act on SIGTERM, holding DMA/CMA buffers and fragmenting memory
the whole time. New stop_rkipc() waits for the process to exit and escalates
to SIGKILL, and is used at every site that previously fired-and-forgot -
including immediately before the app launches, which is the one that matters.
3. Keep a userdata partition on Mini (6M) and Pi (SDK's 256M), matching the Max.
resolve_seedsigner_os_data_dir() now only accepts a real MOUNTPOINT and never
a directory on the rootfs, so without this partition those boards have no
persistent settings at all unless a card is inserted. Mini shrinks OEM
30M->20M and carves userdata out of rootfs (20M oem + 6M userdata + 93M
rootfs = the same 119M total as the previous userdata-less layout); rootfs
stays LAST so it can grow later without moving userdata's fixed offset.
The Mini partition check is also promoted from a warning to a hard failure: a
silently unpatched table ships a board with no persistent settings and no
build-time signal.
NOTE: Mini CMA is already at the 1M minimum (CI overrides the SDK's 24M for
Mini only) - it did not regress. That 1M is in fact why rkipc cannot allocate
and spins; stopping rkipc is the fix, not more CMA. If QR scanning turns out to
need more than 1M once the display works, that is a real trade-off to revisit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: require userdata.img in every bundle; add SPI display probe
The Pi eMMC bundle shipped without userdata.img: EMMC_FILES never listed it,
and the copy loop treats every absent file as optional. The NAND bundle had the
same hole by a different route -- its userdata.img copy was written as a soft
skip back when only some boards had the partition. Both now hard-fail.
This is a failure worth stopping the build for. /userdata is the only non-rootfs
writable store the app saves settings to, so a bundle without it flashes a board
that boots, looks healthy, and silently discards every setting -- nothing is
visibly wrong until a user loses state.
The Pi partition block was announcing "userdata retained" without checking
anything, since it edits nothing; an SDK bump could drop the partition and the
build would keep claiming it shipped. It now asserts both the partition and its
mount config. The Max partition check was a warning despite the comment above it
declaring these hard failures -- promoted to match Mini and the stated policy.
Also adds probe-display.py, an SPI bus-configuration sweep for the "panel is
black but every layer reports success" case. SPI is write-only here (MISO is
disabled so the pin can serve as RST), so nothing can be read back to tell
"the panel got the pixels" from "the bytes went into a wire it is not listening
to" -- the operator's eye is the only instrument. The probe paints a distinct
colour per configuration, sweeping the two axes the adapter's jumpers select
between: kernel-managed CE0 (mode 0) vs SPI_NO_CS with CS strapped low (mode 3),
each at 40 MHz and 10 MHz. Whichever colour appears names the working config; if
none do, the fault is not chip-select, mode, or clock.
A hardened image has no shell to run it from, so it is triggered by dropping an
empty `display-probe` file on the microSD or in /userdata, and takes the place
of the boot splash rather than fighting it for the bus. The marker is consumed
on use so a probe cannot latch on and delay every subsequent boot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: read-only squashfs rootfs with tmpfs overlays (non-dev)
The rootfs was writable UBIFS, and that is the corruption vector. Two confirmed
writers made it self-damaging in normal operation:
* luckfox-config does `sed -i` on /etc/luckfox.cfg on EVERY boot. A power cut
mid-write leaves a truncated or lost file, luckfox_load_cfg then silently
touches an empty one, zero device-tree overlays get created, there is no
/dev/spidev0.0 -- and the board comes up with a black screen and no way to
report why. That is indistinguishable on the bench from a display bug.
* the shipped Python bytecode lives on the same filesystem. A truncated .pyc
surfaced as `EOFError: marshal data is too short` and needed a reflash.
Neither is fixable in application code, because the damage happens during UBIFS
journal replay after an unclean shutdown, to files nobody was deliberately
writing. Removing write capability from the filesystem is the only real control:
squashfs has no journal, no allocator and no write path, so an unclean shutdown
cannot leave it inconsistent, and no change to / can survive a reboot because
none can be committed in the first place.
/etc, /var, /root and /home get overlayfs with a tmpfs upper so software that
expects to write keeps working, with the writes discarded at reboot. /opt is
left read-only on purpose -- a truncated .pyc there is what bricked a board.
/userdata stays writable as the sole persistent store, which is what the earlier
settings-resolver change was groundwork for.
Bootargs are not patched: the SDK derives root=/rootfstype= from the filesystem
type, giving ubi.block=0,rootfs root=/dev/ubiblock0_0 on NAND and
root=/dev/mmcblk0p<n> on eMMC. Hand-patching would fight the generator, so the
build reads the result back instead.
assert-readonly-rootfs.sh checks the GENERATED kernel .config rather than the
defconfig we wrote, because Kconfig silently drops lines whose dependencies are
unmet. The failure that guards against is not cosmetic: a squashfs root on a
kernel without overlayfs boots, mounts, and then fails the first write to /etc,
which presents as the same black screen described above. It has to fail the
build rather than the board.
Gated on the readonly_rootfs input (auto/on/off; auto = on for non-dev only), so
dev images keep a writable root and S01overlay is a no-op there.
Not covered, deliberately: the oem partition is still read-write. Its surface is
much smaller and converting it needs its own hardware verification.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: actually strip core dumps; fix readonly_rootfs ignored on dev
Core dumps were never stripped. strip-kernel-network.sh documented a
STRIP_COREDUMP parameter in its header, but the script never read $5, never
touched CONFIG_COREDUMP, and all three callers passed only four arguments. The
header also claimed harden-nondev.sh set the userspace knobs as defence in
depth; it does not, and never did. Every non-dev image built so far has shipped
with core dumps enabled.
This is not a latent nicety. CONFIG_COREDUMP is ABSENT from the stock defconfig
and its Kconfig default is `y`, so leaving it alone leaves dumping ON, and the
SDK's RkLunch.sh runs `ulimit -c unlimited` and points core_pattern at
/data/core-%p-%e late in boot. A core dump is a verbatim copy of a process's
memory; the SeedSigner app runs as root and holds seed material. That is how 48
dumps / 815 MB of cores ended up on a user's removable microSD.
Group C now disables CONFIG_COREDUMP and CONFIG_ELF_CORE, and the assertion
checks both against the generated .config -- which is what would have caught
this in the first place, since "absent from the defconfig" reads as disabled but
means enabled. With do_coredump() compiled out, RkLunch.sh's two lines are inert
rather than needing to be patched on the oem partition.
Separately, fixes a wiring bug in the read-only rootfs added in ed0c0f3: the
call sat inside a step (and, locally, inside functions) gated on non-dev, so
`readonly_rootfs=on` was silently ignored for dev builds. That is exactly
backwards -- a dev image with a read-only root is the ONLY configuration where
the property can be tested, because a hardened image has no shell to check
`mount` or prove a write to /etc is discarded. It is now its own ungated step /
function in all three build implementations, still defaulting to non-dev only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: fix the bootargs check silently verifying nothing
The read-only rootfs bootargs check globbed output/image/*.txt for
sys_bootargs. The SDK writes them to output/image/.env.txt -- a DOTFILE, which
that glob does not match -- so in build #96 it printed "no sys_bootargs found,
skipping" and verified nothing. The one check that proves the kernel is actually
told to mount squashfs was the one check not running.
Now reads .env.txt directly, falls back to grepping the packed env.img, and
treats "found nothing" as a failure rather than a skip: both boards declare an
env partition, so no bootargs anywhere means the check has stopped working, not
that there is nothing to check. That silent-skip mode is exactly what hid the
problem for a whole build.
Also asserts root=/dev/ubiblock* on NAND, since a squashfs image the bootloader
still points at as ubifs is an unbootable board.
Moved out of the workflow and into assert-readonly-rootfs.sh so os-build.sh and
build-local.sh get it too -- it was CI-only before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: enlarge the boot splash to ~3x, centred
The splash used PIL's default bitmap font (~11px) on a 240x240 panel, which is
hard to read at arm's length. It now uses the app's own OpenSans face at 34px
for the title and 26px for the body, centred as a block.
Centring matters at this size: three left-aligned lines pinned to the top margin
read as a truncated error message, which is the opposite of what a "things are
fine, please wait" screen should convey.
The failure screen's BODY is deliberately left at the bitmap font. Its job is to
carry a reason string -- usually a Python exception -- and tripling that would
truncate the one piece of information the screen exists to deliver. Only its
heading is enlarged.
Fonts ship at /opt/src/seedsigner/resources/fonts and survive the non-dev prune
(only translations are cut). A missing font falls back to the bitmap default
rather than raising: this is a best-effort diagnostic aid and must never become
the reason a board fails to boot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: sweep core dumps off writable storage on a successful boot
Defence in depth behind the kernel strip. CONFIG_COREDUMP=n stops NEW dumps but
cannot retroactively remove ones already on storage — written by an earlier
firmware, by a build made before the strip, or by stock Luckfox images. This
sweeps them from /mnt/microsd, /mnt/sdcard, /userdata and /data.
Hooked into clear_persistent_log, so it runs on a successful boot for the same
reason the boot log is dropped there: a dump is only of diagnostic value while
the device is failing, and once it is up there is no reason to leave a copy of a
root process's memory sitting in flash or on the user's card.
Patterns are deliberately narrow, because this deletes from removable media.
Only what a kernel core_pattern produces — core-<pid>-<exe>, core.<pid>, and the
bare `core` default — at the top level, never recursive, regular files only.
Verified against a card containing core_notes.md, coredir/ and mycore: all three
survive, the four dump forms do not.
Also corrects the Group C header, which claimed the 815 MB of cores found on a
user's microSD came from the SDK's core_pattern. That was asserted without
evidence and looks wrong: RkLunch.sh points core_pattern at /data/core-%p-%e and
/data does not exist anywhere in the SDK rootfs, so the kernel would fail to
write there. The card had also been in the device for months, so the files
cannot be attributed to any particular image. The dumps were real; the mechanism
is unknown. The control stands on the default alone — CONFIG_COREDUMP is absent
from the stock defconfig and defaults to y.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: put the local Docker builds back in sync with CI
Two pieces of build logic were inlined in CI and separately duplicated in
os-build.sh and build-local.sh. Both copies had drifted into shipping a
different device, with no build-time signal either way.
Partition layout: the two local builds DELETED the userdata partition
(20M(oem),99M(rootfs), plus a sed stripping userdata@/userdata@ubifs from the
filesystem config) while CI kept it. Same repo, same board, silently different
images -- and the locally built one had nowhere to persist settings and nowhere
to write the boot log, which on a hardened image is the only diagnostic channel
there is. Since the rootfs became read-only squashfs it had nowhere writable at
all. Now one apply-partition-layout.sh, which hard-fails if the result is wrong
and verifies the Pi's SDK-default userdata rather than asserting it in an echo.
spidev.bufsiz: present only in CI. A locally built Mini kept the vendor default,
hit the order-6 allocation failure in spidev_open(), and came up with no display
-- while the pre-app splash drew correctly on the same boot, which makes it read
as a display bug rather than a build difference. Now pin-spidev-bufsiz.sh,
called by all three.
The Docker wrapper could not reproduce a CI image either: only BUILD_MODEL and
BUILD_JOBS crossed the container boundary, so the variant, USB role, network and
rootfs mode always took os-build.sh's defaults regardless of what was asked for.
It now forwards the SEEDSIGNER_* variables and exposes --variant,
--readonly-rootfs, --usb-mode, --debug-network and --seedsigner-branch, mirroring
the workflow inputs of the same names. --model also accepts pi, which os-build.sh
had always understood and only this wrapper rejected.
Verified apply-partition-layout.sh against the real SDK board configs: correct
layout for all three boards, idempotent, and it fails on a config with userdata
deleted (the exact state the old local path produced).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: make the SeedSigner app branch selectable in local builds
Neither local build could build anything but `dev`, so neither could produce the
image CI has been producing.
os-build.sh assigned SEEDSIGNER_REPO_URL and SEEDSIGNER_BRANCH unconditionally
rather than as ${VAR:-default}, so anything the caller passed in was silently
discarded -- including, as of the previous commit, the --seedsigner-branch flag
I had just added to the Docker wrapper. build-local.sh was worse: the branch was
a literal `-b dev` inside the git clone command, with no variable at all.
The app is the one component this repo does not pin, so this is exactly the axis
that must be selectable. Both now honour SEEDSIGNER_REPO_URL/SEEDSIGNER_BRANCH,
and build-local.sh gains --seedsigner-branch / --seedsigner-repo / --variant /
--readonly-rootfs to match the workflow inputs of the same names.
build-local.sh also now reports the branch and commit of an existing seedsigner
checkout and warns when it differs from the one requested. It reuses a checkout
rather than re-cloning, so passing --seedsigner-branch on a second run otherwise
looks like it worked while the build quietly keeps using whatever was cloned
first -- the same class of silent divergence as the rest of this series.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: build against a release tag, and record which one
`git clone -b` already accepts a tag, so tag builds technically worked -- but
nothing said so, and two things were wrong in practice.
The reuse check I added in the previous commit compared the requested ref against
`rev-parse --abbrev-ref HEAD`, which returns the literal string "HEAD" on the
detached checkout a tag clone produces. It would therefore have warned of a
mismatch on every single tag build. It now resolves an exact tag first and
matches a branch, a tag or a commit prefix.
gen-os-release.sh had the same detached-HEAD blind spot, and it mattered more:
it recorded SEEDSIGNER_APP_BRANCH as "unknown" for a tag build, throwing away the
single most useful piece of provenance an image built from a named release has.
It now prefers an exact tag over the branch name.
The option is renamed --seedsigner-ref across the Docker wrapper and
build-local.sh (--seedsigner-branch kept as an alias, since that is still the CI
input name) because calling it "branch" hid that tags were an option at all. The
CI input description now spells it out.
Verified against the real tag SeSi-0.8.7+ShSi-B11: clones to 662d9db, matching
the remote tag SHA; the checkout helper reports `ref=SeSi-0.8.7+ShSi-B11
commit=662d9db` and matches by tag name and by commit prefix while correctly
rejecting an unrelated branch.
Also fixes the env passthrough list in the Docker wrapper, where a line
continuation had been written as a literal \n -- valid shell, but it silently
dropped SEEDSIGNER_BRANCH from the variables forwarded into the container.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: ship the shared build scripts in the Docker image
The Docker image copied os-build.sh and nothing else, so none of the scripts it
calls existed inside the container. That surfaced now only because
apply-partition-layout.sh is the first unguarded one to run early:
[STEP] Applying SeedSigner SDK Patches
bash: /build/apply-partition-layout.sh: No such file or directory
The unguarded calls failing is the good case. The call sites wrapped in
`[[ -f ... ]]` -- harden-nondev.sh, optimize-nondev.sh, patch-s50usbdevice.sh,
compile-translations.sh -- SILENTLY SKIPPED instead. A Docker build has been
producing images with the hardening, the USB-host fix and the compiled
translations simply absent, reporting success, and looking identical to a good
build until the device was in someone's hands.
The Dockerfile now copies *.sh as a glob rather than a list, so adding a shared
script cannot silently fail to reach the container, and os-build.sh asserts all
14 are present before doing any work. Those `[[ -f ]]` guards exist to tolerate
an optional script, not to tolerate a broken image; the preflight check turns a
silently-degraded build into a five-second failure that names the missing files
and says to rebuild with --force.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: build every board/medium in one run; fix two more CI/local divergences
Adds --all (and --model all) to build every valid hardware/boot combination in
one invocation: mini sd+nand, max sd+nand, pi emmc -- the same five as the CI
matrix. The dispatch is now table-driven from that list instead of an if-chain
per board, which is what let "both" quietly mean mini+max only, with no way to
ask for the Pi and the rest together. A model/media selection that matches
nothing is now a hard error rather than a successful build producing no images.
Also stops requiring --microsd/--nand for the Pico Pi. It boots from eMMC and
nothing else, so demanding a medium flag to build the one image it can produce
was a trap.
Two more things the Docker path had that CI did not:
The buildroot directory was hard-coded to buildroot-2023.02.6 while the SDK now
ships buildroot-2024.11.4, so every Docker build died at "Buildroot directory
not found after buildroot_create". CI discovered the directory all along, which
is exactly why this stayed invisible. Now resolve-buildroot-dir.sh, shared by
all three -- the version is the SDK's business, and pinning it here bought
nothing while costing a hard failure on every SDK bump.
python3-venv was missing from the image, so compile-translations.sh fell back to
"English-only" in a single line that scrolls past. Docker builds have been
shipping images with no translations while CI shipped all of them.
Verified the combination matrix across six selections: all=5 built, both=4 (pi
correctly skipped), single board+medium=1, pi with no media flags builds
pi/emmc, and an empty selection fails loudly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: resolve every buildroot-derived path, not just BUILDROOT_DIR
Follow-up to 73df197. PACKAGE_DIR, CONFIG_IN and PYZBAR_PATCH are all computed
from BUILDROOT_DIR at file scope, where its value is still the UNRESOLVED
placeholder. ensure_buildroot_tree updated BUILDROOT_DIR and PACKAGE_DIR but not
the other two, so the placeholder survived into:
grep: .../sysdrv/source/buildroot/UNRESOLVED/package/Config.in:
No such file or directory
...roughly 700 lines of build output after the path was supposedly resolved,
in whichever step happened to touch CONFIG_IN first.
All four are now set together in _set_buildroot_paths, which also asserts the
resolved tree actually contains package/Config.in. A bad resolve now fails at
the point of resolution naming the path it expected, instead of surfacing much
later as a confusing grep error in an unrelated step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: cap build parallelism by memory, not just cores
The Docker build compiles host-rust from source -- and therefore LLVM -- because
it never caches a toolchain the way CI does (os-build.sh has said "Rust
toolchain: always built from source in Docker (no caching)" all along). LLVM's
X86/AArch64 codegen translation units are among the largest C++ compiles in
common use, several GB of RSS each, so -j$(nproc) on a 32-core host asks for
tens of GB at peak. When the host cannot supply it the compiler is OOM-killed
and the build dies at ~97% of the LLVM build with:
gmake[3]: *** [.../lib/Target/X86/CMakeFiles/LLVMX86CodeGen.dir/all] Error 2
which names no cause and reads like a compiler bug rather than a resource limit.
BUILD_JOBS now defaults to min(nproc, memory_GB / 2) -- the usual rule of thumb
for building LLVM -- and the build header says when memory rather than cores is
the limit, so the number is explicable instead of mysterious. An explicit
BUILD_JOBS or --jobs N still wins.
Verified the formula across host shapes: 32c/16GB -> -j8, 32c/32GB -> -j16,
32c/64GB -> -j32 (uncapped), 4c/3GB -> -j1, and an unreadable /proc/meminfo
falls back to nproc.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: cap LLVM parallelism via x.py --jobs, not just MAKEFLAGS
host-rust's HOST_RUST_BUILD_CMDS runs `x.py build` and HOST_RUST_INSTALL_CMDS
runs `x.py dist`, both with no --jobs, so x.py defaults LLVM's
cmake --build --parallel to num_cpus() and ignores the MAKEFLAGS/BR2_JLEVEL
cap set in 94c1d9e (and CMAKE_BUILD_PARALLEL_LEVEL) too. On a many-core,
memory-tight host the LLVM compile therefore goes wide-open and OOMs at
~94-97% with a bare
gmake[3]: *** [.../lib/Target/X86/CMakeFiles/LLVMX86CodeGen.dir/all] Error 2
Patch package/rust/rust.mk to pass `--jobs $${RUST_BUILD_JOBS}` to both
x.py build and x.py dist; the recipe shell expands RUST_BUILD_JOBS (inherited
from this script's export). Note the `jobs =` config.toml field was removed in
rust 1.82's bootstrap ("unknown field `jobs`"), so the --jobs CLI flag is the
only supported knob. RUST_BUILD_JOBS defaults to BUILD_JOBS / 2 (LLVM ~2 GB/job
vs typical C ~1 GB/job), so on a 16 GB host it caps LLVM at ~4 jobs instead of
~20; override with RUST_BUILD_JOBS=N. Also reverts any stale `jobs =`
config.toml injection left by an earlier build so a re-run after the failed
attempt succeeds. Banner now prints the LLVM job count.
* luckfox: give GnuPG a writable home; define the missing print_warning
Two unrelated fixes from the same build session.
1. os-build.sh runs under `set -e` and called print_warning at two sites without
ever defining it (build-local.sh has always defined it -- the two drifting apart
again). An undefined command exits 127, so the build died outright the first
time either branch was taken, which only happened once it got far enough for
ifd-ccid.bundle to exist. Defined alongside the other print_* helpers.
2. GPG key generation and key import failed with "read-only file system" on the
Pro Max and Pico Pi. The app never sets GNUPGHOME and never passes
`gpg --homedir`, so gpg resolves its home from $HOME -- which under BusyBox init
is "/", i.e. /.gnupg on the read-only squashfs root, a directory S01overlay
deliberately does not overlay. Everything gpg must write then fails: pubring.kbx,
private-keys-v1.d/, trustdb.gpg, random_seed, lock files, and the S.gpg-agent /
S.scdaemon sockets, since there is no /run/user/0 here. Pi/La Frite never hit it
only because their rootfs is writable.
start-seedsigner.sh now sets GNUPGHOME=/tmp/.gnupg, creates it 0700, and seeds
gpg-agent.conf + scdaemon.conf from /usr/share/seedsigner/gnupg (staged by the
new shared install-gnupg-home.sh, called by all three build implementations and
registered in the preflight assert list).
/tmp rather than an overlaid path: it is a plain tmpfs available before
S01overlay runs -- which itself uses /tmp -- so gpg cannot inherit an overlay
failure. And it is wiped at reboot, which makes the requested statelessness a
property of the design rather than a policy: private keys never touch flash.
GNUPGHOME rather than HOME, because HOME would move Path.home() for the whole
app while GNUPGHOME already covers gpg, gpgconf, gpg-agent and scdaemon.
Note the sharpest risk: gpg-agent/scdaemon almost certainly never ran before,
being unable to create their sockets. A working home means they start, and
scdaemon will try to claim the SEC1210. scdaemon.conf ships `disable-ccid` to
route it through pcscd instead -- the same reason the Pi images carry that file.
This needs the smartcard regression run on hardware; rollback is deleting one
file.
3. Also fixes an uncaught EROFS in the same feature area: 15 sites in
gpg_views.py did os.makedirs(get_microsd_dir() / "microsd-images") with no
try/except, and the resolver falls back to the literal /mnt/microsd when there
is no card and no /userdata -- /mnt is not overlaid, so that raised an uncaught
OSError and took the view down. Replaced with resolve_microsd_images_dir(),
which degrades to a tmpfs staging dir and never raises. The BIP85 read path uses
the same resolver so a fallback export stays readable.
App suite: 892 passed, 0 failed. The new tests were verified to fail against the
naive makedirs and pass with the fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* luckfox: set HARDWARE in pin-spidev-bufsiz CI step (was empty)
* luckfox: fail builds on a watchdog-signal-less app; precompile bytecode
Two build-time guards against failures that build green and only show on
hardware:
- assert-app-watchdog-signal.sh: start-seedsigner.sh reboots into Loader
120 s after app start unless the app writes /tmp/seedsigner-ready, which
exists only on app refs containing 689483af (dev and all tags lack it).
A signal-less app boots looking healthy, then Loader-loops on every boot
-- seen on Pico Pi + Pico Pro Max images built with the default 'dev'
app ref. All three build paths now fail right after the app
clone/reuse decision; SEEDSIGNER_ALLOW_NO_WATCHDOG_SIGNAL=1 is the
escape hatch for deliberate old-app builds.
- precompile-bytecode.sh: the read-only squashfs can never cache
__pycache__ at runtime, so every import re-compiled .py source off xz
squashfs, on a single-core A7, on every boot. Precompile /opt/src +
site-packages at build time with the target python's own compileall.py
(discovered via the SDK buildroot tree, host/target major.minor match
enforced), deterministic flags -- the same treatment the Pi post-build
scripts have always given the app.
Both run from all three build paths (CI workflow, os-build.sh,
build-local.sh), per the keep-in-sync rule.
* luckfox: persistent boot log off by default (boot_log build flag)
start-seedsigner.sh wrote /userdata/seedsigner-boot.log on every boot and
deleted it on success. On a failed boot it persisted app output (tracebacks,
paths, values) to flash that survives a reflash, on a device meant to be
air-gapped. Gate init_persistent_log on /etc/seedsigner-boot-log, which only
a build with the boot-log flag bakes in; default images perform zero /userdata
writes. clear_persistent_log still runs on every successful boot to sweep any
stale log/.prev and core dumps. Thread the flag through CI workflow input,
build.sh --boot-log, os-build.sh and build-local.sh.
* luckfox: scope the app watchdog-signal guard to non-dev builds
The guard hard-failed every build whose staged app lacks the /tmp/seedsigner-ready
signal. Automatic push/PR CI always builds the dev variant against the app's
'dev' branch, which predates the signal (689483af) — so every auto run failed
and OS CI was blocked until the app signal merged.
The guard exists to stop a SHIPPED production image from Loader-looping, so it
now scopes by build variant: non-dev (shipped) still hard-fails; dev (debuggable,
never shipped, serial+adb) warns. assert-app-watchdog-signal.sh takes the variant
as its 2nd argument (default non-dev/hard) and all three callers pass it — CI
(inputs.build_variant), os-build.sh (SEEDSIGNER_BUILD_VARIANT), build-local.sh
(BUILD_VARIANT). The SEEDSIGNER_ALLOW_NO_WATCHDOG_SIGNAL=1 escape hatch still
overrides.
* Revert "luckfox: scope the app watchdog-signal guard to non-dev builds"
This reverts commit bb4bfe4b77.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
A custom linux based operating system built to manage software running on airgapped Bitcoin signing device. SeedSigner is both the project name and application running on airgapped hardware. This custom operating system, like all operating systems, manages the hardware resources and provides them to the application code. It's currently designed to run on common Raspberry Pi hardware with accessories. The goal of SeedSigner OS is to provide an easy, fast, and secure way to build microSD card image to securely run SeedSigner code.
⚙️ Under the Hood
SeedSigner OS is built using Buildroot. Buildroot is a simple, efficient and easy-to-use tool to generate embedded Linux systems through cross-compilation. SeedSigner OS does not fork Buildroot, but uses Buildroot with custom configurations to build microSD card images tailor made for running SeedSigner.
🎯 Hardware targets
This repo builds images for two hardware families through two coexisting build pipelines:
Raspberry Pi / La Frite — standard Buildroot (the opt/buildroot submodule) with per-board external trees. Built via docker compose / opt/build.sh. See Building.
Luckfox Pico (Rockchip RV1103/RV1106) — the Luckfox Pico vendor SDK, built under opt/luckfox/. See Building for Luckfox Pico.
A convenience dispatcher at the repo root routes to the right pipeline:
./build.sh --pi0 --smartcard --dev # Raspberry Pi / La Frite
./build.sh --luckfox build --microsd --model mini # Luckfox Pico
🛂 Security
SeedSigner OS is built to reduce the attack surface area and enable additional application functionality. The OS is an order of magnitude smaller in size than Raspberry Pi OS (which is what typically is used to run software on a Pi device). Here are a list of some security and functional advantages of using SeedSigner OS.
Boots 100% from RAM. This means, once you see the SeedSigner splash screen, you can remove the microSD card because no disk I/O is needed after boot!
One FAT32 partition on the microSD card
Removes these standard Raspberry Pi OS Kernel modules:
Networking and Bluetooth
SWAP
I2C
Serial
USB
Pulse-Width Modulation (PWM)
NO HDMI support
NO Serial connection TTL support
NO Software supporting any wireless or networking chips
A single read only zImage file on the boot partition containing the entire Linux kernel and filesystem