Skip to content

fix: repair master after the 09-08 batch merge - #144

Merged
srpatcha merged 7 commits into
embeddedos-org:masterfrom
Kartikey1306:fix/repair-master-else-collision-and-guards
Oct 1, 2026
Merged

srpatcha merged 7 commits into
embeddedos-org:masterfrom
Kartikey1306:fix/repair-master-else-collision-and-guards

Conversation

@Kartikey1306

@Kartikey1306 Kartikey1306 commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

For the merger (2026-09-17): master 9ed6831 fails CI — eos (34664978812) on services/linux/src/linux_security.c:695: #else after #else, so every PR branched from it fails the same eight checks — currently #146 (@srpatcha, 34797285223). This PR's head passes CI — eos (34888516389), is 0 behind master, and nothing needs to land before it. Merging it takes master green and lets #146 go green on a rebase (merge result built and tested: ctest 43/43, pytest 47).

Problem

eos master has not compiled since the 09-08 batch merge (nine PRs within minutes, each replayed onto a master its author never saw). CI — eos, CodeQL, the ARM64 kernel cross-build inside EoS Full-Stack Simulation and Python Unit Tests are all red on d14b62f.

Verified against runs 34176148655 (CI — eos: Linux, Windows MSVC, and the README default configuration all die on the same line), 34176148660 (CodeQL), 34176148692 (simulation) and 34176148714 (Python), and reproduced locally on master before every fix below.

What broke, and which merge broke it

Compile — every C job. services/linux/src/linux_security.c:695: error: #else after #else. #119 (mine) and #132 each added an #else to the same #ifndef _WIN32 in eos_busybox_install_to_rootfs(). The #132 arm ((void)bb;) is the one removed: the #119 arm already uses bb and reports the unsupported platform. I own the interaction.

Python guards from #121 / #93 that later merges walked back (4 failed, 43 passed on master):

