Fix anti-rollback to use authenticated TLV counter - #103
Conversation
srpatcha
left a comment
There was a problem hiding this comment.
Review — eBoot#103 "Fix anti-rollback to use authenticated TLV counter"
head: c9fe84c author: Tanisha1723 ci: none reported
Verdict: Correct and valuable fix to a real anti-rollback bypass on the boot path that
actually runs. The core change conforms to §8.1 and .ai/security.md. What is missing is
coverage of the two production paths it changes, and cleanup of the defective function it
leaves exported.
Findings
| # | Severity | File:line | Finding | Recommended fix |
|---|---|---|---|---|
| 1 | Medium | core/image_verify.c:230 |
After this PR eos_image_check_rollback() has no production caller — git grep at this head finds only the new test. It remains exported, and it is exactly the defect being removed: it compares a firmware image_version against the OTP monotonic floor. Any future caller reintroduces the bypass, silently and with a plausible-looking name. |
Delete it. If the regression test needs to demonstrate the old behaviour, move the comparison into the test file under a name that says it is wrong (legacy_broken_version_floor_check), so no production translation unit can reach it. |
| 2 | Medium | tests/unit/test_tlv_auth.c:304 |
The new regression test does not exercise either changed path. It calls eos_rollback_read_image_counter() and eos_rollback_verify() directly. Neither eboot_jump_to_app() (stage1/jump_app.c:60) nor eos_fw_update_finalize() (core/fw_update.c:191) is invoked. I confirmed this by reading the test: reverting either production hunk leaves the suite green. The bug being fixed lives in the wiring, and the wiring is untested. |
Add one test that drives eos_fw_update_finalize() to completion with an image carrying image_version = 0x00010000 and MIN_SEC_VER = 3 against sim_counter = 9, asserting EOS_ERR_ANTI_ROLLBACK. test_fw_update.c already has the context construction needed. |
| 3 | Medium | stage1/jump_app.c:60, core/secure_boot.c:148 |
Two independent implementations of the same TCB sequence now exist: eboot_main() → eboot_jump_to_app() (stage1/main.c:75, the path stage 1 actually takes) and eos_secure_boot(), which nothing in stage1/ calls. The PR comment acknowledges this — "Same sequence as eos_secure_boot() step 5b" — and duplication is how the two paths diverged in the first place: one got the TLV counter, the other kept image_version. |
Extract the read → verify → stage sequence into one function in core/rollback.c (e.g. eos_rollback_gate_image(addr, bool stage)) and have both call sites use it, so the next change cannot land in one path only. Out of scope for this PR; open a follow-up. |
| 4 | Low | tests/unit/test_tlv_auth.c:339 |
tests_run is still a hand-maintained literal. This PR corrects it to 8, but the next test added under-reports unless someone remembers, and tests_passed == tests_run then passes with a test that never ran. |
Already addressed org-wide by open PR eBoot#95 ("derive the suite totals … instead of restating them"). No change needed here; rebase after #95 lands. |
| 5 | Low | PR body, "Testing" | The body reports the C suite as unrunnable due to "the current MSVC build". The blocking errors are not MSVC-specific and not a Windows artifact — they are two merge defects on master itself that break the gcc host build too (see Not checked). Worth correcting so the next reader does not go looking for a toolchain problem. |
Restate as: blocked by broken master, tracked by eBoot#94 / #98. |
Note on the "Testing" section: the body states plainly that the suite could not be run,
rather than asserting an unsupported PASS. That is the right call and is not a finding.
Architecture conformance
Conforms. Master design §8.1 requires "rollback protection where hardware/policy supports
it"; .ai/security.md ("Boot and update chain") requires "anti-rollback counters actually
checked". Before this PR the counter was not actually checked on the live boot path —
hdr.image_version (scale 0x00MMmmpp) was compared against the OTP monotonic floor
(small integers), so an old image with a high version number cleared a high floor. The
change substitutes the authenticated EOS_TLV_MIN_SEC_VER, which core/rollback.c binds
to the signed header via tlv_len/tlv_hash. That is the value §8.1 intends.
Two secondary points, both correct as written:
- Dropping the
rb_rc != EOS_ERR_NOT_SUPPORTEDtolerance is a genuine tightening and does
not brick counter-less boards:eos_rollback_verify()(core/rollback.c:129) maps
EOS_ERR_NOT_SUPPORTEDfrom the HAL toEOS_OKitself. The now-fatal cases are a
malformed header or a TLV area that does not match its signed hash, which must fail
closed per.ai/security.md("Fail closed"). - Verifying but not staging in
eos_fw_update_finalize()is right. Staging on install
would raise the floor for an image that has not booted, removing the fallback — the
separationinclude/eos_rollback.hdocuments.
No layering violation. eos_rollback.h is within eBoot; nothing points up a tier (§5.1).
The TCB does not grow: core/rollback.c was already linked into eboot_core.
Proposed changes
- Delete
eos_image_check_rollback()fromcore/image_verify.cand its declaration;
move the demonstration of the old comparison intotest_tlv_auth.cunder an explicitly
wrong name. (Finding 1.) - Add the
eos_fw_update_finalize()regression test described in finding 2. This is the
one change I would ask for before merge — as it stands the fix is correct but unguarded. - Leave finding 3 to a follow-up; folding it in here would mix a structural change with a
behavioural one, which.ai/architect.mdprohibits.
Not checked
-
CI:
checks.txtis empty. No check runs were reported for this head at bundle time.
I did not determine whether the workflows had not started, are not required on this
branch, or did not trigger. Read the finding below as the reason it matters, not as a
claim about what CI did. -
masterdoes not compile, and this is not caused by this PR. Verified: I extracted
origin/master(22d8f8b) to a scratch directory and ran the CI's own host
configuration. It fails with two independent, pre-existing defects:
include/eos_image.h:135,142still assert on areservedmember thattlv_len/
tlv_hashreplaced, andcore/ed25519_verify.c:338redefinespoint_is_identity
(a merge artifact — the same function appears at :281). The PR head inherits both.
Consequence for this PR: TCB code is landing with no green build behind it.
This is already covered by open PRs eBoot#94 and eBoot#98, so per the review policy I
have opened nothing. #94 or #98 should merge before this. -
I did run this PR's new test, with those two master defects patched out in a scratch
copy only (git archiveto/tmp; the repository was not touched). Result:$ cmake -B bld2 -DEBLDR_BUILD_TESTS=ON && cmake --build bld2 --target eboot_test_tlv_auth $ ./bld2/tests/eboot_test_tlv_auth test_hw_floor_uses_tlv_counter_not_image_version [PASS] 8/8 passed (exit 0)So the new test does pass and does demonstrate the stated bug. It is not evidence that
the suite passes onmaster, becausemasterdoes not build; the fullctestrun and
the ARM cross build were not run —tests/unit/test_ed25519.c:31,263has its own
duplicate-definition merge artifact that blocks the rest of the test target. -
Not verified on hardware. Whether
eos_hal_monotonic_read/incrementbehave as
assumed on a real OTP-backed board is untested here; the run above uses the simulated
HAL intest_tlv_auth.c. §28.1 would require a named board before calling T-202
mitigated on hardware. -
Not examined: whether
eos_bootctl_confirm()(core/bootctl.c:159, which calls
eos_rollback_commit()) executes in the same address space as theeboot_jump_to_app()
that staged the counter.g_staged_*is file-static RAM state; if confirm runs in the
application rather than in the bootloader image, the staged value is not visible and the
floor never advances. That is pre-existing design, not introduced here, but it decides
whether this fix ends up enforcing anything, and I could not settle it from the tree.
Automated architecture review of c9fe84c4b26c — scheduled, model claude-opus-5, checked against the EmbeddedOS Master Design v2.0. Advisory only: this reviewer never approves, requests changes, or merges. Reply here to discuss or push back — a wrong finding is a bug worth reporting.
srpatcha
left a comment
There was a problem hiding this comment.
Review — eBoot#103 "Fix anti-rollback to use authenticated TLV counter"
head: 8c9a363 author: Tanisha1723 ci: none reported
Verdict: Follow-up review (previous head c9fe84c4). The one new commit does exactly
what the last review asked for on findings 1 and 2 and introduces no defect of its own.
Findings 3, 4 and 5 are untouched. What the new test also does, unintentionally, is
document a defect the previous review missed: the update transport cannot deliver the TLV
area at all, so after this PR no image carrying a security counter can be installed.
Previous findings — status
- Finding 1 (delete
eos_image_check_rollback()) — resolved in8c9a363f.
git grep eos_image_check_rollback 8c9a363freturns nothing. The old comparison now
lives intests/unit/test_tlv_auth.c:304aslegacy_broken_version_floor_check(),
which is the disposition that was recommended. - Finding 2 (test the changed production paths) — partially addressed. See finding 3
below:eos_fw_update_finalize()is now driven end-to-end,eboot_jump_to_app()is not. - Finding 3 (duplicated read → verify → stage sequence) — untouched. Correctly so; the
previous review deferred it. See finding 4 for why it is worth restating. - Finding 4 (
tests_runhand-maintained literal) — untouched. eBoot#95 is still open. - Finding 5 (PR body blames MSVC) — untouched. See finding 6.
Findings
| # | Severity | File:line | Finding | Recommended fix |
|---|---|---|---|---|
| 1 | High | core/fw_update.c:192, root cause core/fw_update.c:139-144 |
After this PR, no image whose header declares tlv_len > 0 can be installed through eos_fw_update_*. eos_fw_update_write() stops writing once payload_written >= payload_total and moves to EOS_FW_STATE_VERIFY (:139); every later byte falls through both branches, is discarded, still increments total_received (:144) and still returns EOS_OK. The TLV area sits after the payload, so it is exactly the region that gets dropped. eos_fw_update_finalize() then reads it from erased flash and tlv_area_matches_header() fails. Verified: streaming the complete image in 64-byte chunks the way uart_raw_receive() (core/fw_transport_uart.c:112) and xmodem_receive() (:240) do leaves flash@tlv = FF FF FF FF FF FF FF FF FF FF FF FF against an expected 07 69 0C 00 50 00 04 00 09 00 00 00, and finalize returns -2 (EOS_ERR_INVALID) for an image whose counter (9) is at the floor (9) and should install. This is a defect of the PR as a whole, not of the new commit — it was present at c9fe84c4 and the previous review did not catch it. The new test does not catch it either: place_tlv_after_payload() (tests/unit/test_fw_update.c:155) pokes those bytes in with a raw eos_hal_flash_write(), and its own comment records why. Fails closed, so it is a denial of updates rather than a bypass — hence High and not Critical. |
Make the write state machine account for the whole container: after the payload completes, enter a TLV state that accepts ctx->header.tlv_len further bytes and writes them at write_addr, only then move to EOS_FW_STATE_VERIFY. finalize() must reject a context whose TLV bytes are short rather than reading whatever is in flash. Independently, eos_fw_update_write() should not return EOS_OK for bytes it discards — a caller that streams a complete artifact and is told every chunk succeeded has no way to learn the artifact was truncated. |
| 2 | Medium | core/rollback.c:59-101 |
The TLV read is bounded against integer overflow but never against the slot. tlv_addr = image_addr + hdr_size + image_size and then tlv_len (up to EOS_TLV_MAX_SIZE = 512) bytes are hashed, with no check that this stays inside the slot. Both callers validate only hdr_size + image_size <= slot_size (stage1/jump_app.c:38-42, core/fw_update.c:90-92). Verified with a probe: a header declaring image_size that fills slot B exactly plus tlv_len = 512 makes eos_rollback_read_image_counter() return EOS_OK with a counter read entirely from outside the slot (slot B = [0x10000..0x18000), tlv area = [0x18000..0x18200), counter = 77). This PR is what makes the path reachable from production for the first time. On the boot path the header is signature-verified first, so this is defence-in-depth; on the install path it is reachable today because finalize() skips signature verification for sig_type < EOS_SIG_ED25519 — which eBoot#104 fixes, so do not duplicate that here. |
Add the bound where all three callers get it: reject in eos_rollback_read_image_counter() when the TLV area leaves the slot. That needs the slot limit at hand — either take it as a parameter, or derive it via eos_hal_slot_size() from the slot containing image_addr. Prefer this over patching jump_app.c, which would leave core/secure_boot.c:134 and core/fw_update.c:192 uncovered, and would collide with eBoot#96. |
| 3 | Medium | stage1/jump_app.c:57-79 |
Previous finding 2, still half open. tests/unit/test_fw_update.c now drives eos_fw_update_finalize() to completion and asserts EOS_ERR_ANTI_ROLLBACK, which is what was asked for and which I ran (1/1 pass). The stage-1 hunk still has no test: git grep -l eboot_jump_to_app 8c9a363f -- tests/ is empty. The untested part now includes the new eos_rollback_stage(img_counter) call at :79, which is the only thing that ever raises the hardware floor — if it were dropped the suite would stay green and anti-rollback would silently stop advancing. |
Add a test_jump_app case that drives eboot_jump_to_app() with a slot whose TLV counter is below the floor and asserts it returns EOS_ERR_ANTI_ROLLBACK without jumping, plus one above the floor that asserts the staged counter is observable through eos_rollback_commit(). eBoot#96 adds tests/unit/test_jump_app_bounds.c with the HAL-stub scaffolding this needs; rebasing on it once it lands is cheaper than building it twice. |
| 4 | Medium | core/secure_boot.c:134,148, stage1/jump_app.c:66,79, core/fw_update.c:192 |
Previous finding 3, untouched and now larger. The read → verify → stage sequence existed in two copies before this PR; it exists in three after it, the third being the read → verify half in fw_update.c. eos_secure_boot() still has no caller anywhere in stage1/. Restating rather than dropping it because finding 1 is precisely a divergence between two of these copies: the boot path reads a TLV that a factory-flashed image has, and the install path reads one that its own transport never wrote. |
Unchanged from the previous review: extract the sequence into one function in core/rollback.c and have all three call sites use it. Still a separate PR — folding it in here would mix a structural change with a behavioural one, which .ai/architect.md prohibits. |
| 5 | Low | tests/unit/test_fw_update.c:79,191 |
Previous finding 4, untouched, and the new file adds a third hand-maintained tests_run literal. Here it is assigned after the tests run, so a forgotten bump reports a spurious failure rather than a silent pass — the safer direction, but still bookkeeping a human has to remember. |
No change needed in this PR. eBoot#95 replaces this org-wide and is still open; rebase after it lands. |
| 6 | Low | PR body | Previous finding 5, untouched, and the body has now gone stale against its own head. It still attributes the unrunnable suite to "the current MSVC build", which is wrong — origin/master does not compile with gcc either (see Not checked). "## Commit" still lists only 28700bc; the head is 8c9a363f and carries a second commit. The Changes section does not mention the removal of eos_image_check_rollback(), which is an exported symbol disappearing, or the new tests/unit/test_fw_update.c. |
Restate the blockage as broken master, tracked by eBoot#94 / #105. Drop the "## Commit" list or regenerate it. Add one line for the removed symbol and one for the new test file. |
Architecture conformance
Conforms, on the same reading as the previous review, and nothing in the new commit
changes it.
- §8.1 "Rollback protection where hardware/policy supports it" and
.ai/security.md
("anti-rollback counters actually checked"): the substitution of the authenticated
EOS_TLV_MIN_SEC_VERforhdr.image_versionis the value §8.1 intends, and
core/rollback.c:101binds the TLV area to the signed header before trusting it. - §5.1: no layering violation.
eos_rollback.his inside eBoot; nothing points up a
tier. The TCB does not grow —core/rollback.cwas already linked intoeboot_core,
and this commit removes 20 lines of TCB code without adding any. - §21: Tier 1 (Foundation). Boot-time rollback policy belongs in eBoot; correct repo.
- §28: the PR body still states plainly that the suite could not be run rather than
asserting an unsupported PASS. That remains the right call and is not a finding; only
the reason it gives is wrong (finding 6).
Finding 1 is a conformance problem in the wider sense — §15's flow is
Download → Verify → Install, and an install that verifies a region the download was
never required to deliver satisfies the diagram while failing every real update. The
design does not currently say otherwise; see the proposal appended for this run.
Proposed changes
- Fix finding 1 before merge. Everything else in this PR is correct, and it is the one
item that makes the shipped result worse than what it replaces: todayfinalize()
accepts these images (against the wrong counter), after this PR it rejects them
outright. A merge in this state converts a security hole into a fleet that cannot take
updates over UART or XMODEM. - Fix finding 2 in
core/rollback.c, not in the callers. - Add the
eboot_jump_to_app()test (finding 3), ideally rebased on eBoot#96. - Correct the PR body (finding 6). One edit, no code.
- Leave findings 4 and 5 to eBoot#95 and a separate extraction PR.
I have opened no fix PR. Finding 1 is the only High, and its fix is a state-machine change
in the TCB install path — not the "small and provable" class the review policy permits —
and it cannot be verified as the policy requires anyway, because origin/master does not
build (below), so fix-verify.sh has nothing green to run against.
Not checked
-
CI: no checks ran.
checks.txtis empty andgh pr checks 103reports
no checks reported on the 'fix/eboot-test-issue' branch. This is not a bundle
artifact — no workflow has run against this head at all. TCB code is being reviewed
with zero automated evidence behind it. eBoot#90 ("add one job branch protection can
require") is open and is what would change this. -
origin/masterstill does not compile, and this PR does not cause it. Verified: I
extracted8c9a363fwithgit archiveto a scratch directory (the repository, its
working tree and its stashes were not touched) and configured the CI's own host build.
It fails on the two defects the previous review reported, both still present:
include/eos_image.h:135,142assert on areservedmember thattlv_len/tlv_hash
replaced, andcore/ed25519_verify.c:338redefinespoint_is_identity(already defined
at:281). eBoot#94 and eBoot#105 both address these and both are still open.
One of them should merge before this. -
What I did run, in that scratch copy with those two master defects patched out and
nothing else changed:$ cmake -B build/host -DEBLDR_BUILD_TESTS=ON && cmake --build build/host -- -k $ ctest --test-dir build/host --output-on-failure --no-tests=error -E ed25519 100% tests passed, 0 tests failed out of 21 $ ./build/host/tests/eboot_test_fw_update test_finalize_rejects_tlv_counter_below_hw_floor [PASS] 1/1 $ ./build/host/tests/eboot_test_tlv_auth 8/8 passedtest_ed25519is excluded because it cannot compile:tests/unit/test_ed25519.c:31,263
has its own duplicate-definition merge artifact onmaster. That is 21 of the 22
registered tests; the ARM cross build was not run. -
Findings 1 and 2 were each verified with a throwaway probe built in that same
scratch copy and deleted with it. Neither probe is proposed as a test here — the real
tests belong with the fixes. -
Not verified on hardware.
eos_hal_monotonic_read/incrementbehaviour on a real
OTP-backed board is untested; everything above uses simulated flash and a simulated
counter. §28.1 would require a named board before calling this mitigated on hardware. -
Still not examined, carried forward from the previous review: whether
eos_bootctl_confirm()(core/bootctl.c, which callseos_rollback_commit()) executes
in the same address space as theeboot_jump_to_app()that staged the counter.
g_staged_counter/g_staged_valid(core/rollback.c:17-18) are file-static RAM; if
confirm runs in the application image rather than in the bootloader, the staged value is
invisible there and the floor never advances. Pre-existing design, not introduced here,
but it decides whether the fix enforces anything, and I still could not settle it from
the tree. Finding 3's second test case would settle it. -
Not reviewed: the interaction with eBoot#104, which also edits
core/fw_update.c
aroundfinalize(). The two will conflict textually. I checked that #104 does not
address the TLV area (gh pr diff 104 | grep -c tlv→ 0) but did not work out the
merge order.
Automated architecture review of 8c9a363fe4fa — scheduled, model claude-opus-5, checked against the EmbeddedOS Master Design v2.0. Advisory only: this reviewer never approves, requests changes, or merges. Reply here to discuss or push back — a wrong finding is a bug worth reporting.
srpatcha
left a comment
There was a problem hiding this comment.
Review — eBoot#103 "Fix anti-rollback to use authenticated TLV counter"
head: b836304 author: Tanisha1723 ci: none reported
Verdict: Follow-up review (previous head 8c9a363f). The one new commit closes all
three open code findings from the last review — the TLV area is now streamed, the slot
bound landed in core/rollback.c where it was asked for, and eboot_jump_to_app() has
tests — and the PR body is now accurate and its test claims reproduce exactly. What the
same commit introduces is the break the last review's own architecture proposal predicted
and then mis-scoped: it asserted that no in-tree caller streams trailing padding. One
does. xmodem_receive() cannot know the container length, always offers a full 128-byte
block, and is now rejected on the final block of almost every image.
Previous findings — status
- Finding 1 (TLV area never delivered; no image with
tlv_len > 0installable) —
resolved inb8363040.eos_fw_update_write()gainsEOS_FW_STATE_TLV
(core/fw_update.c:162-186) andfinalize()rejects a short tail
(core/fw_update.c:207). Verified:test_write_streams_tlv_then_finalize_rejects_below_floor
reachesEOS_FW_STATE_VERIFYwithctx.tlv_written == TLV_AREA_LENand no raw
eos_hal_flash_write()poke — theplace_tlv_after_payload()helper the last review
flagged is gone. See finding 1 below for the side effect. - Finding 2 (TLV read unbounded against the slot) — resolved in
b8363040, in the
recommended place.slot_remain_from()(core/rollback.c:65) derives the limit from
eos_hal_slot_addr/size()andeos_rollback_read_image_counter()bounds
hdr_size + image_size + tlv_lenagainst it (core/rollback.c:103-111), so all three
call sites get it rather thanjump_app.calone. Covered by
test_tlv_area_must_fit_in_slot(tests/unit/test_tlv_auth.c:293). I re-ran the last
review's probe scenario through that test:EOS_ERR_INVALID, as required. - Finding 3 (
eboot_jump_to_app()untested) — resolved inb8363040.
tests/unit/test_jump_app.cadds both cases that were asked for: below-floor returns
EOS_ERR_ANTI_ROLLBACKwithsim_jump_count == 0and the floor unmoved, above-floor
stages a counter observable througheos_rollback_commit()(sim_counter5 → 9). Ran
it: 2/2. It does not settle the address-space question — see Not checked. - Finding 4 (read → verify → stage duplicated three ways) — untouched. Still three
call sites:core/secure_boot.c:134,stage1/jump_app.c:66,core/fw_update.c:252.
eos_secure_boot()still has no caller instage1/. Correctly deferred; restated as
finding 5 only because it stays open. - Finding 5 (
tests_runhand-maintained literal) — untouched, now in four files.
eBoot#95 is still open. No change wanted here. - Finding 6 (PR body stale and wrong about MSVC) — resolved. The body now names the
real blockers (reserved[]assertion, duplicatepoint_is_identity), the stale
"## Commit" list is gone, and the removed symbol and new test files are both listed. Its
five test claims are not just plausible, they reproduce byte for byte — see Not checked.
Findings
| # | Severity | File:line | Finding | Recommended fix |
|---|---|---|---|---|
| 1 | High | core/fw_update.c:188-192, reached from core/fw_transport_uart.c:240 |
The new "reject bytes past the container" rule breaks the XMODEM transport for any image whose container size is not a multiple of 128. XMODEM carries no length field, so xmodem_receive() hands eos_fw_update_write() exactly XMODEM_BLOCK_SIZE bytes for every block including the last, which the sender pads. Those pad bytes now land past the container: offset < len at :188 sets EOS_FW_STATE_ERROR and returns EOS_ERR_INVALID, the transport sends CAN and aborts, and finalize() then refuses the poisoned context. Verified by streaming a valid, up-to-date, correctly built image in fixed 128-byte blocks against both trees. At b8363040: container 424 bytes (hdr 156 + payload 256 + tlv 12) → 4 blocks × 128 = 512, 88 pad bytes; blocks 1–3 rc=0, block 4 rc=-2 state=6 (ERROR), finalize=-2. Against origin/master (22d8f8b), the identical stream: all four blocks rc=0, finalize=0. This is a regression this commit introduces, not a pre-existing defect. It fails closed, so it denies updates rather than admitting a bad one — hence High, not Critical. The last review's proposal stated "In-tree there are none — both UART transports stream exactly the length they were given"; that is true of uart_raw_receive() and of ymodem_receive(), which clamps to the size parsed from block 0 (core/fw_transport_uart.c:413-414), and false of XMODEM, which has nothing to clamp to. The same failure reaches YMODEM whenever block 0 carries no parseable size, because file_size is then 0 and the clamp at :413 is skipped. Scope, stated plainly: eos_fw_transport_uart_xmodem() is exported from include/eos_fw_transport.h:90 but has no in-tree caller, so no eBoot flow selects it today — the break is to an integrator wiring up the standard bootloader recovery console, not to a path stage 1 takes. |
Block framing is the transport's to strip: XMODEM structurally cannot express an exact length, so the trimming belongs where the container length is known. Give eos_fw_update_write() a companion accessor (eos_fw_update_bytes_wanted(const eos_fw_update_ctx_t *) returning hdr_remaining + payload_remaining + tlv_remaining, 0 once EOS_FW_STATE_VERIFY is reached) and have xmodem_receive() clamp write_len to it exactly as ymodem_receive() already clamps to file_size, ACKing but not forwarding a block once 0 is left. Keep the :188 rejection — it is the right rule for a length-carrying caller. For YMODEM, treat an unparseable block-0 size as a protocol error rather than as "unknown", so it cannot silently fall into the same untrimmed path. |
| 2 | Medium | core/fw_transport_uart.c:188-256, tests/unit/test_fw_transport.c |
xmodem_receive() has no test at all, which is why finding 1 landed in a PR that added 512 lines of tests and ran five suites green. grep -rn xmodem tests/ matches only crc16_xmodem, a CRC helper the YMODEM cases borrow. The 12 passing cases in eboot_test_fw_transport cover YMODEM (9) and raw (3); the one transport with no length field, and therefore the only one whose framing can disagree with the container, is the one never driven. |
Add an XMODEM case to tests/unit/test_fw_transport.c alongside the existing YMODEM ones, using the same scripted-UART harness: script a valid image whose container is deliberately not a multiple of 128, pad the last block, and assert the transfer completes and eos_fw_update_finalize() returns EOS_OK. Written today it fails, which is the point — it is the regression test for finding 1. |
| 3 | Medium | core/fw_update.c:65-67 vs :188-192 |
The same byte stream succeeds or fails depending only on where the caller happens to split its chunks, because over-long input is handled in two places with two outcomes. Verified with a probe at this head: container + 128 trailing bytes in one eos_fw_update_write() call → rc=-2, state ERROR, finalize=-2; the identical bytes split at the container boundary → second call rc=-2 but the entry guard at :65 returns before touching ctx->state, so the context stays in EOS_FW_STATE_VERIFY and finalize returns EOS_OK and installs. A caller cannot tell from the return code which of the two it got, and a transport that retries with different chunking changes the outcome. Fixing finding 1 does not remove this — it only stops XMODEM from being the caller that hits it. |
Decide one behaviour and apply it in both places. Rejecting a post-container write without poisoning the context is the more useful of the two (it lets a padded transport be corrected by its caller), so prefer making :188-192 return EOS_ERR_INVALID without setting EOS_FW_STATE_ERROR once tlv_written == tlv_total — but whichever is chosen, both paths must agree, and the choice needs a test that drives the same bytes through both chunkings. |
| 4 | Low | include/eos_fw_update.h:36-42, :57-62 |
Inserting EOS_FW_STATE_TLV = 3 renumbers VERIFY 3→4, COMPLETE 4→5 and ERROR 5→6, and eos_fw_update_ctx_t gains two uint32_t in the middle. Both are in include/, which .ai/architect.md calls "the contract other repos compile against", and eos_fw_update_get_state() returns the renumbered enum. The PR body lists nine behavioural changes and says nothing about either, which is the silence the brief treats as the finding. Bounding it honestly rather than rounding either way: I checked that the enum is not serialised or persisted anywhere — git grep EOS_FW_STATE_ outside core/fw_update.c and the header matches only tests/unit/test_fw_update.c and a stale struct listing in docs/book/book.md:1320 — so no on-flash or on-wire format changes and .efw is untouched. The exposure is source and link level only, to an out-of-tree consumer that does not recompile. |
One line in the PR body under Changes: the state enum is renumbered and the context struct grows, both require recompilation of any out-of-tree consumer, neither affects the image format or any persisted structure. No code change. |
| 5 | Low | core/secure_boot.c:134, stage1/jump_app.c:66, core/fw_update.c:252 |
Previous finding 4, untouched and still open. Restating only so it is not dropped: three copies of read → verify → stage, and finding 1 is again a divergence between two receivers of the same container — the boot path reads a TLV that a factory-flashed image has, and the XMODEM install path now cannot deliver one. | Unchanged: extract the sequence into one function in core/rollback.c and have all three call it. Still a separate PR — folding a structural change into a behavioural one is what .ai/architect.md prohibits. |
| 6 | Low | tests/unit/test_fw_update.c:230, tests/unit/test_jump_app.c:264, tests/unit/test_tlv_auth.c:364 |
Previous finding 5, untouched, now four hand-maintained tests_run literals across the suite. All three new ones assign after the run, so a forgotten bump reports a spurious failure rather than a silent pass — the safe direction. |
No change here. eBoot#95 replaces this org-wide and is still open; rebase after it lands. |
| 7 | Low | docs/book/book.md ch. 16 "Stream-Based Update Pipeline" |
The chapter describes the update pipeline as "processes data in 256-byte chunks" with no mention of a TLV tail, and this PR makes the tail mandatory and makes over-long input an error. The struct it prints at :1320 was already wrong on master (none of its fields match eos_fw_update_ctx_t), so this PR does not newly break that part — but the described contract is newly incomplete, and per project rules a change that makes existing documentation wrong is not finished. The header doc comment in include/eos_fw_update.h:84-88 was updated correctly, so this is the book only. |
Two sentences in ch. 16: the container is [header][payload][TLV area] and eos_fw_update_write() must be fed all three; bytes past the container are rejected, not discarded. Regenerating or deleting the stale struct listing at :1320 is a separate pre-existing cleanup. |
Architecture conformance
Conforms, on the same reading as both previous reviews, and the new commit strengthens
two of the points rather than changing them.
- §8.1 "Rollback protection where hardware/policy supports it" and
.ai/security.md
("anti-rollback counters actually checked"): the authenticatedEOS_TLV_MIN_SEC_VER
now actually arrives on the install path instead of being read from erased flash, which
is what §8.1 intends and what the previous head could not deliver. .ai/security.md"Fail closed": honoured throughout the new commit. A truncated
TLV tail leaves the context inEOS_FW_STATE_TLV, andfinalize()requires
EOS_FW_STATE_VERIFYand re-checkstlv_written == header.tlv_lenat
core/fw_update.c:207, so a forced context cannot hash a partial region. The new
EOS_FW_STATE_IDLEguard at:66also closes a smaller hole: a write before
eos_fw_update_begin()previously fell through every branch and returnedEOS_OK.
Finding 1 is a fail-closed defect too — it denies updates, it does not admit one.- §5.1: no layering violation.
slot_remain_from()reaches down to
eos_hal_slot_addr/size(), which is the correct direction; nothing points up a tier.
The TCB grows by ~44 lines of bounds checking incore/rollback.c, which is the kind
of growth "minimal and auditable" is for. - §21: Tier 1 (Foundation). Boot-time and install-time rollback policy belongs in
eBoot; correct repo. - §28: the body's five test claims are backed, and I reproduced all five exactly (Not
checked). This is the first head of this PR where the Testing section is evidence rather
than a statement of blockage, and the remaining blockage is still stated plainly.
Finding 1 is also a conformance gap in the wider sense, and it is a different one from
the gap the last review filed. That proposal asked what an install must consist of; this
head implements it and then runs into who is responsible for stripping a transport's own
framing when the transport cannot express the container's length. An addendum is appended
for this run.
Proposed changes
- Fix finding 1 before merge, in
xmodem_receive()and not ineos_fw_update_write().
The:188rejection is correct and is the whole point of the commit; what is missing
is the length XMODEM never had. This is the one item that leaves the shipped result
worse than what it replaces on that transport. - Add the XMODEM transport test (finding 2) in the same PR as the fix — it is the
regression test, and without it the next framing change repeats this. - Resolve the chunk-boundary asymmetry (finding 3) while the state machine is open.
- Add one line to the PR body for the enum and struct change (finding 4) and two
sentences to book ch. 16 (finding 7). No code. - Leave findings 5 and 6 to eBoot#95 and a separate extraction PR.
- Merge order, unchanged and now more pressing: eBoot#94 or #105 first — this head
still does not build on its own (Not checked). Note also that this PR now bounds the
TLV area insideeos_rollback_read_image_counter(), which overlaps eBoot#96
("harden application slot boundary validation"), and still conflicts textually with
eBoot#104 aroundfinalize().
I have opened no fix PR. Finding 1 is the only High, and its fix needs a new accessor on a
public header in the TCB plus a transport change — a design decision, not the "small and
provable" class the review policy permits. It could not be verified as the policy requires
in any case: fix-verify.sh has nothing green to run against while master does not
build. I checked the 20 open PRs and none touches core/fw_transport_uart.c or the XMODEM
path, so nothing is duplicated by saying it here instead.
Not checked
-
CI: no checks ran, again.
checks.txtis empty andgh pr checks 103reports
no checks reported on the 'fix/eboot-test-issue' branch. Three heads of TCB code with
zero automated evidence.mergeStateStatusisBLOCKEDandreviewDecisionis
REVIEW_REQUIRED;mergeableisMERGEABLE, so the block is review, not conflict.
eBoot#90 is what would change this and is still open. -
This head still does not compile as-is, and this PR does not cause it. Verified: I
extractedb8363040withgit archiveto a scratch directory — the repository, its
working tree, its branch (fix/ed25519-low-order-keys) and its stashes were not touched
— and configured the CI's own host build. Both defects the previous two reviews reported
are still present at this head:include/eos_image.h:135,142assert on areserved
member thattlv_len/tlv_hashreplaced, andcore/ed25519_verify.c:338redefines
point_is_identity(already at:281). eBoot#94 and eBoot#105 both address these and
both are open. -
What I did run, in that scratch copy with those two
masterdefects patched out and
nothing else changed:$ cmake -B build/host -DEBLDR_BUILD_TESTS=ON && cmake --build build/host --parallel 4 -- -k $ ctest --test-dir build/host --output-on-failure --no-tests=error -E ed25519 100% tests passed, 0 tests failed out of 22 $ ./build/host/tests/eboot_test_fw_update 4/4 passed $ ./build/host/tests/eboot_test_tlv_auth 9/9 passed $ ./build/host/tests/eboot_test_jump_app 2/2 passed $ ./build/host/tests/eboot_test_fw_transport 12/12 passed $ ./build/host/tests/eboot_test_rollback 14/14 passedEvery number the PR body claims reproduces exactly. That is 22 of the 23 registered
tests;test_ed25519is excluded because it cannot compile —
tests/unit/test_ed25519.c:31,263carries its own duplicate-definition merge artifact
onmaster. The ARM cross build was not run. -
Findings 1 and 3 were each verified with a throwaway probe built in that scratch copy
and in a second scratch copy oforigin/master(22d8f8b), both deleted afterwards.
Neither probe is offered as a test here; finding 2 describes where the real one belongs. -
git diff --check— NOT RUN. The body claims it passed; I did not reproduce that one. -
Not verified on hardware.
eos_hal_monotonic_read/incrementon a real OTP-backed
board is still untested — everything above is simulated flash and a simulated counter.
§28.1 would require a named board before calling this mitigated on hardware. -
test_jump_app.cdoes not settle the carried-forward address-space question, and I
still cannot. The last review hoped finding 3's second test case would decide whether
eos_bootctl_confirm()(core/bootctl.c, which callseos_rollback_commit()) runs in
the same address space as theeboot_jump_to_app()that staged the counter, because
g_staged_counter/g_staged_valid(core/rollback.c:17-18) are file-static RAM. It
cannot:test_jump_stages_tlv_counter_above_floorcallseos_rollback_commit()in the
same process as the jump, which assumes the answer rather than establishing it. If
confirm executes in the application image, the staged value is invisible there and the
floor never advances. Pre-existing design, not introduced here, but it still decides
whether any of this enforces anything. -
slot_remain_from()skips a slot whose base address is 0 (core/rollback.c:79),
which would makeeos_rollback_read_image_counter()returnEOS_ERR_INVALIDfor an
image in it — fail-closed, but a boot failure. Not raised as a finding because I checked
all 83 board ports underboards/and none placesslot_a_addrat 0; it is latent, not
live. Worth knowing before a board port does. -
The two
.cfiles underpoc/were not reviewed —poc/tlv_downgrade_poc.ccalls
eos_rollback_read_image_counter()and is unaffected by this diff, but I did not check
whether it still builds or still demonstrates what it claims. -
Not reviewed: the merge order against eBoot#104, which also edits
core/fw_update.c
aroundfinalize(), and against eBoot#96, which now overlaps this PR's slot bound. Both
will conflict textually with this head; I did not work out the resolution.
Automated architecture review of b83630401f21 — scheduled, model claude-opus-5, checked against the EmbeddedOS Master Design v2.0. Advisory only: this reviewer never approves, requests changes, or merges. Reply here to discuss or push back — a wrong finding is a bug worth reporting.
srpatcha
left a comment
There was a problem hiding this comment.
Review — eBoot#103 "Fix anti-rollback to use authenticated TLV counter"
head: 984a0d3 author: Tanisha1723 ci: none reported
Verdict: Follow-up review (previous head b8363040). The one new commit closes the
last review's High finding in the place it was asked for — xmodem_receive() now clamps to
a new eos_fw_update_bytes_wanted() accessor instead of pushing its own block padding into
the container — and closes findings 2, 3, 4 and 7 as well. I reproduced all five of the
body's test claims exactly, and additionally ran the ARM cross build, which no previous
review of this PR ran: it passes. What the same commit leaves is the mirror image of the
finding it fixes: YMODEM was hardened against a missing block-0 size but was not given the
container-aware clamp XMODEM got, so it still clamps to the sender's declared file size
and now aborts a transfer that completed on master. Separately, and larger than this PR:
no in-tree tool can produce an image carrying the TLV counter this PR makes authoritative.
Previous findings — status
- Finding 1 (XMODEM final-block padding rejected; container-not-multiple-of-128
uninstallable) — resolved in984a0d39.xmodem_receive()(core/fw_transport_uart.c:244-262)
loops oneos_fw_update_bytes_wanted()(core/fw_update.c:303) and forwards only what
the container still wants. Verified beyond the shipped test: I streamed a TLV-carrying
424-byte container (hdr 156 + payload 256 + TLV 12, so block 4 carries 88 pad bytes and
the payload→TLV transition) in 128-byte XMODEM blocks through the real ops table —
rc=0, stateVERIFY,tlv_written 12/12, flash byte-identical to the container,
finalize=0. The equivalent stream returned-2atb8363040. - Finding 2 (
xmodem_receive()had no test at all) — resolved.
test_xmodem_padded_final_block_completes_and_finalizes(tests/unit/test_fw_transport.c:565)
drives the ops table through the scripted-UART harness. Ran it: 13/13. One gap in what it
covers — see finding 4 below. - Finding 3 (same bytes succeed or fail depending on chunking) — partially addressed.
The state-poisoning half is fixed:core/fw_update.c:193no longer sets
EOS_FW_STATE_ERROR, so both the entry guard (:65-67) and the trailing check agree —
returnEOS_ERR_INVALID, leave the context inVERIFY.test_trailing_byte_is_rejected_the_same_across_chunk_boundaries
asserts equality of the return code, the state andtlv_written, and it passes. It stops
one field short of the claim in its name; see finding 5. - Finding 4 (enum renumbering and struct growth undocumented) — resolved. The body now
carries a Compatibility section naming theEOS_FW_STATE_TLV = 3renumbering, the two
newuint32_t, and the recompilation requirement. Its assertion that no persisted or
on-wire format changes is correct — I re-checkedgit grep EOS_FW_STATE_at this head:
outsidecore/fw_update.cand the header it matches only the unit tests and the stale
struct listing indocs/book/book.md. - Finding 5 (read → verify → stage duplicated three ways) — untouched. Still
core/secure_boot.c:134,stage1/jump_app.c:66,core/fw_update.c:253, and
eos_secure_boot()still has no caller instage1/. Correctly deferred; restated as
finding 7 only so it is not dropped. - Finding 6 (
tests_runhand-maintained literals) — untouched, now four of them
(test_fw_transport.c:669→ 13,test_fw_update.c:300→ 6,test_tlv_auth.c:364→ 9,
test_jump_app.c:268→ 2). eBoot#95 is still open. No change wanted here. - Finding 7 (book ch. 16 made incomplete) — resolved.
docs/book/book.md:1324adds the
two sentences that were asked for, verbatim in substance. The pre-existing stale struct
listing at:1320is untouched, which is correct — it was already wrong onmaster.
Findings
| # | Severity | File:line | Finding | Recommended fix |
|---|---|---|---|---|
| 1 | Medium | core/fw_transport_uart.c:437-441 |
YMODEM did not get the fix XMODEM got, and a transfer that completed on master now aborts. The write clamp is still write_len = file_size - total_received — the size the sender declared for the file, not the length of the container the header describes. When the file is longer than hdr_size + image_size + tlv_len, the surplus reaches eos_fw_update_write(), hits the offset < len rejection at core/fw_update.c:193, and ymodem_receive() sends CAN and returns. Verified by running the identical scripted transfer against both trees with a tlv_len = 0 image so the only difference is the trailing-byte rule: declared size 512 over a 384-byte container gives rc=0, can=0 at origin/master (22d8f8b) and rc=-2, can=1 at 984a0d39. The image was fully and correctly written to slot B in both cases — the transfer is failed after the bytes are already there, so this is denial of updates, not a bad install, hence Medium and not Critical. Narrower than the XMODEM break the last review filed, because the in-tree packers emit a file whose length equals the container exactly (tools/imgpack.py, tools/eos_sign.py), so the golden path is unaffected; it bites a sender that reports an on-disk size for an artifact with any trailing or padded region, which is the ordinary output of a packaging step that aligns to a flash page. eos_fw_transport_uart_ymodem() is exported from include/eos_fw_transport.h and has no in-tree caller, the same scope the XMODEM finding had. |
Use the same accessor the XMODEM path now uses. Replace the file_size clamp with eos_fw_update_bytes_wanted(ctx), keeping file_size only as the protocol-level end-of-file signal it already is at :456. That makes the container the single authority on how many bytes are image data, on both block transports, and removes the disagreement between them. |
| 2 | Medium | core/fw_transport_uart.c:244-262, verified via probe |
XMODEM silently accepts and discards whole data blocks once the container is complete, which is exactly the behaviour the commit changed core/fw_update.c to stop doing, reinstated one layer up. Once eos_fw_update_bytes_wanted() returns 0 the while breaks immediately, the block is ACKed, expected_blk advances, and the loop reads the next one. Verified: appending one full 128-byte data block of 0xDE after a complete 424-byte container gives rc=0, no NAK, no CAN, and finalize=0 — an entire extra block, not padding, absorbed and reported as a successful transfer. The installed bytes are only ever the container and are hash-verified, so nothing bad is admitted; what is lost is any ability to notice that the sender and the receiver disagree about which artifact was sent. The PR body states "Reject bytes beyond the complete container rather than silently discarding them", which is true of eos_fw_update_write() and not true of the transport that calls it. Untested in either direction. |
XMODEM padding is bounded by construction: it can only ever be the tail of the block in which the container ends. Track that — once wanted reaches 0, tolerate the remainder of the current block and treat a subsequent SOH data block as a protocol error (CAN, EOS_ERR_INVALID), rather than tolerating an unbounded tail. Add the case to tests/unit/test_fw_transport.c next to the padded-block test. |
| 3 | Medium | tools/imgpack.py:74, tools/eos_sign.py:105 |
Nothing in the repository can produce an image carrying the counter this PR makes authoritative. Both packers write b'\x00' * 30 for tlv_len(2) + tlv_hash(28) unconditionally, with a comment saying 0 = no authenticated TLV area; tests/production_test_suite.py:150 does the same. grep -rl EOS_TLV_INFO_MAGIC over .c/.h outside tests/ and poc/ returns only core/image_tlv.c and include/eos_image_tlv.h — the reader, never a writer. So every artifact the tree can build has tlv_len == 0, eos_rollback_read_image_counter() returns counter 0 by the documented conservative path (core/rollback.c:129), and the anti-rollback floor this PR wires into both the boot and the install path can never be raised by an in-tree image. The behaviour is coherent and fail-closed — a 0 counter can only fail against a floor, never advance one — so this is not a bypass and not a defect introduced here. It is why the feature cannot reach §28 Validated however many unit tests pass: the consumer is complete and has no producer. Raised on this PR because this is the PR that makes the counter load-bearing. |
Out of scope for this PR — say so in the body under Scope, next to the existing note about master and the duplicated boot logic, so the gap is recorded rather than implied closed by the passing tests. The work itself is a --min-sec-ver N option on tools/imgpack.py and tools/eos_sign.py that emits an EOS_TLV_MIN_SEC_VER entry and sets tlv_len/tlv_hash over it, plus a round-trip test that packs an image and drives it through eos_rollback_read_image_counter(). That is its own PR, and it is the prerequisite for calling T-202 mitigated on anything. |
| 4 | Medium | tests/fuzz/fuzz_fw_update.c:18-20 |
.ai/security.md requires fuzz coverage, not just unit tests, for every externally reachable parser, and eos_fw_update_write() is one — this PR rewrites its state machine and adds a new region to it. The harness that names it fuzzes an API that does not exist: it declares extern int eos_fw_update_init(void), eos_fw_update_process_chunk(const uint8_t*, size_t) and eos_fw_update_finalize(void), none of which are the real signatures in include/eos_fw_update.h. So the container parser has no fuzz coverage in fact, only in the file name. Not a finding against this PR and no fix is offered here: eBoot#99 ("point every harness at the API it claims to fuzz") and eBoot#101 ("compile the harnesses, and refuse the extern that hid the bugs") are both open and both cover exactly this. Recorded so the new streaming path is re-fuzzed once one of them lands. |
Nothing in this PR. After #99 or #101 merges, add a fuzz_fw_update corpus entry that walks a container across chunk boundaries — header split, payload→TLV straddle, short TLV tail, trailing bytes — since those are the transitions this PR introduced. |
| 5 | Low | core/fw_update.c:193 vs :196, tests/unit/test_fw_update.c:245 |
Previous finding 3 is not quite closed. eos_fw_update_write() returns before ctx->total_received += len, so the container bytes consumed in a call that also carried a trailing byte are never counted. Verified: identical bytes, container + one trailing byte, single (one call) → total_received = 0, eos_fw_update_progress() = 0%; split (two calls) → total_received = 424, progress = 100%. eos_fw_update_progress() is public API in include/eos_fw_update.h, so a transport reporting progress shows 0% for a container it received in full. test_trailing_byte_is_rejected_the_same_across_chunk_boundaries asserts equality of the return code, the state and tlv_written, and stops before the one field that still differs — so the test named for this property passes while the property does not fully hold. |
Count what was actually consumed rather than what was offered: ctx->total_received += (uint32_t)offset; before the offset < len return, and drop the separate += len on the success path (they are the same value when offset == len). Then extend the existing test with ASSERT(single.total_received == split.total_received);. |
| 6 | Low | tests/unit/test_fw_transport.c:521-545 |
The XMODEM regression test builds its image with hdr.tlv_len = 0 (:540) and flags = 0, so the one transport test for the padded final block never drives EOS_FW_STATE_TLV — the only state this PR series added — and never exercises a payload→TLV transition inside a padded block, which is the case the whole PR exists for. The transitions are covered in isolation by test_bytes_wanted_tracks_the_remaining_container, but only at exact chunk boundaries chosen to align with them, never straddling. I confirmed by probe that the real combination works, so this is missing coverage rather than a defect. |
Give build_unaligned_image() a TLV area: 156 + 256 + 12 = 424 bytes is already the length the test uses and already splits the payload→TLV boundary inside block 4. tests/unit/test_fw_update.c:140-155 has the eight lines of TLV construction to lift. |
| 7 | Low | core/secure_boot.c:134, stage1/jump_app.c:66, core/fw_update.c:253 |
Previous finding 5, untouched and still open. Restating only so it is not dropped: three copies of read → verify → stage, and findings 1 and 2 are once again a divergence between two receivers of the same container — XMODEM was taught the container's length this commit and YMODEM was not. | Unchanged: extract the sequence into one function in core/rollback.c and have all three call it. Still a separate PR — folding a structural change into a behavioural one is what .ai/architect.md prohibits. |
| 8 | Low | tests/unit/test_fw_update.c:300, test_fw_transport.c:669, test_jump_app.c:268, test_tlv_auth.c:364 |
Previous finding 6, untouched. Four hand-maintained tests_run literals; all four assign after the run, so a forgotten bump reports a spurious failure rather than a silent pass — the safe direction. |
No change here. eBoot#95 replaces this org-wide and is still open; rebase after it lands. |
Architecture conformance
Conforms, on the same reading as all three previous reviews. The new commit does not move
any boundary.
- §8.1 "Rollback protection where hardware/policy supports it" and
.ai/security.md
("anti-rollback counters actually checked"): unchanged by this commit and still correct —
the authenticatedEOS_TLV_MIN_SEC_VERis what both the boot path and the install path
read, bound to the signed header throughtlv_len/tlv_hash. Finding 3 is the honest
qualifier on that: the check is right and nothing in the tree can yet feed it a counter. .ai/security.md"Fail closed": honoured. Both new rejections fail closed — YMODEM
on an unusable block-0 size (core/fw_transport_uart.c:407) and the container rule at
core/fw_update.c:193. Finding 1 is a fail-closed defect too: it denies an update, it
does not admit one. The one place worth naming explicitly is the removal of
ctx->state = EOS_FW_STATE_ERRORfrom the trailing-byte path: a context that saw
trailing bytes now stays installable, andeos_fw_update_write()reports the error only
through its return value. That is what the last review recommended and it is safe on
inspection — the surplus bytes are never written to flash and never enter the SHA/CRC, so
whatfinalize()installs is exactly the container it verified — but it does move the
burden onto the caller to check a return code. All three in-tree transports do. Recorded
rather than filed as a finding.- §5.1: no layering violation.
eos_fw_update_bytes_wanted()is a read-only accessor
over the caller's own context; the transport calls down into the update core, which is
the correct direction, and nothing points up a tier. TCB growth is ~20 lines of accessor
plus a clamp loop. - §21: Tier 1 (Foundation). Boot-time and install-time rollback policy, and the update
transports, belong in eBoot; correct repo. - §28: the body's five test claims are backed and all five reproduce exactly (Not
checked). The Scope section states plainly what the PR does not address. The one thing
the body asserts that does not hold is the transport-level half of "bytes beyond the
complete container are rejected rather than discarded" — finding 2.
Finding 3 is a conformance gap of the design's own, not of this PR: §8.1 requires rollback
protection to be checked and §15.1 requires signed metadata, and no section makes anyone
responsible for emitting the anti-rollback counter. A proposal is appended for this run.
It is distinct from the two container/transport proposals already filed against this PR —
those ask what an install must consist of and who strips a transport's framing; this one
asks who writes the field in the first place.
Proposed changes
- Fix finding 1 in
ymodem_receive()with the accessorxmodem_receive()already uses.
It is a two-line change and it is the only item that leaves a shipped result worse than
masteron a transport. - Bound the XMODEM tail (finding 2) and add the block-after-container test, in the same
PR as 1 — both are the same question about who owns block framing. - Extend the padded-block test to carry a TLV area (finding 6) and add the
total_receivedassertion (finding 5). Small, and they close the two claims that
currently overstate slightly. - Add one line to Scope for the missing packer support (finding 3). No code here; the
packer work is its own PR and is the prerequisite for any hardware validation. - Leave findings 4, 7 and 8 to eBoot#99/#101, a separate extraction PR, and eBoot#95.
- Merge order, unchanged and now the last thing standing: eBoot#94 or #105 first —
this head still does not build on its own (Not checked). This PR still overlaps eBoot#96
on the slot bound ineos_rollback_read_image_counter()and still conflicts textually
with eBoot#104 aroundfinalize(); I confirmed #104 touchescore/fw_update.cand does
not touchcore/fw_transport_uart.c.
I have opened no fix PR. Nothing at this head is rated High, and the review policy permits
fix PRs only for High findings that are small and provable; finding 1 is provable and
small, but it is Medium, and fix-verify.sh has nothing green to run against in any case
while master does not build. I checked all 20 open PRs: none touches
core/fw_transport_uart.c and none adds TLV emission to tools/imgpack.py or
tools/eos_sign.py (eBoot#107 edits imgpack.py for version-component overflow only), so
nothing here duplicates existing work.
Not checked
-
CI: no checks ran, for the fourth head running.
checks.txtis empty,
gh pr checks 103reportsno checks reported on the 'fix/eboot-test-issue' branch, and
statusCheckRollupis[].mergeStateStatusisBLOCKEDwithmergeable: MERGEABLE
andreviewDecision: REVIEW_REQUIRED, so the block is review, not conflict. Four heads of
TCB code with zero automated evidence behind them. eBoot#90 is what would change this and
is still open. -
This head does not compile as-is, and this PR does not cause it. Verified afresh
rather than inherited: I extracted984a0d39withgit archiveto a scratch directory
— the repository, its working tree, its branch (fix/ed25519-low-order-keys) and its
stashes were not touched — and ran the CI's own host configuration unmodified. The first
translation unit fails:include/eos_image.h:135,142assert on areservedmember that
tlv_len/tlv_hashreplaced; 1 target of 40-odd builds. The duplicate
point_is_identityatcore/ed25519_verify.c:281and:338is present too. eBoot#94 and
eBoot#105 both fix these and both are open. -
What I did run, in a second scratch copy with only those two
masterdefects patched
out and nothing else changed:$ cmake -B build/host -DEBLDR_BUILD_TESTS=ON && cmake --build build/host --parallel 4 -- -k $ ctest --test-dir build/host --output-on-failure --no-tests=error -E ed25519 100% tests passed, 0 tests failed out of 22 $ ./build/host/tests/eboot_test_fw_update 6/6 passed $ ./build/host/tests/eboot_test_tlv_auth 9/9 passed $ ./build/host/tests/eboot_test_jump_app 2/2 passed $ ./build/host/tests/eboot_test_fw_transport 13/13 passed $ ./build/host/tests/eboot_test_rollback 14/14 passedEvery number the PR body claims reproduces exactly. That is 22 of the 23 registered
tests;test_ed25519is excluded because it cannot compile —
tests/unit/test_ed25519.c:31,263carries its own duplicate-definition merge artifact on
master, independent of the two above. -
The ARM cross build PASSES, and this is the first review of this PR to run it. Same
patched scratch copy, the exact configuration from.github/workflows/build.yml:44-52:$ cmake -B build-arm -DCMAKE_SYSTEM_NAME=Generic -DCMAKE_C_COMPILER=arm-none-eabi-gcc \ -DEBLDR_BOARD=stm32f4 \ -DCMAKE_C_FLAGS="-mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -specs=nosys.specs" $ cmake --build build-arm --parallel 4 # exit 0, ebldr_stage0.elf linkedNo warning or error in any file this PR touches, on either the host or the ARM build. The
warnings that do appear (core/power_init.c:84,core/ecc_scrub.c:65,
core/bmc_handoff.c:55-Wformatonuint32_tunder a 32-bitlong;
core/ed25519_verify.c:313unusedkey_has_prime_order) are all pre-existing and in
untouched files. -
git diff --check— PASS.git diff --check 22d8f8b..984a0d39is clean. The body
claims this and it is the one claim the last review did not reproduce; it holds. -
Findings 1, 2 and 5 were each verified with a throwaway probe built in that scratch
copy, and finding 1 additionally in a second scratch copy oforigin/master(22d8f8b) so
the two trees ran the identical scripted transfer. Both scratch copies were deleted
afterwards. None of the probes is offered as a test here; findings 2, 5 and 6 say where
the real ones belong. -
Not verified on hardware.
eos_hal_monotonic_read/incrementon a real OTP-backed
board is still untested — everything above is simulated flash and a simulated counter.
§28.1 would require a named board, and finding 3 says there is not yet an artifact to put
on one. -
The address-space question is still unsettled and I still cannot settle it. Carried
forward from all three previous reviews: whethereos_bootctl_confirm()
(core/bootctl.c, which callseos_rollback_commit()) executes in the same address space
as theeboot_jump_to_app()that staged the counter.g_staged_counter/g_staged_valid
(core/rollback.c:17-18) are file-static RAM;test_jump_stages_tlv_counter_above_floor
calls commit in the same process as the jump, which assumes the answer. If confirm runs in
the application image the staged value is invisible there and the floor never advances.
Pre-existing design, not introduced here, but together with finding 3 it means neither end
of the anti-rollback mechanism has been shown to work outside a unit test. -
slot_remain_from()still skips a slot based at address 0 (core/rollback.c:79).
Unchanged from the last review, still latent — I re-checked and none of the 83 board ports
underboards/placesslot_a_addrat 0. -
The two
.cfiles underpoc/were not reviewed.poc/tlv_downgrade_poc.ccalls
eos_rollback_read_image_counter()and is untouched by this diff; I did not check whether
it still builds or still demonstrates what it claims. -
Merge order against eBoot#96 and eBoot#104 was not worked out. Both will conflict
textually with this head — #96 on the slot bound, #104 onfinalize(). I confirmed the
overlap and did not resolve it. -
existing-comments.txtis empty, so nothing here repeats a human reviewer; there are
none on this PR.
Automated architecture review of 984a0d39a8e8 — scheduled, model claude-opus-5, checked against the EmbeddedOS Master Design v2.0. Advisory only: this reviewer never approves, requests changes, or merges. Reply here to discuss or push back — a wrong finding is a bug worth reporting.
srpatcha
left a comment
There was a problem hiding this comment.
Review — eBoot#103 "Fix anti-rollback to use authenticated TLV counter"
head: 180680c author: Tanisha1723 ci: none reported
Verdict: Follow-up review (previous head 984a0d39). The one new commit closes the two
transport findings from the last review in the places they were asked for — YMODEM now
clamps to the container instead of the sender's declared file size, XMODEM refuses a whole
data block that arrives after the container is complete — plus the total_received
accounting and the TLV-less padded-block test. I reproduced every claim in the body and ran
the ARM cross build; both pass. What the commit does not do is give YMODEM the guard it
gave XMODEM: the shared helper stops offering bytes but the YMODEM loop keeps ACKing whole
blocks past the container and reports the transfer as successful. That is the same finding
the last review filed against XMODEM, now surviving in the other transport — the third
consecutive head where a fix landed on one of the two block transports and not the other.
Previous findings — status
- Finding 1 (YMODEM clamped to
file_size, aborting a transfer that completed on
master) — resolved in180680cc.ymodem_receive()now computesfile_lenas the
YMODEM framing bound only (core/fw_transport_uart.c:472-475) and hands it to the new
sharedwrite_container_bytes()(:203-222, called at:492), which stops at
eos_fw_update_bytes_wanted(). Verified beyond the shipped test: a declared file size of
896 over a 424-byte TLV-carrying container givesrc=0, no CAN,tlv_written 12/12,
flash byte-identical to the container,finalize=0. The recommended fix was taken
verbatim, including keepingfile_sizeas the end-of-file signal. - Finding 2 (XMODEM silently accepted and discarded whole blocks past the container) —
resolved for XMODEM, still open for YMODEM.core/fw_transport_uart.c:275-284now
sends CAN and returnsEOS_ERR_INVALIDwhen a correctly sequenced block arrives with
bytes_wanted() == 0, bounding the tolerated surplus to the remainder of the block the
container ends in, exactly as recommended.
test_xmodem_data_block_after_container_is_rejected(tests/unit/test_fw_transport.c)
covers it; I ran it. The same shape over YMODEM is finding 1 below. - Finding 3 (nothing in the repo can emit the counter this PR makes authoritative) —
untouched. Re-checked at this head:tools/imgpack.py:74andtools/eos_sign.py:105
still writeb'\x00' * 30fortlv_len(2) + tlv_hash(28), and
git grep -l EOS_TLV_INFO_MAGIC 180680cc -- '*.c' '*.h' '*.py'outsidetests/and
poc/still returns only the reader (core/image_tlv.c,include/eos_image_tlv.h). The
one-line Scope addition the last review asked for was not made. Restated as finding 5. - Finding 4 (fuzz harness fuzzes an API that does not exist) — untouched, correctly:
eBoot#99 and eBoot#101 both cover it and both are still open. Not a finding against this
PR. Recorded again only so the new streaming path is re-fuzzed once one of them lands. - Finding 5 (
total_receivednot counted on a call that also carried a trailing byte) —
resolved in180680cc.core/fw_update.c:198now addsoffsetbefore the
offset < lenreturn and the separate+= lenis gone, which is the recommended fix
exactly.test_trailing_byte_is_rejected_the_same_across_chunk_boundariesgained the
three assertions that were asked for, including
eos_fw_update_progress(&single) == eos_fw_update_progress(&split). Verified
independently: a 3-block partial transfer now reportstotal_received = 384,
progress = 93,bytes_wanted = 40, all consistent. - Finding 6 (padded-block test never drove
EOS_FW_STATE_TLV) — resolved.
build_unaligned_image()was replaced bybuild_container(), which builds
156 + 256 + 12 = 424 bytes with a real[tlv_info][entry_hdr][uint32]area authenticated
byhdr.tlv_hash, so block 4 now carries the payload→TLV transition, the whole TLV area
and 88 bytes of padding. Both block-transport tests assertctx.tlv_written == 12. - Finding 7 (read → verify → stage duplicated three ways) — untouched. Still
core/secure_boot.c:134,stage1/jump_app.c:66,core/fw_update.c:259. Correctly
deferred; restated as finding 6 only so it is not dropped. - Finding 8 (
tests_runhand-maintained literals) — untouched, still four
(test_fw_transport.c:768→ 15,test_fw_update.c:307→ 6,test_tlv_auth.c:364→ 9,
test_jump_app.c:268→ 2). eBoot#95 is still open. No change wanted here.
Findings
| # | Severity | File:line | Finding | Recommended fix |
|---|---|---|---|---|
| 1 | Medium | core/fw_transport_uart.c:487-502 |
YMODEM did not get the guard XMODEM got this commit, so it still accepts and discards whole data blocks once the container is complete. write_container_bytes() breaks as soon as eos_fw_update_bytes_wanted() returns 0 and returns EOS_OK (:210, :221); ymodem_receive() then adds the whole block to total_received (:499), ACKs it (:500) and reads the next one. The only thing that ever stops this is file_len == 0 at :481 — the sender's own declared size, parsed from block 0 and bounded only by UINT32_MAX at :427. Verified by probe against this head: three full 128-byte blocks of 0xDE-style filler after a complete 424-byte container, declared size 896, give rc=0, no CAN, tlv_written 12/12, finalize=0 — 384 unaccounted bytes absorbed and the install allowed to proceed. The identical block sequence over XMODEM gives rc=-2 and CAN. Nothing bad is installed (flash is byte-identical to the container and hash-verified), so this is loss of detection, not a bypass — hence Medium, the same rating the last review gave the XMODEM half. The PR body's own justification for the XMODEM guard ("accepting them would accept an unbounded amount of unaccounted data", :277-279) applies unchanged here: file_size is asserted by the same sender whose bytes are in question, so it is not an independent bound. |
Mirror the XMODEM guard. After the retransmit and sequence checks and before write_container_bytes(), reject a block that begins with the container already complete: if (eos_fw_update_bytes_wanted(ctx) == 0) { c = XMODEM_CAN; eos_hal_uart_send(&c, 1); return EOS_ERR_INVALID; }. Padding inside the block the container ends in is still consumed by the existing clamp, which is the distinction the XMODEM comment already draws. Add the case next to test_ymodem_declared_size_larger_than_container_is_clamped, which today covers only surplus within the final block (424 of a declared 512 — 88 bytes, less than one block) and so cannot see this. |
| 2 | Low | core/fw_update.c:292-307, denominator at :299 |
eos_fw_update_progress() reports 100% while the TLV area is still outstanding. total is sizeof(eos_image_header_t) + payload_total and omits tlv_total, but total_received now counts TLV bytes too, so the ratio saturates the moment the payload completes and is then clamped at :304. Verified: after writing header + payload of the 424-byte container, state = EOS_FW_STATE_TLV, eos_fw_update_bytes_wanted() = 12, eos_fw_update_progress() = 100. Both are public API in include/eos_fw_update.h, and they now disagree about whether the transfer is finished. Pre-existing arithmetic, but this PR is what put a fourth region into the container and made the two answers diverge. |
total = sizeof(eos_image_header_t) + payload_total + tlv_total, inside the same __builtin_add_overflow guard, and assert in test_fw_update.c that progress is below 100 while bytes_wanted() != 0. |
| 3 | Low | core/fw_transport_uart.c:238-242 (XMODEM), :356-366 (YMODEM) |
Both receivers return EOS_OK on EOT no matter how much of the container is still outstanding. Verified: three of the container's four blocks then EOT gives receive_rc = 0 with state = EOS_FW_STATE_PAYLOAD and bytes_wanted = 40, on both transports. End-to-end this is fail-closed — eos_fw_transport_update() (:41) calls finalize(), which returns EOS_ERR_INVALID because the state is not VERIFY — so it is Low, not a security defect. It is filed because this commit hardened the opposite direction on both transports, eos_fw_update_bytes_wanted() makes the check one line, and both eos_fw_transport_uart_xmodem() and ..._ymodem() are exported from include/eos_fw_transport.h, so a caller that drives ops->receive itself is told a truncated transfer succeeded. |
At each EOT, before breaking: if (eos_fw_update_bytes_wanted(ctx) != 0) return EOS_ERR_INVALID;. Send the ACK first so the sender still sees a completed handshake. |
| 4 | Low | PR body — "Testing Environment" and "Transport handling" | The body has gone stale against its own head. It claims eboot_test_fw_transport — 13/13 passed; this head has 15 tests and reports 15/15 (I ran it). The Transport handling section describes only the previous head's work — the XMODEM container clamp and the YMODEM block-0 size hardening — and does not mention this commit's two behavioural changes: the new hard rejection of a data block after the container (core/fw_transport_uart.c:275-284) and the YMODEM switch from a file_size clamp to a container clamp (:487-497). The first is a change to what a sender may send and belongs in Compatibility, not only in Changes. Per §28 an unsupported or superseded claim is itself the finding, even where the underlying work is sound — and here it is sound; only the description is behind. |
Regenerate the five test lines from a run of this head (13/13 → 15/15; the other four are still exact). Add two Transport handling bullets for the changes above, and one Compatibility line stating that an XMODEM sender which appended whole blocks past the image now gets CAN where it previously got ACK. While editing, add the Scope line the last review asked for (finding 5 below). |
| 5 | Medium (carried, untouched) | tools/imgpack.py:74, tools/eos_sign.py:105 |
Previous finding 3, restated because it is still open and this PR is still the one that makes the counter load-bearing. No in-tree tool emits an EOS_TLV_MIN_SEC_VER entry, so every artifact this repository can build has tlv_len == 0 and the anti-rollback floor can never be raised by an in-tree image. Coherent and fail-closed — a 0 counter can only fail against a floor, never advance one — and not a defect introduced here. It is why the feature cannot reach §28 Validated: the consumer is complete and has no producer. |
Unchanged: out of scope for this PR, but say so in the body's Scope section next to the existing notes on master and the duplicated boot logic. The work itself is a --min-sec-ver N option on both packers plus a round-trip test through eos_rollback_read_image_counter(), and it is its own PR. |
| 6 | Low (carried, untouched) | core/secure_boot.c:134, stage1/jump_app.c:66, core/fw_update.c:259 |
Previous finding 7. Three copies of read → verify → stage, and eos_secure_boot() still has no caller anywhere in stage1/. Restated only so it is not dropped. Finding 1 is once again a divergence between two receivers of the same container — the third head in a row where one block transport was taught something the other was not. |
Unchanged: extract into one function in core/rollback.c and have all three call it. Separate PR — .ai/architect.md forbids folding a structural change into a behavioural one. |
| 7 | Low (carried, untouched) | test_fw_transport.c:768, test_fw_update.c:307, test_tlv_auth.c:364, test_jump_app.c:268 |
Previous finding 8. Four hand-maintained tests_run literals; all four assign after the run, so a forgotten bump reports a spurious failure rather than a silent pass — the safe direction. The transport literal was correctly bumped 13 → 15 this commit, which is also what makes the PR body's stale 13/13 visible. |
No change here. eBoot#95 replaces this org-wide and is still open; rebase after it lands. |
Architecture conformance
Conforms, on the same reading as all four previous reviews. The new commit moves no
boundary.
- §8.1 "Rollback protection where hardware/policy supports it" and
.ai/security.md
("anti-rollback counters actually checked"): unchanged by this commit and still correct.
The authenticatedEOS_TLV_MIN_SEC_VERis what both the boot path and the install path
read, bound to the signed header throughtlv_len/tlv_hash. Finding 5 remains the
honest qualifier: the check is right and nothing in the tree can yet feed it a counter. .ai/security.md"Fail closed": honoured, and the new XMODEM rejection strengthens
it — a sender and a receiver that disagree about the artifact now fail rather than
proceed. Finding 1 is the gap in that, and it fails open on detection rather than open
on content: the surplus never reaches flash and never enters the SHA/CRC, so what
finalize()installs is still exactly the container it verified. Finding 3 is fail-closed
end-to-end througheos_fw_transport_update()and only misreports at theops->receive
boundary. No security step reports success without having run.- §5.1: no layering violation.
write_container_bytes()is a static helper in the
transport that calls down into the update core through a read-only accessor — the correct
direction. Nothing points up a tier. TCB growth this commit is ~35 lines net in
core/fw_transport_uart.c, all of it framing logic that was previously inline in
xmodem_receive(); the extraction removes the duplication the last review would otherwise
have gained. - §21: Tier 1 (Foundation). Install-time rollback policy and the update transports
belong in eBoot; correct repo. §21.1 is not engaged — nothing is being split out. - §28: the four unchanged test claims in the body reproduce exactly; the fifth is stale
(finding 4). The Scope section still states plainly what the PR does not address, which is
the right posture, but it has not been extended for finding 5.
The design-level question behind finding 1 is not one this PR could have settled from the
document: §8.1 as amended by the proposal filed against b8363040 says a length-less
transport must "stop offering bytes when the answer is zero while still completing its own
handshake", which describes YMODEM's behaviour here and would forbid XMODEM's new CAN. The
security posture the last review asked for and this commit implemented is the opposite. An
addendum is appended for this run.
Proposed changes
- Add the YMODEM guard (finding 1) — five lines, mirroring
:275-284, plus the test case.
It is the only item that leaves the two block transports disagreeing about the same
protocol question for a third consecutive head. - Fix the progress denominator (finding 2) and the EOT completeness check (finding 3) in
the same PR; both are one line and both are consequences of the container gaining a
region. - Correct the PR body (finding 4) and add the Scope line for finding 5. No code.
- Leave findings 5, 6 and 7 to a packer PR, a separate extraction PR, and eBoot#95.
- Merge order, unchanged and still the last thing standing: eBoot#94 or #105 first —
this head still does not build on its own (Not checked). Still overlaps eBoot#96 on the
slot bound ineos_rollback_read_image_counter()and still conflicts textually with
eBoot#104 aroundfinalize().
I have opened no fix PR. Nothing at this head is rated High, and the policy permits fix PRs
only for High findings that are small and provable. Finding 1 is small and provable but is
Medium, and fix-verify.sh has nothing green to run against while master does not build.
I checked all 20 open PRs: none touches core/fw_transport_uart.c (confirmed by
gh pr diff --name-only for #94, #95, #96, #104, #105, #106, #107) and none adds TLV
emission to either packer, so nothing here duplicates existing work.
Not checked
-
CI: no checks ran, for the fifth head running.
checks.txtis empty,
gh pr checks 103reportsno checks reported on the 'fix/eboot-test-issue' branch, and
statusCheckRollupis[].mergeStateStatusisBLOCKEDwithmergeable: MERGEABLE
andreviewDecision: REVIEW_REQUIRED, so the block is review, not conflict. Five heads of
TCB code with zero automated evidence behind them. eBoot#90 is what would change this and
is still open. -
This head does not compile as-is, and this PR does not cause it. Verified afresh: I
extracted180680ccwithgit archiveinto a scratch directory — the repository, its
working tree (clean), its branch (fix/ed25519-low-order-keys) and its stashes were not
touched — and ran the CI's own host configuration unmodified. It fails on
include/eos_image.h:135,142, which assert on areservedmember that
tlv_len/tlv_hashreplaced.core/ed25519_verify.cstill definespoint_is_identity
twice (:281,:338). Both are onorigin/master(22d8f8b); eBoot#94 and eBoot#105 both
fix them and both are open. -
What I did run, in a scratch copy with only those two
masterdefects patched out and
nothing else changed:$ cmake -B build/host -DEBLDR_BUILD_TESTS=ON && cmake --build build/host --parallel 4 -- -k $ ctest --test-dir build/host --output-on-failure --no-tests=error -E ed25519 100% tests passed, 0 tests failed out of 22 $ ./build/host/tests/eboot_test_fw_update 6/6 passed $ ./build/host/tests/eboot_test_tlv_auth 9/9 passed $ ./build/host/tests/eboot_test_jump_app 2/2 passed $ ./build/host/tests/eboot_test_fw_transport 15/15 passed $ ./build/host/tests/eboot_test_rollback 14/14 passedFour of the body's five numbers reproduce exactly; the transport suite is 15/15 at this
head, not the 13/13 the body still claims (finding 4). That is 22 of the 23 registered
tests.test_ed25519is excluded because it cannot compile —
tests/unit/test_ed25519.c:31,263,299,300carries its own duplicate-definition merge
artifact onmaster, independent of the two above. -
The ARM cross build PASSES. Same patched scratch copy, the exact configuration from
.github/workflows/build.yml:44-52:$ cmake -B build-arm -DCMAKE_SYSTEM_NAME=Generic -DCMAKE_C_COMPILER=arm-none-eabi-gcc \ -DEBLDR_BOARD=stm32f4 \ -DCMAKE_C_FLAGS="-mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -specs=nosys.specs" $ cmake --build build-arm --parallel 4 # exit 0ebldr_stage0.elfandeboot_firmware.elfboth link. No warning or error in any file
this PR touches, on either the host or the ARM build. -
git diff --check 22d8f8b 180680cc— PASS, exit 0. The body claims it and it holds. -
Findings 1, 2 and 3 were each verified with throwaway probes compiled into a copy of
tests/unit/test_fw_transport.cin that scratch tree, which was deleted afterwards. None
is offered as a test here; the findings say where the real ones belong. One of my probes
initially reported a garbage state because I evaluated the transfer and the state read in
the sameprintfargument list; the numbers quoted above are from the corrected,
statement-sequenced version. -
XMODEM has no retransmission tolerance, and I could not settle whether it matters.
core/fw_transport_uart.c:258NAKs any block whose number is notexpected_blk,
including a resend of the block just written, whereymodem_receive():457ACKs it
without rewriting. A real sender answering that NAK would resend the same block and see
it NAKed again. The scripted harness replays a fixed byte stream and cannot model a
sender reacting to a NAK, so I confirmed only that the duplicate is not written twice
(flash byte-identical, stateVERIFY). Pre-existing, untouched by this PR, and not
filed as a finding because I could not demonstrate the failure. -
Not verified on hardware.
eos_hal_monotonic_read/incrementon a real OTP-backed
board is still untested — everything above is simulated flash and a simulated counter.
§28.1 would require a named board, and finding 5 says there is not yet an artifact to put
on one. -
The address-space question is still unsettled and I still cannot settle it. Carried
forward from all four previous reviews: whethereos_bootctl_confirm()(core/bootctl.c,
which callseos_rollback_commit()) executes in the same address space as the
eboot_jump_to_app()that staged the counter.g_staged_counter/g_staged_valid
(core/rollback.c:17-18) are file-static RAM. If confirm runs in the application image
the staged value is invisible there and the floor never advances. Pre-existing design;
together with finding 5 it means neither end of the anti-rollback mechanism has been shown
to work outside a unit test. -
slot_remain_from()still skips a slot based at address 0 (core/rollback.c:79).
Unchanged, still latent, and untouched by this commit. -
The two
.cfiles underpoc/were not reviewed.poc/tlv_downgrade_poc.ccalls
eos_rollback_read_image_counter()and is untouched by this diff; I did not check whether
it still builds or still demonstrates what it claims. -
Merge order against eBoot#96 and eBoot#104 was not worked out. Both will conflict
textually with this head — #96 on the slot bound, #104 onfinalize(). I confirmed the
overlap and did not resolve it. -
existing-comments.txtis empty, so nothing here repeats a human reviewer; there are
still none on this PR.
Automated architecture review of 180680cc86c5 — scheduled, model claude-opus-5, checked against the EmbeddedOS Master Design v2.0. Advisory only: this reviewer never approves, requests changes, or merges. Reply here to discuss or push back — a wrong finding is a bug worth reporting.
Summary
Fix anti-rollback enforcement so the authenticated security counter from the image TLV is used instead of the image version field, and strengthen the firmware update pipeline to correctly stream and validate the authenticated TLV area.
Changes
Anti-rollback
Firmware update streaming
eos_fw_update_write()instead of requiring the entire TLV area to be handled separately.Transport handling
eos_fw_update_bytes_wanted()so transports can determine how many container bytes are still required.eos_fw_update_write().Tests
Added and updated tests covering:
Targeted host test results:
eboot_test_fw_update— 6/6 passedeboot_test_tlv_auth— 9/9 passedeboot_test_jump_app— 2/2 passedeboot_test_fw_transport— 13/13 passedeboot_test_rollback— 14/14 passedgit diff --checkalso passes.Compatibility
EOS_FW_STATEvalues are renumbered: addingEOS_FW_STATE_TLV = 3shiftsVERIFYfrom 3 to 4,COMPLETEfrom 4 to 5, andERRORfrom 5 to 6.eos_fw_update_ctx_tgrows twouint32_tfields,tlv_writtenandtlv_total, so its size and layout change.Documentation
Chapter 16 of the documentation now describes the update container as:
[header][payload][TLV area]eos_fw_update_write()must receive all three parts, and bytes beyond the complete container are rejected rather than discarded.Testing Environment
The official full CMake build remains blocked by pre-existing issues on the current master branch:
include/eos_image.h— stalereserved[]static assertions.core/ed25519_verify.c— duplicatepoint_is_identity()definition.core/keystore.c— existing#warningrequiring/std:clatest.__builtin_add_overflowcompiler warning.These issues are unrelated to this PR and were not modified.
The targeted host test suites listed above pass successfully.
Scope
This PR intentionally does not address the pre-existing master build failures or the duplicated read/verify/stage logic across boot paths. Those are separate follow-up issues.
The implementation remains fail-closed: malformed, incomplete, oversized, or unauthenticated update data is rejected rather than silently accepted or discarded.
Out of scope: TLV counter emission. This PR implements and validates the consumer and enforcement side of the authenticated TLV anti-rollback counter — parsing the TLV area, binding it to the signed header hash, enforcing the hardware floor at finalize and at jump, and streaming the TLV area through the update path. The repository's image-packaging and signing tools (tools/imgpack.py, tools/eos_sign.py) do not yet emit EOS_TLV_MIN_SEC_VER, so no in-tree tool currently produces an image carrying the counter. Adding TLV emission and round-trip packer coverage is separate follow-up work, and hardware validation of the end-to-end counter lifecycle is therefore outside this PR's scope.