Repository navigation
fix: fix(security): secure boot crashes on a NULL path before the guard that rejects it - #146
Conversation
…rd that rejects it Opened by the scheduled autoreview pipeline after review of open PRs. Reviewed against the EmbeddedOS Master Design v2.0. Files: services/security/src/security.c tests/test_security.c
srpatcha
left a comment
There was a problem hiding this comment.
Review — eos#146 "fix(security): secure boot crashes on a NULL path before the guard that rejects it"
head: f648600 author: srpatcha ci: fail (9 red, 19 green, 4 skipping)
Role note. Opened by this same autoreview pipeline (
autofix/branch)..ai/reviewer.md— "if you implemented it, you do not approve it" — applies. Comment only; no approval, no change request. eSec code gets the strictest reading per the brief §7.
Verdict: The fix is correct, minimal and fail-closed, the new test genuinely pins it, and the PR body is the most honest in this batch — it discloses the broken trunk, cites the PR that fixes it, and says plainly that ctest-full-suite was produced under ninja -k 0. Two things stand between it and merge: eight of the nine red checks are a pre-existing trunk build break it cannot fix, and the ninth is caused by a missing line in its own body. Separately, the identical defect sits three more times in the file being edited, including in the ACL path.
Findings
| # | Severity | File:line | Finding | Recommended fix |
|---|---|---|---|---|
| 1 | Critical (P0) | services/linux/src/linux_security.c:689,695 on origin/master |
The trunk does not compile. #ifndef _WIN32 opens at :674, #else at :689, a second #else at :695, #endif at :697. GCC: error: #else after #else; MSVC: error C1019: unexpected #else. This takes out Build & Test (Linux x86_64), Build & Test (Windows MSVC), Cross-compile ARM64 kernel, Default configuration (no tests, no product), Analyze (C/C++) (CodeQL cannot build the database), Full-stack integration summary and CI Gate — seven of the nine red checks. Not caused by this PR. Verified: the diff touches only services/security/src/security.c and tests/test_security.c, and git merge-base --is-ancestor origin/master f6486007 succeeds, so the branch is current with master and inherited the break. Every other #ifndef/#else/#endif triple in that 700-line file is well-formed; this is one stray line, introduced in the 1391efd / 4d80b35 / eddb6ac range. |
Already fixed by open PR #144 (fix/repair-master-else-collision-and-guards), whose diff deletes exactly this -#else. Do not open a duplicate fix — per brief §"Opening fix PRs", a second PR for the same defect is worse than none. #146 is blocked behind #144; land #144 and re-run. |
| 2 | High | PR body (no closing-issue reference) | policy / Policy / Linked Issue is red, and this PR caused it. No Fixes #, Closes # or Resolves # in the body, so validate_linked_issue.py exits 1. This is the one red check that is this PR's own and the one it can clear without waiting for anything. eNI#34 in the same batch has the identical omission. |
Append Fixes #NN for the eos issue covering this defect. No code change. |
| 3 | Medium | services/security/src/security.c:13, 106-108, 170, 231-233 |
The fix repairs one of four instances of the same defect, in the same file, and the body does not say why the other three are out of scope. The pattern the PR correctly identifies — strncpy(dst, <caller-supplied pointer>, n) reached before any NULL check — also occurs verbatim in eos_firmware_sign (:106-108, three unguarded params), eos_keystore_add (:170, unguarded id), and eos_acl_add_rule (:231-233, three unguarded params). eos_secureboot_init (:13) does memset(sb, 0, sizeof(*sb)) with no NULL check, six lines above the function this PR hardens with if (!sb) return -1; — so eos_secureboot_init(NULL) still segfaults while eos_secureboot_verify_image(NULL, …) now returns cleanly. eos_acl_add_rule is the one that matters most: it builds the authorization table, and brief §7 puts IPC and policy input validation under the strictest reading. |
Not necessarily this PR — but pick one and say so. Either extend the same three-line guard to the other four entry points and the test to match (it is the same edit four times, and test_security.c already exercises the keystore and ACL, so the cases are cheap), or add a sentence to the body scoping this PR to secure boot and file the rest. Silence reads as "we did not notice", which the body's quality suggests is not the case. |
| 4 | Low | services/security/src/security.c:25 vs :65 |
The new guard rejects NULL only (!image_path || !sig_path || !pubkey_path), while the downstream guard it explicitly cites as precedent rejects NULL and empty (if (!pubkey_path || !*pubkey_path) at :65). So "" still passes the new gate. No defect results — an empty image_path fails at eos_sha256_file, an empty sig_path at fopen, and an empty pubkey_path at :65, all setting EOS_BOOT_FAILED, so every path is fail-closed. But .ai/security.md asks for zero-length input to have defined behaviour at the boundary, and the gate now states a narrower contract than the check it was modelled on. |
Make it !image_path || !*image_path || !sig_path || !*sig_path || !pubkey_path || !*pubkey_path, and add "" cases to test_secureboot_null_paths alongside the NULL ones. |
| 5 | Low | tests/test_security.c:148 |
tests_run = 9; is a hand-maintained literal assigned in main() after every test has run. The TEST(name) macro at :18-26 increments tests_passed but nothing ever increments tests_run, so the count and the calls have to be kept in sync by hand — and this PR is the second edit in a row that has to remember to bump it. It fails loudly rather than silently when they drift (tests_passed == tests_run goes false, main returns 1), so this is a maintenance smell, not a correctness hole. |
While the line is open: add tests_run++; to the run_##name body at :21 and delete the literal at :148. Three-line change, removes the class. |
Architecture conformance
Conforms. eos is Tier 1 — Foundation (master design §21), and services/security/ is the eSec-family code that §5.1 places inside EmbeddedOS Core; §14 owns it. The diff adds no #include, no link line, no CMake target_link_libraries entry and no manifest dependency, so nothing points up a tier and §5.1's law is untouched. No public signature, struct layout, manifest field or serialized format changes, so brief §8 (API and wire compatibility) is not engaged — and the body states this explicitly rather than leaving it silent, which is what §8 actually asks for. No §21.1 split question arises.
The body is right that this is not an eBoot change, so §5.1's "eBoot keeps the trusted computing base minimal and auditable" is not the clause at stake. The clause that is, is .ai/security.md's fail-closed rule, and the change satisfies it: a NULL path now produces EOS_BOOT_FAILED and -1 where it previously produced undefined behaviour, and sb->status is set before returning so an attestation consumer cannot read a stale EOS_BOOT_UNVERIFIED and mistake it for "not yet checked". Guarding before the copies rather than after is the correct ordering, and the comment explaining why the later guard existed is the kind of thing that stops the next person re-introducing it.
Two things I checked because §14.1 and .ai/security.md require it, and which bound the severity of the bug being fixed:
eos_rsa_verify_sha256fails closed.services/crypto/src/rsa_ecc_sha512.c:69-71isEOS_STUB_CRYPTO_REFUSE("eos_rsa_verify_sha256"), not a stub that returns 0. Soeos_secureboot_verify_imagecannot currently reachEOS_BOOT_VERIFIEDat all — it is fail-closed end to end, and the crash this PR fixes was a denial-of-service on the caller, not a verification bypass. That is why this is High-class work and not Critical.- The key-loading path at
:61-82is unchanged by this PR and I am not raising it as a finding here, but for the record:pub_key.eis left zero by thememsetand never populated, andfread(pub_key.n, 1, sizeof(pub_key.n), kf)treats the file's raw bytes as the modulus with no format parsing and no length check beyondkey_len == 0. Today that is harmless because the verify is a refusing stub. It will not be harmless the day a real RSA implementation lands behind it, and.ai/security.md— "reject invalid and low-order public keys explicitly; validate length and encoding before use" — will apply then. Worth an issue now, while it costs nothing.
Under §28's claims policy, eos_secureboot_verify_image remains Experimental after this change, not Implemented: the crash is gone and the input contract is real, but the verification it names does not happen.
Proposed changes
Smallest sequence that keeps everything building:
- Add
Fixes #NNto the body. One line, clears the only red check this PR owns. (2) - Wait for #144. Nothing else can turn the other seven green, and re-running CI before it lands only burns minutes. (1)
- Decide finding 3 explicitly — either extend the guard to
eos_secureboot_init,eos_firmware_sign,eos_keystore_addandeos_acl_add_rulewith matching test cases, or scope the body to secure boot and file the rest. Do not leave it unsaid. (3) - If you take the widening: fold in the
""cases (4) and thetests_run++macro change (5) at the same time, since both are in lines this PR already touches.
Keep in draft until step 1 is done. Steps 4 and 5 are genuinely optional; step 3 is not, because an unexplained partial fix in an authorization file is the thing a future reader will misread as "these were checked and are fine".
Not checked
- Nothing was built or run by this review.
cmake,ctest,cppcheckandclang-tidywere NOT RUN. The localeoscheckout is one37ef4a, which is notorigin/master(9ed6831), and the brief forbids touching the user's working tree. The PR body's verification table (build,cppcheck,ctest,ctest-full-suite, all recorded viafix-verify.sh) was read, not reproduced — including the "41/41 test binaries passed" claim, which I cannot confirm or refute. - Finding 1 was established from source and git metadata, not from a compile. I read the
#if/#else/#endifstructure oforigin/master:services/linux/src/linux_security.c, confirmed the ancestry relationship, and read the GCC and MSVC diagnostics out of the CI logs. I did not compile the file myself. - The CI failures were diagnosed from logs, not reproduced.
gh run view --log-failedfor runs 34797285223, 34797285238, 34797285246 and 34797285682. I did not runpytest tests/unit/locally; the fourRun Python testsfailures (test_ci_gate.py×2,test_cmake_test_registration.py×2) are quoted from that output. All four are repo-hygiene meta-tests about workflow classification and unregistered C suites, all pre-existing, none namingtest_security.c— which is also the evidence that this PR's new test is correctly registered, sincetest_every_test_function_is_callednames only fourtest_kernel.cfunctions. - PR #144 was read at its diff, not merged or tested. That it removes this exact
-#elseis Observed; that merging it makes the tree build is Inferred. - Findings 3, 4 and 5 are Observed in source at
services/security/src/security.candtests/test_security.c; no NULL or""argument was actually passed toeos_firmware_sign,eos_keystore_addoreos_acl_add_ruleto demonstrate a crash. mergeable/mergeStateStatuswere not usable in the bundle, so whether this branch still merges cleanly ontomasteris unconfirmed.
Automated architecture review of f64860070647 — 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.
|
The eight red checks here are inherited from
Every job that builds the tree fails on it, and Verified by building the merge result locally rather than inferring it:
Nothing is needed here. Once #144 lands, a rebase or merge of |
|
Resolved the contribution-caused linked-issue policy failure without changing the branch.
The separate build/test failures remain attributable to current |
Problem
eos_secureboot_verify_image()crashes on exactly the input it was written toreject.
services/security/src/security.c:65refuses a missing public key path:That guard is the author stating plainly that
NULLis an input this functionexpects to receive. But it sits 43 lines below three unconditional copies at
:20-22:strncpy(dst, NULL, n)is undefined behaviour. The guard is unreachable for thecase it names.
Reproduction
Against
origin/master(9ed6831), a harness callingeos_secureboot_verify_image(&sb, "/etc/hostname", "/etc/hostname", NULL)dieswith SIGSEGV (exit 139) before printing anything.
The same is now reproducible from the test suite itself: the first recorded
ctestrun below is this branch's new test case applied to master'sunmodified
security.c, and it reportstest_security ....***Exception: SegFault. The laterctestrecord is the sametest against the fixed source. Both are kept in the verification log
deliberately ΓÇö the failing run is what shows the test actually catches the
defect rather than merely passing alongside it.
Root cause
Guard ordering, nothing subtler. The NULL checks were written after the copies
instead of before them.
Fix
Move the guards above the copies, and add the two that were missing:
sb == NULL→ return-1(cannot set a status through a null pointer).image_path,sig_path,pubkey_pathNULL →EOS_BOOT_FAILED,with a diagnostic on
stderrmatching the existing message style.image_pathandsig_pathhad the same exposure aspubkey_pathand arecovered too; they are dereferenced at
:26and:33a few lines later.The pre-existing
!pubkey_path || !*pubkey_pathcheck at the bottom is leftexactly as it was. It is now redundant for the NULL half, but it still catches
the empty-string case, and removing a security check that works is not something
this change needs to do.
Files changed
services/security/src/security.c— guards hoisted above the three copies.tests/test_security.c— newtest_secureboot_null_pathscase, registered inmain(),tests_run8 → 9. Placed in the existing suite, which already linkseos_security eos_cryptoand already holdstest_secureboot_init, ratherthan adding a new file and a new CMake target for four assertions.
Expected impact
A NULL path fails the boot closed with
EOS_BOOT_FAILEDinstead of killing theprocess.
.ai/security.mdrequires a verification step that cannot run to failrather than produce a result; a segfault is neither.
Note
eos_firmware_verify_sig()(:150-155) forwards its three argumentsstraight into this function, so it inherits the same repair at no cost.
Risks and compatibility
-1andEOS_BOOT_FAILED. No caller could have depended on the oldbehaviour, since the old behaviour was undefined.
differently. No key handling, no crypto and no policy logic is touched ΓÇö this
changes only which inputs reach them.
eos's own security service.What could not be verified here
origin/masterdoes not currently compile:services/linux/src/linux_security.c:695has an
'#else' after '#else', which is a pre-existing break unrelated to thischange and already addressed by open PR #144. A full clean
cmake --buildofthe tree therefore cannot pass on this branch or on master.
The recorded
ctest-full-suiteresult below was produced by building withninja's
-k 0so the one broken target does not stop the rest: 41 testbinaries built and 41/41 passed, which is the regression evidence for this
change. The
buildrecord covers thetest_securitytarget specifically. Ifyou want a green whole-tree build, #144 has to land first.
Surfaced by the maintenance sweep of 2026-09-13; recorded in the backlog on
2026-09-05 as "Secure boot dereferences pubkey_path before the NULL check it
already contains" and deferred then for budget, not for doubt.
Verification
Executed in an isolated worktree branched from
origin/master:buildcmake --build build/host --target test_security --parallel 4cppcheck/home/srpatcha/eos/.ai/toolchains/run-in-toolchain.sh /home/srpatcha/eos/.ai/autoreview/state/worktrees/eos__secureboot-null-path-crashes-before-its-own-guard -- cppcheck --enable=warning --inline-suppr --quiet --template={file}:{line}: {severity}: {message} services/security/src/security.cctestctest --test-dir build/host --output-on-failure --no-tests=error -R ^test_security$ctest-full-suitectest --test-dir build/host --output-on-failure --no-tests=errorOpened by the scheduled autoreview pipeline (model
claude-opus-5), branched fromorigin/master. No human has reviewed this yet. Close it freely if the fix is wrong - a bad automated PR is a bug worth reporting.Fixes #167