Guard What it found Cause Fix
test_ci_gate.py windows-test runs on PRs but ci-gate does not wait for it #132 added the job after #121's gate added to needs
test_ci_gate.py eosim-sanity.yml, simulation-test.yml unclassified #129 (mine) gave both a path-filtered pull_request trigger so they test their own edits recorded in NOT_REQUIRED — a path-filtered check cannot be required, same reason as book-build.yml; the guard's own check that the reason is true of the workflow passes
test_cmake_test_registration.py test_linux_security_paths.c (#119), test_pkg_fetch.c (#115) have no add_executable() #115's replay replaced #119's block with its own; #118's replay replaced that both registered again; 41 → 43 suites, both pass
test_cmake_test_registration.py four test_kernel.c functions defined, never called their RUN() lines did not survive the replay of main() (#130, #131) called from main()

And the one that was not bookkeeping. Calling test_idle_task_is_permanent fails on master: eos_task_delete(0) == EOS_KERN_INVALID is false — the idle task can be deleted. #130 was merged after #131 from a base that predates it, and its copy of kernel/src/task.c replaced #131's. #131 had also flattened 574 CRLF line endings in that file, so its 1166-line diff hid a 39/21 change and the replay took #130's side wholesale. Master kept #131's tests and lost its kernel: the idle task could be deleted and suspended, a half-initialised TCB was published to the scheduler before its stack existed, and eos_schedule() pointed g_current_sp at the outgoing task. #131's task.c diff is re-applied on top of #130's here; verified by git diff 3a00bd9 -- kernel/src/task.c that the result is #131's file plus #130's wake_armed hunk and nothing else. kernel_internal.h and context_switch.S from #131 had survived.

Two more, found by this PR's own CI once the MSVC leg could get past compile for the first time:

Test plan

Verified, macOS / clang:

cmake -DEOS_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release   → 0 errors
ctest --timeout 120                                      → 100% tests passed, 43/43 (was 41 registered)
cmake -DCMAKE_BUILD_TYPE=Release   (README quick start)  → builds
pytest tests/                                            → 47 passed

Not verified here: the MSVC leg and the ARM Cortex-M4 cross build (no toolchain on this machine). The #else fix is preprocessor structure only, identical under every compiler; CI on this PR is the check.

Not in this PR

Review follow-ups (2026-09-14)

Rebased onto the governance commit; findings applied as separate commits on top (the earlier commits are untouched, so the tests( type in two of them stays as reviewed — noted for the squash):

  • API behaviour change, stated plainly: eos_task_delete(0) and eos_task_suspend(0) now return EOS_KERN_INVALID. Handle 0 is the idle task and it is permanent; the two declarations in kernel/include/eos/kernel.h now carry doc comments saying so (finding 2). This restores fix(kernel): preserve scheduler task invariants #131's contract, which Fix task wake timeout at tick zero #130's replay had rolled back.
  • kernel/src/task.c: the two trailing blank lines are gone, so git diff --ignore-cr-at-eol 3a00bd9 -- kernel/src/task.c is now Fix task wake timeout at tick zero #130's wake_armed hunk plus one new comment block at the next < 0 return and nothing else. That comment records why the return is unreachable in normal operation (slot 0 cannot be deleted or suspended, idle_task_func() never blocks, find_next_task() can now return the idle task) and what actually happens if the canary check deletes the idle task: find_next_task() scans every slot including the outgoing one, so next < 0 means nothing is READY and the return leaves state as the canary check left it — nothing recovers from that, and nothing should; an overflowed idle stack is a fault handler's job.
  • services/linux/src/linux_security.c: eos_busybox_install_to_rootfs() refuses NULL arguments before dereferencing bb (finding 3), with a test in test_linux_security_paths.c; verified the (NULL, dir) call segfaulted without the guard (exit 139).
  • CHANGELOG: Fixed entry for the repair, and a Changed entry for the handle-0 contract.
  • sizeof(EosConfig) = 3,953,236 is filed as sizeof(EosConfig) is 3.95 MB — too large for a stack, for MSVC, and for any MCU target #148 (finding 1), not fixed here.

Verified on the current head: Release build clean, ctest 43/43, pytest 47 passed, README default configuration builds.

Closing issue

Fixes #147

srpatcha
srpatcha previously approved these changes Sep 14, 2026

@srpatcha srpatcha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the forensic work here, @Kartikey1306 — this is the rare "repair" PR where every line of the description is checkable, and it all checked out.

What I verified locally (Linux, GCC 13, Release, vbox_test):

  • master fails exactly as described: services/linux/src/linux_security.c:695: error: #else after #else.
    • git diff 3a00bd9 pr-144 -- kernel/src/task.c is only #130's wake_armed hunk, and git show ec59df9 confirms #130 had rolled back #131's scheduler fixes (idle task deletable/suspendable, TCB published before stack_ptr was set, g_current_sp pointing at the outgoing task). context_switch.S and kernel_internal.h from #131 are still on master, so the restored task.c matches the port.
      • Build clean, 43/43 ctest (including test_linux_security_paths and test_pkg_fetch), 47/47 pytest.
        Not verified here: the MSVC leg (the static EosConfig change and the if(UNIX) guard).

Two nits, neither blocking:

  • kernel/src/task.c:595-596 — two trailing blank lines at EOF that aren't in #131's version; removing them makes "nothing else" literally true.
    • The follow-up commits use tests(...) as the type; the repo convention is test. Only matters if you reword when squashing.
      Approving.

@srpatcha srpatcha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — eos#144 "fix: repair master after the 09-08 batch merge"

head: a7f81f3 author: Kartikey1306 ci: pass (28 checks green, 3 skipping)

Verdict: master genuinely does not compile and this genuinely fixes it. I rebuilt origin/master and got exactly the claimed failure, then built this head clean and ran the full suite. Every number in the body is exact, including sizeof(EosConfig) to the byte and the git diff 3a00bd9 restoration claim. The kernel restoration is correct where it matters most — I traced g_current_sp/g_next_sp against context_switch.S and the old code was writing the incoming task's PSP into the outgoing task's TCB. Three follow-ups below, none blocking.

Findings

# Severity File:line Finding Recommended fix
1 Medium core/include/eos/config.h:99; workaround at tests/test_firmware.c:78,103,121,157,171 static is the right unblock for MSVC, but it papers over the real defect this PR discovered. Verified: sizeof(EosConfig) == 3953236 bytes (3.77 MB) — exactly the figure in the body — and the five statics put 18.9 MB into test_firmware's .bss (size -A reports .bss 19776352). A 3.8 MB configuration struct is a problem for an embedded OS well beyond this test binary: §6.1 requires "Memory APIs should support static allocation strategies for constrained/safety-oriented deployments", and any target under 4 MB of RAM cannot hold one of these at all, in BSS or on a stack. §25.1's MVP targets are MCUs. Out of scope here — do not grow this PR. Open a follow-up issue against EosConfig: the size will be fixed-width char[EOS_MAX_PATH] arrays multiplied by array counts, and the fix is indirection or a smaller path bound, not a bigger stack. Reference this PR as where it surfaced.
2 Low kernel/include/eos/kernel.h:103-104 eos_task_delete(0) and eos_task_suspend(0) now return EOS_KERN_INVALID where they previously succeeded. That is a behavioural change to two public API functions, and neither declaration carries a doc comment — nothing in the header, and nothing I could find in the tree, tells a caller that handle 0 is the idle task or that it is permanent. Brief items 8 and 11: the change is right, the silence about it is the finding. The PR body explains it as restoring #131, which is true, but the header a caller reads does not. Two lines in kernel.h: /* Handle 0 is the idle task and cannot be deleted; returns EOS_KERN_INVALID. */ above each. The PR body should also say plainly that this is an API behaviour change for handle 0, not only a repair.
3 Low services/linux/src/linux_security.c:674 Pre-existing, adjacent to your diff, raised because you are the one editing this function and .ai/reviewer.md asks about null paths: eos_busybox_install_to_rootfs dereferences bb at is_path_safe(bb->source_dir) before any NULL check, so eos_busybox_install_to_rootfs(NULL, dir) segfaults. The sibling rootfs_dir is passed to is_path_safe too, which may or may not tolerate NULL, but bb-> is an unconditional dereference either way. `if (!bb

Scope note, not a finding. This is four unrelated classes of change in one PR: a one-line build unbreak, CI-gate bookkeeping, test registration, and a substantive kernel behaviour restoration. .ai/architect.md says not to restructure and change behaviour in one commit. The justification holds — the kernel change restores a merge master lost, it is not new work — and the author supplied an exact verification for it, which I reproduced. But a reviewer reading only the title will not look hard at eos_schedule(), and eos_schedule() is the part that deserved the hardest look. Worth splitting next time even when the parts share a root cause.

The kernel change is correct — here is why, checked rather than assumed

This is the only part of the PR that could break a device, so I traced it against the assembly rather than reading the diff.

g_current_sp/g_next_sp (task.c:160-170). context_switch.S:43-46 saves the outgoing PSP through g_current_sp before bl eos_schedule (:49), and :53-55 loads from g_next_sp after. So on exit from eos_schedule both pointers must name the task that is now current — g_next_sp for this switch's restore, g_current_sp for the next PendSV's save. The new g_current_sp = &g_tasks[next].stack_ptr; g_next_sp = g_current_sp; is exactly that. The old line — g_current_sp = &g_tasks[g_current >= 0 ? g_current : next].stack_ptr; evaluated before g_current = next — left g_current_sp pointing at the outgoing task, so the next PendSV would have written the newly-running task's PSP into the previous task's TCB. That is stack-pointer corruption on the second context switch, not a cosmetic bug. Restoring #131 here was not optional.

find_next_task (task.c:128). The added best < 0 || is load-bearing and easy to mistake for a tidy-up. best_prio initialises to 255 and the idle task is created at priority 255 (task.c:236), so t->priority < best_prio is 255 < 255 — false. Before this change find_next_task() could never return the idle task, and eos_kernel_start's if (first < 0) first = 0; at :258 was compensating for it at exactly one call site. eos_schedule had no such fallback.

The entry == NULL skip plus g_tasks[slot].state = EOS_TASK_DELETED (task.c:301). Both are needed together. EOS_TASK_READY == 0 (kernel.h:69), so the memset(&g_tasks[slot], 0, ...) at :300 leaves a half-built TCB in READY — the explicit DELETED closes that window and the entry == NULL guard is the second line of defence. Publishing entry and READY last (:313-314) is the right ordering.

The next < 0 early return (task.c:157). The demotion of the outgoing task now happens before find_next_task(), so on that path the current task is left READY rather than RUNNING. I checked whether it is reachable: it is not, and only because of the other changes in this PR. Slot 0 can no longer be deleted or suspended, the idle task never blocks (idle_task_func only spins/WFI), and find_next_task can now actually return it. Self-healing anyway — the next eos_schedule finds it READY and marks it RUNNING. Worth a comment saying the path is unreachable by construction, since three separate invariants are holding it up.

Critical-section balance. eos_port_enter_critical is paired on all three exits including the two new early returns. tests/test_kernel.c:437-445 tracks depth and asserts on unbalanced exit, so the host suite really does cover the pairing — not the mutual exclusion, which a single-threaded host cannot test.

Evidence check

Reproduced against git archive exports of origin/master and head a7f81f30, Linux, GCC:

Claim in the PR Result here
master does not compile, linux_security.c:695: error: #else after #else Confirmed. cmake --build on origin/master exits 2 with services/linux/src/linux_security.c:695:2: error: '#else' after '#else' and :674: error: the conditional began here.
cmake -DEOS_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release → 0 errors Confirmed, build rc=0. 89 warnings, all pre-existing -Wstringop-truncation/-Wformat-truncation/-Wunused-result; the only one in a file this PR touches is linux_security.c:458, ~230 lines from the change. No new warning.
ctest --timeout 120 → 100% tests passed, 43/43 (was 41 registered) 43/43 passed, 0 failed, 0.92s. test_linux_security_paths and test_pkg_fetch are #42 and #43.
pytest tests/ → 47 passed 47 passed in 0.60s.
EosConfig is 3,953,236 bytes 3953236, to the byte.
"git diff 3a00bd9 -- kernel/src/task.c … #131's file plus #130's wake_armed hunk and nothing else" Confirmed — 5 insertions, 1 deletion: the wake_armed hunk with its comment. Small correction: it is that plus two trailing blank lines at EOF, which is 2 of the 5 insertions. Harmless, but "and nothing else" is not quite exact; drop them so the claim is.

The #else choice is the right one of the two: both arms use bb->source_dir, so deleting the (void)bb; arm leaves no unused-parameter warning, and the arm that survives is the one that reports the unsupported platform and returns -1 rather than silently succeeding — fail-closed, per .ai/security.md.

ci-gate is a real gate, not a formality: ci.yml:265-268 pipes toJSON(needs) into .github/scripts/ci-gate-check.sh, which refuses empty input and fails on any result other than success, skipped and cancelled included. Adding windows-test to needs therefore does make the Windows leg blocking. I checked this specifically because "add a job to needs" is worthless against a gate whose step is exit 0.

Architecture conformance

Conforms.

  • §21 / Tier 1. eos is Tier 1 — Foundation. kernel/src/task.c is the scheduler and sync core, which §6's "EoS Kernel / Execution / Scheduler, Threads/Tasks, Priorities" places exactly there.
  • §5.1. Not engaged. No import, include, link line or manifest entry is added in any direction. kernel/src/task.c includes only eos/kernel.h, eos/kernel_internal.h and eos/arch.h — all at or below its own tier. Nothing in this diff makes the kernel depend on eAI, eNI, eBrowser, eOffice, health or aerospace.
  • §6, kernel responsibility boundary. Respected: the change is scheduling, memory-region and synchronisation work. Nothing networking, database, AI or distributed is added.
  • §6.1, "Deterministic scheduling behavior must be specified by configuration and target": the round-robin start = (g_current + 1) % EOS_MAX_TASKS with strict < gives first-found-wins among equal priorities, which is deterministic and is what the restored tests assert.
  • Brief item 8. The one API-compatibility item is finding 2 — eos_task_delete/eos_task_suspend on handle 0. No signature, struct layout, manifest field or serialized format changes.
  • if(UNIX) around test_linux_security_paths narrows coverage rather than weakening a check, and the reason given is verifiable: linux_security.c:690 refuses every install on the #else arm before is_path_safe() is reached, so there is nothing for the suite to assert on MSVC. Accepted. The NOT_REQUIRED entries added to test_ci_gate.py are the same shape as the existing book-build.yml entry and the guard's own reason-check passes, so they are a classification, not a loosened gate.
  • No architecture proposal appended. Nothing here shows the master design wrong, stale or silent — the failure was a merge-process failure, and the services/linux/ host-orchestration placement question it brushes against is already covered by the 2026-09-03 proposal "Two Tier-1 repositories both implement build orchestration, and §21 draws no boundary between them".

Proposed changes

  1. Doc comments on eos_task_delete / eos_task_suspend for the handle-0 contract, and one line in the PR body naming it as an API behaviour change (finding 2).
  2. Drop the two trailing blank lines at the end of kernel/src/task.c so the restoration claim is exact.
  3. Open a follow-up issue for sizeof(EosConfig) (finding 1). Do not fix it here.
  4. Optional: the NULL guard at linux_security.c:674 (finding 3), and a comment on task.c:157 recording why next < 0 is unreachable.

Not checked

  • MSVC — NOT RUN. No Windows host. The static EosConfig fix, the if(UNIX) guard and the #else repair on the MSVC leg rest on this PR's own green Build & Test (Windows MSVC) check, not on anything I ran. The author says the same.
  • ARM Cortex-M4 and ARM64 cross builds — NOT RUN. No cross toolchain here. This matters more than usual: context_switch.S is ARM-only, so the g_current_sp/g_next_sp correction is the one change in this PR that host ctest cannot exercise at all. My analysis of it is reading the assembly against the C, not running it. The green Cross-compile ARM Cortex-M4 and Cross-compile ARM64 kernel checks prove it compiles, not that the switch is right on hardware. §28's evidence model would call this Implemented, not Validated.
  • Mutual exclusion in eos_task_create — NOT RUN. The host eos_port_enter_critical is a depth counter (test_kernel.c:437); it verifies the enter/exit pairing on the new early-return paths but cannot verify that interrupts are actually masked. That needs the ARM port on a target.
  • QEMU simulation — NOT RUN locally. The PR's own QEMU ARM64 simulation (13 TCs) and QEMU Boot Test checks are green; I did not reproduce them.
  • CodeQL, cppcheck, clang-tidy — NOT RUN locally. Green on the PR.
  • I did not verify the merge-provenance narrative (which of #115/#118/#119/#130/#131/#132 dropped what). I verified the outcome: master is broken in the ways described, and the 3a00bd9 task.c comparison holds. The attribution to specific PRs is taken on the author's word.
  • I did not check whether the three skipping checks (Create GitHub Release, deep-review, mention) are supposed to be required. They are not in ci-gate's needs and test_ci_gate.py passes, so the repo's own guard is satisfied; I did not independently audit the exclusion list.

Automated architecture review of a7f81f30a1c6 — 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.

Nine PRs were merged into master within minutes on 09-08, each on the base it
was written against. Master has not compiled since, and the Python guards
that would have named the rest never ran because the C build failed first.

Compile (every C job, CodeQL, and the ARM64 kernel in the simulation):
- services/linux/src/linux_security.c: embeddedos-org#119 and embeddedos-org#132 each added an #else to
  the same #ifndef _WIN32 in eos_busybox_install_to_rootfs(), so master has
  "#else after #else". The embeddedos-org#132 arm (`(void)bb;`) is the one removed -- the
  embeddedos-org#119 arm already uses bb and reports the unsupported platform.

Guards from embeddedos-org#121 / embeddedos-org#93 that later merges walked back:
- ci.yml: embeddedos-org#132 added windows-test after embeddedos-org#121's gate; the gate did not wait
  for it, so "CI Gate" could be green with the MSVC leg red.
- test_ci_gate.py: embeddedos-org#129 gave eosim-sanity.yml and simulation-test.yml a
  path-filtered pull_request trigger (they test their own edits); a
  path-filtered check cannot be required, so both are recorded in
  NOT_REQUIRED with the book-build.yml reason.
- tests/CMakeLists.txt: test_linux_security_paths (embeddedos-org#119) and test_pkg_fetch
  (embeddedos-org#115) had no add_executable(). embeddedos-org#115's replay replaced embeddedos-org#119's block with
  its own, and embeddedos-org#118's replay replaced that; two suites compiled against
  nothing. Both registered again. 41 -> 43 suites.
- tests/test_kernel.c: four tests from embeddedos-org#130 and embeddedos-org#131 were defined and never
  called -- their RUN() lines did not survive the replay of main().

And the one that was not bookkeeping:
- kernel/src/task.c: embeddedos-org#130 was merged after embeddedos-org#131 from a base that predates it,
  and its copy of task.c replaced embeddedos-org#131's. embeddedos-org#131 had also flattened 574 CRLF
  line endings, so its 1166-line diff hid a 39/21 change and the replay took
  embeddedos-org#130's side wholesale. Master kept embeddedos-org#131's tests and lost its kernel: the
  idle task could be deleted and suspended, a half-initialised TCB was
  published to the scheduler before its stack existed, and eos_schedule()
  pointed g_current_sp at the outgoing task. test_idle_task_is_permanent
  fails on master the moment it is called. embeddedos-org#131's task.c diff re-applied
  on top of embeddedos-org#130's; the result is embeddedos-org#131's file plus embeddedos-org#130's wake_armed hunk
  and nothing else (verified by diff against 3a00bd9).

Verified locally (macOS, clang): Release build clean, 43/43 ctest; the
README default configuration builds; 47/47 pytest.

Not in this PR: the nightly "Upstream drift" job builds eBoot at master and
eBoot master is broken separately (its own repair PR); bump EBOOT_COMMIT in
eos-simulation.yml once that lands.
Re-registering it put it in front of the MSVC leg (embeddedos-org#132) for the first time,
where <unistd.h> does not exist. The suite probes with access(), open(O_CREAT)
and rmdir to prove refused inputs never reach a shell; on Windows the
library's own #else arm refuses every install/build call before
is_path_safe() matters, so there is nothing for it to test there.
… gives 1 MB

Re-registering the POSIX-only suite let the MSVC leg reach ctest for the
first time, and test_firmware died there with SEGFAULT: five tests declare
a stack EosConfig (sizeof 3,953,236 on this ABI), which the 8 MB default
stack on Linux and macOS absorbs and MSVC's 1 MB does not. test_config.c
already keeps its EosConfig static for the same reason; the same here.
kernel.h declared eos_task_delete() and eos_task_suspend() without saying
what handle 0 is. It is the idle task, it is permanent, and both calls
return EOS_KERN_INVALID for it -- as they do for an out-of-range handle or
an empty slot. The doc comments now say so, in the header's Doxygen style.

eos_schedule() has an early return for "no runnable task" that is
unreachable by construction: slot 0 cannot be deleted or suspended, the
idle task only spins or waits for an interrupt and so never blocks, and
find_next_task() can select it because of the "best < 0 ||" term. The
comment records that reasoning, and that the return is self-healing if it
were ever taken: the outgoing task stays READY and the next eos_schedule()
marks it RUNNING.

task.c also carried two stray blank lines at EOF from the re-apply of
embeddedos-org#131; with them gone, the diff against 3a00bd9 is embeddedos-org#130's wake_armed hunk
plus this comment and nothing else.
…rootfs

is_path_safe() refuses NULL, but eos_busybox_install_to_rootfs() read
bb->source_dir to hand it to the predicate, so a NULL bb reached the
dereference before the guard could see it: a crash rather than the -1
every other refusal in this file produces. Both arguments are now checked
first, before either is read.

test_linux_security_paths gains a case for it: (NULL, dir) and (&bb, NULL)
both return -1, and the directory gets no /init. With the guard removed
and the target rebuilt, the suite dies with SIGSEGV (exit 139) on the
first call; with it restored, the suite passes.
Records under Unreleased what embeddedos-org#144 did and why: the "#else after #else"
that stopped master compiling after the 09-08 batch merge, the guards the
replays walked back (two suites re-registered, four test_kernel tests
wired into main(), windows-test in the CI gate, the two path-filtered
simulation workflows classified), and the NULL guard in
eos_busybox_install_to_rootfs().

The scheduler change gets its own Changed entry because it is a contract:
eos_task_delete(0) and eos_task_suspend(0) return EOS_KERN_INVALID again,
as embeddedos-org#131 specified before embeddedos-org#130's replay replaced its task.c. Changed sits
above Fixed, the order the older releases in this file use.
…end checks

The eos_schedule() comment claimed the early return was self-healing. It is
not: find_next_task() scans every slot including the outgoing one, so
next < 0 means nothing is READY, and the return leaves the state exactly as
the canary check left it. The comment now says so, and that recovery is a
fault handler's job rather than the scheduler's.

eos_task_suspend() refuses a slot whose entry is NULL, which is what the
doc comment now says instead of the looser "holds no task"; a slot the
canary check marked DELETED still has its entry set and is accepted.

tests/CMakeLists.txt: the reason test_linux_security_paths is POSIX-only is
that the shell-building bodies are compiled out on Windows, not that the
predicate is skipped there.
@Kartikey1306

Copy link
Copy Markdown
Contributor Author

Thanks for tracing g_current_sp/g_next_sp against the assembly — that was the part I most wanted a second reader on. Follow-ups on the branch, as separate commits:

  • Finding 2: doc comments on eos_task_delete() / eos_task_suspend() for the handle-0 contract, and the PR body now says plainly that this is an API behaviour change (restoring fix(kernel): preserve scheduler task invariants #131's contract, which Fix task wake timeout at tick zero #130's replay had rolled back).
  • Trailing blank lines: gone; git diff --ignore-cr-at-eol 3a00bd9 -- kernel/src/task.c is now the wake_armed hunk plus one comment block at the next < 0 return, and nothing else.
  • That comment: it records the three invariants that make the return unreachable in normal operation, and what happens if the canary check deletes the idle task. A first draft called that path "self-healing"; it is not — find_next_task() scans every slot including the outgoing one, so next < 0 means nothing is READY and the return leaves state exactly as the canary check left it. The comment says so, and that recovery is a fault handler's job.
  • Finding 3: eos_busybox_install_to_rootfs() refuses NULL arguments first; test added, and the (NULL, dir) call segfaulted without the guard (exit 139).
  • Finding 1: sizeof(EosConfig) = 3,953,236 filed as sizeof(EosConfig) is 3.95 MB — too large for a stack, for MSVC, and for any MCU target #148; not touched here.
  • CHANGELOG: Fixed entry for the repair, Changed entry for handle 0.
  • The tests( commit type in two earlier commits is left as reviewed; noted for the squash.

Current head: Release build clean, ctest 43/43, pytest 47 passed, README default configuration builds. Two small PRs are stacked on this one so their CI is green: #151 (the upstream alignment-doc correction, eos#149) and #152 (opting the eBoot builds into the development key ahead of eBoot#122).

@Kartikey1306

Copy link
Copy Markdown
Contributor Author

@srpatcha — your approval at a7f81f3 was dismissed automatically by the follow-up push (9569226 is the current head). Everything since is the items in the comment above, each a separate commit on top of what you verified: the handle-0 doc comments, the trailing-blank-line removal, the next < 0 comment, the NULL guard + test on eos_busybox_install_to_rootfs(), and the CHANGELOG entries. No change to the task.c restoration you traced against the assembly. Could you re-approve when you have a moment? I'll hold further pushes to this branch so it does not get dismissed again.

@srpatcha srpatcha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — eos#144 "fix: repair master after the 09-08 batch merge"

head: 9569226 author: Kartikey1306 ci: pass (29 green, 0 red; only Create GitHub Release skipped)

Verdict: Follow-up review. All three previous findings are answered, and two of the new
commits correct mistakes in my previous report rather than merely acting on it — the author
is right both times and I have checked both against the code. One new Low, on a silent-skip
idiom the new test inherits from the file around it. Nothing blocking.

Status of the previous findings (eos-144-a7f81f30.md)

# Prev. severity Status Evidence
1 Medium — sizeof(EosConfig) is 3.95 MB Correctly deferred, and filed eos#148 is open: "sizeof(EosConfig) is 3.95 MB — too large for a stack, for MSVC, and for any MCU target". Not touched in this PR, which is what the previous review asked for — it is an EosConfig layout problem, not a merge repair. Still an open defect against §6.1 and §25.1; it is now tracked rather than carried in a PR body.
2 Low — handle-0 API change undocumented Resolved 5c19f01. kernel/include/eos/kernel.h:103-123 documents both functions. I checked the doc comments against the implementations rather than assuming: eos_task_delete at task.c:358-360 returns EOS_KERN_INVALID for h == 0, h >= EOS_MAX_TASKS, state == DELETED or entry == NULL — the header says "handle 0, an out-of-range handle, or a slot that holds no task", which is exactly that set. eos_task_suspend at :373 is h == 0 || h >= EOS_MAX_TASKS || entry == NULL, and the header says so including the "never created, or deleted" gloss, which is right because eos_task_delete sets entry = NULL. The CHANGELOG carries it under Changed, not Fixed, which is the correct section for a behaviour change.
3 Low — eos_busybox_install_to_rootfs(NULL, …) segfaults Resolved, with a test that proves it 9c0ada7. services/linux/src/linux_security.c:673 is now if (!bb || !rootfs_dir) return -1; ahead of both is_path_safe() calls. Negative control: with that one line deleted and the suite rebuilt, test_linux_security_paths dies with exit 139 (SIGSEGV); with it restored, exit 0, "All linux_security path checks passed". The author's reported 139 is exact. The test also asserts the rootfs got no /init, which is the right assertion — "refused" has to mean "wrote nothing", not just "returned −1".
— Trailing blank lines at EOF of task.c Resolved 9569226 removes both, so the "git diff 3a00bd9 is the wake_armed hunk and nothing else" claim is now exact rather than nearly exact.

Two corrections to my previous report

Recording these because the previous report is in this PR's thread and a reader will hit it
first. In both cases the author's new text is right and mine was wrong.

1. "The next < 0 path is self-healing anyway." It is not, and the new comment at
kernel/src/task.c:159-171 says so correctly. find_next_task() (:124-133) iterates
for (int i = 0; i < EOS_MAX_TASKS; i++) over every slot, including the outgoing task,
which eos_schedule() has just demoted RUNNING → READY two lines earlier. So if anything at
all were READY, find_next_task() would have returned it; next < 0 means nothing is, and
there is no later eos_schedule() that finds a READY task to recover with. The return leaves
g_current_sp and g_next_sp naming the outgoing task and its state as the canary check left
it. The comment's conclusion — that this is a fault handler's job and nothing here should
pretend to recover — is the correct reading, and it is better than the "unreachable, and
self-healing anyway" I wrote.

2. "On Windows the library's own #else arm refuses every install/build call before
is_path_safe() matters."
I accepted that from the previous PR body; it is wrong twice, and
1043ea6's rewritten comment in tests/CMakeLists.txt:359-361 is right. First, is_path_safe()
is called at linux_security.c:674, before the #ifndef _WIN32 at :675 — so the
predicate is reached on Windows, it is not bypassed. Second, the #else arm at :691-696 only
returns −1 when bb->source_dir[0] is non-empty; with an empty source_dir it falls straight
through to the /init write, so it does not refuse "every" call. The new comment states the
actual reason the suite is POSIX-only — the shell-building bodies are compiled out, and the
predicate itself is portable and covered by the POSIX legs — which is both true and a better
justification for the if(UNIX) guard than the one I signed off on.

Findings

# Severity File:line Finding Recommended fix
1 Low tests/test_linux_security_paths.c:741 (and 19 other sites in the same file) A failed mkdtemp() silently removes a security assertion from the run. The new test_busybox_install_refuses_null_arguments opens with if (!mkdtemp(rootfs)) { fprintf(stderr, "[SKIP] mkdtemp failed\n"); return; }, so if /tmp is unwritable, read-only or out of inodes the test returns before CHECK(eos_busybox_install_to_rootfs(NULL, rootfs) == -1) ever runs, PASS_IF_CLEAN is never reached, failures stays 0, the binary exits 0 and ctest is green. .ai/security.md names this shape first: "a verification step that cannot run must fail, not pass". This is a pre-existing idiom, not a regression — the file has 20 [SKIP] sites and documents the policy at :40 — and for the genuinely optional ones (make absent at :275, evmctl present at :394) a skip is the right call, because a missing build tool is not a security failure. A mkdtemp() failure is a different thing: it is the harness breaking, not an absent optional dependency, and the two are treated identically. Separate them. Leave the missing-tool skips alone; make the mkdtemp() failures fail: if (!mkdtemp(dir)) { fprintf(stderr, "[FAIL] mkdtemp: %s\n", strerror(errno)); failures++; return; }. Twenty one-line changes, or one macro. Out of scope for this PR if you would rather keep the repair tight — say so and it stands; I am recording it because this PR adds the twenty-first site.

One new finding, Low, on 343 lines across seven commits. The substantive kernel work was
reviewed in detail last time and is unchanged at this head; I re-verified the build and suite
rather than re-deriving the analysis.

Verification performed for this review

Detached worktree at .ai/autoreview/state/scratch/eos-144 off refs/pull/144/head. The eos
checkout was clean before I started and is clean now (0 entries); nothing was committed or
pushed.

Check Result
cmake -B build -DEOS_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release → cmake --build --parallel 4 PASS — 0 errors, 89 warnings, all pre-existing -Wstringop-truncation/-Wformat-truncation/-Wunused-result. Same count as the previous head: the seven new commits add no warning.
ctest --output-on-failure --no-tests=error --timeout 120 PASS — 43/43, 0.86s
pytest tests/ -q PASS — 47 passed, 0.39s
./build/tests/test_linux_security_paths directly exit 0, "All linux_security path checks passed"
Negative control, finding 3: deleted if (!bb || !rootfs_dir) return -1;, rebuilt the suite, re-ran exit 139, SIGSEGV, core dumped. Restored; git diff --stat clean. The guard is load-bearing and the test proves it.
Doc comments vs. implementations (task.c:356-380) CONFIRMED accurate for both functions, including the "never created, or deleted" gloss on eos_task_suspend.
find_next_task() scans every slot CONFIRMED — task.c:125, for (int i = 0; i < EOS_MAX_TASKS; i++). This is what makes the new next < 0 comment right and my previous "self-healing" claim wrong.
is_path_safe() is reached on Windows CONFIRMED — linux_security.c:674 precedes the #ifndef _WIN32 at :675, and the #else arm at :691 only returns −1 for a non-empty source_dir.
eos#148 exists and is about EosConfig CONFIRMED — open, titled for the 3.95 MB figure

Was anything weakened? No. Across the seven commits: no test disabled, no lint loosened, no
assertion removed, no || true added, no permission widened. The if(UNIX) guard's comment
changed; the guard itself did not.

Architecture conformance

Conforms, unchanged from the previous review and re-checked against the new commits. §21: eos
is Tier 1 — Foundation, and kernel/src/task.c is the scheduler core that §6 places exactly
there. §5.1 is not engaged: the new commits add two doc comments, one comment block, one NULL
guard, one test function and a CHANGELOG entry — no include, link line, CMake entry or manifest
dependency changes in any direction, so nothing points up a tier and the kernel still depends
on nothing above it. §6's responsibility boundary holds: nothing networking, database, AI or
distributed enters the kernel. Brief item 8 (API compatibility) is now satisfied rather than
outstanding
— the handle-0 behaviour change is documented in the public header and declared in
the CHANGELOG's Changed section, which is precisely what finding 2 asked for; silence about
a breaking change was the finding, and the silence is gone. Brief item 11 (documentation) is
likewise closed for this PR: the behaviour that changed and the docs that describe it now agree.

No architecture proposal appended. Nothing in the new commits shows the master design wrong,
stale or silent; the root cause remains a merge-process failure, and the services/linux/
placement question this repo keeps brushing against is already covered by the existing
2026-09-03 proposal.

Proposed changes

None blocking.

Optional, and explicitly out of scope if you want the repair tight:
  test_linux_security_paths.c   make mkdtemp() failure a failure, not a [SKIP]
                                (20 sites; missing-tool skips stay as they are)

Tracked elsewhere, do not fix here:
  eos#148   sizeof(EosConfig) == 3,953,236

Not checked

  • MSVC — NOT RUN. No Windows host. The static EosConfig workaround, the if(UNIX) guard
    and the #else repair on the MSVC leg rest on this PR's green Build & Test (Windows MSVC)
    check, not on anything I ran. My statement above about the Windows #else arm is read from
    the source, not observed on Windows.
  • ARM Cortex-M4 / ARM64 cross builds — NOT RUN. No cross toolchain here, and this remains
    the most important gap: context_switch.S is ARM-only, so the g_current_sp/g_next_sp
    correction — the one change in this PR that could break a device — cannot be exercised by
    host ctest at all.
    The previous review traced it against the assembly and I did not redo
    that; the green cross-compile checks prove it compiles, not that the switch is right on
    hardware. Under §28 this is Implemented, not Validated.
  • The next < 0 reasoning is analysis, not execution. I confirmed find_next_task() scans
    every slot and that the comment's claims follow from the code. I did not construct a
    kernel state in which the idle task is DELETED and observe what happens; no host test can,
    since the canary check needs a real stack overflow.
  • Mutual exclusion in eos_task_create — NOT RUN. The host eos_port_enter_critical is a
    depth counter; it verifies enter/exit pairing, not that interrupts are masked.
  • QEMU simulation, CodeQL, cppcheck, clang-tidy — NOT RUN locally. Green on the PR; logs
    not read.
  • The merge-provenance narrative (which of #115/#119/#129/#130/#131/#132 dropped what) is
    still taken on the author's word. I verified outcomes, not attribution.
  • sizeof(EosConfig) was not re-measured at this head. The previous review measured
    3,953,236 bytes at a7f81f30; nothing in these seven commits touches config.h, so I expect
    it unchanged, but I did not re-run size -A.

Automated architecture review of 95692266f8f5 — 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.

@Kartikey1306

Copy link
Copy Markdown
Contributor Author

Follow-up review read; taking the option you offered on the one open finding.

mkdtemp() failure as [SKIP] — deferred, deliberately, and it stands. The finding is right and the distinction is the useful part: a missing make or evmctl is an absent optional tool and a skip is honest; a failed mkdtemp() is the harness itself breaking, and treating it the same way lets /tmp being unwritable turn a security assertion into a green run. That is the .ai/security.md "cannot run must fail" shape. I'm not folding it into this PR: #144 is the master repair and the base of #151, #152, #154 and #156, so a 20-site change here restacks four PRs and stales every base..head range in their bodies — and this PR only added the 21st site of a pre-existing idiom. It will come as its own PR, stacked on this one: one macro for the mkdtemp() sites that counts a failure and returns, leaving the missing-tool skips as they are, with a test that proves the macro fails when mkdtemp() does (a TMPDIR pointing at a read-only directory).

Nothing else is outstanding here. eos#148 (sizeof(EosConfig)) stays tracked there, not here. No push to this branch this round — 9569226 remains the head, so nothing to re-review.

@Kartikey1306

Copy link
Copy Markdown
Contributor Author

Finding 1 (the mkdtemp() skips) is taken up in #160, stacked on this PR, rather than here — keeping the repair tight, as you offered. It converts the fifteen mkdtemp() calls plus the fixture mkdir() and chmod() to counted failures via one macro, and leaves the five genuinely environmental skips (make, evmctl, setfiles, running as root) exactly as they are. The negative control there is the one that settles it: a stub mkdtemp() that always fails, linked into the suite at this PR's head, gives exit 0 with 12 [SKIP] lines — green with twelve assertions silently gone — and exit 1 with 12 [FAIL] lines after the change.

Nothing pushed here. The re-approve request above stands; 9569226 is still the head.

@Kartikey1306

Copy link
Copy Markdown
Contributor Author

The follow-up I said would come for the mkdtemp()-as-[SKIP] finding is #160 (stacked on this PR): REQUIRE_TMPDIR fails and stops the test at all twelve sites, the missing-tool skips are untouched, and at 0fced8b it carries a committed test that fails when the macro is reverted to skipping — so the class cannot come back silently. Nothing further on this branch; 9569226 is still the head.

@Kartikey1306

Copy link
Copy Markdown
Contributor Author

Status for the merger, 2026-09-17: this PR is green on all checks, 0 behind master (9ed6831), and is the only open repair for master's linux_security.c:695: #else after #else. While it waits, every other PR based on master fails the same eight checks — currently @srpatcha's #146, whose merge result with this PR I built and tested (build OK, ctest 43/43, pytest 47; no conflicts). Nothing else in the queue can go green for that reason until this lands, and the fix is not duplicated anywhere else. Ready when you are.

@Kartikey1306

Copy link
Copy Markdown
Contributor Author

@srpatcha — re-approval request, with the facts that make it a two-minute read rather than a re-review.

Your approval at a7f81f3 (2026-09-14 05:27Z) was dismissed by follow-up pushes answering the automated review. The branch has not moved since 9569226 (2026-09-14 19:21Z).

The repair commit you approved is unchanged. Then d069f8e, now f076c97 after rebasing onto current master — git patch-id --stable is identical for both, so the diff you verified against the assembly is byte-for-byte what is here.

What is new since your approval: 6 follow-up commits, +81/−10 across 7 files, each answering a finding from your automated review — 9c0ada7 (refuse NULL args in eos_busybox_install_to_rootfs, with a test that segfaults without it), 73be875 (EosConfig off the stack for MSVC's 1 MB), 1043ea6 (POSIX-only registration for the path suite), 5c19f01/9569226 (kernel doc comments), 1c03da0 (changelog). Compare: a7f81f3...9569226 — the automated review's final verdict on this head: "All three previous findings are answered."

What this unblocks: 8 open PRs are stacked on this head (#151, #152, #154, #156, #158, #160, #163, #165 — each drops to its own diff once this lands), plus your #146, whose only red is inherited from master.

Nothing further will be pushed to this branch. Merge order: this first.

@srpatcha
srpatcha merged commit c11afcd into embeddedos-org:master Oct 1, 2026
34 checks passed
srpatcha pushed a commit that referenced this pull request Oct 2, 2026
…e, not a skip

Twelve tests in tests/test_linux_security_paths.c opened with
"if (!mkdtemp(dir)) { fprintf(stderr, "[SKIP] mkdtemp failed"); return; }"
-- fifteen mkdtemp() calls -- and one mkdir() and one chmod() were handled
the same way. On a read-only or full /tmp each test returned before its
CHECK()s ran, failures stayed 0, the binary exited 0 and ctest was green.
The review on #144 found it as that PR added the twenty-first site.

The file's policy is right for what it was written for: a missing build
tool -- make, evmctl, setfiles -- or a host that ignores file modes means
the case cannot be exercised here, and [SKIP] is the honest answer. Those
five sites are unchanged. A temp directory the harness cannot create is
not that; it is the harness failing. REQUIRE_TMPDIR() now counts it, names
the file, line, template and errno, says which assertions did not run, and
stops the test. FIXTURE_FAILED() does the same for the mkdir() of the
directory whose name is the injection and for the chmod() of the read-only
rootfs.

Proved both ways with a stub mkdtemp() that always fails, linked into the
suite:

  #144's head, old idiom:   exit 0, 12 [SKIP] lines, 6 [PASS] lines --
                            green, twelve assertions silently gone
  this commit:              exit 1, 12 [FAIL] lines naming mkdtemp and
                            EROFS, 6 [PASS] lines

A first attempt at that negative control redefined mkdtemp as a macro and
collided with unistd.h's declaration; rc 127 was "no binary", not a result.
The stub is the mutation that actually ran.

Normal run: build clean, all linux_security path checks passed.

Closes #159
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

master does not compile after the 09-08 batch merge, and its guards never ran

2 participants