Skip to main content

TECH VEDA

Linux kernel & Device drivers starts on 31st Oct 2026 enrollingCorporate on-site training - Submit proposal Pick your modulesSharpen your kernel skills: deep dives, drivers, Yocto, CVEs, careers — updated daily. Read the blog →
Insights

Bootconfig: Boot Parameters Your Kernel Command Line Cannot Hold

The kernel command line is a fixed-size buffer that truncates without a warning. Bootconfig has carried boot parameters since Linux 5.6.

Bootconfig: Boot Parameters Your Kernel Command Line Cannot Hold

The Linux kernel command line is a fixed-size buffer: 2048 bytes on arm64 and RISC-V, 1024 on 32-bit Arm. When a device tree bootargs string is longer, the kernel copies what fits and prints no warning. Bootconfig, in every supported kernel since 5.6, moves boot parameters into a structured file attached to the initrd or built into the image. One limit decides how much it helps: parameters registered with early_param(), such as earlycon= and mem=, are read before that file is parsed, so they have no effect when set there. Linux 7.3 closes that gap, and only on x86.

Every longterm kernel an embedded product can ship today carries a way of passing boot parameters that is not the kernel command line. Bootconfig has been there since Linux 5.6, with an entry in the admin guide, a tool in the kernel tree and its state exposed in /proc. Most teams building Arm or RISC-V products have never used it, because the problem it solves does not announce itself: a command line grows one parameter at a time, and the release in which it becomes too long reports no error.

The command line is a fixed-size buffer

The kernel command line is not a dynamically allocated string. Each architecture defines COMMAND_LINE_SIZE, the size of the buffer the bootloader’s string is copied into. Read at the v7.3 tag:

arch/arm64/include/uapi/asm/setup.h:  #define COMMAND_LINE_SIZE 2048
arch/riscv/include/uapi/asm/setup.h:  #define COMMAND_LINE_SIZE 2048
arch/arm/include/uapi/asm/setup.h:    #define COMMAND_LINE_SIZE 1024

On a device tree platform the string arrives as the bootargs property of the /chosen node, and early_init_dt_scan_chosen() in drivers/of/fdt.c copies it. The copy is bounded, which is safe, and is also where the problem lives:

strscpy(cmdline, p, min(l, COMMAND_LINE_SIZE));

strscpy() truncates rather than overflowing, and records nothing about having truncated. The only log call here is a pr_debug(), which produces no output in a normal build, and the function returns 0 either way. A bootargs property that overruns the buffer therefore gives a kernel that boots, prints no complaint, and ignores the overrun.

That matters because of how command lines grow. Bring-up starts short:

console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait rw

Then the product needs persistent crash logs, so reserve_mem= and the ramoops.* parameters go on. Then a real-time partition, so isolcpus= and nohz_full=. Then a camera pipeline, so cma=. Then field support wants different log levels per build. Each addition is small and reviewed on its own; nobody reviews the total, because nothing reports it.

On a 32-bit Arm board that is easy to reach. Read the property out of the compiled device tree and count it:

raghu@techveda.org:~$ fdtget -t s board.dtb /chosen bootargs | wc -c
1063
raghu@techveda.org:~$ fdtget -t s board.dtb /chosen bootargs | tail -c 40
 cma=128M coherent_pool=2M isolcpus=2,3

That is 1062 characters plus the newline wc counted. COMMAND_LINE_SIZE is 1024 there, and strscpy() writes at most one byte less than the size it is given, so the kernel keeps 1023 characters and discards 39 — exactly the tail printed above: the CMA reservation, the coherent pool and the CPU isolation list, the three most recent additions.

The parameter that stops working is the last one appended, usually the newest, so whoever debugs it assumes the feature is broken rather than that the parameter never arrived. On 32-bit Arm, where the buffer is half the size, this happens sooner, and the usual checks do not help: the device tree source is correct and the bootloader reports passing the string.

