feat(extensions): add netboot extension for TFTP+NFS diskless boot#9656
feat(extensions): add netboot extension for TFTP+NFS diskless boot#9656iav wants to merge 26 commits intoarmbian:mainfrom
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds a netboot extension to produce TFTP/NFS boot artifacts and NFS-root deployments, integrates Changes
Sequence Diagram(s)sequenceDiagram
participant Builder as Armbian Builder
participant Ext as Netboot Extension
participant KernelCfg as Kernel Config
participant ImgGen as Image Generator
participant TFTP as TFTP Staging
participant Hook as netboot_artifacts_ready Hook
Builder->>Ext: triggered with ROOTFS_TYPE=nfs-root
Ext->>Ext: normalize & validate vars (NETBOOT_SERVER, NETBOOT_CLIENT_MAC, paths)
Ext->>KernelCfg: enable NFS-root kernel options
Ext->>ImgGen: customize image (disable resize service, skip firstlogin)
ImgGen->>ImgGen: build rootfs
Ext->>ImgGen: create/archive or export rootfs based on ROOTFS_COMPRESSION/ROOTFS_EXPORT_DIR
Ext->>TFTP: collect kernel (Image→zImage→vmlinuz), DTBs, optional uInitrd
Ext->>TFTP: generate PXELINUX entries (per-MAC or default)
Ext->>Hook: invoke netboot_artifacts_ready with artifact context
sequenceDiagram
participant DHCP as DHCP Server
participant Client as Target Device
participant UBoot as U-Boot
participant TFTP as TFTP Server
participant NFS as NFS Server
Client->>DHCP: request IP + boot options
DHCP->>Client: reply (serverip, bootfile)
Client->>UBoot: obtain bootfile (pxe/extlinux)
UBoot->>TFTP: fetch pxelinux.cfg, kernel, dtb, uInitrd
TFTP->>UBoot: deliver artifacts
UBoot->>NFS: mount nfsroot (from kernel cmdline)
NFS->>Client: provide root filesystem
Client->>Client: kernel boots with NFS root
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@extensions/netboot/netboot.sh`:
- Around line 38-42: The function
extension_prepare_config__netboot_force_nfs_rootfs sets ROOTFS_TYPE too late (it
runs during do_extra_configuration) so NFS-specific branches in
do_main_configuration never see it; move the ROOTFS_TYPE assignment earlier by
ensuring extension_prepare_config__netboot_force_nfs_rootfs (or its logic) runs
before do_main_configuration—either call that function in the init/startup hook
that executes prior to main configuration (e.g., extension_init or top-level
script before do_main_configuration) or export/set declare -g ROOTFS_TYPE="nfs"
at script initialization so the global variable is in effect for
do_main_configuration and the NFS setup in
lib/functions/configuration/main-config.sh executes.
- Around line 147-149: The PXE staging copies ${MOUNT}/boot/uInitrd but never
references it in the generated extlinux.cfg, so U-Boot won't load the initramfs;
modify the netboot stanza generation to set an initrd_line variable when the
file exists (mirror how fdt_line is created — e.g. check for
"${MOUNT}/boot/uInitrd", set initrd_line="INITRD ${tftp_prefix_dir}/uInitrd")
and then include ${initrd_line} in the emitted PXE stanza alongside fdt_line so
the INITRD directive is present for U-Boot.
In `@extensions/netboot/README.md`:
- Around line 137-155: The README has multiple unlabeled fenced code blocks
(e.g., the directory tree block showing "/srv/netboot/" and other blocks around
lines 159-164, 171-175, 465-489, 544-547); update each triple-backtick fence to
include an appropriate info string (for example use sh, ini, or text as
appropriate) so markdownlint stops flagging them—locate the fences by searching
for the blocks that display the directory tree and configuration snippets and
add the matching language tag to each opening ``` line.
In `@lib/functions/image/rootfs-to-image.sh`:
- Around line 82-93: The rsync into ROOTFS_EXPORT_DIR can leave stale files
behind when the export tree is reused; update the rsync invocation in
rootfs-to-image.sh (the run_host_command_logged rsync call) to include --delete
(and optionally --delete-excluded) so files removed from the source are removed
from "${ROOTFS_EXPORT_DIR}" as well; add the flag either into $rsync_ea or
directly in the rsync command invocation to ensure exported NFS root mirrors the
built image.
- Around line 69-76: The archive creation pipeline (tar … | pv … |
${archive_filter} > "${ROOTFS_ARCHIVE_PATH}") must be guarded with pipefail so
failures in tar or pv aren't masked by a later stage; wrap that pipeline in a
shell context with set -o pipefail (or enable set -o pipefail locally before
running the pipeline) so a nonzero exit from tar or pv will cause the script to
fail and propagate the error from creating ROOTFS_ARCHIVE_PATH; update the block
around display_alert/ tar / pv / ${archive_filter} to ensure pipefail is active
for that pipeline.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: fca3bef5-36a8-4aaa-81dd-645853e7579e
📒 Files selected for processing (5)
config/templates/nfs-boot.cmd.templateextensions/netboot/README.mdextensions/netboot/netboot.shlib/functions/configuration/main-config.shlib/functions/image/rootfs-to-image.sh
`nfs-root` is a new rootfs type distinct from the existing `nfs` hybrid mode. Selecting it wires the `netboot` extension from the core `ROOTFS_TYPE` dispatch in `do_main_configuration`, so callers no longer need a separate `ENABLE_EXTENSIONS=netboot`. The legacy `nfs` branch (kernel+DTB on local boot partition, `/` over NFS) is untouched — both paths coexist until the hybrid mode's future is decided. Core plumbing mirrors the `nfs` branch for all paths where local root storage would be meaningless: partition layout skip (`prepare_partitions`), archive/export gate and version suffix (`rootfs-to-image.sh`), and the host-side filesystem compatibility check in `main-config.sh`. Extension hooks now key on `ROOTFS_TYPE=nfs-root` instead of guessing from `nfs`, removing the `force ROOTFS_TYPE=nfs` shim that ran too late relative to `do_main_configuration`. Also folded in from CodeRabbit review on PR armbian#9656: - pipefail around tar|pv|compressor so truncated archives no longer slip through on an intermediate stage failure - `rsync --delete` on `ROOTFS_EXPORT_DIR` so stale files from a previous build don't linger in the NFS export tree - explicit `INITRD` directive in extlinux when `uInitrd` is staged; U-Boot only loads an initramfs when the stanza names it - README updated to document `ROOTFS_TYPE=nfs-root` as the single switch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Very nice. With recent u-boot (2026.04, thanks to Kwiboo), one can enable LWIP networking stack for better TFTP performance (loading kernel and initramfs) -- there's also some TFTP Window Size stuff that can be tuned. U-boot with LWIP + u-boot's |
|
Thanks for the pointer! Let's get this merged first — iterating on something that works is the easy part. |
a76e4b2 to
fec99ac
Compare
a10feb4 to
4ff6016
Compare
|
@coderabbitai review |
|
Pull my Helios64 to v2026.04 in #9675 |
a61ff64 to
dac19c8
Compare
The previous template was hardcoded for sunxi arm32 — `zImage`/`bootz`,
`console=tty1 console=ttyS0,115200`, `disp.screen0_output_mode=...`,
`ext4load/fatload mmc 0`. It did not work on arm64 boards (e.g.
rockchip64/helios64) and baked in a 115200 baud that breaks non-standard
UART speeds (helios64 runs at 1500000).
Rewrites the U-Boot hush script to:
- Prefer `Image` + `booti` (arm64), fall back to `zImage` + `bootz` (arm32).
- Load via `${devtype} ${devnum}:${distro_bootpart}` from U-Boot's distro
bootflow scanner instead of hardcoded `mmc 0`.
- Drop the `console=` overrides — let the kernel resolve the console from
DTB `/chosen/stdout-path`, keeping `earlycon` for early output.
- Drop sunxi-specific `disp.screen0_output_mode` and legacy `.next`/
`script.bin` probe paths.
For the full TFTP+NFS netboot flow see the new `netboot` extension; this
template remains the hybrid path where the kernel + DTB live on a local
boot partition and only the rootfs is NFS-mounted.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…the tree The archive was created inside the if/else block at the top of create_image_from_sdcard_rootfs(), BEFORE update_initramfs and pre_umount_final_image hooks run. Any rootfs changes made by those hooks (initramfs regeneration, fstab fixups, extension tweaks) were missing from the archive. Move archive creation to after pre_umount_final_image so the tarball captures the fully finalized rootfs tree.
…TFS_COMPRESSION
Split the rootfs pipeline so it can produce either (or both) of:
* a tarball — via ROOTFS_COMPRESSION=gzip (default, .tar.gz) / zstd
(.tar.zst); matches existing behaviour, with zstd now available for
faster creation and decompression on modern hardware.
* an exported tree — via ROOTFS_EXPORT_DIR=<abs path>, which rsyncs
the built rootfs to a host-side directory (e.g. an NFS export)
without an intermediate archive.
Set ROOTFS_COMPRESSION=none when only the exported tree is wanted;
this skips archive creation entirely. If both ROOTFS_COMPRESSION=none
and -z ROOTFS_EXPORT_DIR are set the run is aborted — otherwise the
rootfs stage would produce nothing.
This is infrastructure; no existing callers need to change. The
netboot extension (added in a following commit) consumes
ROOTFS_EXPORT_DIR to populate the NFS export tree.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Hooks:
- extension_prepare_config: validate variables, compute defaults for
NETBOOT_TFTP_PREFIX / NETBOOT_NFS_PATH (shared by
LINUXFAMILY/BOARD/BRANCH/RELEASE, or per-host when NETBOOT_HOSTNAME
is set), normalize NETBOOT_CLIENT_MAC to PXELINUX 01-<mac> form,
fail fast on bad ROOTFS_COMPRESSION/ROOTFS_EXPORT_DIR combinations.
- custom_kernel_config: enable ROOT_NFS, NFS_FS, NFS_V3, IP_PNP,
IP_PNP_DHCP so root=/dev/nfs ip=dhcp works without an initrd.
- post_customize_image: drop armbian-resize-filesystem.service
(meaningless on NFS root) and /root/.not_logged_in_yet (the
armbian-firstlogin interactive wizard blocks bring-up when there is
no interactive console). armbian-firstrun.service stays — it only
regenerates SSH host keys.
- host_pre_docker_launch: append a bind-mount for ROOTFS_EXPORT_DIR to
DOCKER_EXTRA_ARGS when the directory lives outside ${SRC}, using the
hook's documented mechanism.
- pre_umount_final_image: assemble the TFTP tree (Image/zImage, dtb/,
uInitrd), write pxelinux.cfg/{default.example | 01-<mac>} with the
right FDT/FDTDIR line and explicit INITRD directive when uInitrd is
present, expose a netboot_artifacts_ready hook for userpatches.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New rootfs type for full network boot: the only thing on the device's local storage is U-Boot itself. Kernel, DTB, optional uInitrd and PXE config come from TFTP; rootfs is mounted over NFS. A new case branch in do_main_configuration auto-enables the netboot extension, symmetric with existing fs-f2fs-support / fs-btrfs wiring. The legacy ROOTFS_TYPE=nfs (hybrid: kernel on local storage, only / over NFS) is untouched — both paths coexist. - nfs-root case branch in ROOTFS_TYPE dispatch calls enable_extension "netboot" - prepare_partitions skips root partition creation and SD-size sanity check - check_filesystem_compatibility_on_host skipped for nfs-root - create_image_from_sdcard_rootfs early-returns for nfs-root after the pre_umount hook has grabbed TFTP artifacts from /boot: SDCARD.raw is dropped, the .img pipeline (mv to DESTIMG, write-to-SD, fingerprint, compress) is skipped. For nfs-root the only deliverables are the rootfs archive / ROOTFS_EXPORT_DIR tree and the TFTP staging dir — producing a boot-partition .img would be misleading (nothing on the device reads it). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents the netboot extension: artifact server setup (tftpd-hpa + nfs-kernel-server), TFTP tree layout, DHCP options 66/67 on the network DHCP server, userpatches.conf knobs, the netboot_artifacts_ready hook, a full end-to-end helios64 walkthrough, and a troubleshooting section. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address coderabbitai nitpicks (review 2026-04-22): - netboot.sh:72 error message advertised gzip|zstd|none while the case already accepts zst; realign the message with the actual accepted set. - README.md: netboot_artifacts_ready is dispatched from post_create_rootfs_archive__100_netboot_deploy, not from pre_umount_final_image__900_collect_netboot_artifacts. Assisted-by: Claude:claude-opus-4-7
Address coderabbitai round 2026-04-22 21:43: - netboot.sh: fail-fast when ROOTFS_EXPORT_DIR is relative; the Docker bind-mount at host_pre_docker_launch needs an absolute path and would otherwise error out long after config prep. - README.md /etc/exports: replace wildcard '*' exports with an example 192.168.1.0/24 subnet placeholder and note that no_root_squash means the export MUST be restricted to trusted hosts. Copying the previous example into production gave root-equivalent write access to anyone who could reach NFS. - README.md firstlogin: document the conditional suppression accurately. The extension removes /root/.not_logged_in_yet only when empty; a non-empty trigger file is kept so preset-firstrun can apply PRESET_* values non-interactively. Troubleshooting note updated to distinguish stale images from intentionally preserved preset files. Assisted-by: Claude:claude-opus-4-7
The earlier /etc/exports section was updated to use a subnet placeholder; the helios64 walk-through still copied wildcard '*' with no_root_squash. Align the two so readers cannot paste the unsafe form into production by following the step-by-step. Assisted-by: Claude:claude-opus-4-7
…ble ROOTFS_COMPRESSION
…ble ROOTFS_COMPRESSION
ee8e0e7 to
88c8e6f
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@extensions/netboot/netboot.sh`:
- Around line 188-200: The conditional and assignments that reference
BOOT_FDT_FILE must use a safe fallback expansion to avoid unbound-variable
errors under set -u; update all occurrences that read BOOT_FDT_FILE in this
block (the conditional, the declare fdt_file assignment, and the .dts check) to
use parameter expansion with a default (e.g. ${BOOT_FDT_FILE-}) so the test [[
-n ... ]] and subsequent logic can fall through to setting fdt_line="FDTDIR
${NETBOOT_TFTP_PREFIX}/dtb" when BOOT_FDT_FILE is unset.
In `@lib/functions/image/rootfs-to-image.sh`:
- Around line 147-162: Reject and normalize dangerous ROOTFS_EXPORT_DIR values
before creating or rsyncing: compute a canonical path (e.g. with realpath -m or
similar) and if the result is empty, "/" or otherwise equals the host root or SD
card root (compare against "$SDCARD" and "/"), abort with a clear error via
display_alert and non-zero exit; add this guard immediately before the
run_host_command_logged mkdir -pv "${ROOTFS_EXPORT_DIR}" and before the rsync
invocation so the run_host_command_logged mkdir and the rsync -aHWh --delete
--delete-excluded ... "$SDCARD/" "${ROOTFS_EXPORT_DIR}/" never run with a
dangerous target.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e9d5e2a3-2e6e-4178-94ba-bd86daa392f4
📒 Files selected for processing (6)
config/templates/nfs-boot.cmd.templateextensions/netboot/README.mdextensions/netboot/netboot.shlib/functions/configuration/main-config.shlib/functions/image/partitioning.shlib/functions/image/rootfs-to-image.sh
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
lib/functions/image/rootfs-to-image.sh (1)
147-170:⚠️ Potential issue | 🟠 MajorAlso reject export paths that resolve to the live rootfs.
The guard rejects
/, butROOTFS_EXPORT_DIRcan still resolve to${SDCARD},${MOUNT}, or a child of either. Withrsync --delete --delete-excluded, that can delete excluded paths from the rootfs being archived/exported or recurse into the export directory itself. This is the same dangerous-target class previously flagged, but the source/mount checks are still missing.Proposed guard extension
declare rootfs_export_dir_resolved rootfs_export_dir_resolved="$(realpath -m "${ROOTFS_EXPORT_DIR}")" - if [[ "${rootfs_export_dir_resolved}" == "/" ]]; then - exit_with_error "ROOTFS_EXPORT_DIR must not resolve to /" "${ROOTFS_EXPORT_DIR}" + declare rootfs_source_resolved rootfs_mount_resolved + rootfs_source_resolved="$(realpath -m "${SDCARD}")" + rootfs_mount_resolved="$(realpath -m "${MOUNT}")" + if [[ "${rootfs_export_dir_resolved}" == "/" || + "${rootfs_export_dir_resolved}" == "${rootfs_source_resolved}" || + "${rootfs_export_dir_resolved}" == "${rootfs_source_resolved}/"* || + "${rootfs_export_dir_resolved}" == "${rootfs_mount_resolved}" || + "${rootfs_export_dir_resolved}" == "${rootfs_mount_resolved}/"* ]]; then + exit_with_error "ROOTFS_EXPORT_DIR must not resolve to / or the live rootfs tree" "${ROOTFS_EXPORT_DIR}" fi🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/functions/image/rootfs-to-image.sh` around lines 147 - 170, The current guard only rejects "/" but must also reject export paths that resolve to the live rootfs/mounts; update the check after rootfs_export_dir_resolved="$(realpath -m "${ROOTFS_EXPORT_DIR}")" to compute resolved paths for "${SDCARD}" and "${MOUNT}" (e.g., sdcard_resolved and mount_resolved via realpath -m) and fail if rootfs_export_dir_resolved equals either of those or is a descendant (prefix) of either (use a trailing-slash prefix comparison to avoid partial matches); call exit_with_error with a clear message if the export target is the same as or inside SDCARD or MOUNT before running run_host_command_logged and rsync to prevent destructive --delete behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@extensions/netboot/netboot.sh`:
- Around line 193-199: The PXE stanza normalization sets fdt_file from
BOOT_FDT_FILE but doesn't verify the compiled ${fdt_file} actually exists under
${NETBOOT_TFTP_PREFIX}/dtb; add a validation step inside the existing if [[ -n
"${BOOT_FDT_FILE:-}" ... ]] block (where fdt_file is derived) to check for the
file's presence in the dtb tree and if missing emit a clear error via
echo/process logger and exit non‑zero so the build fails fast; reference
BOOT_FDT_FILE, fdt_file, NETBOOT_TFTP_PREFIX and the dtb/ destination when
implementing the existence check and failure path.
In `@extensions/netboot/README.md`:
- Line 75: The README currently documents NETBOOT_TFTP_OUT as pointing to
output/images but the implementation stages it at
${FINALDEST}/${version}-netboot-tftp; update the NETBOOT_TFTP_OUT description to
read "Absolute path of the staging directory
(${FINALDEST}/${version}-netboot-tftp)" and replace any other occurrences that
hard-code "output/images/<version>-netboot-tftp" (the other instances in the
same README) to use ${FINALDEST}/${version}-netboot-tftp so docs match the
actual behavior.
In `@lib/functions/image/rootfs-to-image.sh`:
- Around line 108-115: The comment and error message for accepted ROOTFS
compression values are inconsistent: the case handler accepts "zst" but the top
comment and the exit_with_error message only advertise "gzip|zstd|none"; update
the human-facing text to include "zst" everywhere. Edit the comment line that
lists accepted values and the exit_with_error call that mentions expected:
gzip|zstd|none to include zst (e.g., gzip|zstd|zst|none) so the
ROOTFS_COMPRESSION, rootfs_compression case and error paths are consistent.
---
Duplicate comments:
In `@lib/functions/image/rootfs-to-image.sh`:
- Around line 147-170: The current guard only rejects "/" but must also reject
export paths that resolve to the live rootfs/mounts; update the check after
rootfs_export_dir_resolved="$(realpath -m "${ROOTFS_EXPORT_DIR}")" to compute
resolved paths for "${SDCARD}" and "${MOUNT}" (e.g., sdcard_resolved and
mount_resolved via realpath -m) and fail if rootfs_export_dir_resolved equals
either of those or is a descendant (prefix) of either (use a trailing-slash
prefix comparison to avoid partial matches); call exit_with_error with a clear
message if the export target is the same as or inside SDCARD or MOUNT before
running run_host_command_logged and rsync to prevent destructive --delete
behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 486fa577-2c72-4fae-b71d-d9b68e2fb384
📒 Files selected for processing (6)
config/templates/nfs-boot.cmd.templateextensions/netboot/README.mdextensions/netboot/netboot.shlib/functions/configuration/main-config.shlib/functions/image/partitioning.shlib/functions/image/rootfs-to-image.sh
🚧 Files skipped from review as they are similar to previous changes (2)
- lib/functions/configuration/main-config.sh
- config/templates/nfs-boot.cmd.template
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
lib/functions/image/rootfs-to-image.sh (1)
147-170:⚠️ Potential issue | 🟠 MajorComplete the
ROOTFS_EXPORT_DIRsafety guard for source aliases.The guard rejects
/, butrsync --delete --delete-excluded "$SDCARD/" "$ROOTFS_EXPORT_DIR/"is still dangerous if the export path resolves to${SDCARD},${MOUNT}, or a child of either. In those cases the receiver is the staged rootfs itself or inside it, so excluded paths like/boot//homecan be deleted or recursively copied.🛡️ Proposed guard
declare rootfs_export_dir_resolved rootfs_export_dir_resolved="$(realpath -m "${ROOTFS_EXPORT_DIR}")" - if [[ "${rootfs_export_dir_resolved}" == "/" ]]; then - exit_with_error "ROOTFS_EXPORT_DIR must not resolve to /" "${ROOTFS_EXPORT_DIR}" + declare sdcard_resolved mount_resolved + sdcard_resolved="$(realpath -m "${SDCARD}")" + mount_resolved="$(realpath -m "${MOUNT}")" + if [[ "${rootfs_export_dir_resolved}" == "/" || + "${rootfs_export_dir_resolved}" == "${sdcard_resolved}" || + "${rootfs_export_dir_resolved}" == "${sdcard_resolved}/"* || + "${rootfs_export_dir_resolved}" == "${mount_resolved}" || + "${rootfs_export_dir_resolved}" == "${mount_resolved}/"* ]]; then + exit_with_error "ROOTFS_EXPORT_DIR must not resolve to /, SDCARD, MOUNT, or a child of them" "${ROOTFS_EXPORT_DIR}" fiRun this read-only check to confirm the current guard only rejects
/and has no source-alias checks:#!/bin/bash # Description: Inspect ROOTFS_EXPORT_DIR validation in rootfs-to-image.sh. # Expected before fix: only "/" is rejected. # Expected after fix: SDCARD/MOUNT equality and child-path checks are present. fd -i '^rootfs-to-image\.sh$' -x awk 'NR>=147 && NR<=170 { printf "%s:%d:%s\n", FILENAME, NR, $0 }' {}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/functions/image/rootfs-to-image.sh` around lines 147 - 170, The current safety guard only rejects "/" via rootfs_export_dir_resolved but misses cases where ROOTFS_EXPORT_DIR points at or inside the source (SDCARD or MOUNT), which makes the rsync (in the rsync ... "$SDCARD/" "${rootfs_export_dir_resolved}/") destructive; update the validation around rootfs_export_dir_resolved to also realpath -m SDCARD and MOUNT into variables (e.g. sdcard_resolved, mount_resolved) and then reject if rootfs_export_dir_resolved equals or is a descendant of either (string-equals or prefix-with-slash checks to avoid false matches), calling exit_with_error (same message style) when detected; keep using run_host_command_logged and the existing rsync logic otherwise.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@extensions/netboot/netboot.sh`:
- Around line 80-84: The path validation for ROOTFS_EXPORT_DIR is insufficient
and can allow dangerous values like "/" or the build checkout (SRC) which rsync
--delete could wipe; update the validation in the netboot export logic to reject
ROOTFS_EXPORT_DIR when it is "/" or when it resolves to the same path as the
source/checkout (SRC), and also reject empty when ROOTFS_COMPRESSION="none" as
before; use the existing exit_with_error call to fail with a clear message
referencing EXTENSION and ROOTFS_EXPORT_DIR when the value is "/" or equals SRC
(or resolves to it), and ensure you perform resolution/comparison (realpath or
equivalent) before comparing to avoid symlink tricks.
In `@extensions/netboot/README.md`:
- Line 65: The README's ROOTFS_COMPRESSION documentation and any workflow text
referencing accepted values should be updated to include the accepted alias
"zst" in addition to "zstd" (e.g., list as "gzip|zstd|zst|none"); specifically
update the variable table entry for ROOTFS_COMPRESSION and any mentions around
the create_image_from_sdcard_rootfs workflow so they advertise "zst" as a valid
value alongside "zstd".
- Line 80: The README incorrectly calls NETBOOT_CLIENT_MAC and NETBOOT_HOSTNAME
"sanitized" values; update the text to state that NETBOOT_CLIENT_MAC is the
original user-provided value and that any normalized/cleaned MAC is exposed via
NETBOOT_PXE_FILE (or another appropriate variable), and clarify NETBOOT_HOSTNAME
is the extension-used hostname rather than a sanitized hook value; edit the
table row describing `NETBOOT_HOSTNAME, NETBOOT_CLIENT_MAC` to explicitly
distinguish "user-provided/raw" vs "normalized/used by extension
(NETBOOT_PXE_FILE)" and adjust wording accordingly.
---
Duplicate comments:
In `@lib/functions/image/rootfs-to-image.sh`:
- Around line 147-170: The current safety guard only rejects "/" via
rootfs_export_dir_resolved but misses cases where ROOTFS_EXPORT_DIR points at or
inside the source (SDCARD or MOUNT), which makes the rsync (in the rsync ...
"$SDCARD/" "${rootfs_export_dir_resolved}/") destructive; update the validation
around rootfs_export_dir_resolved to also realpath -m SDCARD and MOUNT into
variables (e.g. sdcard_resolved, mount_resolved) and then reject if
rootfs_export_dir_resolved equals or is a descendant of either (string-equals or
prefix-with-slash checks to avoid false matches), calling exit_with_error (same
message style) when detected; keep using run_host_command_logged and the
existing rsync logic otherwise.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 49bee0c1-488d-4f80-8b38-daede5b3bc82
📒 Files selected for processing (6)
config/templates/nfs-boot.cmd.templateextensions/netboot/README.mdextensions/netboot/netboot.shlib/functions/configuration/main-config.shlib/functions/image/partitioning.shlib/functions/image/rootfs-to-image.sh
🚧 Files skipped from review as they are similar to previous changes (1)
- lib/functions/configuration/main-config.sh
| if [[ -n "${ROOTFS_EXPORT_DIR}" && "${ROOTFS_EXPORT_DIR}" != /* ]]; then | ||
| exit_with_error "${EXTENSION}: ROOTFS_EXPORT_DIR must be an absolute path (got '${ROOTFS_EXPORT_DIR}')" | ||
| fi | ||
| if [[ "${ROOTFS_COMPRESSION}" == "none" && -z "${ROOTFS_EXPORT_DIR}" ]]; then | ||
| exit_with_error "${EXTENSION}: ROOTFS_COMPRESSION=none requires ROOTFS_EXPORT_DIR (otherwise nothing is produced)" |
There was a problem hiding this comment.
Reject export targets that can be wiped by rsync --delete.
ROOTFS_EXPORT_DIR=/ or ROOTFS_EXPORT_DIR=${SRC} passes the current absolute-path validation; the latter is later translated to /armbian in Docker. Since the export flow uses delete-sync semantics, these values can erase the container root or the build checkout instead of only refreshing an NFS export tree.
🛡️ Proposed validation hardening
- if [[ -n "${ROOTFS_EXPORT_DIR}" && "${ROOTFS_EXPORT_DIR}" != /* ]]; then
- exit_with_error "${EXTENSION}: ROOTFS_EXPORT_DIR must be an absolute path (got '${ROOTFS_EXPORT_DIR}')"
+ if [[ -n "${ROOTFS_EXPORT_DIR}" ]]; then
+ if [[ "${ROOTFS_EXPORT_DIR}" != /* ]]; then
+ exit_with_error "${EXTENSION}: ROOTFS_EXPORT_DIR must be an absolute path (got '${ROOTFS_EXPORT_DIR}')"
+ fi
+ if [[ "${ROOTFS_EXPORT_DIR}" == "/" || "${ROOTFS_EXPORT_DIR%/}" == "${SRC%/}" ]]; then
+ exit_with_error "${EXTENSION}: ROOTFS_EXPORT_DIR must point to a dedicated export directory" "${ROOTFS_EXPORT_DIR}"
+ fi
fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if [[ -n "${ROOTFS_EXPORT_DIR}" && "${ROOTFS_EXPORT_DIR}" != /* ]]; then | |
| exit_with_error "${EXTENSION}: ROOTFS_EXPORT_DIR must be an absolute path (got '${ROOTFS_EXPORT_DIR}')" | |
| fi | |
| if [[ "${ROOTFS_COMPRESSION}" == "none" && -z "${ROOTFS_EXPORT_DIR}" ]]; then | |
| exit_with_error "${EXTENSION}: ROOTFS_COMPRESSION=none requires ROOTFS_EXPORT_DIR (otherwise nothing is produced)" | |
| if [[ -n "${ROOTFS_EXPORT_DIR}" ]]; then | |
| if [[ "${ROOTFS_EXPORT_DIR}" != /* ]]; then | |
| exit_with_error "${EXTENSION}: ROOTFS_EXPORT_DIR must be an absolute path (got '${ROOTFS_EXPORT_DIR}')" | |
| fi | |
| if [[ "${ROOTFS_EXPORT_DIR}" == "/" || "${ROOTFS_EXPORT_DIR%/}" == "${SRC%/}" ]]; then | |
| exit_with_error "${EXTENSION}: ROOTFS_EXPORT_DIR must point to a dedicated export directory" "${ROOTFS_EXPORT_DIR}" | |
| fi | |
| fi | |
| if [[ "${ROOTFS_COMPRESSION}" == "none" && -z "${ROOTFS_EXPORT_DIR}" ]]; then | |
| exit_with_error "${EXTENSION}: ROOTFS_COMPRESSION=none requires ROOTFS_EXPORT_DIR (otherwise nothing is produced)" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@extensions/netboot/netboot.sh` around lines 80 - 84, The path validation for
ROOTFS_EXPORT_DIR is insufficient and can allow dangerous values like "/" or the
build checkout (SRC) which rsync --delete could wipe; update the validation in
the netboot export logic to reject ROOTFS_EXPORT_DIR when it is "/" or when it
resolves to the same path as the source/checkout (SRC), and also reject empty
when ROOTFS_COMPRESSION="none" as before; use the existing exit_with_error call
to fail with a clear message referencing EXTENSION and ROOTFS_EXPORT_DIR when
the value is "/" or equals SRC (or resolves to it), and ensure you perform
resolution/comparison (realpath or equivalent) before comparing to avoid symlink
tricks.
| | `NETBOOT_NFS_PATH` | see below | Absolute NFS path of the rootfs on the server. The APPEND line uses exactly this string for `nfsroot=...`. | | ||
| | `NETBOOT_HOSTNAME` | _(empty)_ | Per-host deployment. When set, the default `NETBOOT_NFS_PATH` becomes `/srv/netboot/rootfs/hosts/<hostname>` — each machine owns its own writable rootfs copy. When empty, the default is `/srv/netboot/rootfs/shared/${LINUXFAMILY}/${BOARD}/${BRANCH}-${RELEASE}` (one image, potentially reused by identical boards). | | ||
| | `NETBOOT_CLIENT_MAC` | _(empty)_ | Client MAC (`aa:bb:cc:dd:ee:ff` or `aa-bb-cc-dd-ee-ff`). When set, the PXE config is written as `pxelinux.cfg/01-<mac>` (the PXELINUX per-MAC override) instead of `default.example`; multiple boards then coexist on one TFTP root, each picking its own extlinux. | | ||
| | `ROOTFS_COMPRESSION` | `zstd` | Format of the rootfs archive produced by `create_image_from_sdcard_rootfs`. `zstd` → `.tar.zst`, `gzip` → `.tar.gz`, `none` → no archive at all. The `none` case requires `ROOTFS_EXPORT_DIR`. | |
There was a problem hiding this comment.
Include the accepted zst alias in the README.
The implementation accepts ROOTFS_COMPRESSION=zst, but the variable table and workflow text only advertise gzip|zstd|none.
📝 Proposed documentation fix
-| `ROOTFS_COMPRESSION` | `zstd` | Format of the rootfs archive produced by `create_image_from_sdcard_rootfs`. `zstd` → `.tar.zst`, `gzip` → `.tar.gz`, `none` → no archive at all. The `none` case requires `ROOTFS_EXPORT_DIR`. |
+| `ROOTFS_COMPRESSION` | `zstd` | Format of the rootfs archive produced by `create_image_from_sdcard_rootfs`. `zstd`/`zst` → `.tar.zst`, `gzip` → `.tar.gz`, `none` → no archive at all. The `none` case requires `ROOTFS_EXPORT_DIR`. |
@@
- Use `ROOTFS_COMPRESSION=gzip|zstd` and rsync/ssh the archive via a
+ Use `ROOTFS_COMPRESSION=gzip|zstd|zst` and rsync/ssh the archive via aAlso applies to: 315-317
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@extensions/netboot/README.md` at line 65, The README's ROOTFS_COMPRESSION
documentation and any workflow text referencing accepted values should be
updated to include the accepted alias "zst" in addition to "zstd" (e.g., list as
"gzip|zstd|zst|none"); specifically update the variable table entry for
ROOTFS_COMPRESSION and any mentions around the create_image_from_sdcard_rootfs
workflow so they advertise "zst" as a valid value alongside "zstd".
| | `NETBOOT_NFS_PATH` | As above. | | ||
| | `NETBOOT_PXE_FILE` | `default.example` or `01-<mac>` — the file that was written under `pxelinux.cfg/`. | | ||
| | `NETBOOT_ROOTFS_ARCHIVE` | Full path to the produced rootfs archive (empty when `ROOTFS_COMPRESSION=none`). | | ||
| | `NETBOOT_HOSTNAME`, `NETBOOT_CLIENT_MAC` | The sanitized values the extension used. | |
There was a problem hiding this comment.
Do not describe raw hook values as sanitized.
NETBOOT_CLIENT_MAC remains the user-provided value; the normalized value is reflected in NETBOOT_PXE_FILE. This wording can mislead hook authors.
📝 Proposed documentation fix
-| `NETBOOT_HOSTNAME`, `NETBOOT_CLIENT_MAC` | The sanitized values the extension used. |
+| `NETBOOT_HOSTNAME`, `NETBOOT_CLIENT_MAC` | The hostname and original MAC value supplied to the extension. Use `NETBOOT_PXE_FILE` for the normalized per-MAC PXE filename. |🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@extensions/netboot/README.md` at line 80, The README incorrectly calls
NETBOOT_CLIENT_MAC and NETBOOT_HOSTNAME "sanitized" values; update the text to
state that NETBOOT_CLIENT_MAC is the original user-provided value and that any
normalized/cleaned MAC is exposed via NETBOOT_PXE_FILE (or another appropriate
variable), and clarify NETBOOT_HOSTNAME is the extension-used hostname rather
than a sanitized hook value; edit the table row describing `NETBOOT_HOSTNAME,
NETBOOT_CLIENT_MAC` to explicitly distinguish "user-provided/raw" vs
"normalized/used by extension (NETBOOT_PXE_FILE)" and adjust wording
accordingly.
…ble ROOTFS_COMPRESSION
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (3)
lib/functions/image/rootfs-to-image.sh (1)
147-162:⚠️ Potential issue | 🟠 MajorValidate
ROOTFS_EXPORT_DIRbefore running destructive rsync.Line 154 runs
rsync --delete --delete-excludedagainst a raw user path. IfROOTFS_EXPORT_DIRresolves to/,${SDCARD},${MOUNT}, or a child of the source tree, this can delete host/container files or the build rootfs itself. Keep the guard at the destructive operation site so it covers bothROOTFS_TYPE=nfsandROOTFS_TYPE=nfs-root.🛡️ Proposed guard
if [[ -n "${ROOTFS_EXPORT_DIR}" ]]; then - display_alert "Exporting rootfs tree" "${ROOTFS_EXPORT_DIR}" "info" - run_host_command_logged mkdir -pv "${ROOTFS_EXPORT_DIR}" + declare rootfs_export_dir_resolved sdcard_resolved mount_resolved + rootfs_export_dir_resolved="$(realpath -m "${ROOTFS_EXPORT_DIR}")" + sdcard_resolved="$(realpath -m "${SDCARD}")" + mount_resolved="$(realpath -m "${MOUNT}")" + + case "${rootfs_export_dir_resolved}" in + / | "${sdcard_resolved}" | "${sdcard_resolved}/"* | "${mount_resolved}" | "${mount_resolved}/"*) + exit_with_error "Refusing unsafe ROOTFS_EXPORT_DIR" "${ROOTFS_EXPORT_DIR} resolves to ${rootfs_export_dir_resolved}" + ;; + esac + + display_alert "Exporting rootfs tree" "${rootfs_export_dir_resolved}" "info" + run_host_command_logged mkdir -pv "${rootfs_export_dir_resolved}" # --delete so files removed from the source rootfs don't survive in a # reused export tree (otherwise the NFS root silently drifts from the image). # --delete-excluded additionally purges receiver-side files that match our # excludes (e.g. stale /home/* left over from a prior INCLUDE_HOME_DIR=yes build). @@ --exclude="/tmp/*" \ --exclude="/sys/*" \ $exclude_home \ - --info=progress0,stats1 "$SDCARD/" "${ROOTFS_EXPORT_DIR}/" + --info=progress0,stats1 "$SDCARD/" "${rootfs_export_dir_resolved}/" fi🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/functions/image/rootfs-to-image.sh` around lines 147 - 162, Add a safety check immediately before the destructive rsync invocation that verifies ROOTFS_EXPORT_DIR is a sane, non-root, non-source path: ensure ROOTFS_EXPORT_DIR is non-empty, absolute, not "/" and not equal to or a parent/child of the source variables (SDCARD or MOUNT/build rootfs), and fail early with a clear error if it does; implement this guard in the same scope around the rsync call (the block that calls run_host_command_logged rsync) so it covers both ROOTFS_TYPE=nfs and nfs-root scenarios and prevents accidental deletes of host/container files.extensions/netboot/README.md (2)
80-80:⚠️ Potential issue | 🟡 MinorDo not call raw hook values sanitized.
NETBOOT_CLIENT_MACremains the user-supplied value; the normalized MAC is represented byNETBOOT_PXE_FILEwhen a per-MAC PXE filename is generated.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@extensions/netboot/README.md` at line 80, Update the README description to stop implying hook outputs are sanitized: clarify that NETBOOT_CLIENT_MAC is the raw user-supplied MAC and not normalized, and that any normalized/per-MAC PXE filename is provided via NETBOOT_PXE_FILE; adjust the table row mentioning `NETBOOT_HOSTNAME`, `NETBOOT_CLIENT_MAC` to explicitly state that `NETBOOT_CLIENT_MAC` is the original user value and `NETBOOT_PXE_FILE` contains the normalized/derived PXE filename when applicable.
65-65:⚠️ Potential issue | 🟡 MinorInclude the accepted
zstalias in the README.The implementation accepts
ROOTFS_COMPRESSION=zst, but the variable table and workflow text still advertise onlygzip|zstd|none.Also applies to: 315-317
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@extensions/netboot/README.md` at line 65, Update the README entries that document the ROOTFS_COMPRESSION variable and the workflow text around create_image_from_sdcard_rootfs to include the accepted alias "zst" alongside "zstd", i.e. advertise accepted values as gzip|zstd|zst|none; ensure both the variable table cell and any prose that lists allowed values (including the paragraphs covering create_image_from_sdcard_rootfs) mention "zst" so the docs match the implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@extensions/netboot/netboot.sh`:
- Line 62: Reject any path-traversal in NETBOOT_TFTP_PREFIX before using it to
build tftp_prefix_dir and before calling mkdir/cp: validate NETBOOT_TFTP_PREFIX
(and any places where it’s re-read at the later block around the logic currently
at lines 171-174) to ensure it is not empty, does not contain “..” segments or
leading “/”, and only contains safe characters (letters, digits, -, _, /). If
needed, normalize it to remove redundant slashes and resolve/deny any ..
segments (or refuse the value) and then construct tftp_prefix_dir by joining the
validated prefix to the known netboot staging root so mkdir/cp cannot write
outside the staging area; also mirror this validation when composing PXE paths
to avoid generating unusable paths.
- Around line 104-113: The normalize function rewrites container-side bind-mount
targets like /armbian/netboot-export into ${SRC}/output/netboot-export/...
causing rsync to miss the mounted path; update _netboot_normalize_export_dir to
treat the Docker bind-mount target as already normalized by returning early when
ROOTFS_EXPORT_DIR is exactly /armbian/netboot-export or starts with
/armbian/netboot-export/ (in addition to the existing check for
${SRC}/output/netboot-export/), and apply the same guard in the analogous logic
around the other occurrence (lines ~153-164) so in-container config prep
preserves the bind-mount target instead of prefixing
${SRC}/output/netboot-export/.
In `@extensions/netboot/README.md`:
- Line 66: Update the ROOTFS_EXPORT_DIR documentation to reflect the confinement
behavior: explain that any path is rewritten into ${SRC}/output/netboot-export/,
so setting ROOTFS_EXPORT_DIR=/srv/netboot/rootfs/... will not populate
/srv/netboot/rootfs/... directly; instead document two supported workflows—(a)
specify a relative subpath inside ROOTFS_EXPORT_DIR (e.g.
ROOTFS_EXPORT_DIR=relative/path) and then create or document a host-side symlink
or NFS export that maps ${SRC}/output/netboot-export/relative/path →
/srv/netboot/rootfs, or (b) keep ROOTFS_EXPORT_DIR as an absolute host path but
explicitly state that the NFS server must export the confined directory
${SRC}/output/netboot-export/... (i.e., export the rewritten path). Update the
paragraph describing ROOTFS_EXPORT_DIR and the corresponding guidance around
lines 274-299 to include this behavior and example commands or symlink/export
notes.
---
Duplicate comments:
In `@extensions/netboot/README.md`:
- Line 80: Update the README description to stop implying hook outputs are
sanitized: clarify that NETBOOT_CLIENT_MAC is the raw user-supplied MAC and not
normalized, and that any normalized/per-MAC PXE filename is provided via
NETBOOT_PXE_FILE; adjust the table row mentioning `NETBOOT_HOSTNAME`,
`NETBOOT_CLIENT_MAC` to explicitly state that `NETBOOT_CLIENT_MAC` is the
original user value and `NETBOOT_PXE_FILE` contains the normalized/derived PXE
filename when applicable.
- Line 65: Update the README entries that document the ROOTFS_COMPRESSION
variable and the workflow text around create_image_from_sdcard_rootfs to include
the accepted alias "zst" alongside "zstd", i.e. advertise accepted values as
gzip|zstd|zst|none; ensure both the variable table cell and any prose that lists
allowed values (including the paragraphs covering
create_image_from_sdcard_rootfs) mention "zst" so the docs match the
implementation.
In `@lib/functions/image/rootfs-to-image.sh`:
- Around line 147-162: Add a safety check immediately before the destructive
rsync invocation that verifies ROOTFS_EXPORT_DIR is a sane, non-root, non-source
path: ensure ROOTFS_EXPORT_DIR is non-empty, absolute, not "/" and not equal to
or a parent/child of the source variables (SDCARD or MOUNT/build rootfs), and
fail early with a clear error if it does; implement this guard in the same scope
around the rsync call (the block that calls run_host_command_logged rsync) so it
covers both ROOTFS_TYPE=nfs and nfs-root scenarios and prevents accidental
deletes of host/container files.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4833b431-e485-46e9-ad62-6393c9235918
📒 Files selected for processing (6)
config/templates/nfs-boot.cmd.templateextensions/netboot/README.mdextensions/netboot/netboot.shlib/functions/configuration/main-config.shlib/functions/image/partitioning.shlib/functions/image/rootfs-to-image.sh
| # Declared unconditionally so later `[[ -n "${NETBOOT_CLIENT_MAC_NORMALIZED}" ]]` | ||
| # checks remain safe under `set -u` when no MAC is configured. | ||
| declare -g NETBOOT_CLIENT_MAC_NORMALIZED="" | ||
| declare -g NETBOOT_TFTP_PREFIX="${NETBOOT_TFTP_PREFIX:-armbian/${LINUXFAMILY}/${BOARD}/${BRANCH}-${RELEASE}}" |
There was a problem hiding this comment.
Reject traversal in NETBOOT_TFTP_PREFIX before staging files.
NETBOOT_TFTP_PREFIX flows into tftp_prefix_dir; values like ../../foo can make mkdir/cp write outside the netboot staging directory and also generate unusable PXE paths.
🛡️ Proposed validation
declare -g NETBOOT_TFTP_PREFIX="${NETBOOT_TFTP_PREFIX:-armbian/${LINUXFAMILY}/${BOARD}/${BRANCH}-${RELEASE}}"
+ case "${NETBOOT_TFTP_PREFIX}" in
+ "" | /* | . | .. | ../* | */../* | */..)
+ exit_with_error "${EXTENSION}: NETBOOT_TFTP_PREFIX must be a relative path inside the TFTP root" "${NETBOOT_TFTP_PREFIX}"
+ ;;
+ esac
if [[ -n "${NETBOOT_HOSTNAME}" ]]; thenAlso applies to: 171-174
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@extensions/netboot/netboot.sh` at line 62, Reject any path-traversal in
NETBOOT_TFTP_PREFIX before using it to build tftp_prefix_dir and before calling
mkdir/cp: validate NETBOOT_TFTP_PREFIX (and any places where it’s re-read at the
later block around the logic currently at lines 171-174) to ensure it is not
empty, does not contain “..” segments or leading “/”, and only contains safe
characters (letters, digits, -, _, /). If needed, normalize it to remove
redundant slashes and resolve/deny any .. segments (or refuse the value) and
then construct tftp_prefix_dir by joining the validated prefix to the known
netboot staging root so mkdir/cp cannot write outside the staging area; also
mirror this validation when composing PXE paths to avoid generating unusable
paths.
| function _netboot_normalize_export_dir() { | ||
| [[ -z "${ROOTFS_EXPORT_DIR}" ]] && return 0 | ||
| # Already normalized — extension_prepare_config may fire after host_pre_docker_launch | ||
| # has already run on the host side. | ||
| [[ "${ROOTFS_EXPORT_DIR}" == "${SRC}/output/netboot-export/"* ]] && return 0 | ||
| case "${ROOTFS_EXPORT_DIR}" in | ||
| *..*) exit_with_error "${EXTENSION}: ROOTFS_EXPORT_DIR must not contain '..'" "${ROOTFS_EXPORT_DIR}" ;; | ||
| esac | ||
| declare -g ROOTFS_EXPORT_DIR="${SRC}/output/netboot-export/${ROOTFS_EXPORT_DIR}" | ||
| } |
There was a problem hiding this comment.
Preserve the Docker bind-mount target during in-container config prep.
host_pre_docker_launch__netboot_mount_export_dir exports ROOTFS_EXPORT_DIR=/armbian/netboot-export, but _netboot_normalize_export_dir does not treat that as already normalized. When extension_prepare_config runs in the container, it rewrites the value to ${SRC}/output/netboot-export//armbian/netboot-export, so rsync no longer writes to the bind mount.
🐛 Proposed fix
function _netboot_normalize_export_dir() {
[[ -z "${ROOTFS_EXPORT_DIR}" ]] && return 0
+ declare container_export_dir="${NETBOOT_CONTAINER_EXPORT_DIR:-/armbian/netboot-export}"
+ [[ "${ROOTFS_EXPORT_DIR}" == "${container_export_dir}" || "${ROOTFS_EXPORT_DIR}" == "${container_export_dir}/"* ]] && return 0
# Already normalized — extension_prepare_config may fire after host_pre_docker_launch
# has already run on the host side.
[[ "${ROOTFS_EXPORT_DIR}" == "${SRC}/output/netboot-export/"* ]] && return 0
@@
# Host-phase normalization: extension_prepare_config runs inside the container,
# which is too late — docker needs an absolute source path for the bind-mount below.
_netboot_normalize_export_dir
- declare container_export_dir="/armbian/netboot-export"
+ declare container_export_dir="${NETBOOT_CONTAINER_EXPORT_DIR:-/armbian/netboot-export}"Also applies to: 153-164
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@extensions/netboot/netboot.sh` around lines 104 - 113, The normalize function
rewrites container-side bind-mount targets like /armbian/netboot-export into
${SRC}/output/netboot-export/... causing rsync to miss the mounted path; update
_netboot_normalize_export_dir to treat the Docker bind-mount target as already
normalized by returning early when ROOTFS_EXPORT_DIR is exactly
/armbian/netboot-export or starts with /armbian/netboot-export/ (in addition to
the existing check for ${SRC}/output/netboot-export/), and apply the same guard
in the analogous logic around the other occurrence (lines ~153-164) so
in-container config prep preserves the bind-mount target instead of prefixing
${SRC}/output/netboot-export/.
| | `NETBOOT_HOSTNAME` | _(empty)_ | Per-host deployment. When set, the default `NETBOOT_NFS_PATH` becomes `/srv/netboot/rootfs/hosts/<hostname>` — each machine owns its own writable rootfs copy. When empty, the default is `/srv/netboot/rootfs/shared/${LINUXFAMILY}/${BOARD}/${BRANCH}-${RELEASE}` (one image, potentially reused by identical boards). | | ||
| | `NETBOOT_CLIENT_MAC` | _(empty)_ | Client MAC (`aa:bb:cc:dd:ee:ff` or `aa-bb-cc-dd-ee-ff`). When set, the PXE config is written as `pxelinux.cfg/01-<mac>` (the PXELINUX per-MAC override) instead of `default.example`; multiple boards then coexist on one TFTP root, each picking its own extlinux. | | ||
| | `ROOTFS_COMPRESSION` | `zstd` | Format of the rootfs archive produced by `create_image_from_sdcard_rootfs`. `zstd` → `.tar.zst`, `gzip` → `.tar.gz`, `none` → no archive at all. The `none` case requires `ROOTFS_EXPORT_DIR`. | | ||
| | `ROOTFS_EXPORT_DIR` | _(empty)_ | Absolute path. When set, the rootfs tree is rsync'd directly into this directory in addition to (or instead of) the archive. Primary use: builder host is also the NFS server — single-step `build → boot` loop with no tar/unpack/rsync step. If this path is outside `${SRC}`, the extension bind-mounts it into the build container so the same path works on host and inside Docker. | |
There was a problem hiding this comment.
Align ROOTFS_EXPORT_DIR docs with the confinement behavior.
The script now rewrites any export path under ${SRC}/output/netboot-export/, so ROOTFS_EXPORT_DIR=/srv/netboot/rootfs/... does not directly populate /srv/netboot/rootfs/.... Update this workflow to either use a relative subpath plus a documented symlink/export-root setup, or state that the NFS server should export the confined output directory.
Also applies to: 274-299
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@extensions/netboot/README.md` at line 66, Update the ROOTFS_EXPORT_DIR
documentation to reflect the confinement behavior: explain that any path is
rewritten into ${SRC}/output/netboot-export/, so setting
ROOTFS_EXPORT_DIR=/srv/netboot/rootfs/... will not populate
/srv/netboot/rootfs/... directly; instead document two supported workflows—(a)
specify a relative subpath inside ROOTFS_EXPORT_DIR (e.g.
ROOTFS_EXPORT_DIR=relative/path) and then create or document a host-side symlink
or NFS export that maps ${SRC}/output/netboot-export/relative/path →
/srv/netboot/rootfs, or (b) keep ROOTFS_EXPORT_DIR as an absolute host path but
explicitly state that the NFS server must export the confined directory
${SRC}/output/netboot-export/... (i.e., export the rewritten path). Update the
paragraph describing ROOTFS_EXPORT_DIR and the corresponding guidance around
lines 274-299 to include this behavior and example commands or symlink/export
notes.
Summary
Adds an opt-in
netbootextension that produces a full network-boot payload — kernel, DTB, optionaluInitrd,pxelinux.cfgand an NFS-exportable rootfs — from a regular Armbian build. For netboot the only thing that has to live on the device's local storage is U-Boot itself: the kernel and DTB come from TFTP,/is mounted over NFS, nothing else on mmc/eMMC/USB is touched during early boot.Today
ROOTFS_TYPE=nfsalone only gives a hybrid image (kernel+DTB still on SD/eMMC, only/over NFS). This PR keeps that path working and layers a real netboot flow on top via a newROOTFS_TYPE=nfs-root.What changes
lib/functions/configuration/main-config.sh— newROOTFS_TYPE=nfs-rootcase branch, symmetric with the existingfs-f2fs-support/fs-btrfswiring: the branch auto-enables thenetbootextension soROOTFS_TYPE=nfs-rootis the single switch that selects the full netboot flow.check_filesystem_compatibility_on_hostis skipped for bothnfsandnfs-root— the host-side check is a sanity net for block-device targets and falsely rejects valid NFS configurations.extensions/netboot/netboot.sh— new, directory-based extension. Directory layout (rather than a singleextensions/netboot.shfile) exists so the long-form usage guide can live next to the code asREADME.md; inlining documentation of that size into the script body would be unwieldy. Hooks:extension_prepare_config— validate variables, compute defaults forNETBOOT_TFTP_PREFIX/NETBOOT_NFS_PATH(shared byLINUXFAMILY/BOARD/BRANCH/RELEASE, or per-host whenNETBOOT_HOSTNAMEis set), normalizeNETBOOT_CLIENT_MACto the01-<mac>PXELINUX form, fail fast on badROOTFS_COMPRESSION/ROOTFS_EXPORT_DIRcombinations before debootstrap.custom_kernel_config— enableROOT_NFS,NFS_FS,NFS_V3,IP_PNP,IP_PNP_DHCPsoroot=/dev/nfs ip=dhcpworks without an initrd.post_customize_image— droparmbian-resize-filesystem.service(meaningless on NFS root) and/root/.not_logged_in_yet(thearmbian-firstlogininteractive wizard blocks bring-up when there is no interactive console).armbian-firstrun.servicestays — it only regenerates SSH host keys.host_pre_docker_launch— bind-mountROOTFS_EXPORT_DIRinto the build container when it lives outside${SRC}, so the single-step builder-as-NFS-server workflow works the same inside and outside Docker.pre_umount_final_image— assemble the TFTP tree (Image/zImage,dtb/,uInitrd), writepxelinux.cfg/{default.example | 01-<mac>}with the rightFDT/FDTDIRline and an explicitINITRDdirective whenuInitrdis present (U-Boot's PXE parser only loads an initramfs when the stanza names it), expose anetboot_artifacts_readyhook for userpatches that deploy to a real server.lib/functions/image/rootfs-to-image.sh— two new controls for NFS-rootfs builds (bothROOTFS_TYPE=nfsandnfs-root):ROOTFS_COMPRESSION=zstd|gzip|none(defaultzstd).zstd→.tar.zst,gzip→.tar.gz,none→ no archive at all. Rejected early ifnoneis set withoutROOTFS_EXPORT_DIR. Thetar | pv | compressorpipeline now runs in aset -o pipefailsubshell so a broken archive step actually fails the build instead of producing a silently truncated file.ROOTFS_EXPORT_DIR=/abs/path— also (or only) rsync the rootfs tree into this directory. Can point anywhere on the build host's filesystem — inside or outside the Armbian tree (the extension bind-mounts external paths into the Docker container automatically). If the path is an NFS mount or any other network filesystem, the build writes directly to the remote server with no intermediate archive. The rsync uses--deleteso a re-used export dir stays in sync with the new build instead of accumulating stale files from previous runs. Combined withROOTFS_COMPRESSION=nonethis turns deploy-to-NFS-server into a single build step.ROOTFS_ARCHIVE_PATHis exported so downstream hooks (the newnetboot_artifacts_ready) can reference the produced archive.lib/functions/image/partitioning.sh—prepare_partitionsskips the root partition entry fornfs-rootthe same way it does fornfs, and the SD-size sanity check no longer applies to either (there is no image file to size).config/templates/nfs-boot.cmd.template— previously sunxi/arm32-only (zImage+bootz). Made arch-agnostic so the hybridROOTFS_TYPE=nfspath builds for arm64 boards too.extensions/netboot/README.md— long-form guide: variable reference, server setup (tftpd-hpa+nfs-kernel-server), DHCP 66/67 config (OpenWRT + others), per-host / per-MAC deployments, firstrun vs firstlogin, end-to-end helios64 example, troubleshooting.Variables (quick reference)
All optional.
ROOTFS_TYPE=nfs-rootalone gives you a shared-rootfs build with apxelinux.cfg/default.examplefile.NETBOOT_SERVERnfsroot=keeps${serverip}literal for U-Boot to fill from DHCP siaddr.NETBOOT_TFTP_PREFIXarmbian/${LINUXFAMILY}/${BOARD}/${BRANCH}-${RELEASE}NETBOOT_NFS_PATH/srv/netboot/rootfs/shared/...or/srv/netboot/rootfs/hosts/<hostname>nfsroot=.NETBOOT_HOSTNAMEhosts/<hostname>/— each machine owns its own writable copy.NETBOOT_CLIENT_MAC01-<mac>(per-MAC PXELINUX override). Accepts:or-, normalized to lowercase.ROOTFS_COMPRESSIONzstdzstd/gzip/none.ROOTFS_EXPORT_DIRTest plan
Validated end-to-end on helios64 (
rockchip64,edge/resolute,ttyS2 @ 1500000), Armbian 26.05:ROOTFS_TYPE=nfs-root NETBOOT_SERVER=192.168.1.125, shared NFS path, FDTDIR fallback for boards withoutBOOT_FDT_FILE, no hardcodedconsole=). 6:57 min, all artifacts correct.NETBOOT_HOSTNAME=helios64-a NETBOOT_CLIENT_MAC=aa:bb:cc:dd:ee:ff).pxelinux.cfg/01-aa-bb-cc-dd-ee-ffcontainsnfsroot=.../hosts/helios64-a. 7:44 min.ROOTFS_COMPRESSION=gzip— produces.tar.gz(defaultzstd→.tar.zst). On the helios64 rootfs: 503 MB gzip vs 460 MB zstd.ROOTFS_COMPRESSION=none ROOTFS_EXPORT_DIR=...— single-step tree workflow, 1.5 GB tree in export dir, no archive produced. 5:17 min.ROOTFS_COMPRESSION=nonewithoutROOTFS_EXPORT_DIR— fails fast inextension_prepare_configbefore debootstrap, not hours later.tftpd-hpa+ NFS rootfs via rsync tonfs-kernel-server. DHCP options 66/67 on OpenWRT. Helios64 cold boot → U-Bootbootflow scan -lb→ TFTP →booti→ kernel →ip_auto_config→ NFS mount → systemdgraphical.target→ login prompt onttyS2@1500000. Noarmbian-firstloginwizard, noresize2fserrors,armbian-firstrun.serviceregenerates SSH host keys normally. Verified twice: once with archive workflow + manual unpack, once with tree workflow + rsync deploy.mvebu,armhf, U-Boot v2025.10) — second SoC family. PXE netboot: DHCP → TFTP (zImage + DTB) →bootz→ kernel → NFS root → systemd → login prompt. No initrd needed (see notes below).BOOT_FDT_FILEset (the FDTDIR vs FDT code path is exercised by helios64's missingBOOT_FDT_FILE, but a board with it set should also work).Known issues found during helios4 testing (not blockers for this PR):
ramdisk_addr_r(0x2880000) is only 8 MB abovekernel_addr_r(0x2080000), but zImage 6.12 is 8.1 MB → U-Boot refuses to boot with initrd. Workaround: omitINITRDfrom PXE config — kernel withCONFIG_ROOT_NFS=ymounts NFS root directly without initrd.ip=dhcpwrites DNS to/proc/net/pnpbutsystemd-resolveddoesn't read it → "No DNS servers known". Workaround:resolvectl dns <iface> <gateway>. Needs a networkd.networkfile or oneshot service inpost_customize_image(future improvement).Post-review update: architecture reworked after CodeRabbit #1 — the extension no longer forces
ROOTFS_TYPEfrom insideextension_prepare_config(too late in the config lifecycle); instead a newROOTFS_TYPE=nfs-rootcase branch inmain-config.shenables the extension, symmetric with existing filesystem-support extensions.pipefailon the archive pipeline (#4),rsync --deleteon the export dir (#5), and an explicitINITRDdirective whenuInitrdis present (#2) are all in.Post-review update (round 3): archive creation moved from the early
if/elseblock to afterpre_umount_final_imagehooks so the tarball/export tree captures the fully finalized rootfs (includesupdate_initramfsoutput and all hook edits). Split out as a separate pre-feature commit — also fixes the same bug in the originalROOTFS_TYPE=nfshybrid flow. Additionally: the netboot extension now setsBOOTSIZE=0fornfs-root, preventing the phantom/bootfstab entry at its source; the compensatingpre_umount_final_image__100_netboot_clean_fstabhook is gone.Post-review update (round 4):
config/templates/nfs-boot.cmd.templatenow wraps the DTB load in anif load ... ; then true; else echo FATAL; reset; fiblock so a missing/wrongdtb/${fdtfile}aborts the boot with a clear message instead of continuing with an invalid${fdt_addr_r}— consistent with the ramdisk and kernel handling on the following lines.Related work
A companion PR to armbian/documentation will add
Developer-Guide_Netboot.mdwith the short overview + variable reference; this extension'sREADME.mdis the long-form guide and is linked from that page.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes / Reliability
Documentation