U-Boot Environment Flags: Write-Once and Change-Default Check Different State
A factory default already present in the environment is an existing
value. In U-Boot v2025.01, ordinary overwrite validation for a
write-once variable rejects replacing that value. The change-default
policy instead compares the current value with the compiled default and
allows an overwrite while they match.
Choose between these policies by asking whether provisioning creates
an absent variable or replaces a supplied default. Neither policy is a
persistent write counter, and neither alone establishes a
hardware-backed provisioning lock.
Read the access
masks before choosing the label
The environment
flag definitions name the access classes. Their implementation in env/flags.c
maps the ordinary policies to operation restrictions:
- Read-only prevents creation, deletion and overwrite.
- Write-once prevents deletion and overwrite, leaving creation allowed
by this access mask. - Change-default prevents deletion and rejects an overwrite when the
existing value differs from its default.
These are access checks, not the complete success conditions for
changing an environment entry. Type validation, callbacks and allocation
can still reject an otherwise permitted operation. The discussion here
assumes a correctly typed value and ordinary unforced updates; default
installation, external import policy and configuration-dependent force
handling require separate analysis.
Existing and
absent variables take different paths
For an existing write-once entry, overwrite is prohibited without
consulting whether its current contents happen to equal the compiled
default. Calling a value a factory placeholder does not turn replacing
it into creation.
For an absent entry, the write-once mask permits creation. Once that
entry exists, the next ordinary replacement reaches the overwrite
restriction. This can suit a variable deliberately absent until
provisioning, provided the surrounding initialization and persistence
behavior actually preserve that assumption.
The hash-table
overwrite path, _compare_and_overwrite_entry(), invokes
the change validator before the callback and before replacing the stored
string. It does not first skip validation merely because the requested
string matches the existing string. A repeated same-value request is
therefore still an overwrite for this path.

Change-default
compares values, not update history
In env_flags_validate(), the change-default overwrite
branch obtains the default with env_get_default(). A
missing default is treated as an empty string for this comparison. The
validator compares the old value with that default, rather than counting
previous operations or inspecting whether the new value is
different.
Consider a hypothetical existing variable whose compiled default is
unassigned. While its current value remains
unassigned, the access check permits an overwrite. A
request that leaves it at unassigned does not consume a
hidden one-time allowance. After a successful change to another value,
the next ordinary overwrite fails because the current value no longer
matches the default.
Creation is also allowed by the change-default access mask. If a
newly created variable has no compiled default and contains a nonempty
value, a later overwrite encounters a comparison against the empty
default. This behavior follows the current strings; it is not evidence
that a provisioning transaction was recorded durably.
Define the
provisioning state explicitly
For a board-specific policy review, record whether each protected
variable is absent or initialized when provisioning starts, its compiled
default, its active access flags and the operation the provisioning tool
requests. Then distinguish an accepted environment edit from
successfully saving that edit to the selected backend.
A useful proposed fixture covers absent creation, replacement of an
existing default, a repeated same-value request and replacement after
leaving the default. Observe both the return value and the resulting
entry. No environment modification or provisioning operation was
executed here.
Finally, check reboot and recovery behavior separately. Reloading
defaults, importing another environment or changing the compiled
defaults can alter the state against which these checks run. The
engineering contract must describe those paths instead of interpreting
the name “write-once” as irreversible storage protection.