What a bootconfig file is, and where it lives

It is a second source of boot parameters, parsed by the kernel rather than the bootloader: a text file of dot-separated keys and values, with braces for grouping.

kernel {
        root = "/dev/mmcblk0p2"
        rootwait
        isolcpus = "2,3"
        nohz_full = "2,3"
        reserve_mem = "1M:4096:oops"
        ramoops.mem_name = "oops"
        ramoops.record_size = "32768"
}
init {
        splash
}

Keys use letters, digits, hyphen and underscore. Quoted values may contain spaces; unquoted ones may not contain a semicolon, comma, hash or closing brace. A key with no value is a boolean, which is how rootwait above is expressed, and arrays are comma-separated. = defines a key and errors if it already has one, := overrides, += appends.

Two subtrees have meaning to the kernel. Everything under kernel becomes a kernel parameter; everything under init is passed to init, as if it had followed a -- on the command line. Any other key is stored and readable later, giving a build system somewhere to record its own values.

Call that file boot.conf: 244 bytes and 17 nodes, the numbers the tool and the kernel report below. It reaches the kernel in one of two ways. The original method appends it to the initrd, in this layout:

[initrd][bootconfig][padding][size(le32)][checksum(le32)][#BOOTCONFIG\n]

The magic word is twelve bytes, the whole is padded to a four-byte boundary, and size and checksum are 32-bit little-endian. The kernel tree ships the tool that does this, in tools/bootconfig: at v7.3 its options are -a <config> to apply, -d to delete, -l to list, and -C to render the kernel subtree as a flat command line on standard output. The second method, from Linux 5.19, builds the file into the image with CONFIG_BOOT_CONFIG_EMBED=y and CONFIG_BOOT_CONFIG_EMBED_FILE. An initrd copy overrides an embedded one.

How the kernel finds and applies it

Neither method is active by default. The kernel looks for the file only if the word bootconfig appears on the command line, or if CONFIG_BOOT_CONFIG_FORCE=y was set at build time; that option arrived in Linux 6.3 and defaults to y when the embedded option is enabled. The check is bootconfig_cmdline_requested() in lib/bootconfig.c, which parses a private copy of the command line.

If the file is requested, get_boot_config_from_initrd() looks for the magic word at the end of the initrd, trying up to four byte positions backward because a bootloader may have rounded the size up. It then reads the eight-byte header before the magic, rejects the data if the recorded size exceeds XBC_DATA_MAX or runs past the end of the initrd, and verifies the checksum. Each refusal prints its own error, so a mis-sized or corrupt attachment is visible in the log:

bootconfig size %u is greater than max size %d
bootconfig size %u is greater than initrd size %lu
bootconfig checksum failed

On success the kernel moves initrd_end back, so the appended bytes are not presented to user space as part of the archive, then renders two strings from the parsed tree: the kernel subtree into extra_command_line and the init subtree into extra_init_args. A successful parse logs one line, the quickest confirmation on a running board:

Load bootconfig: %ld bytes %d nodes\n

Two more messages matter. If the word was on the command line but nothing was found, the kernel prints 'bootconfig' found on command line, but no bootconfig found, usually because the bootloader passed an initrd size larger than the real file. If the file is malformed, Failed to parse bootconfig: %s at %d. names the problem and the offset.

Finally, setup_command_line() builds the strings the rest of the kernel uses, placing extra_command_line before the bootloader’s string in both saved_command_line, which /proc/cmdline prints, and static_command_line, which the main parser reads. A source comment explains the order: the extra parameters must come first because the bootloader’s string may itself contain a -- separator.

The limit nobody mentions: early parameters

Kernel parameters are registered in two ways. __setup("name=", handler) registers an ordinary parameter, parsed in start_kernel() by parse_args("Booting kernel", static_command_line, ...). early_param("name", handler) registers one that must act earlier, before memory management and the console are up, and those are parsed by parse_early_param().

That distinction decides what this mechanism can and cannot do, because of one line in parse_early_param(): it copies boot_command_line into a temporary buffer and parses that. It does not read static_command_line. Since the keys live in extra_command_line, which is only joined to the command line when static_command_line and saved_command_line are built, no parameter registered with early_param() can be set from a bootconfig on an architecture that has not opted into the 7.3 mechanism described below.

On arm64 the ordering makes it starker. setup_arch() in arch/arm64/kernel/setup.c calls parse_early_param() itself, immediately after setup_machine_fdt() has parsed the device tree. That is long before start_kernel() reaches setup_boot_config(), which it does five calls after setup_arch() returns, and parse_early_param() carries a static done flag, so the later call does nothing. On such a board the early parameters have been acted upon before the kernel has looked for a boot config at all.

Read from the source at v7.3, these use __setup() and so work from the file:

  • console=, in kernel/printk/printk.c
  • root=, rootwait, rootwait= and rootfstype=, in init/do_mounts.c
  • isolcpus= and nohz_full=, in kernel/sched/isolation.c

These use early_param(), and therefore do not:

  • earlycon, in drivers/tty/serial/earlycon.c
  • mem, in arch/arm64/mm/init.c on arm64
  • cma, in kernel/dma/contiguous.c
  • loglevel, quiet and debug, in init/main.c

The isolcpus= and nohz_full= entries are worth noting: the intuition that anything scheduler-related must be early is wrong here. The only way to know is to look, and the check is a single grep shown below.

Now the trap. Suppose a team moves the whole command line across, including kernel.mem = "512M". The board boots. /proc/cmdline shows mem=512M, because saved_command_line is built from extra_command_line followed by the bootloader’s string. /proc/bootconfig shows the key. Nothing in the log says the parameter was ignored. But early_mem() on arm64 prints Memory limited to %lldMB when it runs, that line is absent, and the board is using all of its RAM. Every indication says the parameter was applied, except the kernel’s behaviour.

What Linux 7.3 added, and why only on x86

Linux 7.3, released on 18 October 2026, added CONFIG_CMDLINE_FROM_BOOTCONFIG, a series by Breno Leitao posted in July 2026 and merged through the tracing tree. Its cover letter states the problem in one sentence: the embedded file is parsed at runtime, after parse_early_param() has already run, so early_param() handlers cannot see embedded values. The goal is an equivalent of CONFIG_CMDLINE without maintaining two command line sources.

The approach is to do the work at build time. The build runs tools/bootconfig -C over the embedded file, rendering the kernel subtree into one flat string and storing it in a new object, lib/embedded-cmdline.S, in .init.rodata; the tool is built on the host even in a cross-compilation. At boot the architecture’s own setup_arch() prepends that string to boot_command_line before parse_early_param() runs. On x86 that is three lines:

if (bootconfig_cmdline_requested(boot_command_line, NULL) ||
    IS_ENABLED(CONFIG_BOOT_CONFIG_FORCE))
        xbc_prepend_embedded_cmdline(boot_command_line,
                                     COMMAND_LINE_SIZE);

The reason this is x86-only is a single line of Kconfig. The series added a silent symbol, ARCH_SUPPORTS_CMDLINE_FROM_BOOTCONFIG, which an architecture selects once it has placed that call in its own setup_arch(); at v7.3 the only selector is arch/x86/Kconfig. The option also needs a non-empty CONFIG_BOOT_CONFIG_EMBED_FILE and an empty CONFIG_CMDLINE, and defaults to n. On an arm64, 32-bit Arm or RISC-V board running 7.3, nothing about early parameters has changed: the embedded file is still parsed late and the list above still applies.

Two further details. It applies to the embedded file only, because the prepend happens before the initrd is looked at. And an oversized render is a soft failure: xbc_prepend_embedded_cmdline() checks that the string fits alongside what is already in the buffer, and if not, logs an error and leaves the command line untouched. Keys applied this way appear in /proc/cmdline and not in /proc/bootconfig.

Which kernels carry what

Everything below was read from the mainline tag of init/Kconfig and include/linux/bootconfig.h, one tag at a time. The supported set is the kernel.org release data for 30 October 2026: mainline 7.3, stable 7.3.1 and 7.2.14, longterm 6.18 down to 5.10.

SeriesBOOT_CONFIGEMBEDFORCECMDLINE_FROM_BOOTCONFIGXBC_NODE_MAX
5.10yesnonono1024
5.15yesnonono1024
6.1yesyesnono1024
6.6yesyesyesno8192
6.12yesyesyesno8192
6.18yesyesyesno8192
7.2yesyesyesno8192
7.3yesyesyesyes, x86 only8192

Each introduction point was pinned by checking the release before it and that release: BOOT_CONFIG no at v5.5, yes at v5.6; BOOT_CONFIG_EMBED no at v5.18, yes at v5.19; BOOT_CONFIG_FORCE no at v6.2, yes at v6.3; CMDLINE_FROM_BOOTCONFIG no at v7.2, yes at v7.3.

Two honest limits on that table. These are mainline tags, not the tip of each stable branch; they are configuration features rather than fixes, so a stable backport is unlikely, but this article did not verify that. And XBC_NODE_MAX rose from 1024 to 8192 somewhere between 6.2 and 6.6, a release not pinned here. That limit usually bites before the 32 KB data limit, because every key word and every value is a node. Note also that at v7.3 the header says 8192 while the admin guide in the same tree still says 1024; the header is what the parser uses.

Check your own board

Four checks, most informative first. How close the command line is to the limit, and whether the kernel has the option:

raghu@techveda.org:~$ wc -c /proc/cmdline
287 /proc/cmdline
raghu@techveda.org:~$ zcat /proc/config.gz | grep BOOT_CONFIG
CONFIG_BOOT_CONFIG=y
# CONFIG_BOOT_CONFIG_FORCE is not set
# CONFIG_BOOT_CONFIG_EMBED is not set

If /proc/config.gz is absent, the same grep works on /boot/config-$(uname -r) or the build tree’s .config. Compare the byte count against the COMMAND_LINE_SIZE for your architecture, remembering that what you want is the bootargs the bootloader passes, which /proc/cmdline reflects only if no extra parameters are in play.

Next, whether anything was loaded on this boot:

raghu@techveda.org:~$ dmesg | grep -i bootconfig
[    0.000000] Load bootconfig: 244 bytes 17 nodes
raghu@techveda.org:~$ cat /proc/bootconfig
kernel.root = "/dev/mmcblk0p2"
kernel.rootwait = ""
kernel.isolcpus = "2,3"
kernel.nohz_full = "2,3"
kernel.reserve_mem = "1M:4096:oops"
kernel.ramoops.mem_name = "oops"
kernel.ramoops.record_size = "32768"
init.splash = ""
# Parameters from bootloader:
# console=ttyS0,115200 bootconfig

Then the check that prevents the silent failure: whether a parameter can be moved at all. Grep the source for its registration, and the macro that appears is the answer:

raghu@techveda.org:~$ grep -r 'early_param("cma"' kernel/ mm/
kernel/dma/contiguous.c:early_param("cma", early_cma);
raghu@techveda.org:~$ grep -r '__setup("isolcpus=' kernel/
kernel/sched/isolation.c:__setup("isolcpus=", housekeeping_isolcpus_setup);

A hit on early_param means it stays in bootargs; a hit on __setup means it can move. Last, build the tool and try it on a copy of your initrd:

raghu@techveda.org:~$ make -C tools/bootconfig
raghu@techveda.org:~$ cp initramfs.cpio.gz initramfs-bc.cpio.gz
raghu@techveda.org:~$ ./tools/bootconfig/bootconfig -a boot.conf initramfs-bc.cpio.gz
Apply boot.conf to initramfs-bc.cpio.gz
	Number of nodes: 17
	Size: 244 bytes
	Checksum: 18794
raghu@techveda.org:~$ ./tools/bootconfig/bootconfig -l initramfs-bc.cpio.gz

Then add bootconfig to the bootloader’s bootargs — the one parameter that cannot move — and boot. Board bring-up and boot parameter handling are covered in the Embedded Linux and Yocto training.

What it costs

The initrd method needs an initrd, and needs the bootloader to pass its exact size: a length padded beyond the four bytes the kernel tolerates makes the search for the magic word fail, with the no bootconfig found message as the only symptom. The embedded method avoids that, at the cost of a kernel rebuild for a parameter change.

Secure boot does not remove the benefit. Appending to an initrd changes the initrd, so a signed FIT image must be re-signed; this is no way around a signature. It is a way around the size limit, and a way to keep boot parameters in a reviewable file rather than a 900-character string.

Three smaller traps. Any tool that rebuilds the initrd drops the appended data unless the append is part of the build, so it belongs in the image recipe. /proc/cmdline becomes weaker evidence, since it lists keys whether or not they took effect. And the keyword still has to be on the command line unless CONFIG_BOOT_CONFIG_FORCE is set, so the bootloader’s string can never be empty.

What this changes for a product

The practical division is simple. Early parameters stay in bootargs: on an Arm or RISC-V board that means earlycon, mem, cma, loglevel, quiet and debug, plus the bootconfig keyword itself. Everything else moves into a file that lives in the build system, carries comments, and can differ per product variant without a new device tree — which leaves the command line short enough that the limit stops being a risk.

Key takeaways

  • COMMAND_LINE_SIZE is 2048 bytes on arm64 and RISC-V and 1024 on 32-bit Arm; a longer bootargs is truncated with no warning, because the copy is a bounded strscpy() and the only log call there is a pr_debug().
  • It has been available since Linux 5.6 and is in every supported longterm series, but is inactive unless bootconfig is on the command line or CONFIG_BOOT_CONFIG_FORCE is set.
  • Parameters registered with __setup() work from it; those registered with early_param() do not, because parse_early_param() reads boot_command_line while the keys land in a different string.
  • That failure is silent, and /proc/cmdline still shows the ignored parameter. Check the registration macro before moving one.
  • Linux 7.3 adds CONFIG_CMDLINE_FROM_BOOTCONFIG, which renders the embedded kernel subtree at build time and prepends it before parse_early_param(). It is default n, and only x86 selects the supporting symbol.
Was this worth your time?

Frequently asked questions

Does bootconfig replace the kernel command line?
No. It adds to it. The word bootconfig must still appear on the command line unless the kernel was built with CONFIG_BOOT_CONFIG_FORCE, and any parameter registered with early_param() must still be passed on the command line on every architecture except x86 on Linux 7.3.

How do I tell whether a parameter can be moved out of the command line?
Grep the kernel source for its registration. If it appears in an early_param() call it cannot be set this way; if it appears in a __setup() call it can. At Linux 7.3, console=, root=, rootwait, rootfstype=, isolcpus= and nohz_full= use __setup(), while earlycon, mem, cma, loglevel, quiet and debug use early_param().

Does this break secure boot?
It does not break it, and it does not get around it either. Appending to the initrd changes the initrd, so a signed FIT image containing that initrd has to be re-signed. The benefit is the removal of the size limit and a reviewable parameter file, not a way to change boot parameters on a signed image.

References

RB
Raghu Bharadwaj

Founder, TECH VEDA — 20+ years teaching the Linux kernel, device drivers and embedded systems.

Follow on LinkedIn

Get new posts by email

Kernel, embedded Linux and AI-era engineering — a few sharp reads a month. No spam.

We email occasionally and never share your address.