fix(multicore): an AMP core starts only on an image that verified - #146
Open
Kartikey1306 wants to merge 7 commits into
Open
Kartikey1306 wants to merge 7 commits into
Kartikey1306 wants to merge 7 commits into
Conversation
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.
… -1.0.0 as an option test_out_of_range_version_is_rejected[-1.0.0] passed locally (Python 3.14) and failed in CI (ubuntu-22.04, Python 3.10) with argparse's own "expected one argument": the older negative-number matcher does not accept -1.0.0, so the token was read as an unknown option and imgpack.py's range check -- the thing under test -- never ran. The joined form is unambiguous on every interpreter and the test now reaches the tool's message.
tests/vectors/fw_update_test_sigs.h is the output of tools/gen_fw_update_test_sigs.py, committed because eBoot has no Ed25519 signer in C. Nothing checked that the two agree: a generator edit without a regeneration leaves test_fw_update and test_fw_transport verifying against stale signatures, failing with EOS_ERR_SIGNATURE and nothing to say why. tests/unit/test_fw_update_test_sigs.py runs the generator with the test's own interpreter and compares its stdout to the committed header byte for byte, so a line-ending change counts too. It follows the same dependency rule as test_eos_sign_payload_offset.py: with EOS_REQUIRE_SIGNING_TESTS set (the CI workflow sets it before the pytest step) a missing cryptography module fails the job instead of skipping. Negative control: one flipped hex byte in the header fails the test with a unified diff naming the line. build_image() and build_container() now carry a comment naming the coupling: the signed prefix is assembled both there and in the generator, and changing any field in it means changing the generator's copy and regenerating the header.
…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.
The regeneration test's failure path -- the unified diff under the regenerate command -- ran only when the header was stale, so a green run never executed it and the coverage report said so. The report is now a helper the match asserts with, and a second test drives the helper with two byte strings that differ in one byte and checks the command and both sides of the changed line appear.
eos_multicore_start() parsed the header of the AMP slot and started the core on it. Parsing proves the slot is shaped like an image; it authenticates nothing, and whatever can write the slot chooses entry_addr. Every other slot consumer here -- slot_manager.c, stage1/jump_app.c, recovery.c, fw_update.c -- checks integrity and signature; this was the one that did not. The checks go in eos_multicore_start() rather than in the eos_multicore_start_amp() wrapper: both are public, and a caller can reach the former with a hand-built config, so guarding only the wrapper would leave an unverified way in. entry_addr must also equal the one the signed header names. Verifying an image and then branching elsewhere would authenticate an image nobody runs; the wrapper already derives entry_addr from that header, so this rejects only a config that disagrees with the image it points at. Five cases in test_multicore.c cover it: a verified image starts the core, a failed signature and a failed integrity check do not, an entry_addr the header does not name does not, and the wrapper inherits all of it. Removing each of the three checks on its own fails exactly its own case and no other test.
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #145
Stacked on #115; review
e152d8e..97f3bc3for this change alone — one commit, two files (+155/−1). #115 repairs master, which does not build today, so this branch sits on it rather than onmaster; nothing here depends on the rest of the eBoot stack. Merge order: #115, then this (it is independent of #116/#122/#123/#125/#127/#129/#131/#133/#135/#138/#139/#142 and can land in any order among them).The defect
core/multicore.c:64-72parsed the AMP slot's header and started the core on it:Parsing proves the slot is shaped like an image. It authenticates nothing, and whatever can write the slot chooses
entry_addr. Every other slot consumer in the tree checks integrity and signature —slot_manager.c:62,68,stage1/jump_app.c:47,54,recovery.c:324,328,fw_update.c:259. This was the only one that did not.Measured before the fix, with a tampered image in slot A (
load_addr == 0, soimage_verify.c:101skips itsentry_addrrange check too):The change
core/multicore.c, +26/−1: after the existing parse,eos_image_verify_integrity(), theneos_image_verify_signature(), thencfg->entry_addrmust equalhdr.entry_addr.Why in
eos_multicore_start()and not in theeos_multicore_start_amp()wrapper. Both are public (include/eos_multicore.h). A caller can build aneos_core_config_tby hand and reacheos_multicore_start()directly, so guarding only the wrapper would leave an unverified twin. The wrapper inherits the checks instead of carrying a second copy.Why
entry_addris pinned to the header. Verifying an image and then branching somewhere else authenticates an image nobody runs. The wrapper already setscfg.entry_addr = hdr.entry_addr, so this rejects only a config that disagrees with the image it points at. This is the one behaviour change a direct caller could notice, and it is deliberate — stated here rather than left to be discovered.Untouched: SMP, lockstep, and AMP with
image_slot == EOS_SLOT_NONE, which name an address with no image to authenticate and are unchanged by this.Tests
Five cases in
tests/unit/test_multicore.c(+130). The file had no slot geometry and no image mocks, so this adds a minimaleos_board_ops_t(onlyslot_a_addr/slot_a_size; every other accessor NULL-guards) and link-time overrides ofeos_image_parse_header/verify_integrity/verify_signature— the same techniquetest_slot_manager.calready uses, so each stage can be failed independently without signing real images.test_amp_starts_a_verified_image— all three pass, core startstest_amp_refuses_a_failed_signaturetest_amp_refuses_a_failed_integrity_checktest_amp_refuses_an_entry_addr_the_image_does_not_nametest_amp_wrapper_refuses_a_failed_signature— the wrapper inherits itEach check was removed on its own and fails exactly its own case — not the collective revert, which only proves the first one:
With all three removed, exactly 1 of 31 ctest tests fails (
test_multicore) and no other.Verified at
97f3bc3, gcc/Ninja/Debug,-DEBLDR_BUILD_TESTS=ON:ctest --no-tests=error31/31 passed;pytest tests/82 passed;test_multicoreitself 15/15.Not run here: the ARM and release legs, and any real AMP board — there is no hardware in this loop, so the board's
start_core()is the mock. What is proven is that the core is not started at all unless the image verifies.