Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/init-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ regardless, so the worst case is slow, never weak.
| `/etc/inittab` | **adapted** (3 documented deviations) | Full stock shape reproduced (`::sysinit:/media/fat/MiSTer &`, `/etc/resync &`, `rcS`, shutdown sequence) with: **(1)** the remount-rw sysinit line kept **commented out**, exactly as stock has it — Buildroot's own skeleton default inittab ships this line **uncommented**, which would remount `/` rw at every sysinit and defeat the whole read-only-root design (ADR 0011); confirmed this project's own finalize hook tries to uncomment it too (see below) and is overridden by the overlay running last. **(2)** the serial console runs `ttyS0::respawn:/sbin/agetty --nohostname -L ttyS0 115200 vt100` — util-linux `agetty`, matching stock (whose inittab also uses `agetty --nohostname`), now that `BR2_PACKAGE_UTIL_LINUX_AGETTY` is enabled (see `docs/util-linux-parity.md`). It still targets `ttyS0` explicitly rather than stock's `console` alias, because this board's actual cmdline is `console=ttyS0,115200` (`docs/boot-chain.md`). agetty treats a numeric positional argument as the baud rate, so `ttyS0 115200` and `115200 ttyS0` are equivalent; the port-first order is kept for continuity with the previous BusyBox `getty` line. (Earlier revisions used BusyBox `getty` because `BR2_PACKAGE_UTIL_LINUX_BINARIES` was not selected — that constraint no longer holds; the overlapping BusyBox `getty` applet is now disabled so `agetty` is the console.) **(3)** `gpm` is invoked at `/usr/sbin/gpm`, not stock's `/sbin/gpm` — originally **a real bug caught during acceptance verification**: gpm is a real package (not a BusyBox applet, which lands under `/sbin` regardless of usr-merge), and when this row was first written (P2.3) the rootfs was **not** usr-merged — `/sbin` was a real directory, not `-> /usr/sbin` the way stock's `work/imgroot/sbin` is — so `/sbin/gpm` genuinely did not exist in the image. An inittab `sysinit` line is an **absolute path**, so unlike `/etc/profile`'s `PATH` it gets no shell search at all; it would have silently failed to spawn on every single boot. Caught by exhaustively grep'ing every absolute path referenced anywhere in the overlay against the actually-built image (see the task report) — first build had this wrong; fixed and rebuilt before acceptance. **Since P2.9 v2** (`7be9ee5`) set `BR2_ROOTFS_MERGED_USR=y`, that premise no longer holds: this build is now usr-merged like stock (verified: `output/target/sbin -> usr/sbin`), so `/sbin/gpm` *would* resolve today. **The deviation is kept anyway, deliberately.** Buildroot builds gpm with `--prefix=/usr`, so the binary's real path is `/usr/sbin/gpm` in *either* layout (gpm's own file list: `gpm,./usr/sbin/gpm`) — stock's binary is at that same physical path, and stock's inittab only reaches it through the `/sbin -> usr/sbin` compat symlink that exists solely because stock is usr-merged. So our path is correct in both layouts and stock's is correct in only one; ours survives an unmerge, stock's would break on one. Given P2.3 already got bitten by exactly that (the unmerged build had no `/sbin/gpm` at all), pointing at the real path rather than a merge-dependent alias is the more durable choice, even though a future unmerge is unlikely. This is an **accepted permanent deviation from stock**, not a cleanup waiting to happen. **Also dropped — later RESTORED, guarded (T3, 2026-07-27):** `loadkeys /etc/kbd.map` and `setfont` were dropped by P2.3 because this BusyBox build has no `loadkeys` applet at all and `CONFIG_SETFONT` is explicitly not set (still true — re-verified in `output/build/busybox-1.38.0/.config`; busybox's `loadkmap` applet is not a substitute, it reads binary bkeymap, not stock's text keymap); keeping either line verbatim would just fail every boot. T3 vendored stock's `etc/kbd.map` (it blanks the F12/Mute/Vol± keycodes Main_MiSTer consumes via evdev) and restored both lines wrapped in `[ -x /usr/bin/... ]` guards: with the parallel T5 task's `BR2_PACKAGE_KBD` (the same package stock's own loadkeys/setfont came from) the lines do stock's exact job, without it they are silent no-ops instead of boot errors. See the inittab's own note 3 and `docs/stock-reconciliation.md` §3c. `gpm -m /dev/input/mice -t imps2` itself **is** kept (just at the corrected path) — `BR2_PACKAGE_GPM=y` was deliberately selected in P2.1 for this, and the binary is present. |
| `/etc/profile` | **adapted** (1 documented deviation) | Full stock content reproduced (PATH, `PS1='$(pwd)# '`, `EDITOR=/bin/vi`, `/etc/profile.d/*.sh` sourcing, `LC_ALL=en_US.UTF-8`, and critically the login-time `mount -o remount,rw /` — this is how `/` ever becomes writable at all, matching stock and ADR 0011's own description of the mechanism). **Deviation:** `PATH` gains an explicit `/bin:/sbin:` prefix stock's literal string doesn't have. Stock's rootfs is usr-merged (`work/imgroot`: `/bin -> usr/bin`, `/sbin -> usr/sbin`), so its `PATH="/usr/bin:/usr/sbin"` already covered `/bin`/`/sbin` for free. When this deviation was introduced (P2.3) *this* build was a plain (non-merged) skeleton layout, so omitting `/bin:/sbin` from `PATH` would have silently dropped most BusyBox applets from every interactive shell; changing `BR2_ROOTFS_MERGED_USR` was then out of P2.3's scope ("do NOT disturb ... the package set"), making the overlay `PATH` the correct-altitude fix. **Since P2.9 v2** (`7be9ee5`) set `BR2_ROOTFS_MERGED_USR=y` — the change that fixed the `/lib/security/pam_unix.so` SSH lockout — this build is usr-merged too, so the `/bin:/sbin:` prefix is now redundant. It is kept because it is harmless (the paths resolve to the same directories) and matches Buildroot's own skeleton default profile. The file's own header comment records the same history. |
| `/usr/lib/dhcpcd/dhcpcd-hooks/90-timezone` | **added (divergence, [ADR 0025](decisions/0025-first-boot-timezone-autodetect.md))** | Stock has no equivalent, and that is the gap it closes: `/etc/localtime` points at `/media/fat/linux/timezone`, which **does not exist on a fresh card**, so glibc falls back to UTC silently and permanently. The first time dhcpcd brings an interface up with an address, this asks `ip-api.com` for the zone of the box's public IP and copies `/usr/share/zoneinfo/posix/<Zone>` to that path — the *same* provider, destination and file format as the community `Scripts_MiSTer/timezone.sh` "Automatic" mode, so the two are interchangeable. No new package (`curl` and tzdata were already in the image). **Note this adds no init script**: an earlier revision had an `S48timezone` as well, but on a DHCP box the lease has usually not landed by S48, so it was near-redundant with this hook — and it carried a `/proc/net/route` check whose IPv6 arm silently matched the kernel's own `ip6_null_entry`. A static-IP box configured only in `/etc/network/interfaces` never runs dhcpcd and so never autodetects; accepted deliberately, since setting a static address is already a by-hand act. Properties worth stating because they are the design: the guess is spent **once, and only when it was actually made** (gated on the timezone file *and* on a `timezone.autodetect` stamp written only when a provider answered *with a zone name*, so neither being offline nor a captive portal's HTTP 200 burns it), it **never overwrites** a timezone anyone has already set (re-checked immediately before the write, not only at the gate), and it **delays nothing** — the body is a backgrounded subshell, which also keeps it from leaking a single variable or function into dhcpcd's shell. Sourced, not executed (`dhcpcd-run-hooks`: `. "$hook"`), hence no shebang, no exec bit, and no `exit` — an `exit` here would end dhcpcd's whole hook run and take `20-resolv.conf`/`30-hostname` with it. Zone names arrive off the network, so they are validated against the shipped zoneinfo before being used as a path; `scripts/test-timezone.sh` asserts each rejection, and mutation-checks the two that could otherwise pass vacuously. |
| `/usr/lib/dhcpcd/dhcpcd-hooks/` (the set) | **pinned (build-host independence)** | dhcpcd's `configure` chooses which hooks to install by probing the **build host** for `ntpd`/`chronyd`/`systemd-timesyncd`/`ypbind`, and Buildroot's `dhcpcd.mk` passes no `--with-hooks`, so the image inherited whatever daemons the build machine happened to have: a GitHub runner produced `50-ntp.conf`, a developer box with timesyncd produced `50-timesyncd.conf` — with a green build both times (found 2026-09-02 by diffing a CI image against a local one). `external.mk` now appends `--with-hooks=ntp.conf --with-eghooks=yp.conf` to `DHCPCD_CONFIG_OPTS`, which reproduces the canonical CI image exactly (`01-test`, `20-resolv.conf`, `30-hostname`, `50-ntp.conf`, plus our `90-timezone`; `50-yp.conf` stays an example under `/usr/share/dhcpcd/hooks`), and `scripts/check-linux-img.sh` asserts that exact set. `50-ntp.conf` is the right hook: we ship classic `ntpd` (stock parity) and it writes DHCP-offered servers into `/etc/ntp.conf`. Upstream-worthy: Buildroot's `dhcpcd.mk` should pass `--with-hooks` itself. |
| `/etc/resync` | **identical** | Byte-for-byte match (53 bytes: `( while [ 1 ]; do sync; sleep 5; done ) &`). Executable bit set. |
| `/etc/proftpd.conf` | **identical, with a flagged security caveat** | Byte-for-byte reproduction of stock: `User root`/`Group root`, `RootLogin on`, anonymous `<Limit WRITE> AllowAll`, `Umask 000`. **Note, not acted on:** the package's own default `proftpd.conf` is meaningfully more hardened (`User nobody`, no root login, anonymous write denied). This project's task scope names exactly two sanctioned security improvements (resolv.conf's upstream default, ADR 0015's SSH host keys); silently hardening FTP as a third, undocumented one would change a well-known MiSTer workflow (anonymous/root FTP to `/media/fat`) without a maintainer decision or its own ADR. Shipped as stock parity; flagged here and in the task report as a candidate for a future ADR, not decided unilaterally. |
| `/etc/samba/smb.conf` | **identical** | Byte-for-byte reproduction of stock (276 lines). `S91smb`'s stock double-guard (see above) means Samba cannot auto-start regardless — `/media/fat/linux/samba.sh` will never exist on a fresh image. |
Expand Down
36 changes: 36 additions & 0 deletions external.mk
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,39 @@ endef
LINUX_KCONFIG_FIXUP_CMDS += $(sep)$(MISTER_LINUX_INITRAMFS_FIXUP)

endif # BR2_LINUX_KERNEL && BR2_arm

################################################################################
#
# dhcpcd: pin the hook set so it cannot depend on the BUILD HOST.
#
# dhcpcd's own ./configure decides which dhcpcd-hooks to install by probing the
# machine it runs on -- `which ntpd`, `which chronyd`,
# /usr/lib/systemd/systemd-timesyncd, `which ypbind` -- unless it is handed the
# list explicitly (configure: `if ! $HOOKSET` around the probes; HOOKSET is
# only set by --with-hooks). Buildroot's package/dhcpcd/dhcpcd.mk passes no
# --with-hooks, so the TARGET image inherited the host's daemons: a GitHub
# runner (ntpd/chronyd on PATH) shipped 50-ntp.conf, a developer box with
# systemd-timesyncd and no ntpd shipped 50-timesyncd.conf instead -- and the
# build was green either way. Found 2026-09-02 by diffing a CI image against a
# local one (docs/init-parity.md, dhcpcd row).
#
# This appends to the package's own CONFIG_OPTS -- legal because Buildroot
# includes BR2_EXTERNAL .mk files after package/*/*.mk (work/buildroot/
# Makefile: package includes first, then $(BR2_EXTERNAL_MKS)) and the
# DHCPCD_CONFIGURE_CMDS `define` expands $(DHCPCD_CONFIG_OPTS) when the recipe
# RUNS, not when the .mk is parsed. No Buildroot patch, no dhcpcd patch.
#
# The values reproduce the canonical (CI-built) image exactly: the base hooks
# 01-test/20-resolv.conf/30-hostname are unconditional in hooks/Makefile;
# `ntp.conf` selects 50-ntp.conf (we ship classic ntpd, BR2_PACKAGE_NTP, so
# DHCP-offered NTP servers land in /etc/ntp.conf as on stock); `yp.conf` keeps
# the 50-yp.conf EXAMPLE hook under /usr/share/dhcpcd/hooks, which CI also
# shipped. Names are hook stems: configure's find_hook() matches
# [0-9][0-9]-<stem>[.conf], so "ntp.conf" is right and "50-ntp.conf" silently
# matches nothing (verified against dhcpcd 10.2.4's configure).
# scripts/check-linux-img.sh asserts the resulting hook set, fail-closed.
#
################################################################################
ifeq ($(BR2_PACKAGE_DHCPCD),y)
DHCPCD_CONFIG_OPTS += --with-hooks=ntp.conf --with-eghooks=yp.conf
Comment thread
mcfbytes marked this conversation as resolved.
endif
46 changes: 46 additions & 0 deletions scripts/check-linux-img.sh
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,53 @@ else
bad "ssh_host_* file(s) found IN THE IMAGE -- this ships a private key:"
printf '%s\n' "$found" | while IFS= read -r f; do note "$f"; done
fi
# dhcpcd's hook set must be the PINNED one, never the build host's. dhcpcd's
# configure probes the machine it runs on for ntpd/chronyd/timesyncd/ypbind and
# installs hooks accordingly unless told otherwise; external.mk pins it with
# --with-hooks/--with-eghooks (see the comment there). This assertion is what
# turns a future regression -- a Buildroot bump that changes the option, a host
# that grows a daemon -- from a silent image difference into a red build.
# Expected set = the canonical CI-built image: dhcpcd's three unconditional
# hooks, 50-ntp.conf (we ship ntpd), and our own 90-timezone (ADR 0025).
EXPECT_DHCPCD_HOOKS="01-test 20-resolv.conf 30-hostname 50-ntp.conf 90-timezone"
hooks_dir=$(find "$dump_dir" -type d -path '*/dhcpcd/dhcpcd-hooks' 2>/dev/null | head -1)
if [ -z "$hooks_dir" ]; then
bad "no dhcpcd-hooks directory in the image (dhcpcd or its hooks are missing)"
else
got_hooks=$(find "$hooks_dir" -mindepth 1 -maxdepth 1 -printf '%f\n' | sort | tr '\n' ' ' | sed 's/ $//')
# shellcheck disable=SC2086 # word-splitting the space-separated list is the point
want_hooks=$(printf '%s\n' $EXPECT_DHCPCD_HOOKS | sort | tr '\n' ' ' | sed 's/ $//')
if [ "$got_hooks" = "$want_hooks" ]; then
ok "dhcpcd hook set is exactly the pinned one ($want_hooks)"
else
bad "dhcpcd hook set differs from the pinned one -- a build-host daemon leaked into the image?"
note "expected: $want_hooks"
note "found: $got_hooks"
note "(50-timesyncd.conf here means configure probed the host; see external.mk's dhcpcd block)"
fi
fi
# The EXAMPLE hooks are pinned too (--with-eghooks=yp.conf): they are the
# other half of what the host probe used to decide, and a bump that stops
# honouring the option would otherwise change the canonical file set with
# a green build. Expected set = dhcpcd's unconditional examples + 50-yp.conf.
EXPECT_DHCPCD_EGHOOKS="10-wpa_supplicant 15-timezone 29-lookup-hostname 50-yp.conf"
eghooks_dir=$(find "$dump_dir" -type d -path '*/share/dhcpcd/hooks' 2>/dev/null | head -1)
if [ -z "$eghooks_dir" ]; then
bad "no /usr/share/dhcpcd/hooks (example hooks) directory in the image"
else
got_eg=$(find "$eghooks_dir" -mindepth 1 -maxdepth 1 -printf '%f\n' | sort | tr '\n' ' ' | sed 's/ $//')
# shellcheck disable=SC2086 # word-splitting the space-separated list is the point
want_eg=$(printf '%s\n' $EXPECT_DHCPCD_EGHOOKS | sort | tr '\n' ' ' | sed 's/ $//')
if [ "$got_eg" = "$want_eg" ]; then
ok "dhcpcd example-hook set is exactly the pinned one ($want_eg)"
else
bad "dhcpcd example-hook set differs from the pinned one (--with-eghooks not honoured, or a host probe leaked)"
note "expected: $want_eg"
note "found: $got_eg"
fi
fi
fi

rm -f "$dump_dir.rdump.log"

# Belt-and-suspenders: the specific mount point ADR 0015 names must exist and
Expand Down
Loading