Skip to content

fix(kernel): restore mutex owner priority after waiter timeout - #55

Merged
srpatcha merged 2 commits into
embeddedos-org:masterfrom
Noor-Nabi4:fix/mutex-pi-timeout-restore
Aug 29, 2026
Merged

srpatcha merged 2 commits into
embeddedos-org:masterfrom
Noor-Nabi4:fix/mutex-pi-timeout-restore

Conversation

@Noor-Nabi4

Copy link
Copy Markdown
Contributor

Priority inheritance boosted the owner when a higher-priority task
blocked, but a timeout left that boost in place and could starve
other work. Recompute owner priority from remaining waiters, and
reject a full waiter table without applying a boost.

Signed-off-by: Noor Nabi gcnoor6@gmail.com
Co-authored-by: Cursor cursoragent@cursor.com

Priority inheritance boosted the owner when a higher-priority task
blocked, but a timeout left that boost in place and could starve
other work. Recompute owner priority from remaining waiters, and
reject a full waiter table without applying a boost.

Signed-off-by: Noor Nabi <gcnoor6@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@srpatcha

Copy link
Copy Markdown
Member

Review — correct fix, verified; overlaps with #67

Built merged onto current master: 24/24 ctest pass.

The bug is real and well diagnosed: a waiter that times out was removed from the queue but the owner kept the inherited boost, so a low-priority task could run elevated indefinitely after the waiter that justified it had gone.

Two things you did that I'd highlight:

  • Refusing the lock with EOS_KERN_NO_MEMORY when the waiter table is full, instead of boosting and then blocking a caller that was never enqueued. That was a lost-wakeup waiting to happen with EOS_WAIT_FOREVER.
  • The eos_port_yield hook that samples owner priority mid-lock. With a no-op yield the boosted state exists only during that call, so observing it from inside the stub is the only way to test this. Neat.

Overlap: #67 rebuilds priority inheritance around a recomputed invariant (effective = min(base, all waiters on all held mutexes)) and adds transitive propagation. It covers this timeout case and the nested ones. I verified the two conflict in kernel/src/sync.c.

Not a reason to close this. Two paths, both reasonable:

One thing worth raising either way: eos_task_set_current_internal is exposed in kernel_internal.h purely so host tests can drive multi-task paths. That is legitimate, but it deserves the comment you gave it — it is a test seam in a shipped header.

