Repository navigation
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.
srpatcha
left a comment
There was a problem hiding this comment.
Review — eBoot#103 "Fix anti-rollback to use authenticated TLV counter"
head: fb978c8 author: Tanisha1723 ci: none reported
Verdict: Follow-up review (previous head 180680cc). The one new commit closes every
open code finding from the last review — the YMODEM block-after-container guard, the
progress denominator, and the EOT completeness check on both transports — and it is the
first head of this PR where the two block transports agree about the same protocol question
instead of one being taught and the other not. The PR body has been regenerated and all five
of its test claims now reproduce exactly. I ran the host suite (22/22), the ARM cross build
and git diff --check; all pass. What the commit does not reach is the third transport:
uart_raw_receive() still returns EOS_OK for a container it delivered only part of, which
is the same shape as the finding just fixed on the other two.
Previous findings — status
- Finding 1 (YMODEM accepted and discarded whole data blocks once the container was
complete) — resolved infb978c8e.ymodem_receive()now rejects a correctly sequenced
block that begins witheos_fw_update_bytes_wanted(ctx) == 0
(core/fw_transport_uart.c:481-490): CAN,EOS_ERR_INVALID. The recommended fix was taken
verbatim, including placing the guard after the retransmit-tolerance branch at:466-473,
so a resent final block is still ACKed rather than CANed — the ordering XMODEM does not
need and YMODEM does.test_ymodem_data_block_after_container_is_rejected
(tests/unit/test_fw_transport.c:616) is the case the last review asked for, and it is
built correctly: a declared size of 640 over the 424-byte container leaves room for the
extra block inside the file-size bound, so it exercises the container check and not the
file_len == 0bound. Ran it; passes. - Finding 2 (
eos_fw_update_progress()reported 100% with the TLV area outstanding) —
resolved infb978c8e.core/fw_update.c:302-303foldstlv_totalinto the denominator
inside the same__builtin_add_overflowguard, which is the recommended fix exactly. The
short-circuit is correct in both directions: a wrap in either add lands on0xFFFFFFFF.
test_write_does_not_reach_verify_until_tlv_arrivesgained the four assertions that were
asked for, includingprogress() < 100whilebytes_wanted() != 0and== 100once the
TLV area lands. - Finding 3 (both receivers returned
EOS_OKon EOT however much was outstanding) —
resolved infb978c8e, on both transports.core/fw_transport_uart.c:245and:375
each ACK first and then returnEOS_ERR_INVALIDif the container is short — the ACK-first
ordering the last review recommended, so the sender still sees a completed handshake.
Verified independently that the YMODEM branch also closes the EOT-before-block-0 case its
comment claims:eos_fw_update_begin()leaves the context inEOS_FW_STATE_HEADER
(core/fw_update.c:41), sobytes_wanted()is 156 and not 0, and the
EOS_FW_STATE_IDLE → default → 0arm ofbytes_wanted()(:331-332) is not reached there.
Both new tests assert!tx_contains(XM_CAN), which is the right distinction — truncation
is an error, not a protocol abort. - Finding 4 (PR body stale against its own head) — resolved.
test_fw_transportnow
reads18/18and reproduces; the Changes section gained the YMODEM extra-block rejection,
the EOT rejections and the progress denominator; Compatibility gained the two lines about
what a sender may now no longer send. Nothing in the body overstates this head. - Finding 5 (nothing in the repo can emit the counter this PR makes authoritative) —
the requested treatment is done; the gap itself is still open. The Scope section now names
tools/imgpack.pyandtools/eos_sign.pyexplicitly and says the emission side is
follow-up work, which is exactly what was asked for. Re-checked the code at this head:
both packers still writeb'\x00' * 30fortlv_len(2) + tlv_hash(28)
(tools/imgpack.py:74,tools/eos_sign.py:105), andgit grep -l EOS_TLV_INFO_MAGIC
outsidetests/andpoc/still returns only readers. Restated with new information as
finding 3 below — eBoot#88 changes the outlook. - Finding 6 (read → verify → stage duplicated three ways) — untouched, correctly. Still
core/secure_boot.c:134,stage1/jump_app.c:66,core/fw_update.c:259. Restated as
finding 4 only so it is not dropped; it is now also listed in the body's own Follow-up
section, which is the right place for it. - Finding 7 (
tests_runhand-maintained literals) — untouched, and the transport literal
was correctly bumped 15 → 18 (tests/unit/test_fw_transport.c:854). eBoot#95 is still open.
No change wanted here.
Findings
| # | Severity | File:line | Finding | Recommended fix |
|---|---|---|---|---|
| 1 | Low | core/fw_transport_uart.c:123-127 |
uart_raw_receive() is the one transport that still reports a truncated container as a successful transfer — the same defect this commit just fixed on XMODEM and YMODEM, surviving in the third receiver. It streams exactly the total_len its 4-byte prefix declared, sends UART_RAW_ACK and returns EOS_OK, with no eos_fw_update_bytes_wanted() check anywhere in the function. Verified by probe against this head: a prefix of CONT_LEN - 40 over the 424-byte TLV-carrying container gives receive_rc = 0, state = EOS_FW_STATE_PAYLOAD, bytes_wanted = 40, ACK sent. The identical shortfall over XMODEM now gives rc = -2. End-to-end it is fail-closed — eos_fw_transport_update() (:41) calls finalize(), which returns EOS_ERR_INVALID because the state is not VERIFY (I measured finalize = -2) — so this is loss of detection at the ops->receive boundary, not a bad install: hence Low, the same rating the last review gave the XMODEM and YMODEM halves before they were fixed. It matters because eos_fw_transport_uart_raw() is exported from include/eos_fw_transport.h:85 and the receive op is documented as blocking "until complete" (:46-48) without saying what complete means — a definition that now differs between the three implementations behind that one pointer. The sender's own length prefix is not an independent bound on its own bytes, which is the reasoning the commit already applies to YMODEM's file_size. |
One line, mirroring :245 and :375, before the ACK at :123: if (eos_fw_update_bytes_wanted(ctx) != 0) { uint8_t nak = UART_RAW_NAK; eos_hal_uart_send(&nak, 1); return EOS_ERR_INVALID; } — NAK rather than ACK, since raw has no separate abort byte and the prefix contract is already NAK-on-reject at :92. Add the case to tests/unit/test_fw_transport.c next to test_raw_valid_transfer_is_written, which today only covers a prefix equal to the image. While editing, tighten the receive doc comment in include/eos_fw_transport.h:46-48: "complete" should say the update container is complete, so the three implementations are held to one contract rather than three. |
| 2 | Low | core/fw_transport_uart.c:527-529 |
Dead code in a function this commit rewrote: if (file_size > 0 && total_received >= file_size) { /* File complete — wait for EOT */ } has an empty body and no effect. It produces no compiler warning (the braces suppress -Wempty-body), so it will not be found by the build. Pre-existing and untouched by this diff — filed because the diff is what makes ymodem_receive()'s end-of-transfer handling load-bearing, and because a reader now has to work out that the real end-of-transfer decision moved to the EOT branch at :361-377. |
Delete the three lines. If the comment is worth keeping, move it to the EOT branch, which is where the file-complete decision actually happens. |
| 3 | Medium (carried, treatment discharged) | tools/imgpack.py:74, tools/eos_sign.py:105 |
Previous finding 5. 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. The documentation half of the last review's recommendation is now done, so nothing further is owed by this PR. New information, which narrows the remaining work: eBoot#88 ("fix(eos_sign): put the payload where the bootloader looks for it") is open and gives tools/eos_sign.py a real authenticated TLV area — a build_tlv() helper, tlv_len/tlv_hash parameters written into the signed prefix, and output ordered [header][payload][tlv], which is where core/rollback.c looks. Its TLV area carries a payload hash and a key hash, not EOS_TLV_MIN_SEC_VER, so it does not close this — but it builds the mechanism, and a follow-up written before it lands would duplicate that work. |
Unchanged for this PR. For the follow-up: land eBoot#88 first, then add --min-sec-ver N as one further entry through its build_tlv() rather than a second TLV writer, plus a round-trip test through eos_rollback_read_image_counter(). tools/imgpack.py needs the same option and has no equivalent in flight. |
| 4 | Low (carried, untouched) | core/secure_boot.c:134, stage1/jump_app.c:66, core/fw_update.c:259 |
Previous finding 6. Three copies of read → verify → stage, and eos_secure_boot() still has no caller anywhere in stage1/. Worth noting that finding 1 above is the same shape one layer up — three receivers of one container, two of which now enforce a rule the third does not — so this is the second place in the file where a per-implementation copy is what let the behaviours drift. Restated only so it is not dropped. |
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. |
| 5 | Low (carried, untouched) | test_fw_transport.c:854, test_fw_update.c:318, test_tlv_auth.c:364, test_jump_app.c:268 |
Previous finding 7. 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. Correctly bumped 15 → 18 this commit. |
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 five 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 3 remains the honest
qualifier: the check is right and nothing in the tree can yet feed it a counter. .ai/security.md"Fail closed": strengthened in three places by this commit. A short
transfer is now an error on both block transports rather than a success the installer has
to catch later, and a surplus frame is a protocol abort. No security step reports success
without having run. Finding 1 is the remaining gap and it fails open on detection only —
the truncated container never reachesEOS_FW_STATE_VERIFY, sofinalize()cannot install
it.- §5.1: no layering violation. The three new checks all call down into the update core
through the read-onlyeos_fw_update_bytes_wanted()accessor — the correct direction.
Nothing points up a tier. TCB growth this commit is ~30 lines net in
core/fw_transport_uart.cand 2 incore/fw_update.c, all of it rejection logic, which is
the kind of growth "minimal and auditable" is for. - §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: all five of the body's test claims reproduce exactly at this head (Not checked
below), which is the first time that has been true of the transport suite. The Scope and
Follow-up sections state plainly what the PR does not address, including the packer gap the
last review asked for. Nothing in the body asserts a check that was not run. - §23.2 / API compatibility: nothing further changes here. The enum renumbering and struct
growth were declared at984a0d39and are unchanged; this commit alters no public
signature, struct layout, manifest field or serialized format. The one behavioural break to
an out-of-tree integrator — an EOT that used to yieldEOS_OKfor a short transfer now
yieldsEOS_ERR_INVALID— is declared in Compatibility. That is the disclosure the brief
asks for.
No proposal is appended this run, and that is deliberate. The design question behind
finding 1 is already settled by two proposals filed against earlier heads of this same PR:
the 2026-09-05 entry "An install is verified over the whole image container and no rule says
the transport has to have delivered all of it" already requires a receiver to "report an
error for a stream that ends before the container does", and its addendum
"'stop offering bytes, but complete the handshake' gives a transport no point at which
surplus becomes an error" already states that a sender-declared length "may be used to detect
end of transfer; it may not be used to license discarding bytes". Both cover
uart_raw_receive() exactly. The design is ahead of the code here, not behind it, so finding
1 is a code finding and nothing is owed to .docx.
Proposed changes
- Add the raw-transport completeness check and its test (finding 1), and tighten the
receivedoc comment in the same edit. One line of production code; it is the last of the
three transports. - Delete the dead
ifatcore/fw_transport_uart.c:527-529(finding 2) while that function
is open. - Leave findings 3, 4 and 5 to a packer PR sequenced after eBoot#88, 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). It still overlaps eBoot#96 on the
slot bound ineos_rollback_read_image_counter()and still conflicts textually with
eBoot#104 aroundfinalize(). eBoot#88 also touchesinclude/eos_image.hand will need
sequencing against #94/#105.
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
Low, 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 every open number), and none adds EOS_TLV_MIN_SEC_VER emission
to either packer — eBoot#88 rewrites eos_sign.py's TLV area but emits a payload hash and a
key hash, and eBoot#107 edits imgpack.py for version-component overflow only. So nothing
here duplicates existing work.
Not checked
-
CI: no checks ran, for the sixth head running.
checks.txtis empty,gh pr checks 103
reportsno checks reported on the 'fix/eboot-test-issue' branch, andstatusCheckRollup
is[].mergeStateStatusisBLOCKEDwithmergeable: MERGEABLEandreviewDecision: REVIEW_REQUIRED, so the block is review, not conflict. Six 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
extractedfb978c8ewithgit 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 from.github/workflows/build.yml:26
unmodified. Configure succeeds; the build fails on
include/eos_image.h:135,142, which assert on areservedmember thattlv_len/tlv_hash
replaced.core/ed25519_verify.cstill definespoint_is_identitytwice (: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 -DEBLDR_BUILD_TESTS=ON -DEBLDR_BOARD=none $ cmake --build build --parallel 4 -- -k $ ctest --test-dir build --output-on-failure --no-tests=error -E ed25519 100% tests passed, 0 tests failed out of 22 $ ./build/tests/eboot_test_fw_update 6/6 passed $ ./build/tests/eboot_test_tlv_auth 9/9 passed $ ./build/tests/eboot_test_jump_app 2/2 passed $ ./build/tests/eboot_test_fw_transport 18/18 passed $ ./build/tests/eboot_test_rollback 14/14 passedAll five of the body's numbers reproduce exactly. 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 PR body's "Build / CI Status" list of base-branch blockers names the two real
gcc ones and omits this third; it also lists two MSVC-specific items that are not gcc
blockers. Not filed as a finding — the section's claim is that the repo-wide build is
blocked by pre-existing base-branch defects, and that claim is true. -
The ARM cross build PASSES. Same patched scratch copy, the exact configuration from
.github/workflows/build.yml:41-49:$ 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 — I grepped both logs for
fw_transport_uart,fw_update.c,rollback.candjump_appspecifically. -
git diff --check 22d8f8b fb978c8e— PASS, exit 0. The body claims it and it holds. -
Finding 1 was verified with a throwaway probe compiled into a copy of
tests/unit/test_fw_transport.cin that scratch tree, alongside a contrast probe running
the identical shortfall over XMODEM. The scratch file was restored fromgit show
afterwards and the probe is not offered as a test here; the finding says where the real one
belongs. The repository itself was never modified. -
cppcheck(the CIlintjob) — NOT RUN. It is|| truein the workflow
(.github/workflows/build.yml:73) and so cannot fail the build; I did not run it separately
and make no claim about what it would say. -
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 five 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 3 it means neither end of the anti-rollback mechanism has been shown to work
outside a unit test. -
XMODEM still has no retransmission tolerance, and I still could not settle whether it
matters.core/fw_transport_uart.c:263NAKs any block whose number is notexpected_blk,
including a resend of the block just written, whereymodem_receive():466-473ACKs it
without rewriting. This commit did not change that, and it deliberately placed the new
extra-block guard after YMODEM's retransmit branch so the two do not interact — which is
correct. The scripted harness replays a fixed byte stream and cannot model a sender reacting
to a NAK, so this stays unfiled for the second review running: I cannot demonstrate the
failure. -
slot_remain_from()still skips a slot based at address 0 (core/rollback.c:79).
Unchanged, still latent, 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, #104 and now #88 was not worked out. All three will
conflict or overlap — #96 on the slot bound, #104 onfinalize(), #88 on
include/eos_image.hand on the packer this PR's Scope section defers to. I confirmed the
overlaps and did not resolve them. -
existing-comments.txtis empty, so nothing here repeats a human reviewer; there are
still none on this PR.
Automated architecture review of fb978c8e1b20 — 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: d722e9a author: Tanisha1723 ci: none reported
Verdict: Follow-up review (previous head fb978c8e). The one new commit
(d722e9a fix(transport): reject incomplete raw transfers) closes both open code findings
from the last review — the raw transport's completeness check and the dead if in
ymodem_receive() — and it does so in the places they were asked for, including the doc-comment
tightening that was recommended in the same edit. No new code finding. I proved the new test
is a real regression test (it fails with the guard removed, passes with it restored) and probed
the opposite direction and the one-byte boundary, both of which behave correctly. This is the
first head of this PR where all three transports behind one receive pointer enforce the same
container contract, and the header now states that contract. What the commit did not do is update
the PR body, which had been brought current at the last head and has gone stale again — including
on a behavioural break to an exported transport that Compatibility does not mention.
Previous findings — status
- Finding 1 (
uart_raw_receive()reported a truncated container as a successful transfer) —
resolved ind722e9a0.core/fw_transport_uart.c:123-130now rejects before the ACK:
eos_fw_update_bytes_wanted(ctx) != 0sendsUART_RAW_NAKand returnsEOS_ERR_INVALID. The
recommended fix was taken verbatim, including NAK rather than a new abort byte, matching the
prefix contract's existing NAK-on-reject at:92.test_raw_transfer_shorter_than_container_is_rejected
(tests/unit/test_fw_transport.c:816-829) is the case that was asked for and it is placed next to
test_raw_valid_transfer_is_writtenas recommended. Verified beyond running it: I removed the
guard alone from a scratch copy and rebuilt — the new test is the only one that fails
(test_fw_transport.c:825: run_raw_ctx(&ctx) == EOS_ERR_INVALID), 18 of 19 still pass; restoring
the guard returns 19/19. So the test genuinely guards the fix rather than passing incidentally.
I also probed the boundary and the opposite direction against this head: a prefix short by a
single byte givesrc = -2, NAK,bytes_wanted = 1; a prefix 64 bytes longer than the container
givesrc = -2, NAK, no ACK (caught byeos_fw_update_write()'s trailing-byte rule, not the new
guard, which is correct —bytes_wantedis 0 there); and the exact-length golden path is
unchanged atrc = 0,finalize = 0. The raw transport is now bounded in both directions. - Finding 2 (dead
if (file_size > 0 && total_received >= file_size) { }at:527-529) —
resolved ind722e9a0. Deleted, which is the recommended disposition. Checked the fallout the
deletion could have had:file_sizeandtotal_receivedare both still live in
ymodem_receive()(:504-505,:530), so neither becomes an unused variable, and neither the
host nor the ARM build emits any diagnostic in this file. - Finding 3 (nothing in the repo can emit the counter this PR makes authoritative) — untouched,
and nothing further is owed by this PR. Re-checked at this head:tools/imgpack.py:74and
tools/eos_sign.py:105still writeb'\x00' * 30fortlv_len(2) + tlv_hash(28), no.pyin
the tree mentionsMIN_SEC_VER, andEOS_TLV_INFO_MAGICoutsidetests/andpoc/still
resolves only to the reader (core/image_tlv.c,include/eos_image_tlv.h). The documentation
half was discharged atfb978c8eand the Scope section still carries it. Restated as finding 3
below only so it is not dropped. - Finding 4 (read → verify → stage duplicated three ways) — untouched, correctly. Still
core/secure_boot.c:134,stage1/jump_app.c:66,core/fw_update.c:259, andeos_secure_boot()
still has no caller anywhere instage1/—stage1/jump_app.c:64only names it in a comment.
Restated as finding 4. - Finding 5 (
tests_runhand-maintained literals) — untouched, and the transport literal was
correctly bumped 18 → 19 (tests/unit/test_fw_transport.c:882). eBoot#95 is still open. No change
wanted here.
Findings
| # | Severity | File:line | Finding | Recommended fix |
|---|---|---|---|---|
| 1 | Low | PR body — "Testing", "Changes", "Compatibility" | The body has gone stale against its own head again, and this time it is silent about a behavioural break. It claims test_fw_transport — 18/18 passed; this head has 19 tests and reports 19/19 (I ran it). The Changes section is titled "XMODEM/YMODEM transport handling" and describes only the previous head's work — it never mentions this commit's three changes: the raw transport's new rejection (core/fw_transport_uart.c:123-130), the receive contract now written into the public header (include/eos_fw_transport.h:45-56, the rule itself at :50-56), and the dead-code removal at the old :527-529. Compatibility is the part that matters: a raw sender whose 4-byte length prefix under-declares the container previously received UART_RAW_ACK and EOS_OK, and now receives UART_RAW_NAK and EOS_ERR_INVALID — verified by probe in both directions. eos_fw_transport_uart_raw() is exported from include/eos_fw_transport.h:93 and has no in-tree caller, so the only party this can break is an out-of-tree integrator, which is exactly the party Compatibility exists for. The body already declares the equivalent XMODEM/YMODEM break in that section, so the omission is an oversight, not a position. Per brief §5 and §8, silence about a behavioural change to a documented contract is itself the finding — the underlying work here is correct and this is only the description lagging it. |
Regenerate the five test lines from a run of this head (18/18 → 19/19; the other four are still exact — I reproduced all of them). Rename the Changes subsection to cover all three transports and add three bullets: the raw completeness rejection, the tightened receive contract in the public header, and the removed dead branch. Add one Compatibility line: a raw-transport sender whose length prefix is shorter than the update container now receives NAK and EOS_ERR_INVALID where it previously received ACK and EOS_OK. |
| 2 | Low | docs/book/book.md:732-753 (ch. 7.1, 7.2) |
The commit makes completeness a requirement on every implementation of ops->receive, including the out-of-tree ones ch. 7 exists to enable — §7.2's table lists Custom ("User-defined … Network, CAN bus, SPI, etc.") as a supported transport class and §7.3 walks through writing one. Nothing in ch. 7 states the rule the header now states at include/eos_fw_transport.h:50-56, so a reader implementing a custom transport from this chapter writes one that returns EOS_OK for a short stream and violates the contract with nothing to catch it. Filed narrowly: it is the contract that is newly absent here, not the listing. The eos_fw_transport_t listing at :736-743 is independently wrong on master — :740 shows int (*receive)(uint8_t *buf, size_t max_len, size_t *actual_len), which is not the real signature — and correcting that is pre-existing work this PR did not create and should not absorb. §16.1 already got the container sentence at an earlier head (:1324), so this is the one place the series has not reached. |
Two sentences after the §7.1 listing, mirroring the header: a receive implementation must treat its own framing as framing only and the update container as the authority on completeness; returning EOS_OK while eos_fw_update_bytes_wanted() is non-zero is a contract violation, not a partial success. Leave the stale struct listing alone — it belongs with whichever PR repairs ch. 7. |
| 3 | Medium (carried, treatment discharged) | tools/imgpack.py:74, tools/eos_sign.py:105 |
Previous finding 3. 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. The documentation half of the recommendation was discharged at fb978c8e and nothing further is owed by this PR. Re-confirmed against the open PR list at this run: eBoot#88 gives tools/eos_sign.py a real authenticated TLV area and mentions EOS_TLV_MIN_SEC_VER once, in a docstring explaining why tlv_len/tlv_hash being inside EOS_IMG_SIGNED_LEN is what would make such a value trustworthy — it builds the mechanism and does not emit the counter. eBoot#107 edits imgpack.py for version-component overflow only (0 hits for MIN_SEC_VER). |
Unchanged for this PR. For the follow-up: land eBoot#88 first, then add --min-sec-ver N as one further entry through its TLV builder rather than a second TLV writer, plus a round-trip test through eos_rollback_read_image_counter(). tools/imgpack.py needs the same option and has nothing in flight that provides it. |
| 4 | Low (carried, untouched) | core/secure_boot.c:134, stage1/jump_app.c:66, core/fw_update.c:259 |
Previous finding 4. Three copies of read → verify → stage, and eos_secure_boot() still has no caller anywhere in stage1/. Worth recording that the per-implementation drift this finding is about has now been fully worked out one layer up: three receivers of one container behaved three ways across five heads and agree at this one. The same argument applies here and has not been acted on. Restated only so it is not dropped. |
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. |
| 5 | Low (carried, untouched) | test_fw_transport.c:882, test_fw_update.c:318, test_tlv_auth.c:364, test_jump_app.c:268 |
Previous finding 5. 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. Correctly bumped 18 → 19 this commit, which is also what makes the body's stale 18/18 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 six 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 3 remains the honest qualifier: the check
is right and nothing in the tree can yet feed it a counter. .ai/security.md"Fail closed": completed for the transport layer by this commit. I traced
everyEOS_OKexit from all three receivers:uart_raw_receive()returnsEOS_OKonly past the
new check at:126;xmodem_receive()only via the EOT branch at:254;ymodem_receive()only
via the EOT branch at:384. Each is gated oneos_fw_update_bytes_wanted(ctx) == 0, so the
contract the header now asserts — "EOS_OKtherefore means the whole[header][payload][TLV area]
container was written" — holds for every implementation behind that pointer. No security step
reports success without having run.- §5.1: no layering violation. The new check calls down into the update core through the
read-onlyeos_fw_update_bytes_wanted()accessor — the correct direction. Nothing points up a
tier. TCB change this commit is +9 / −4 lines incore/fw_transport_uart.c: rejection logic in,
dead code out. That is the direction "minimal and auditable" wants. - §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. - §23.2 / API compatibility: no public signature, struct layout, manifest field or serialized
format changes here. What does change is the documented contract ofops->receiveand the
observable behaviour of an exported transport. The header change is the right place to declare it
and the commit made it; the body does not carry it, which is finding 1. - §28: four of the body's five test claims reproduce exactly at this head; the transport line is
stale (finding 1). The Scope and Follow-up sections still state plainly what the PR does not
address, including the packer gap, which is the right posture and unchanged.
No proposal is appended this run, and that is deliberate. The design question behind the fixed
finding was already settled by two proposals filed against earlier heads of this same PR: the
2026-09-05 entry "An install is verified over the whole image container and no rule says the
transport has to have delivered all of it" (proposals/2026-09.md:4042) requires a receiver to
"report an error for a stream that ends before the container does", and its addendum
"'stop offering bytes, but complete the handshake' gives a transport no point at which surplus
becomes an error" (:4427) states that a sender-declared length "may be used to detect end of
transfer; it may not be used to license discarding bytes" — which is the raw transport's 4-byte
prefix exactly. This commit implements both. The packer gap behind finding 3 is covered by the
2026-09-05 entry "§8.1 requires the rollback counter to be checked and never says anyone has to
write it" (:4321). The design is ahead of the code on every open question here, so nothing is owed
to the .docx.
Proposed changes
- Correct the PR body (finding 1) — the test count, the three missing Changes bullets, and above
all the one Compatibility line for the raw-transport break. No code. - Add the two-sentence contract note to book ch. 7.1 (finding 2). No code.
- Leave findings 3, 4 and 5 to a packer PR sequenced after eBoot#88, 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). It still overlaps eBoot#96 on the slot bound in
eos_rollback_read_image_counter()and still conflicts textually with eBoot#104 around
finalize(). eBoot#88 also touchesinclude/eos_image.hand will need sequencing against
#94/#105.
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; both findings here are Low and both are documentation.
fix-verify.sh also has nothing green to run against while master does not build. I checked all
19 open PRs: none touches core/fw_transport_uart.c (confirmed by gh pr diff --name-only for
every open number), and none adds EOS_TLV_MIN_SEC_VER emission to either packer. Nothing here
duplicates existing work.
Not checked
-
CI: no checks ran, for the seventh head running.
checks.txtis empty,gh pr checks 103
reportsno checks reported on the 'fix/eboot-test-issue' branch, andstatusCheckRollupis[].
mergeStateStatusisBLOCKEDwithmergeable: MERGEABLEandreviewDecision: REVIEW_REQUIRED,
so the block is review, not conflict. Seven 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 extracted
d722e9a0withgit 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 from.github/workflows/build.yml:26unmodified. Configure succeeds; the build
fails with exit 2 oninclude/eos_image.h:135,142, which assert on areservedmember that
tlv_len/tlv_hashreplaced. I confirmed the attribution directly this time rather than
inheriting it:git show origin/master:include/eos_image.hcarries both thetlv_len/tlv_hash
fields (:53-54) and the stalereservedasserts (:135,142), and this PR does not touch that
file at all.core/ed25519_verify.cstill definespoint_is_identitytwice (: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 -DEBLDR_BUILD_TESTS=ON -DEBLDR_BOARD=none $ cmake --build build --parallel 4 -- -k $ ctest --test-dir build --output-on-failure --no-tests=error -E ed25519 100% tests passed, 0 tests failed out of 22 $ ./build/tests/eboot_test_fw_update 6/6 passed $ ./build/tests/eboot_test_tlv_auth 9/9 passed $ ./build/tests/eboot_test_jump_app 2/2 passed $ ./build/tests/eboot_test_fw_transport 19/19 passed $ ./build/tests/eboot_test_rollback 14/14 passedFour of the body's five numbers reproduce exactly; the transport suite is 19/19 at this head, not
the 18/18 the body claims (finding 1). 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:41-49:$ 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 — I grepped both logs forfw_transport_uart,
fw_update.c,rollback.c,jump_appandimage_verifyspecifically. -
git diff --check 22d8f8b d722e9a0— PASS, exit 0. The body claims it and it holds. -
The guard-removal experiment and the three raw-transport probes were done in that scratch
tree. The production file and the test file were each backed up, modified, and restored from the
backup, and the suite was re-run after each restore to confirm 19/19. The probes are not offered
as tests here; the finding-1 resolution note says what they showed. The repository itself was
never modified — its working tree was clean at the start of this run and is clean now. -
cppcheck(the CIlintjob) — NOT RUN. It is|| truein the workflow
(.github/workflows/build.yml:73) and so cannot fail the build; I did not run it separately and
make no claim about what it would say. -
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 six previous reviews: whethereos_bootctl_confirm()(core/bootctl.c, which calls
eos_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 the staged value is invisible there and the floor
never advances. Pre-existing design; together with finding 3 it means neither end of the
anti-rollback mechanism has been shown to work outside a unit test. -
XMODEM still has no retransmission tolerance, and I still could not settle whether it matters.
core/fw_transport_uart.c:272NAKs any block whose number is notexpected_blk, including a
resend of the block just written, whereymodem_receive():477-482ACKs it without rewriting. This
commit did not change either. The scripted harness replays a fixed byte stream and cannot model a
sender reacting to a NAK, so this stays unfiled for the third review running: I cannot demonstrate
the failure. -
slot_remain_from()still skips a slot based at address 0 (core/rollback.c:79). Unchanged,
still latent, 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, #104 and #88 was not worked out. All three will conflict or
overlap — #96 on the slot bound, #104 onfinalize(), #88 oninclude/eos_image.hand on the
packer this PR's Scope section defers to. I confirmed the overlaps and did not resolve them. -
existing-comments.txtis empty, so nothing here repeats a human reviewer; there are still
none on this PR after seven heads.
Automated architecture review of d722e9a0ae53 — 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: 0aec419 author: Tanisha1723 ci: none reported
Verdict: Follow-up review (previous head d722e9a0). Both open findings from the
last review are resolved and there is no new finding. The one new commit
(0aec419 docs: document transport completeness contract) is documentation only —
docs/book/book.md +8/−0, no code changed at all — and it closes finding 2 in the place
it was asked for. Finding 1 was closed out-of-band by an edit to the PR body, which I
re-verified line by line against a fresh run of this head rather than taking it on trust.
This is the eighth head of this PR and the first at which nothing is outstanding that the
PR itself owes. What remains is entirely external: it still does not build on origin/master
through no fault of its own, it still has zero CI, and it still needs sequencing behind
three other open PRs.
Previous findings — status
- Finding 1 (PR body stale against its own head, and silent on a behavioural break to an
exported transport) — resolved. Every part of the recommendation was taken. The
Testing section now readstest_fw_transport — 19/19 passed; I ran all five suites at
this head and all five numbers reproduce exactly (6/6,9/9,2/2,19/19,14/14—
command output under "What I ran"). The Changes subsection was renamed from
"XMODEM/YMODEM transport handling" to "Transport completeness — XMODEM, YMODEM and raw"
and gained the three missing bullets: the raw completeness rejection
(core/fw_transport_uart.c:123-130), thereceivecontract now stated in the public
header (include/eos_fw_transport.h:45-56), and the removed no-op YMODEM branch. Most
importantly Compatibility now carries the line that was missing: "A raw transfer whose
declared length is shorter than the complete update container is now rejected with
UART_RAW_NAKandEOS_ERR_INVALIDinstead of the previousUART_RAW_ACKandEOS_OK."
That was the substance of the finding — the break toeos_fw_transport_uart_raw()is now
declared alongside the XMODEM/YMODEM ones it belongs with. - Finding 2 (book ch. 7 never states the completeness contract the header now imposes on
everyops->receive, including the custom transports ch. 7 exists to enable) — resolved
in0aec419e.docs/book/book.md:745-752, immediately after the §7.1 listing and
before §7.2, exactly where it was recommended. I checked the prose against the header
rather than just its presence: the book says framing "bounds the transfer, but the update
container is authoritative for completeness", thateos_fw_update_bytes_wanted()reports
what is outstanding, and that returningEOS_OKwith a non-zero count "is a contract
violation rather than a partial success" — which is the header's rule at
include/eos_fw_transport.h:50-56in the header's own terms. I also checked the doc's
factual claim against the implementation, not just against the other doc:
eos_fw_update_bytes_wanted()(core/fw_update.c:313-334) does return the outstanding
remainder of[header][payload][TLV area]in each of the three receiving states —
HEADERreturns the header remainder (payload and TLV lengths are not yet known, which
the code comments),PAYLOADreturns payload plus TLV remainder,TLVreturns the TLV
remainder. The sentence is accurate. As recommended, the commit left the pre-existing
staleeos_fw_transport_tlisting at:736-743alone, and the body's Follow-up section
now names that cleanup explicitly as separate work. - Finding 3 (nothing in the repo can emit the counter this PR makes authoritative) —
untouched, and nothing further is owed by this PR. Re-confirmed at this head:
tools/imgpack.py:74andtools/eos_sign.py:105still writeb'\x00' * 30for
tlv_len(2) + tlv_hash(28), andMIN_SEC_VERappears in zero.pyfiles in the tree.
Restated as finding 3 below only so it is not dropped. - Finding 4 (read → verify → stage duplicated three ways) — untouched, correctly.
Stillcore/secure_boot.c:134,stage1/jump_app.c:66,core/fw_update.c:259, and
eos_secure_boot()still has no caller outsidetests/. Restated as finding 4. - Finding 5 (
tests_runhand-maintained literals) — untouched, correctly. All four
still hand-assigned (test_fw_update.c:318,test_tlv_auth.c:364,test_jump_app.c:268,
test_fw_transport.c:882, the last still19). eBoot#95 is still open. No change wanted.
Findings
| # | Severity | File:line | Finding | Recommended fix |
|---|---|---|---|---|
| 3 | Medium (carried, treatment discharged) | tools/imgpack.py:74, tools/eos_sign.py:105 |
Previous finding 3, unchanged. 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. Fail-closed and coherent — a 0 counter can only fail against a floor, never advance one — and not a defect this PR introduced. It is why the feature cannot reach §28 Validated: the consumer is complete and has no producer. The documentation half of the recommendation was discharged at fb978c8e and the Scope section still carries it, so nothing further is owed by this PR. Re-confirmed against the open PR list this run: eBoot#88 builds a real authenticated TLV area in tools/eos_sign.py but does not emit the counter; eBoot#107 touches imgpack.py for version-component overflow only. |
Unchanged for this PR. Follow-up: land eBoot#88 first, then add --min-sec-ver N as one further entry through its TLV builder rather than a second TLV writer, plus a round-trip test through eos_rollback_read_image_counter(). tools/imgpack.py needs the same option and has nothing in flight that provides it. |
| 4 | Low (carried, untouched) | core/secure_boot.c:134, stage1/jump_app.c:66, core/fw_update.c:259 |
Previous finding 4, unchanged. Three copies of read → verify → stage. | 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. |
| 5 | Low (carried, untouched) | test_fw_transport.c:882, test_fw_update.c:318, test_tlv_auth.c:364, test_jump_app.c:268 |
Previous finding 5, unchanged. 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. |
No new finding is filed at this head. The new commit adds no code, and I re-read the
transport code it documents rather than assuming the last review's conclusion still held:
I traced every EOS_OK exit from all three receivers again and each is still gated on
eos_fw_update_bytes_wanted(ctx) == 0 (uart_raw_receive():126, xmodem_receive():254,
ymodem_receive():384). I specifically probed whether the default: return 0 arm of
eos_fw_update_bytes_wanted() — which covers ERROR as well as COMPLETE — could let a
failed write reach a completeness check that then reads as complete. It cannot: every
eos_fw_update_write() / write_container_bytes() failure returns immediately with NAK or
CAN before any completeness check (core/fw_transport_uart.c:111-116, :302-307, :522-527),
so no errored context can reach an EOS_OK exit. Fail-closed holds.
Architecture conformance
Conforms, on the same reading as all seven previous reviews. A documentation-only commit
moves no boundary; I record the sections rather than re-arguing them.
- §8.1 "Rollback protection where hardware/policy supports it" and
.ai/security.md
("anti-rollback counters actually checked"): unchanged. The authenticated
EOS_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 remains the honest qualifier. - §8.1 "Explicit separation between implemented, experimental and planned security
features": this is the requirement the last two heads have been serving, and this
commit completes it. The rule now exists in the public header, in book §7.1 for the
custom-transport author, and in book §16.1 (:1332) for the container itself — the three
places a reader can arrive from. The PR body's Scope and Follow-up sections still state
plainly what is not implemented, including the packer gap. .ai/security.md"Fail closed": unchanged and re-verified above.- §5.1: no layering violation; nothing points up a tier. TCB delta at this commit is
zero lines — documentation only. That is the direction "minimal and auditable" wants. - §21: Tier 1 (Foundation). Install-time rollback policy and update transports belong
in eBoot; correct repo. §21.1 not engaged. - §23.2 / API compatibility: no public signature, struct layout, manifest field or
serialized format changes at this head. The behavioural break toeos_fw_transport_uart_raw()
landed atd722e9a0, is declared in the header, and is now declared in the PR body's
Compatibility section as well (finding 1, resolved). - §28: all five of the body's test claims now reproduce exactly at this head. The
build/CI section of the body is honest about what does not build and why.
No proposal is appended this run, and that is deliberate. The design questions behind
this PR were settled by three proposals already filed against earlier heads — the
2026-09-05 entries at proposals/2026-09.md:4042 (a receiver must error on a stream that
ends before the container does), :4427 (a sender-declared length "may be used to detect
end of transfer; it may not be used to license discarding bytes"), and :4321 (§8.1
requires the counter to be checked and never says anyone has to write it). This commit
documents the first two; nothing in it reveals a new gap in the master design.
Proposed changes
- Nothing on this PR. Findings 1 and 2 are resolved and findings 3, 4 and 5 are not
this PR's to fix. From the author's side this branch is finished. - Merge order — the only thing still standing, and all of it external:
- eBoot#94 or #105 must land first. This head does not build on its own, and does
not cause that (evidence below). Both PRs fix the twoorigin/masterdefects. - eBoot#96 touches
stage1/jump_app.candtests/CMakeLists.txt; eBoot#104
touchescore/fw_update.candtests/CMakeLists.txt. This PR touches all three
files, so both will conflict textually. Verified bygh pr diff --name-onlythis run. - eBoot#88 touches
include/eos_image.handtools/eos_sign.py; it will need
sequencing against #94/#105 rather than against this PR, and it is the prerequisite
for the finding-3 follow-up. - No open PR touches
core/fw_transport_uart.cordocs/book/book.md— I checked all
19 other open PRs by name-only diff. Nothing here duplicates work in flight.
- eBoot#94 or #105 must land first. This head does not build on its own, and does
I have opened no fix PR. There is no High finding at this head, and the policy permits fix
PRs only for High findings that are small and provable. The two master build defects are
High in their own right but are already fixed by two open PRs (#94, #105), so opening a
third would be exactly the duplicate the brief forbids.
Not checked
-
CI: no checks ran, for the eighth 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. Eight 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 at
this head, not inherited. I extracted0aec419ewithgit archiveinto a scratch
directory — the repository, its clean working tree, its branch
(fix/ed25519-low-order-keys) and its stashes were never touched — and ran the CI's own
host configuration from.github/workflows/build.yml:26-31unmodified. Configure
succeeds; the build fails exit 2 oninclude/eos_image.h:135,142, which assert on a
reservedmember thattlv_len/tlv_hashreplaced. This PR does not touch that file.
core/ed25519_verify.cstill definespoint_is_identitytwice (:281,:338). Both
defects are onorigin/master(22d8f8b) — I confirmed by reading both files at that
commit directly. eBoot#94 and eBoot#105 both fix them and both are open. -
What I ran, in a scratch copy with only those two
masterdefects patched out and
nothing else changed:$ cmake -B build -DEBLDR_BUILD_TESTS=ON -DEBLDR_BOARD=none # exit 0 $ cmake --build build --parallel 4 -- -k $ ctest --test-dir build --output-on-failure --no-tests=error -E ed25519 100% tests passed, 0 tests failed out of 22 $ ./build/tests/eboot_test_fw_update 6/6 passed $ ./build/tests/eboot_test_tlv_auth 9/9 passed $ ./build/tests/eboot_test_jump_app 2/2 passed $ ./build/tests/eboot_test_fw_transport 19/19 tests passed $ ./build/tests/eboot_test_rollback 14/14 tests passedAll five of the body's numbers reproduce exactly — the stale
18/18of the last head is
gone. 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 defects above and untouched by this PR. -
The ARM cross build PASSES. Same patched scratch copy, the exact configuration from
.github/workflows/build.yml:41-49:$ 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. I grepped both build logs for
fw_transport_uart,fw_update.c,rollback.c,jump_appandimage_verify: no
warning or error in any file this PR touches, on either build. The ARM log does carry
pre-existing-Wformatwarnings incore/power_init.c:84,core/ecc_scrub.c:65and
core/bmc_handoff.c:55— none of those files is in this diff. -
git diff --check 22d8f8b 0aec419e— PASS, exit 0. The body claims it and it holds. -
Two of the four base-branch blockers the body lists are NOT VERIFIED by me. I
confirmed the duplicatepoint_is_identityand the stalereserved[]asserts directly.
The other two are MSVC-specific: I have no MSVC toolchain here and no workflow in
.github/workflows/build.ymlexercises one, so/std:clatestincore/keystore.cand
the__builtin_add_overflowwarning handling are unchecked claims either way. I did
check that__builtin_add_overflowis genuinely pre-existing rather than introduced
here —origin/master:core/fw_update.c:222already uses it, so the body's "existing" is
the correct characterisation. -
cppcheck(the CIlintjob) — NOT RUN. It is|| truein the workflow
(.github/workflows/build.yml:73) and so cannot fail the build; I did not run it
separately and make no claim about what it would say. -
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
from all seven 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 3 it means neither end of the anti-rollback mechanism has been
shown to work outside a unit test. -
XMODEM still has no retransmission tolerance, and I still could not settle whether it
matters.core/fw_transport_uart.c:272NAKs any block whose number is not
expected_blk, including a resend of the block just written, where
ymodem_receive():477-482ACKs it without rewriting. Unchanged by this commit. The
scripted harness replays a fixed byte stream and cannot model a sender reacting to a NAK,
so this stays unfiled for the fourth review running: I cannot demonstrate the failure. -
slot_remain_from()still skips a slot based at address 0 (core/rollback.c:79).
Unchanged, still latent, 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. -
Book ch. 7 is still wrong about the
receivesignature, and I did not re-file it.
docs/book/book.md:740still showsint (*receive)(uint8_t *buf, size_t max_len, size_t *actual_len)and the §7.3 and §7.4 examples are written against that shape, so the new
contract paragraph now sits between a listing and an example that contradict it. This is
pre-existingmasterbreakage that this PR did not create, the last review explicitly
ruled it out of scope, and the body's Follow-up section now names it as separate work.
Recording it here so whoever repairs ch. 7 knows the contract paragraph is already in
place and needs no second copy. -
Merge conflicts against eBoot#96 and #104 were confirmed but not resolved. I verified
the file overlaps by name-only diff and did not attempt a trial merge. -
existing-comments.txtis empty, so nothing here repeats a human reviewer. There are
still none on this PR after eight heads.
Automated architecture review of 0aec419ee5e5 — 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.
Twenty PRs were merged into master in ninety minutes on 09-07, each on the base it was written against. Master has not configured, compiled, linked or passed its tests since. Every nightly and every eos simulation run (which builds eBoot master) has been red for the same reason. Configure: - tests/CMakeLists.txt registered eboot_test_fdt_loader twice. embeddedos-org#84 added it, and embeddedos-org#85 -- stacked on embeddedos-org#84 -- re-added it at a different anchor when it was replayed onto a master that already had embeddedos-org#84. Same hunk as embeddedos-org#111. Compile (eboot_core): - embeddedos-org#55 restored a core/sha512.c from 02b7dac that keeps its byte count in ctx->count; the eos_sha512_ctx_t master ships has bitlen[2]/buffer_len (embeddedos-org#69/embeddedos-org#93). Back to the pre-embeddedos-org#55 file, blob-identical (d9aa57c). - embeddedos-org#55's source-list "correction" dropped core/boot_log.c, core/secure_boot.c and core/fdt_loader.c, which embeddedos-org#72/embeddedos-org#84 had added after embeddedos-org#55 was written. CMakeLists.txt is blob-identical to pre-embeddedos-org#55 again (f8fe6eb). - embeddedos-org#55 replaced the eos_boot_log_get_head() declaration with a second copy of eos_boot_log_read(); embeddedos-org#91 had already fixed the prototype it meant to fix. Header restored (964ebb8). - embeddedos-org#94 and embeddedos-org#105 each repaired the Ed25519 verifier and each added an identical static scalarbase(); both merged. One copy removed. - The same pair each added k_low_order[]/messages[] to test_ed25519.c. The embeddedos-org#105 copy is removed; embeddedos-org#94's stays because it also carries k_non_canonical[]. Tests that stopped passing because two merged PRs disagree on behaviour: - embeddedos-org#104 verifies the image signature at install unconditionally, before the anti-rollback check embeddedos-org#103 added, so embeddedos-org#103's unsigned images are refused as EOS_ERR_SIGNATURE before they can be refused as EOS_ERR_ANTI_ROLLBACK, and test_fw_transport's XMODEM install can no longer finalize. Both suites now stream genuinely signed images. eBoot has no Ed25519 signer in C, so tools/gen_fw_update_test_sigs.py signs the exact header prefixes those suites build under the RFC 8032 section 7.1 TEST 1 key and emits tests/vectors/fw_update_test_sigs.h; the suites serve that key from a simulated OTP slot 0. Negative control: one flipped signature byte fails test_write_streams_tlv_then_finalize_rejects_below_floor with EOS_ERR_SIGNATURE. - embeddedos-org#103's step 5b reads the TLV counter through the HAL slot containing the image; test_secure_boot_policy (embeddedos-org#82) declared no slots, so eos_secure_boot() returned EOS_SBOOT_ERR_BAD_HEADER two steps before the one under test. The fixture now places its image in slot A. Guards from embeddedos-org#95 that later merges walked back, never run until now because the C configure step failed first: - embeddedos-org#103 replayed the hand-written Valgrind foreach over the derived one. Restored foreach(TEST_NAME ${EBLDR_UNIT_TESTS}); eleven registered suites had no list(APPEND ...) and so no Valgrind run. - Seven suites assign tests_run = <literal> and their TEST() does not count; four suites have no TEST() macro at all. Counted, and classified. - embeddedos-org#101 added fuzz-build after embeddedos-org#90's gate; the gate did not wait for it. CI plumbing: - eosim-sanity.yml: the install-validate job is written in bash but ran under PowerShell on the Windows legs (no shell:), where SITE_PACKAGES=$(...) is an unknown command and `|| { exit 1 }` is an unexecuted script block. - scorecard.yml: ossf/scorecard-action@v2.4.0 pulls gcr.io, which now demands GCP billing. v2.4.3 pulls ghcr.io; eos already pins it and is green. Verified locally (macOS, clang): Release build clean, 31/31 ctest; the same under -DEBLDR_SANITIZE=ON (ASan+UBSan); 78/78 pytest. Not fixed here, reported separately: core/keystore.c's compiled-in default_dev_key is described as the RFC 8032 TEST 1 public key but differs from byte 21 on and is not a point on the curve, so nothing can verify against it on any board without OTP. With embeddedos-org#104 that makes firmware update refuse every image on such boards.
…epair embeddedos-org#103 and embeddedos-org#104 were both merged and disagree on whether the anti-rollback counter or the signature is checked first in eos_fw_update_finalize(). The master design orders boot as verify image, then version policy (section 8.1), but its update flow (section 15) never places the anti-rollback check, so the order the install path uses existed only in a PR body. ADR-020 records it: the signature over the signed header prefix is verified first, the TLV counter is read only after the prefix that binds it is authenticated, and an image that fails verification is refused as EOS_ERR_SIGNATURE without its counter being consulted. docs/adr/README.md is added in the shape of the eos repository's index; 020 avoids reusing 001 through 019. CHANGELOG.md gains the Unreleased entries for the repair: the configure, compile and link breakage after the 09-07 batch merge, the settled check ordering with the suites streaming signed images, the re-derived Valgrind list, fuzz-build in the CI gate, counted tests_run, the EoSim Windows legs running under bash, and the Scorecard action on its ghcr.io-hosted release.
The changelog said eleven suites had no Valgrind run. Eleven were missing from EBLDR_UNIT_TESTS, but four of those were named in the hand-written foreach and did run; seven had no run at all. Say which. ADR-020: its design-document citation now says where it comes from (the architecture review of embeddedos-org#115), and the sentence about the two PRs' bases is replaced with what the history shows -- embeddedos-org#103's commits predate embeddedos-org#104's merge, and embeddedos-org#104 was written without embeddedos-org#103's check in place.
Twenty PRs were merged into master in ninety minutes on 09-07, each on the base it was written against. Master has not configured, compiled, linked or passed its tests since. Every nightly and every eos simulation run (which builds eBoot master) has been red for the same reason. Configure: - tests/CMakeLists.txt registered eboot_test_fdt_loader twice. embeddedos-org#84 added it, and embeddedos-org#85 -- stacked on embeddedos-org#84 -- re-added it at a different anchor when it was replayed onto a master that already had embeddedos-org#84. Same hunk as embeddedos-org#111. Compile (eboot_core): - embeddedos-org#55 restored a core/sha512.c from 02b7dac that keeps its byte count in ctx->count; the eos_sha512_ctx_t master ships has bitlen[2]/buffer_len (embeddedos-org#69/embeddedos-org#93). Back to the pre-embeddedos-org#55 file, blob-identical (d9aa57c). - embeddedos-org#55's source-list "correction" dropped core/boot_log.c, core/secure_boot.c and core/fdt_loader.c, which embeddedos-org#72/embeddedos-org#84 had added after embeddedos-org#55 was written. CMakeLists.txt is blob-identical to pre-embeddedos-org#55 again (f8fe6eb). - embeddedos-org#55 replaced the eos_boot_log_get_head() declaration with a second copy of eos_boot_log_read(); embeddedos-org#91 had already fixed the prototype it meant to fix. Header restored (964ebb8). - embeddedos-org#94 and embeddedos-org#105 each repaired the Ed25519 verifier and each added an identical static scalarbase(); both merged. One copy removed. - The same pair each added k_low_order[]/messages[] to test_ed25519.c. The embeddedos-org#105 copy is removed; embeddedos-org#94's stays because it also carries k_non_canonical[]. Tests that stopped passing because two merged PRs disagree on behaviour: - embeddedos-org#104 verifies the image signature at install unconditionally, before the anti-rollback check embeddedos-org#103 added, so embeddedos-org#103's unsigned images are refused as EOS_ERR_SIGNATURE before they can be refused as EOS_ERR_ANTI_ROLLBACK, and test_fw_transport's XMODEM install can no longer finalize. Both suites now stream genuinely signed images. eBoot has no Ed25519 signer in C, so tools/gen_fw_update_test_sigs.py signs the exact header prefixes those suites build under the RFC 8032 section 7.1 TEST 1 key and emits tests/vectors/fw_update_test_sigs.h; the suites serve that key from a simulated OTP slot 0. Negative control: one flipped signature byte fails test_write_streams_tlv_then_finalize_rejects_below_floor with EOS_ERR_SIGNATURE. - embeddedos-org#103's step 5b reads the TLV counter through the HAL slot containing the image; test_secure_boot_policy (embeddedos-org#82) declared no slots, so eos_secure_boot() returned EOS_SBOOT_ERR_BAD_HEADER two steps before the one under test. The fixture now places its image in slot A. Guards from embeddedos-org#95 that later merges walked back, never run until now because the C configure step failed first: - embeddedos-org#103 replayed the hand-written Valgrind foreach over the derived one. Restored foreach(TEST_NAME ${EBLDR_UNIT_TESTS}); eleven registered suites had no list(APPEND ...) and so no Valgrind run. - Seven suites assign tests_run = <literal> and their TEST() does not count; four suites have no TEST() macro at all. Counted, and classified. - embeddedos-org#101 added fuzz-build after embeddedos-org#90's gate; the gate did not wait for it. CI plumbing: - eosim-sanity.yml: the install-validate job is written in bash but ran under PowerShell on the Windows legs (no shell:), where SITE_PACKAGES=$(...) is an unknown command and `|| { exit 1 }` is an unexecuted script block. - scorecard.yml: ossf/scorecard-action@v2.4.0 pulls gcr.io, which now demands GCP billing. v2.4.3 pulls ghcr.io; eos already pins it and is green. Verified locally (macOS, clang): Release build clean, 31/31 ctest; the same under -DEBLDR_SANITIZE=ON (ASan+UBSan); 78/78 pytest. Not fixed here, reported separately: core/keystore.c's compiled-in default_dev_key is described as the RFC 8032 TEST 1 public key but differs from byte 21 on and is not a point on the curve, so nothing can verify against it on any board without OTP. With embeddedos-org#104 that makes firmware update refuse every image on such boards.
…epair embeddedos-org#103 and embeddedos-org#104 were both merged and disagree on whether the anti-rollback counter or the signature is checked first in eos_fw_update_finalize(). The master design orders boot as verify image, then version policy (section 8.1), but its update flow (section 15) never places the anti-rollback check, so the order the install path uses existed only in a PR body. ADR-020 records it: the signature over the signed header prefix is verified first, the TLV counter is read only after the prefix that binds it is authenticated, and an image that fails verification is refused as EOS_ERR_SIGNATURE without its counter being consulted. docs/adr/README.md is added in the shape of the eos repository's index; 020 avoids reusing 001 through 019. CHANGELOG.md gains the Unreleased entries for the repair: the configure, compile and link breakage after the 09-07 batch merge, the settled check ordering with the suites streaming signed images, the re-derived Valgrind list, fuzz-build in the CI gate, counted tests_run, the EoSim Windows legs running under bash, and the Scorecard action on its ghcr.io-hosted release.
The changelog said eleven suites had no Valgrind run. Eleven were missing from EBLDR_UNIT_TESTS, but four of those were named in the hand-written foreach and did run; seven had no run at all. Say which. ADR-020: its design-document citation now says where it comes from (the architecture review of embeddedos-org#115), and the sentence about the two PRs' bases is replaced with what the history shows -- embeddedos-org#103's commits predate embeddedos-org#104's merge, and embeddedos-org#104 was written without embeddedos-org#103's check in place.
Summary
Fix eBoot firmware update handling so the authenticated TLV anti-rollback counter is correctly consumed and enforced, while ensuring transport framing cannot report incomplete transfers as successful or accept unbounded data after the update container is complete.
The update path now treats the authenticated TLV area as part of the complete firmware container and keeps transport-level framing separate from the container's actual byte requirements.
Changes
Anti-rollback
EOS_TLV_MIN_SEC_VERTLV rather than using the image version field.eos_image_check_rollback()path.Streaming TLV handling
Transport completeness — XMODEM, YMODEM and raw
eos_fw_update_bytes_wanted().CANand returnEOS_ERR_INVALIDwhen an extra post-container XMODEM/YMODEM block is received.file_sizeas the protocol/framing bound while the update container remains the authority for actual bytes written.UART_RAW_NAKand returningEOS_ERR_INVALID.EOS_OKmeans the complete[header][payload][TLV area]container has been written.Compatibility
EOS_ERR_INVALID.UART_RAW_NAKandEOS_ERR_INVALIDinstead of the previousUART_RAW_ACKandEOS_OK.Testing
Targeted host tests:
test_fw_update— 6/6 passedtest_tlv_auth— 9/9 passedtest_jump_app— 2/2 passedtest_fw_transport— 19/19 passedtest_rollback— 14/14 passedAdditional regression coverage verifies:
git diff --checkpasses for the submitted changes.Build / CI Status
The targeted host tests pass as listed above.
The repository-wide CMake build is currently blocked by pre-existing issues on the base branch, including:
point_is_identitydefinition incore/ed25519_verify.c__builtin_add_overflowMSVC warning/error handling/std:clatestrequirement incore/keystore.creserved[]static assertions ininclude/eos_image.hThese issues are unrelated to the changes in this PR and are tracked separately.
The ARM cross-build has also been verified successfully during review.
Scope
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 emitEOS_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.The remaining rollback-code duplication and dedicated test-infrastructure cleanup are also separate follow-up work.
Follow-up
The following items are intentionally left for separate issues/PRs:
Transport contract documentation
The transport receive framing only bounds the transfer; the firmware update container remains authoritative for completeness.
Returning
EOS_OKwhileeos_fw_update_bytes_wanted()is nonzero is a contract violation.EOS_OKmeans the complete[header][payload][TLV area]update container has been written.