srpatcha
srpatcha previously approved these changes Aug 29, 2026
@srpatcha
srpatcha merged commit 02c490a into embeddedos-org:master Aug 29, 2026
srpatcha added a commit that referenced this pull request Aug 30, 2026
…it does (#90)

* fix: repair master — four merges landed code that does not compile

`master` does not configure, and once it does it does not build, and once it
builds one suite fails. Each break is the same failure: a PR verified green on
its own branch, squash-merged onto a base that had moved, and nothing
re-verified the result.

1. tests/CMakeLists.txt registered test_crypto_aes and test_crypto_sha512
   twice (#59 landing beside the block that already had them), so CMake
   refused to configure at all:

       add_test given test NAME "test_crypto_aes" which already exists

2. eos_task_set_current_internal was defined twice in task.c and declared
   twice in kernel_internal.h, with two different behaviours — one clears
   g_current and marks the task RUNNING, the other silently returns. Kept the
   first, which is what the header documents ("Handles >= EOS_MAX_TASKS clear
   it") and what the mutex tests rely on; folded the second declaration's
   comment into the surviving one, since it records why the function exists.

3. sync.c would not compile: g_blocked_on, pi_propagate and task_valid were
   used and defined nowhere, alongside a reference to a struct member
   original_prio that mtx_t does not have.

   #67 and #55 implement incompatible designs for the same feature. #67
   recomputes effective priority from

       effective = min(base_priority, priority of every waiter on every
                       mutex the task holds)

   on every change; #55 saves and restores a value around each lock. Merging
   #55 after #67 deleted #67's implementation while leaving its call sites,
   so neither design was left intact.

   Restored #67's. It is the stronger of the two — save/restore is wrong when
   a task holds two mutexes and releases one, and wrong again when a task is
   boosted while already holding a mutex — and it subsumes what #55 fixes:
   recomputing on timeout is one of the cases it recomputes on, and it
   already rejects a full waiter table rather than blocking a task that was
   never enqueued.

4. #74's tests were merged without its parser change. Six assertions in
   test_config failed because a "- name:" item arriving while section was
   SEC_PKG_DEPS fell into the dependency branch, so the package after a deps
   list was swallowed as a dependency of the previous one. Restored the
   SEC_PKG_DEPS arm of the outer condition, nested the dependency handling as
   the else of the name check, and added the deps transition under
   SEC_PKG_BUILD.

    before   cmake: configure fails
    after    build clean, ctest 28/28

Also re-checked the package-overflow guard that arrived via #61/#65, since it
landed in a different shape than #78 proposed: AddressSanitizer is clean on a
129-package config that previously overflowed, and the 129th entry is refused
with a message rather than silently dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(core): make eos cross-compile for Cortex-M4 again

eos does not build for the target it exists to build for. From a clean
checkout of master, with the toolchain the CI job installs:

    $ cmake -B barm -DCMAKE_TOOLCHAIN_FILE=toolchains/arm-cortex-m4.cmake
    $ cmake --build barm
    core/src/log.c:113:9: error: implicit declaration of function 'clock_gettime'
    core/src/log.c:113:23: error: 'CLOCK_MONOTONIC' undeclared

get_timestamp_ms() had two branches, _WIN32 and "else, assume POSIX". Bare-
metal newlib is neither: arm-none-eabi declares neither clock_gettime nor
CLOCK_MONOTONIC, so the POSIX branch cannot compile for any Cortex-M target.
The macro is the precise test — where it is absent, so is the function.

There is a timebase on a target, eos_get_tick_ms() in the HAL, but core/ sits
below hal/ and must not depend upward, so log entries carry a zero timestamp
on a freestanding build until a port supplies one. Ordering within the ring
buffer is unaffected; only absolute time is missing.

Also fixed the format specifier one line down. uint32_t is `unsigned int` on
x86-64 and `unsigned long` on 32-bit ARM, so the literal %u was wrong on
exactly the target this build is for. It is PRIu32 now, which is right on
both.

Why CI did not catch it. The ARM job points CMAKE_TOOLCHAIN_FILE at
cmake/arm-cortex-m4.cmake; the file is at toolchains/. That fails loudly --
"Could not find toolchain file" -- so the job has been red rather than
silently passing, but it dies at configure and never reaches the compile step
where this bug lives.

The path is not new breakage: #75 fixed it, #79 and #51 carried the fix, and
#59 reverted it. That is the same stale-base merge pattern as the other three
repairs on this branch.

    before   ARM: configure fails on a missing toolchain file; with the real
             path, 2 compile errors in core/src/log.c
    after    ARM: 70/70 targets built, artifacts report `architecture:
             armv7e-m`; host: 28/28 ctest, unchanged

Kernel and services together measure 15.8 KB flash and 26 KB RAM on
Cortex-M4, which is the first real footprint number this repo has produced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: make the quick start clone a repository that exists

Every quick start in the tree opened with

    git clone https://github.com/anthropic/EoS.git

That repository does not exist — `gh api repos/anthropic/EoS` returns 404. It
is step one of the documented onboarding path in seven files: GETTING_STARTED,
the host/stm32/rpi4/nrf52 quickstarts, the integration guide and the STM32
tutorial. Any external developer following the docs stopped there.

The §30 12-month framework lists "tested quick start" among the 0–3 month
Foundation deliverables. It was not tested, and could not have been.

Two further defects behind the URL, both of which only surface once the clone
succeeds:

- The docs then `cd EoS/eos/examples/blink-gpio`, but cloning eos.git creates
  a directory called `eos` with `examples/` at its root. `EoS/eos/...` belongs
  to a multi-repo workspace that the single clone command never produces. The
  single-repo quick starts now use the path the clone actually creates. The
  guides that genuinely describe the multi-repo workspace are left alone —
  their paths are right, and what they are missing is a step that sets the
  workspace up, which is a larger documentation question than this change.

- They pass -DCMAKE_TOOLCHAIN_FILE=toolchains/arm-none-eabi.cmake, which does
  not exist. The ARM toolchain files are arm-cortex-m4.cmake,
  arm-none-eabi-stm32f4.cmake and arm-none-eabi-r5.cmake. Pointed at
  arm-cortex-m4.cmake, which is the one the CI job and the Cortex-M reference
  class use.

Verified by running the host quick start verbatim against the published repo,
from an empty directory:

    git clone https://github.com/embeddedos-org/eos.git
    cd eos/examples/blink-gpio
    cmake -B build -DEOS_PRODUCT=iot
    cmake --build build
    ./build/blink-gpio
    [blink] Starting LED blink on pin 13
    [blink] LED ON

The example itself was always fine. Only the instructions were wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: publish the §25 feature maturity matrix

§25 requires every significant feature to carry a maturity state with the
evidence that state demands. No such record existed on master — grepping the
tree for Implemented / Experimental / Planned / Validated / Certified returned
nothing.

That is the failure §25 exists to prevent, and it is load-bearing here. Without
it, 84 board descriptors read as 84 supported boards, a 190-line facade reads
as a networking subsystem, and a set of signature routines that verify nothing
reads as secure boot.

A version of this file was written on the reconcile/tier-1 branch and never
merged. This is that work brought to master and re-measured, because several
of its rows are now wrong in both directions.

Six subsystems are recorded as Planned rather than Experimental, each citing
the reason:

- eNet. ADR-014 point 4 requires this correction in the PR that merges the ADR;
  the ADR merged and the correction did not. net/src/net.c is 190 lines with 20
  (void)param casts and eos_net_connect() is `return -1`. test_net passes
  because it tests the facade's argument validation.
- RSA/ECC signatures. eos_ecc_verify() accepted any 64-byte signature and
  eos_rsa_verify_sha256() compared only the trailing 32 bytes. Both refuse to
  run now, which is the correct behaviour and still not verification.
- The HAL host backend. hal_linux.c implements one, but
  eos_hal_linux_register() is never called and appears in no public header, so
  on a host build eos_hal_init() returns -1 and every HAL call silently
  no-ops.
- The extended HAL. hal_extended_stubs.c is 740 of the HAL's 3,185 lines and
  carries 144 (void)param casts.
- Boards. 84 descriptors, 71 of them named generic-*.

Two rows move the other way, because the evidence improved:

- ARM Cortex-M4/R5 goes from Planned to Experimental. The tree cross-compiles
  clean and objdump reports armv7e-m and armv7 on the artifacts. It has still
  never been executed, which the row says.
- ctest is 28/28, not the 21/21 the old file recorded.

RISC-V and Cortex-A are Unknown rather than Planned: the toolchain files exist,
the toolchains are not installed here, so nothing was built. NOT RUN is an
answer; a guess dressed as Planned is not.

No subsystem claims Implemented, Validated or Certified. Each of those needs
evidence nothing in the tree currently supports.

Not included: component-manifest.json, which also sits on reconcile/tier-1. It
describes an arch/ tier that does not exist on master, so bringing it forward
needs its own pass rather than riding along here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(net): implement the eNet socket API on POSIX sockets

ADR-014 point 3: "On the Compute profile, where a host OS is present, the same
API maps to POSIX sockets." This is that mapping.

Before this, every socket function in eNet was a stub. eos_net_connect() was

    int eos_net_connect(eos_socket_t sock, const eos_net_addr_t *addr)
    {
        (void)sock; (void)addr;
        return -1;
    }

and eos_net_socket() returned an incrementing integer naming nothing. Anything
built on eNet could not be exercised on any target — including the host, which
is where EoSim runs and where the org's own ecosystem gate would test it.

    [PASS] net tcp round trip
    [PASS] net resolve localhost
    [PASS] net connect to a closed port fails

The fd is the handle. eos_socket_t is an int, EOS_SOCKET_INVALID is -1, and
that is exactly what socket(2) returns on failure, so there is no descriptor
table to keep in step and no way for the two to drift.

Three details that are not obvious from the API shape:

- eos_net_addr_t documents ip as network byte order, so it is copied into
  sin_addr rather than passed through htonl, which would swap it twice.
- send uses MSG_NOSIGNAL. Writing to a peer that has gone away raises SIGPIPE
  by default, which kills the process instead of returning an error the caller
  can act on.
- A zero timeout means "block forever" to SO_RCVTIMEO, which is the opposite of
  what a caller passing 0 expects, so the option is only set when non-zero. A
  timed-out recv returns 0 rather than -1: no bytes arrived, nothing failed.

The freestanding stubs stay in net.c behind #ifndef EOS_NET_POSIX rather than
being deleted. On a target with no IP stack, failing is the honest answer, and
that is the Planned state ADR-014 records for the Nano and Edge profiles until
lwIP lands.

Gated on CMAKE_CROSSCOMPILING alone, deliberately not on the
`EOS_PLATFORM STREQUAL "linux" OR NOT CMAKE_CROSSCOMPILING` form used a few
lines above for hal_linux.c. EOS_PLATFORM defaults to "linux" and a cross
toolchain file does not change it, so that condition is true even for a
Cortex-M build — I used it first and the ARM build failed on <netdb.h>.
hal_linux.c survives the same condition only because it uses portable headers;
it is compiled into the Cortex-M build today, which is worth a separate look.

Scope is the socket layer. ADR-014 point 5 keeps MQTT, CoAP and HTTP out as
separate adoption decisions on top of a working IP stack; they remain stubs in
net.c. This does not make eNet Implemented — lwIP is still the decision for the
profiles with no host OS to borrow sockets from.

Verification: host ctest 28/28, with test_net going from 16 to 19 cases;
net.c compiles for bare-metal Cortex-M with EOS_ENABLE_NET=1 and defines
eos_net_connect exactly once, as does the host build; full ARM cross-build
clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(hal): eos_hal_init() selects the platform backend, as hal.h says it does

hal.h documents eos_hal_init() as initialising "the HAL subsystem for the
current platform". It did not. It returned -1 unless the application had
already called eos_hal_register_backend() itself — and nothing in the tree,
none of the examples, and none of ebuild's scaffolded templates ever did.

The consequence was silent. eos_hal_init() returned -1, the generated
hello-world ignored it as generated code does, and every subsequent HAL call
dispatched through a NULL backend and did nothing. The MVP's own first program
ran its loop and produced no output.

Both platform backends already existed and neither was reachable:
eos_hal_linux_register() in hal_linux.c and eos_hal_rtos_register() in
hal_rtos.c, declared in no header and called from nowhere. They are mutually
exclusive — hal_linux.c is guarded on __linux__ and hal_rtos.c on its absence —
so exactly one is compiled and init can pick it without ambiguity. Both are now
declared in hal.h, because a registrar nobody can find is a registrar nobody
uses.

The interesting case is the one the existing test_no_backend covers:

    eos_hal_register_backend(NULL);
    ASSERT(eos_hal_init() == -1);

That is a deliberate act, and a HAL that re-arms itself after being disarmed
would be worse than one that never armed. So "no backend has ever been chosen"
is tracked separately from "a backend was chosen and it was NULL". The default
fills only the first. An application backend registered before init is likewise
kept.

Three tests added, and their ordering is load-bearing: they run before any
other case, because every later test calls setup(), which registers a mock
backend. Placed after it they passed while exercising the mock, testing
nothing. I wrote them in the wrong order first and they passed for that reason.

Not fixed here, and worth knowing: the Linux backend drives real GPIO through
/sys/class/gpio, so eos_gpio_init() still fails on a workstation with no
exported pins. That is correct — it is a hardware backend, not a simulator —
and it is why "simulation before hardware is available" is an MLP item that
needs a simulation backend rather than a change to this one.

Verification: host ctest 28/28 with test_hal going 5 -> 8 cases; the new tests
fail against the unfixed hal_common.c and pass with it; ARM cross-build clean;
eos_hal_init() returns 0 and the tick source advances 20 ms across a 20 ms
delay, where it previously reported 0 forever.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: z <x@y>
srpatcha added a commit that referenced this pull request Sep 1, 2026
* fix: repair master — four merges landed code that does not compile

`master` does not configure, and once it does it does not build, and once it
builds one suite fails. Each break is the same failure: a PR verified green on
its own branch, squash-merged onto a base that had moved, and nothing
re-verified the result.

1. tests/CMakeLists.txt registered test_crypto_aes and test_crypto_sha512
   twice (#59 landing beside the block that already had them), so CMake
   refused to configure at all:

       add_test given test NAME "test_crypto_aes" which already exists

2. eos_task_set_current_internal was defined twice in task.c and declared
   twice in kernel_internal.h, with two different behaviours — one clears
   g_current and marks the task RUNNING, the other silently returns. Kept the
   first, which is what the header documents ("Handles >= EOS_MAX_TASKS clear
   it") and what the mutex tests rely on; folded the second declaration's
   comment into the surviving one, since it records why the function exists.

3. sync.c would not compile: g_blocked_on, pi_propagate and task_valid were
   used and defined nowhere, alongside a reference to a struct member
   original_prio that mtx_t does not have.

   #67 and #55 implement incompatible designs for the same feature. #67
   recomputes effective priority from

       effective = min(base_priority, priority of every waiter on every
                       mutex the task holds)

   on every change; #55 saves and restores a value around each lock. Merging
   #55 after #67 deleted #67's implementation while leaving its call sites,
   so neither design was left intact.

   Restored #67's. It is the stronger of the two — save/restore is wrong when
   a task holds two mutexes and releases one, and wrong again when a task is
   boosted while already holding a mutex — and it subsumes what #55 fixes:
   recomputing on timeout is one of the cases it recomputes on, and it
   already rejects a full waiter table rather than blocking a task that was
   never enqueued.

4. #74's tests were merged without its parser change. Six assertions in
   test_config failed because a "- name:" item arriving while section was
   SEC_PKG_DEPS fell into the dependency branch, so the package after a deps
   list was swallowed as a dependency of the previous one. Restored the
   SEC_PKG_DEPS arm of the outer condition, nested the dependency handling as
   the else of the name check, and added the deps transition under
   SEC_PKG_BUILD.

    before   cmake: configure fails
    after    build clean, ctest 28/28

Also re-checked the package-overflow guard that arrived via #61/#65, since it
landed in a different shape than #78 proposed: AddressSanitizer is clean on a
129-package config that previously overflowed, and the 129th entry is refused
with a message rather than silently dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(core): make eos cross-compile for Cortex-M4 again

eos does not build for the target it exists to build for. From a clean
checkout of master, with the toolchain the CI job installs:

    $ cmake -B barm -DCMAKE_TOOLCHAIN_FILE=toolchains/arm-cortex-m4.cmake
    $ cmake --build barm
    core/src/log.c:113:9: error: implicit declaration of function 'clock_gettime'
    core/src/log.c:113:23: error: 'CLOCK_MONOTONIC' undeclared

get_timestamp_ms() had two branches, _WIN32 and "else, assume POSIX". Bare-
metal newlib is neither: arm-none-eabi declares neither clock_gettime nor
CLOCK_MONOTONIC, so the POSIX branch cannot compile for any Cortex-M target.
The macro is the precise test — where it is absent, so is the function.

There is a timebase on a target, eos_get_tick_ms() in the HAL, but core/ sits
below hal/ and must not depend upward, so log entries carry a zero timestamp
on a freestanding build until a port supplies one. Ordering within the ring
buffer is unaffected; only absolute time is missing.

Also fixed the format specifier one line down. uint32_t is `unsigned int` on
x86-64 and `unsigned long` on 32-bit ARM, so the literal %u was wrong on
exactly the target this build is for. It is PRIu32 now, which is right on
both.

Why CI did not catch it. The ARM job points CMAKE_TOOLCHAIN_FILE at
cmake/arm-cortex-m4.cmake; the file is at toolchains/. That fails loudly --
"Could not find toolchain file" -- so the job has been red rather than
silently passing, but it dies at configure and never reaches the compile step
where this bug lives.

The path is not new breakage: #75 fixed it, #79 and #51 carried the fix, and
#59 reverted it. That is the same stale-base merge pattern as the other three
repairs on this branch.

    before   ARM: configure fails on a missing toolchain file; with the real
             path, 2 compile errors in core/src/log.c
    after    ARM: 70/70 targets built, artifacts report `architecture:
             armv7e-m`; host: 28/28 ctest, unchanged

Kernel and services together measure 15.8 KB flash and 26 KB RAM on
Cortex-M4, which is the first real footprint number this repo has produced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(tests): describe what each crypto test covers

Deduplicating test_crypto_aes and test_crypto_sha512 left the grouped
comment that had been sitting above the block:

    # --- test_crypto_aes / _ecc / _rsa / _sha512: were sitting in tests/ unused ---

That records how the files got here, which stops being interesting the
moment they are wired up. #93 fixes the same two duplicates and keeps the
per-test descriptions instead:

    # --- test_crypto_aes: AES-128/192/256 ECB + CBC, NIST FIPS-197 / SP 800-38A ---
    # --- test_crypto_ecc: ECC point arithmetic and ECDSA ---
    # --- test_crypto_rsa: RSA modular exponentiation and verify ---
    # --- test_crypto_sha512: SHA-512 against NIST vectors ---

Naming the standard each vector set comes from is worth more to the next
reader than a note about the file's history. Adopting that here so the
better wording survives the conflict between the two PRs.

Comment-only; no target, source or link line changes.

    28/28 tests passed, 0 build errors

Co-authored-by: prakhar7017 <prakhar7017@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(sync): say why each priority-inheritance branch acts as it does

#94 repairs the same spliced priority-inheritance code as this PR, and
converged on the same design independently — g_blocked_on, task_valid,
pi_recompute, pi_propagate, recomputing from the invariant rather than
saving and restoring a value around each lock. The two implementations of
sync.c differ by nothing except these two comments, which explain the
consequence rather than only the action:

    "...it would sleep forever."
    -> "...it would sleep forever. Refusing also avoids boosting the owner
        on behalf of a caller that never waits."

    "Timeout — leave the queue and drop the boost we were causing."
    -> "...so a waiter that gave up no longer keeps the owner running
        elevated."

Both name the priority-inheritance effect of the branch, which is the part
a reader is likely to get wrong when editing it later.

Comment-only; sync.c is otherwise byte-identical to #94's.

    28/28 tests passed, 0 build errors

Co-authored-by: prakhar7017 <prakhar7017@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: prakhar7017 <prakhar7017@users.noreply.github.com>
srpatcha added a commit that referenced this pull request Sep 1, 2026
* fix: repair master — four merges landed code that does not compile

`master` does not configure, and once it does it does not build, and once it
builds one suite fails. Each break is the same failure: a PR verified green on
its own branch, squash-merged onto a base that had moved, and nothing
re-verified the result.

1. tests/CMakeLists.txt registered test_crypto_aes and test_crypto_sha512
   twice (#59 landing beside the block that already had them), so CMake
   refused to configure at all:

       add_test given test NAME "test_crypto_aes" which already exists

2. eos_task_set_current_internal was defined twice in task.c and declared
   twice in kernel_internal.h, with two different behaviours — one clears
   g_current and marks the task RUNNING, the other silently returns. Kept the
   first, which is what the header documents ("Handles >= EOS_MAX_TASKS clear
   it") and what the mutex tests rely on; folded the second declaration's
   comment into the surviving one, since it records why the function exists.

3. sync.c would not compile: g_blocked_on, pi_propagate and task_valid were
   used and defined nowhere, alongside a reference to a struct member
   original_prio that mtx_t does not have.

   #67 and #55 implement incompatible designs for the same feature. #67
   recomputes effective priority from

       effective = min(base_priority, priority of every waiter on every
                       mutex the task holds)

   on every change; #55 saves and restores a value around each lock. Merging
   #55 after #67 deleted #67's implementation while leaving its call sites,
   so neither design was left intact.

   Restored #67's. It is the stronger of the two — save/restore is wrong when
   a task holds two mutexes and releases one, and wrong again when a task is
   boosted while already holding a mutex — and it subsumes what #55 fixes:
   recomputing on timeout is one of the cases it recomputes on, and it
   already rejects a full waiter table rather than blocking a task that was
   never enqueued.

4. #74's tests were merged without its parser change. Six assertions in
   test_config failed because a "- name:" item arriving while section was
   SEC_PKG_DEPS fell into the dependency branch, so the package after a deps
   list was swallowed as a dependency of the previous one. Restored the
   SEC_PKG_DEPS arm of the outer condition, nested the dependency handling as
   the else of the name check, and added the deps transition under
   SEC_PKG_BUILD.

    before   cmake: configure fails
    after    build clean, ctest 28/28

Also re-checked the package-overflow guard that arrived via #61/#65, since it
landed in a different shape than #78 proposed: AddressSanitizer is clean on a
129-package config that previously overflowed, and the 129th entry is refused
with a message rather than silently dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(core): make eos cross-compile for Cortex-M4 again

eos does not build for the target it exists to build for. From a clean
checkout of master, with the toolchain the CI job installs:

    $ cmake -B barm -DCMAKE_TOOLCHAIN_FILE=toolchains/arm-cortex-m4.cmake
    $ cmake --build barm
    core/src/log.c:113:9: error: implicit declaration of function 'clock_gettime'
    core/src/log.c:113:23: error: 'CLOCK_MONOTONIC' undeclared

get_timestamp_ms() had two branches, _WIN32 and "else, assume POSIX". Bare-
metal newlib is neither: arm-none-eabi declares neither clock_gettime nor
CLOCK_MONOTONIC, so the POSIX branch cannot compile for any Cortex-M target.
The macro is the precise test — where it is absent, so is the function.

There is a timebase on a target, eos_get_tick_ms() in the HAL, but core/ sits
below hal/ and must not depend upward, so log entries carry a zero timestamp
on a freestanding build until a port supplies one. Ordering within the ring
buffer is unaffected; only absolute time is missing.

Also fixed the format specifier one line down. uint32_t is `unsigned int` on
x86-64 and `unsigned long` on 32-bit ARM, so the literal %u was wrong on
exactly the target this build is for. It is PRIu32 now, which is right on
both.

Why CI did not catch it. The ARM job points CMAKE_TOOLCHAIN_FILE at
cmake/arm-cortex-m4.cmake; the file is at toolchains/. That fails loudly --
"Could not find toolchain file" -- so the job has been red rather than
silently passing, but it dies at configure and never reaches the compile step
where this bug lives.

The path is not new breakage: #75 fixed it, #79 and #51 carried the fix, and
#59 reverted it. That is the same stale-base merge pattern as the other three
repairs on this branch.

    before   ARM: configure fails on a missing toolchain file; with the real
             path, 2 compile errors in core/src/log.c
    after    ARM: 70/70 targets built, artifacts report `architecture:
             armv7e-m`; host: 28/28 ctest, unchanged

Kernel and services together measure 15.8 KB flash and 26 KB RAM on
Cortex-M4, which is the first real footprint number this repo has produced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: make the quick start clone a repository that exists

Every quick start in the tree opened with

    git clone https://github.com/anthropic/EoS.git

That repository does not exist — `gh api repos/anthropic/EoS` returns 404. It
is step one of the documented onboarding path in seven files: GETTING_STARTED,
the host/stm32/rpi4/nrf52 quickstarts, the integration guide and the STM32
tutorial. Any external developer following the docs stopped there.

The §30 12-month framework lists "tested quick start" among the 0–3 month
Foundation deliverables. It was not tested, and could not have been.

Two further defects behind the URL, both of which only surface once the clone
succeeds:

- The docs then `cd EoS/eos/examples/blink-gpio`, but cloning eos.git creates
  a directory called `eos` with `examples/` at its root. `EoS/eos/...` belongs
  to a multi-repo workspace that the single clone command never produces. The
  single-repo quick starts now use the path the clone actually creates. The
  guides that genuinely describe the multi-repo workspace are left alone —
  their paths are right, and what they are missing is a step that sets the
  workspace up, which is a larger documentation question than this change.

- They pass -DCMAKE_TOOLCHAIN_FILE=toolchains/arm-none-eabi.cmake, which does
  not exist. The ARM toolchain files are arm-cortex-m4.cmake,
  arm-none-eabi-stm32f4.cmake and arm-none-eabi-r5.cmake. Pointed at
  arm-cortex-m4.cmake, which is the one the CI job and the Cortex-M reference
  class use.

Verified by running the host quick start verbatim against the published repo,
from an empty directory:

    git clone https://github.com/embeddedos-org/eos.git
    cd eos/examples/blink-gpio
    cmake -B build -DEOS_PRODUCT=iot
    cmake --build build
    ./build/blink-gpio
    [blink] Starting LED blink on pin 13
    [blink] LED ON

The example itself was always fine. Only the instructions were wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: publish the §25 feature maturity matrix

§25 requires every significant feature to carry a maturity state with the
evidence that state demands. No such record existed on master — grepping the
tree for Implemented / Experimental / Planned / Validated / Certified returned
nothing.

That is the failure §25 exists to prevent, and it is load-bearing here. Without
it, 84 board descriptors read as 84 supported boards, a 190-line facade reads
as a networking subsystem, and a set of signature routines that verify nothing
reads as secure boot.

A version of this file was written on the reconcile/tier-1 branch and never
merged. This is that work brought to master and re-measured, because several
of its rows are now wrong in both directions.

Six subsystems are recorded as Planned rather than Experimental, each citing
the reason:

- eNet. ADR-014 point 4 requires this correction in the PR that merges the ADR;
  the ADR merged and the correction did not. net/src/net.c is 190 lines with 20
  (void)param casts and eos_net_connect() is `return -1`. test_net passes
  because it tests the facade's argument validation.
- RSA/ECC signatures. eos_ecc_verify() accepted any 64-byte signature and
  eos_rsa_verify_sha256() compared only the trailing 32 bytes. Both refuse to
  run now, which is the correct behaviour and still not verification.
- The HAL host backend. hal_linux.c implements one, but
  eos_hal_linux_register() is never called and appears in no public header, so
  on a host build eos_hal_init() returns -1 and every HAL call silently
  no-ops.
- The extended HAL. hal_extended_stubs.c is 740 of the HAL's 3,185 lines and
  carries 144 (void)param casts.
- Boards. 84 descriptors, 71 of them named generic-*.

Two rows move the other way, because the evidence improved:

- ARM Cortex-M4/R5 goes from Planned to Experimental. The tree cross-compiles
  clean and objdump reports armv7e-m and armv7 on the artifacts. It has still
  never been executed, which the row says.
- ctest is 28/28, not the 21/21 the old file recorded.

RISC-V and Cortex-A are Unknown rather than Planned: the toolchain files exist,
the toolchains are not installed here, so nothing was built. NOT RUN is an
answer; a guess dressed as Planned is not.

No subsystem claims Implemented, Validated or Certified. Each of those needs
evidence nothing in the tree currently supports.

Not included: component-manifest.json, which also sits on reconcile/tier-1. It
describes an arch/ tier that does not exist on master, so bringing it forward
needs its own pass rather than riding along here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: z <x@y>
srpatcha added a commit that referenced this pull request Sep 1, 2026
* fix: repair master — four merges landed code that does not compile

`master` does not configure, and once it does it does not build, and once it
builds one suite fails. Each break is the same failure: a PR verified green on
its own branch, squash-merged onto a base that had moved, and nothing
re-verified the result.

1. tests/CMakeLists.txt registered test_crypto_aes and test_crypto_sha512
   twice (#59 landing beside the block that already had them), so CMake
   refused to configure at all:

       add_test given test NAME "test_crypto_aes" which already exists

2. eos_task_set_current_internal was defined twice in task.c and declared
   twice in kernel_internal.h, with two different behaviours — one clears
   g_current and marks the task RUNNING, the other silently returns. Kept the
   first, which is what the header documents ("Handles >= EOS_MAX_TASKS clear
   it") and what the mutex tests rely on; folded the second declaration's
   comment into the surviving one, since it records why the function exists.

3. sync.c would not compile: g_blocked_on, pi_propagate and task_valid were
   used and defined nowhere, alongside a reference to a struct member
   original_prio that mtx_t does not have.

   #67 and #55 implement incompatible designs for the same feature. #67
   recomputes effective priority from

       effective = min(base_priority, priority of every waiter on every
                       mutex the task holds)

   on every change; #55 saves and restores a value around each lock. Merging
   #55 after #67 deleted #67's implementation while leaving its call sites,
   so neither design was left intact.

   Restored #67's. It is the stronger of the two — save/restore is wrong when
   a task holds two mutexes and releases one, and wrong again when a task is
   boosted while already holding a mutex — and it subsumes what #55 fixes:
   recomputing on timeout is one of the cases it recomputes on, and it
   already rejects a full waiter table rather than blocking a task that was
   never enqueued.

4. #74's tests were merged without its parser change. Six assertions in
   test_config failed because a "- name:" item arriving while section was
   SEC_PKG_DEPS fell into the dependency branch, so the package after a deps
   list was swallowed as a dependency of the previous one. Restored the
   SEC_PKG_DEPS arm of the outer condition, nested the dependency handling as
   the else of the name check, and added the deps transition under
   SEC_PKG_BUILD.

    before   cmake: configure fails
    after    build clean, ctest 28/28

Also re-checked the package-overflow guard that arrived via #61/#65, since it
landed in a different shape than #78 proposed: AddressSanitizer is clean on a
129-package config that previously overflowed, and the 129th entry is refused
with a message rather than silently dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(core): make eos cross-compile for Cortex-M4 again

eos does not build for the target it exists to build for. From a clean
checkout of master, with the toolchain the CI job installs:

    $ cmake -B barm -DCMAKE_TOOLCHAIN_FILE=toolchains/arm-cortex-m4.cmake
    $ cmake --build barm
    core/src/log.c:113:9: error: implicit declaration of function 'clock_gettime'
    core/src/log.c:113:23: error: 'CLOCK_MONOTONIC' undeclared

get_timestamp_ms() had two branches, _WIN32 and "else, assume POSIX". Bare-
metal newlib is neither: arm-none-eabi declares neither clock_gettime nor
CLOCK_MONOTONIC, so the POSIX branch cannot compile for any Cortex-M target.
The macro is the precise test — where it is absent, so is the function.

There is a timebase on a target, eos_get_tick_ms() in the HAL, but core/ sits
below hal/ and must not depend upward, so log entries carry a zero timestamp
on a freestanding build until a port supplies one. Ordering within the ring
buffer is unaffected; only absolute time is missing.

Also fixed the format specifier one line down. uint32_t is `unsigned int` on
x86-64 and `unsigned long` on 32-bit ARM, so the literal %u was wrong on
exactly the target this build is for. It is PRIu32 now, which is right on
both.

Why CI did not catch it. The ARM job points CMAKE_TOOLCHAIN_FILE at
cmake/arm-cortex-m4.cmake; the file is at toolchains/. That fails loudly --
"Could not find toolchain file" -- so the job has been red rather than
silently passing, but it dies at configure and never reaches the compile step
where this bug lives.

The path is not new breakage: #75 fixed it, #79 and #51 carried the fix, and
#59 reverted it. That is the same stale-base merge pattern as the other three
repairs on this branch.

    before   ARM: configure fails on a missing toolchain file; with the real
             path, 2 compile errors in core/src/log.c
    after    ARM: 70/70 targets built, artifacts report `architecture:
             armv7e-m`; host: 28/28 ctest, unchanged

Kernel and services together measure 15.8 KB flash and 26 KB RAM on
Cortex-M4, which is the first real footprint number this repo has produced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: make the quick start clone a repository that exists

Every quick start in the tree opened with

    git clone https://github.com/anthropic/EoS.git

That repository does not exist — `gh api repos/anthropic/EoS` returns 404. It
is step one of the documented onboarding path in seven files: GETTING_STARTED,
the host/stm32/rpi4/nrf52 quickstarts, the integration guide and the STM32
tutorial. Any external developer following the docs stopped there.

The §30 12-month framework lists "tested quick start" among the 0–3 month
Foundation deliverables. It was not tested, and could not have been.

Two further defects behind the URL, both of which only surface once the clone
succeeds:

- The docs then `cd EoS/eos/examples/blink-gpio`, but cloning eos.git creates
  a directory called `eos` with `examples/` at its root. `EoS/eos/...` belongs
  to a multi-repo workspace that the single clone command never produces. The
  single-repo quick starts now use the path the clone actually creates. The
  guides that genuinely describe the multi-repo workspace are left alone —
  their paths are right, and what they are missing is a step that sets the
  workspace up, which is a larger documentation question than this change.

- They pass -DCMAKE_TOOLCHAIN_FILE=toolchains/arm-none-eabi.cmake, which does
  not exist. The ARM toolchain files are arm-cortex-m4.cmake,
  arm-none-eabi-stm32f4.cmake and arm-none-eabi-r5.cmake. Pointed at
  arm-cortex-m4.cmake, which is the one the CI job and the Cortex-M reference
  class use.

Verified by running the host quick start verbatim against the published repo,
from an empty directory:

    git clone https://github.com/embeddedos-org/eos.git
    cd eos/examples/blink-gpio
    cmake -B build -DEOS_PRODUCT=iot
    cmake --build build
    ./build/blink-gpio
    [blink] Starting LED blink on pin 13
    [blink] LED ON

The example itself was always fine. Only the instructions were wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: publish the §25 feature maturity matrix

§25 requires every significant feature to carry a maturity state with the
evidence that state demands. No such record existed on master — grepping the
tree for Implemented / Experimental / Planned / Validated / Certified returned
nothing.

That is the failure §25 exists to prevent, and it is load-bearing here. Without
it, 84 board descriptors read as 84 supported boards, a 190-line facade reads
as a networking subsystem, and a set of signature routines that verify nothing
reads as secure boot.

A version of this file was written on the reconcile/tier-1 branch and never
merged. This is that work brought to master and re-measured, because several
of its rows are now wrong in both directions.

Six subsystems are recorded as Planned rather than Experimental, each citing
the reason:

- eNet. ADR-014 point 4 requires this correction in the PR that merges the ADR;
  the ADR merged and the correction did not. net/src/net.c is 190 lines with 20
  (void)param casts and eos_net_connect() is `return -1`. test_net passes
  because it tests the facade's argument validation.
- RSA/ECC signatures. eos_ecc_verify() accepted any 64-byte signature and
  eos_rsa_verify_sha256() compared only the trailing 32 bytes. Both refuse to
  run now, which is the correct behaviour and still not verification.
- The HAL host backend. hal_linux.c implements one, but
  eos_hal_linux_register() is never called and appears in no public header, so
  on a host build eos_hal_init() returns -1 and every HAL call silently
  no-ops.
- The extended HAL. hal_extended_stubs.c is 740 of the HAL's 3,185 lines and
  carries 144 (void)param casts.
- Boards. 84 descriptors, 71 of them named generic-*.

Two rows move the other way, because the evidence improved:

- ARM Cortex-M4/R5 goes from Planned to Experimental. The tree cross-compiles
  clean and objdump reports armv7e-m and armv7 on the artifacts. It has still
  never been executed, which the row says.
- ctest is 28/28, not the 21/21 the old file recorded.

RISC-V and Cortex-A are Unknown rather than Planned: the toolchain files exist,
the toolchains are not installed here, so nothing was built. NOT RUN is an
answer; a guess dressed as Planned is not.

No subsystem claims Implemented, Validated or Certified. Each of those needs
evidence nothing in the tree currently supports.

Not included: component-manifest.json, which also sits on reconcile/tier-1. It
describes an arch/ tier that does not exist on master, so bringing it forward
needs its own pass rather than riding along here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(net): implement the eNet socket API on POSIX sockets

ADR-014 point 3: "On the Compute profile, where a host OS is present, the same
API maps to POSIX sockets." This is that mapping.

Before this, every socket function in eNet was a stub. eos_net_connect() was

    int eos_net_connect(eos_socket_t sock, const eos_net_addr_t *addr)
    {
        (void)sock; (void)addr;
        return -1;
    }

and eos_net_socket() returned an incrementing integer naming nothing. Anything
built on eNet could not be exercised on any target — including the host, which
is where EoSim runs and where the org's own ecosystem gate would test it.

    [PASS] net tcp round trip
    [PASS] net resolve localhost
    [PASS] net connect to a closed port fails

The fd is the handle. eos_socket_t is an int, EOS_SOCKET_INVALID is -1, and
that is exactly what socket(2) returns on failure, so there is no descriptor
table to keep in step and no way for the two to drift.

Three details that are not obvious from the API shape:

- eos_net_addr_t documents ip as network byte order, so it is copied into
  sin_addr rather than passed through htonl, which would swap it twice.
- send uses MSG_NOSIGNAL. Writing to a peer that has gone away raises SIGPIPE
  by default, which kills the process instead of returning an error the caller
  can act on.
- A zero timeout means "block forever" to SO_RCVTIMEO, which is the opposite of
  what a caller passing 0 expects, so the option is only set when non-zero. A
  timed-out recv returns 0 rather than -1: no bytes arrived, nothing failed.

The freestanding stubs stay in net.c behind #ifndef EOS_NET_POSIX rather than
being deleted. On a target with no IP stack, failing is the honest answer, and
that is the Planned state ADR-014 records for the Nano and Edge profiles until
lwIP lands.

Gated on CMAKE_CROSSCOMPILING alone, deliberately not on the
`EOS_PLATFORM STREQUAL "linux" OR NOT CMAKE_CROSSCOMPILING` form used a few
lines above for hal_linux.c. EOS_PLATFORM defaults to "linux" and a cross
toolchain file does not change it, so that condition is true even for a
Cortex-M build — I used it first and the ARM build failed on <netdb.h>.
hal_linux.c survives the same condition only because it uses portable headers;
it is compiled into the Cortex-M build today, which is worth a separate look.

Scope is the socket layer. ADR-014 point 5 keeps MQTT, CoAP and HTTP out as
separate adoption decisions on top of a working IP stack; they remain stubs in
net.c. This does not make eNet Implemented — lwIP is still the decision for the
profiles with no host OS to borrow sockets from.

Verification: host ctest 28/28, with test_net going from 16 to 19 cases;
net.c compiles for bare-metal Cortex-M with EOS_ENABLE_NET=1 and defines
eos_net_connect exactly once, as does the host build; full ARM cross-build
clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: z <x@y>
srpatcha added a commit that referenced this pull request Sep 8, 2026
)

* fix(build): remove duplicate test targets from tests/CMakeLists.txt

tests/CMakeLists.txt declared test_crypto_aes and test_crypto_sha512
twice each. A repeated add_executable target name is a hard CMake error,
not a warning:

    CMake Error at tests/CMakeLists.txt:77 (add_executable):
      add_executable cannot create target "test_crypto_aes" because
      another target with the same name already exists.
    CMake Error at tests/CMakeLists.txt:79 (add_test):
      add_test given test NAME "test_crypto_aes" which already exists.

Configuration aborted, so the command the README and CLAUDE.md give for
building the test suite produced no build system at all and no test
could be built or run.

Kept the second copy of each pair, whose comments describe what the
suite actually covers, and gave test_crypto_ecc and test_crypto_rsa the
same one-line description for consistency. No target, source file or
link dependency changes.

* fix(kernel): repair the broken priority-inheritance merge in sync.c

sync.c and task.c did not compile. #67 replaced the save/restore
priority-inheritance scheme with one that recomputes an owner's
effective priority from an invariant, and #55 — written against the
pre-#67 tree — landed on top and put half of the old scheme back
without removing the new call sites.

The result referenced neither design completely:

    sync.c:66  error: 'mtx_t' has no member named 'original_prio'
    sync.c:149 error: implicit declaration of function 'task_valid'
    sync.c:150 error: 'g_blocked_on' undeclared
    sync.c:197 error: 'g_blocked_on' undeclared
    sync.c:206 error: implicit declaration of function 'pi_propagate'
    sync.c:225 error: 'g_blocked_on' undeclared
    task.c:576 error: redefinition of 'eos_task_set_current_internal'

Restored the #67 design, which the kernel documentation already
describes and tests/test_priority_inherit.c already tests:

    effective = min( base_priority,
                     priority of every task waiting on every mutex held )

Recomputing from that invariant is what makes the nested and chained
cases come out right, and it subsumes what #55 set out to fix: a waiter
that times out is removed from the queue and pi_propagate() then drops
the boost it was causing, because the boost is derived from the live
waiter set rather than stashed in the mutex.

#55's other criterion is kept too — a full waiter table returns
EOS_KERN_NO_MEMORY without boosting the owner.

Also removed the duplicate eos_task_set_current_internal() that #55 added
to task.c, keeping #67's version, which clears g_current on an
out-of-range handle and marks the task RUNNING as the header documents.
Folded the surviving declaration's two doc comments together in
kernel_internal.h.

---------

Co-authored-by: Srikanth Patchava <srikanth.patchava@outlook.com>
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.

2 participants