diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 431a67ca..86ef8da1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -257,7 +257,7 @@ jobs: ci-gate: name: CI Gate runs-on: ubuntu-22.04 - needs: [test, default-config, build-arm, static-analysis] + needs: [test, windows-test, default-config, build-arm, static-analysis] if: always() steps: - uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2212ffba..480b4a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ ## [Unreleased] +### Changed +- **`eos_task_delete(0)` and `eos_task_suspend(0)` return `EOS_KERN_INVALID`.** Handle 0 is the idle task and it is permanent: it is the task the scheduler falls back to when nothing else is runnable. This restores the scheduler contract #131 established and #130's replay rolled back -- `kernel/src/task.c` on master was #130's copy, in which the idle task could be deleted or suspended, `eos_task_create()` published a TCB to the scheduler before its stack existed, and `eos_schedule()` left `g_current_sp` pointing at the outgoing task. The file is #131's again, plus #130's `wake_armed` hunk. + ### Fixed +- **Master did not compile after the 09-08 batch merge.** Nine PRs were merged within minutes, each on the base it was written against. #119 and #132 each added an `#else` to the same `#ifndef _WIN32` in `eos_busybox_install_to_rootfs()`, so `linux_security.c` read "#else after #else" and every C job, CodeQL and the simulation's ARM64 kernel failed to compile -- before the Python guards that would have named the rest could run. The duplicate arm is removed. Behind it: `test_linux_security_paths` (#119) and `test_pkg_fetch` (#115), whose `add_executable()` blocks later replays had overwritten, are registered again (41 to 43 suites); four `test_kernel` tests from #130 and #131 that were defined but never called are wired into `main()`; `windows-test` (#132) is in the CI gate's `needs`, so the gate cannot be green with the MSVC leg red; and `eosim-sanity.yml` and `simulation-test.yml` (#129), whose `pull_request` trigger is path-filtered and so cannot be a required check, are classified in `test_ci_gate.py`'s `NOT_REQUIRED`. +- **`eos_busybox_install_to_rootfs()` refuses `NULL`:** it read `bb->source_dir` to hand it to `is_path_safe()`, so a `NULL` `bb` was a crash rather than a -1. Both arguments are now refused before either is dereferenced. - **`linux_security` shell commands:** `is_path_safe()` was a denylist of shell metacharacters that missed the backtick and the newline, was applied at 2 of the 9 call sites that build a command, and returned "safe" for `NULL`. A path of the form `/tmp/id` reached `system()` and ran `id`. The predicate now rejects control characters, backslash, backtick and newline, refuses `NULL`, and guards every shell-building entry point. `eos_busybox_configure()` validated `source_dir` *before* filling in its default, so the default path -- the one a caller gets by not setting the field -- validated the empty string and handed `.eos/build/src/busybox-` to the shell unchecked; the fill now happens first, and `eos_busybox_set_version()` refuses a version that is not one shell word. - **One shell word is an allowlist.** `defconfig` and `cross_compile` are interpolated unquoted. The predicate guarding them was a second denylist (space, tab, `*`, `?`, `~`) that missed `[`/`]` and `{`/`}`, and accepted a leading `-`. It is now `isalnum()` plus `._/+=:-` and nothing else. - **Security steps no longer report success without having run.** `eos_ima_sign_file()`, `eos_selinux_install_to_rootfs()`, `eos_selinux_label_rootfs()`, `eos_ima_install_to_rootfs()` and `eos_busybox_install_to_rootfs()` ended their commands with `|| true` or `|| echo …` and discarded `system()`'s result, so a missing `evmctl` or `setfiles`, a failed policy or key copy, and a `make install` that never ran all returned 0. Each now distinguishes a step that completed from one that could not, logs through `EOS_ERROR` and returns -1. **Contract change:** these five functions previously always returned 0; no caller in this tree relies on that. diff --git a/docs/hardware-alignment.md b/docs/hardware-alignment.md index 9f37ebe3..aa43d191 100644 --- a/docs/hardware-alignment.md +++ b/docs/hardware-alignment.md @@ -50,6 +50,8 @@ Legacy/Niche: **Result: 25/25 boards aligned.** Every eboot board has a matching eos board definition, and every eos board has a matching eboot port. +> Superseded: measured 2026-09-14, the full inventories are 84 eos board YAMLs against 83 eBoot board directories, and only 7 names match across them; the 25 rows above are the subset the original audit chose, not the whole. Both halves of the sentence above are false for the full inventories. See `docs/three-way-alignment.md` for the measured status table. + --- ## Detailed Board Inventory @@ -96,7 +98,7 @@ Legacy/Niche: | `imx8m/` | ARM64 | `EOS_PLATFORM_ARM_CA53` | Cortex-A53 | eMMC | 2 GB | | `am64x/` | ARM | `EOS_PLATFORM_ARM_CA53` | A53+R5F | 64 MB | 2 GB | | `stm32mp1/` | ARM | `EOS_PLATFORM_ARM_CA72` | A7+M4 | eMMC | 512 MB | -| `qemu-arm64/` | ARM64 | `EOS_PLATFORM_ARM_CA53` | Cortex-A57 | virtio | 512 MB | +| `qemu_arm64/` | ARM64 | `EOS_PLATFORM_ARM_CA53` | Cortex-A57 | virtio | 512 MB | | `riscv64_virt/` | RISC-V | `EOS_PLATFORM_RISCV64` | RV64GC | virtio | 256 MB | | `sifive_u/` | RISC-V | `EOS_PLATFORM_RISCV64` | U74 | SPI | 8 GB | | `esp32/` | Xtensa | `EOS_PLATFORM_XTENSA` | LX6 | 4 MB | 520 KB | @@ -123,7 +125,7 @@ Legacy/Niche: | Architecture | eos Toolchain | eos Boards | eboot Boards | Platform Enum | |-------------|--------------|-----------|-------------|---------------| | **ARM Cortex-M** | `arm-none-eabi` | nRF52840, STM32F4, STM32H7, SAMD51 | nrf52, stm32f4, stm32h7, samd51 | `ARM_CM0`..`ARM_CM33` | -| **ARM Cortex-A** | `aarch64-linux-gnu` | RPi4, i.MX8M, AM64x, STM32MP1, QEMU | rpi4, imx8m, am64x, stm32mp1, qemu-arm64 | `ARM_CA53`, `ARM_CA72` | +| **ARM Cortex-A** | `aarch64-linux-gnu` | RPi4, i.MX8M, AM64x, STM32MP1, QEMU | rpi4, imx8m, am64x, stm32mp1, qemu_arm64 (eos: `qemu-arm64.yaml` — names differ) | `ARM_CA53`, `ARM_CA72` | | **RISC-V 64** | `riscv64-linux-gnu` | generic-riscv64, SiFive U | riscv64_virt, sifive_u | `RISCV64` | | **Xtensa** | (vendor SDK) | ESP32 | esp32 | `XTENSA` | | **x86 / x86_64** | `x86_64-linux-gnu` | generic-x86, generic-x86_64 | x86, x86_64_efi | `X86`, `X86_64` | diff --git a/docs/three-way-alignment.md b/docs/three-way-alignment.md index 57102a92..6774cfdb 100644 --- a/docs/three-way-alignment.md +++ b/docs/three-way-alignment.md @@ -4,23 +4,145 @@ This document tracks alignment between all three EoS components to ensure they r --- -## Alignment Status: ✅ ALL ALIGNED +## Alignment Status: ⚠️ NOT ALIGNED — the inventories disagree and nothing cross-checks them + +Measured on 2026-09-14 against origin/master of eos, eBoot and ebuild (eos `9ed6831`, eBoot `221325c`, ebuild `76970c9`). Every number below comes from the commands under "How this was measured"; the previous banner and its counts (25 / 25 / 41 / 24 / 33 / 39 / 5) did not reproduce. | Dimension | eos | eboot | ebuild | Status | |-----------|-----|-------|--------|--------| -| Board definitions | 25 YAML files in `eos/boards/` | 25 board ports in `eboot/boards/` | `MCU_TO_EBOOT_BOARD` + `EOS_BOARD_MAP` in project generator | ✅ Aligned | -| Product profiles | 41 profiles in `eos/products/*.h` | — | `PRODUCT_MAP` (41 entries) in project generator | ✅ Aligned | -| Platform enum | — | 24 `eos_platform_t` entries in `eos_hal.h` | MCU_DATABASE (100+ MCUs) in hw analyzer | ✅ Aligned | -| Peripheral keywords | 33 HAL APIs in `hal.h` + `hal_extended.h` | — | `PERIPHERAL_KEYWORDS` (24 types) + `ComponentDB` (200+ parts) | ✅ Aligned | -| Multicore support | `multicore.h` API | `eos_multicore.h` boot API | `MULTICORE_MCUS` set in project generator | ✅ Aligned | -| Config generation | `eos_config.h` (39 `EOS_ENABLE_*` flags) | `eboot_flash_layout.h` | `EosConfigGenerator` produces both | ✅ Aligned | -| Templates | — | — | 5 templates in `ebuild/templates/` | ✅ Aligned | -| CLI commands | cmake build | cmake build | `ebuild new`, `build`, `analyze`, `generate-project` | ✅ Aligned | +| Board definitions | 84 `boards/*.yaml` (89 entries in `boards/`; the other 5 are `lm3s6965evb.ld`, `startup_stm32f407.S`, `stm32f407vg.ld`, `stm32f407_discovery/`, `validation/`) | 83 `boards/*/` directories, 83 `eboot_add_board()` calls in `CMakeLists.txt`, 83 `boards/*/board_*.h` | `ebuild/sdk_generator.py`: `TARGET_ARCH` 14, `EBOOT_BOARD` 14, `MCU_TO_EBOOT_BOARD` 136 keys (56 distinct eboot dirs); `ebuild/eos_ai/eos_project_generator.py`: `EOS_BOARD_MAP` 116 keys (81 distinct eos YAMLs) | ⚠️ 84 / 83 / 14+136 are differently-keyed inventories of different sizes and no tool compares them. Every `MCU_TO_EBOOT_BOARD` value is an eboot dir (56/56) but 27 eboot dirs are never targeted; every `EOS_BOARD_MAP` value is an eos YAML (81/81) but `hiletgo-esp-wroom-32.yaml`, `qemu-arm64.yaml` and `tms570.yaml` are never mapped; only 7 eos YAML stems share a name with an eboot dir. | +| Product profiles | 48 `products/*.h`; 42 `#if`/`#elif defined(EOS_PRODUCT_*)` branches in `include/eos/eos_config.h` | — | `PRODUCT_MAP` 41 keys in `eos_project_generator.py` | ⚠️ 48 / 42 / 41. All 41 `PRODUCT_MAP` keys have a header and a config branch. `cast_device`, `desktop`, `home_camera`, `iptv_stb`, `smart_speaker`, `tv_os` have a header but neither a config branch nor a `PRODUCT_MAP` entry; `vbox_test` has a header and a config branch but no `PRODUCT_MAP` entry. | +| Platform enum | — | 24 `eos_platform_t` enumerators in `include/eos_hal.h` | `MCU_DATABASE` 171 keys in `eos_hw_analyzer.py`, using 52 distinct `arch` strings | ⚠️ The 24 reproduces. `MCU_DATABASE` describes MCUs by `arch` strings (`arm`, `mips64`, `sh`, …) that share no vocabulary with the enumerators, and nothing maps one onto the other. | +| Peripheral keywords | 33 peripheral families with an `eos__init()`: 5 in `hal/include/eos/hal.h`, 28 in `hal/include/eos/hal_extended.h` | — | `PERIPHERAL_KEYWORDS` 46 keywords mapping to 24 distinct types in `eos_hw_analyzer.py`; `ComponentDB` 114 parts in `component_db.py` (its docstring says 200+) | ⚠️ 33 and 24 reproduce; 200+ does not (114). Only 21 of the 24 types are HAL family names: `ethernet`, `gps`, `watchdog` are spelled `eth`, `gnss`, `wdt` in the HAL, and `cellular`, `flash`, `gpu`, `haptic`, `hdmi`, `ir`, `pcie`, `radar`, `touch` have no keyword at all. | +| Multicore support | `kernel/include/eos/multicore.h` (38 declarations: `eos_core_*`, `eos_spin_*`, `eos_ipi_*`, `eos_rproc_*`, `eos_shmem_*`, …) | `include/eos_multicore.h` (12 `eos_multicore_*` declarations) | `MULTICORE_MCUS` 13 names in `eos_project_generator.py` | ⚠️ All three exist, and that is all that was checked: the two headers are different APIs, and nothing compares either of them to `MULTICORE_MCUS`. | +| Config generation | 34 `#define EOS_ENABLE_*` in `include/eos/eos_config.h` | `eboot_flash_layout.h` is not a file in eBoot (0 tracked files); it is generated output that eBoot only documents (`configs/README.md`, `docs/quickstart.md`, `docs/book/book.md`) | `EosConfigGenerator.generate_eos_config_h()` writes `eos_product_config.h`; `EosBootIntegrator.generate_from_boot_yaml()` writes `eboot_flash_layout.h` | ⚠️ 39 did not reproduce (34), and "`EosConfigGenerator` produces both" was wrong: two classes, two files. Across `ebuild/**/*.py` (`eos_hw_analyzer.py` 24, `eos_project_generator.py` 24, `component_db.py` 23; 29 distinct) ebuild names 29 `EOS_ENABLE_*` flags; 24 are defined in `eos_config.h`, `EOS_ENABLE_GPIO`/`I2C`/`SPI`/`TIMER`/`UART` are defined nowhere in eos, and 10 `eos_config.h` flags (`EOS_ENABLE_DISPLAY_DRV`, `EOS_ENABLE_ETHERNET_DRV`, `EOS_ENABLE_GPU`, `EOS_ENABLE_HDMI`, `EOS_ENABLE_MULTICORE`, `EOS_ENABLE_NET`, `EOS_ENABLE_PCIE`, `EOS_ENABLE_PCIE_DRV`, `EOS_ENABLE_SDIO`, `EOS_ENABLE_USB_HOST`) are never named by ebuild. | +| Templates | — | — | 6 directories in `templates/` at the ebuild repo root (`bare-metal`, `ble-sensor`, `linux-app`, `rtos-app`, `safety-critical`, `secure-boot`); `ebuild/templates/` does not exist | ⚠️ "5 in `ebuild/templates/`" did not reproduce: 6, at a different path. eos names four of them by their `ebuild new --template` spelling (`bare-metal` in `GETTING_STARTED.md` and `docs/quickstart-host.md`, `ble-sensor` in `docs/quickstart-nrf52.md`, `linux-app` in `docs/quickstart-rpi4.md`); all four exist in ebuild's `templates/`. eBoot names none. Nothing checks that a documented template name exists. | +| CLI commands | cmake build (`CMakeLists.txt`) | cmake build (`CMakeLists.txt`) | `new`, `build`, `analyze`, `generate-project` are `@cli.command` functions in `ebuild/cli/commands.py` (23 `@cli.command` entries plus the `repos` group with 6 subcommands) | ✅ The four named subcommands exist by name. That is a presence check on ebuild alone, not an inventory comparison. | + +--- + +## How this was measured + +Run from a directory holding sibling checkouts `eos/`, `eBoot/` and `ebuild/` with `origin/master` fetched (`git -C fetch origin`). Every command reads `origin/master`, not the working tree, so it gives the same answer whatever is checked out. + +```bash +git -C eos ls-tree --name-only origin/master boards/ | wc -l # 89 +git -C eos ls-tree --name-only origin/master boards/ | grep -c '\.yaml$' # 84 +git -C eos ls-tree --name-only origin/master boards/ | grep -v '\.yaml$' # the other 5 +git -C eBoot ls-tree origin/master boards/ | grep -c '^040000' # 83 +git -C eBoot show origin/master:CMakeLists.txt | grep -c 'eboot_add_board(' # 83 +git -C eBoot ls-tree -r --name-only origin/master boards/ | grep -cE '^boards/[^/]+/board_[^/]+\.h$' # 83 +git -C eos ls-tree --name-only origin/master products/ | grep -c '\.h$' # 48 +git -C eBoot show origin/master:include/eos_hal.h | awk '/typedef enum/{f=1} f && /^[[:space:]]*EOS_PLATFORM_/{n++} /} eos_platform_t;/{f=0} END{print n}' # 24 +git -C eos show origin/master:include/eos/eos_config.h | grep -cE '^\s*#\s*define\s+EOS_ENABLE_' # 34 +git -C ebuild ls-tree origin/master templates/ | grep -c '^040000' # 6 +git -C ebuild ls-tree --name-only origin/master ebuild/templates/ | wc -l # 0: the path does not exist +git -C ebuild show origin/master:ebuild/eos_ai/component_db.py > /tmp/component_db.py +python3 -c "import importlib.util as u, sys; s=u.spec_from_file_location('component_db', sys.argv[1]); m=u.module_from_spec(s); sys.modules['component_db']=m; s.loader.exec_module(m); print(len(m.ComponentDB()._db))" /tmp/component_db.py # 114 +git -C ebuild show origin/master:ebuild/cli/commands.py | grep -cE '^@cli\.command' # 23 +git -C ebuild show origin/master:ebuild/cli/commands.py | grep -cE '^@repos\.command' # 6 +git -C ebuild show origin/master:ebuild/cli/commands.py | grep -oE '^def (new|build|analyze|generate_project)\(' # all four +git -C eos show origin/master:kernel/include/eos/multicore.h | grep -oE '\beos_[a-z0-9_]+\s*\(' | sort -u | wc -l # 38 +git -C eBoot show origin/master:include/eos_multicore.h | tr -d '\r' | grep -oE '\beos_multicore_[a-z0-9_]+\s*\(' | sort -u | wc -l # 12 +git -C ebuild show origin/master:ebuild/eos_ai/eos_config_generator.py | grep -n 'eos_product_config.h' # written by generate_eos_config_h() +git -C ebuild show origin/master:ebuild/eos_ai/eos_boot_integrator.py | grep -n 'eboot_flash_layout.h' # written by generate_from_boot_yaml() +git -C eBoot ls-tree -r --name-only origin/master | grep -c 'eboot_flash_layout\.h' # 0 +git -C eBoot grep -l 'eboot_flash_layout' origin/master -- ':!build_sim' # configs/README.md, docs/book/book.md, docs/quickstart.md +``` + +The dictionary sizes and every set comparison in the table come from this script. It parses the ebuild sources with `ast` instead of grepping them: a grep for `":` counts every quoted key in the file, not the entries of one dict. + +```python +import ast, re, subprocess + +def show(repo, path): + return subprocess.run(["git", "-C", repo, "show", f"origin/master:{path}"], + check=True, capture_output=True, text=True).stdout + +def consts(repo, path, *names): + out = {} + for node in ast.walk(ast.parse(show(repo, path))): + if isinstance(node, (ast.Assign, ast.AnnAssign)): + for t in (node.targets if isinstance(node, ast.Assign) else [node.target]): + if isinstance(t, ast.Name) and t.id in names: + out[t.id] = ast.literal_eval(node.value) + return out + +sdk = consts("ebuild", "ebuild/sdk_generator.py", "TARGET_ARCH", "EBOOT_BOARD", "MCU_TO_EBOOT_BOARD") +gen = consts("ebuild", "ebuild/eos_ai/eos_project_generator.py", "EOS_BOARD_MAP", "PRODUCT_MAP", "MULTICORE_MCUS") +hw = consts("ebuild", "ebuild/eos_ai/eos_hw_analyzer.py", "PERIPHERAL_KEYWORDS", "MCU_DATABASE") +for name, d in {**sdk, **gen, **hw}.items(): + print(name, len(d)) +print("PERIPHERAL_KEYWORDS distinct values", len(set(hw["PERIPHERAL_KEYWORDS"].values()))) +print("MCU_DATABASE distinct arch", len({v["arch"] for v in hw["MCU_DATABASE"].values()})) + +yaml = {l.split("/")[-1] for l in show("eos", "boards/").split() if l.endswith(".yaml")} +dirs = {l.split()[-1].split("/")[-1] for l in subprocess.run( + ["git", "-C", "eBoot", "ls-tree", "origin/master", "boards/"], + check=True, capture_output=True, text=True).stdout.splitlines() if l.startswith("040000")} +m2e, ebm = sdk["MCU_TO_EBOOT_BOARD"], gen["EOS_BOARD_MAP"] +print("eos yaml", len(yaml), "| eboot dirs", len(dirs), + "| yaml stems that are eboot dirs", len({y[:-5] for y in yaml} & dirs)) +print("MCU_TO_EBOOT_BOARD distinct values", len(set(m2e.values())), + "| values that are eboot dirs", len(set(m2e.values()) & dirs), + "| eboot dirs never targeted", len(dirs - set(m2e.values()))) +print("EOS_BOARD_MAP distinct values", len(set(ebm.values())), + "| values that are eos yaml", len(set(ebm.values()) & yaml), + "| eos yaml never mapped", sorted(yaml - set(ebm.values()))) + +prods = {l.split("/")[-1][:-2] for l in show("eos", "products/").split() if l.endswith(".h")} +cfg = show("eos", "include/eos/eos_config.h") +chain = {m.lower() for m in re.findall(r"#\s*(?:el)?if\s+defined\s*\(\s*EOS_PRODUCT_([A-Z0-9_]+)\s*\)", cfg)} +pm = set(gen["PRODUCT_MAP"]) +print("products/*.h", len(prods), "| eos_config.h product branches", len(chain), "| PRODUCT_MAP", len(pm)) +print("headers without PRODUCT_MAP entry", sorted(prods - pm)) +print("headers without config branch", sorted(prods - chain), "| PRODUCT_MAP keys without header", sorted(pm - prods)) + +enables = set(re.findall(r"^\s*#\s*define\s+(EOS_ENABLE_[A-Z0-9_]+)", cfg, re.M)) +py_files = [l for l in subprocess.run(["git", "-C", "ebuild", "ls-tree", "-r", "--name-only", + "origin/master", "ebuild/"], capture_output=True, + text=True, check=True).stdout.split() if l.endswith(".py")] +named = set(re.findall(r"EOS_ENABLE_[A-Z0-9_]+", "".join(show("ebuild", f) for f in py_files))) +print("EOS_ENABLE_* defines", len(enables), "| named in ebuild/**/*.py", len(named), "| common", len(enables & named), + "| ebuild-only", sorted(named - enables), "| eos-only", sorted(enables - named)) + +fam = set() +for h in ("hal/include/eos/hal.h", "hal/include/eos/hal_extended.h"): + fam |= set(re.findall(r"\beos_([a-z0-9]+)_init\s*\(", show("eos", h))) - {"hal"} +kw = set(hw["PERIPHERAL_KEYWORDS"].values()) +print("HAL init families", len(fam), "| keyword types that are HAL families", len(kw & fam), + "| keyword types that are not", sorted(kw - fam), "| HAL families with no keyword", sorted(fam - kw)) +``` + +Its output on 2026-09-14: + +``` +MCU_TO_EBOOT_BOARD 136 +TARGET_ARCH 14 +EBOOT_BOARD 14 +MULTICORE_MCUS 13 +PRODUCT_MAP 41 +EOS_BOARD_MAP 116 +MCU_DATABASE 171 +PERIPHERAL_KEYWORDS 46 +PERIPHERAL_KEYWORDS distinct values 24 +MCU_DATABASE distinct arch 52 +eos yaml 84 | eboot dirs 83 | yaml stems that are eboot dirs 7 +MCU_TO_EBOOT_BOARD distinct values 56 | values that are eboot dirs 56 | eboot dirs never targeted 27 +EOS_BOARD_MAP distinct values 81 | values that are eos yaml 81 | eos yaml never mapped ['hiletgo-esp-wroom-32.yaml', 'qemu-arm64.yaml', 'tms570.yaml'] +products/*.h 48 | eos_config.h product branches 42 | PRODUCT_MAP 41 +headers without PRODUCT_MAP entry ['cast_device', 'desktop', 'home_camera', 'iptv_stb', 'smart_speaker', 'tv_os', 'vbox_test'] +headers without config branch ['cast_device', 'desktop', 'home_camera', 'iptv_stb', 'smart_speaker', 'tv_os'] | PRODUCT_MAP keys without header [] +EOS_ENABLE_* defines 34 | named in ebuild/**/*.py 29 | common 24 | ebuild-only ['EOS_ENABLE_GPIO', 'EOS_ENABLE_I2C', 'EOS_ENABLE_SPI', 'EOS_ENABLE_TIMER', 'EOS_ENABLE_UART'] | eos-only ['EOS_ENABLE_DISPLAY_DRV', 'EOS_ENABLE_ETHERNET_DRV', 'EOS_ENABLE_GPU', 'EOS_ENABLE_HDMI', 'EOS_ENABLE_MULTICORE', 'EOS_ENABLE_NET', 'EOS_ENABLE_PCIE', 'EOS_ENABLE_PCIE_DRV', 'EOS_ENABLE_SDIO', 'EOS_ENABLE_USB_HOST'] +HAL init families 33 | keyword types that are HAL families 21 | keyword types that are not ['ethernet', 'gps', 'watchdog'] | HAL families with no keyword ['cellular', 'eth', 'flash', 'gnss', 'gpu', 'haptic', 'hdmi', 'ir', 'pcie', 'radar', 'touch', 'wdt'] +``` --- ## Board-Level Alignment (25 boards) +> Kept from the original audit as a 25-board subset; the full inventories are 84 / 83 / 136 / 116 (status table above). On 2026-09-14 all 25 YAMLs and all 25 eboot directories still exist. The two ebuild columns are shorthand: in 9 rows (`mips`, `powerpc`, `m68k`, `sh4`, `frv`, `h8300`, `mn103`, `strongarm`, `xscale`) the name left of the arrow is not a literal key in at least one of the two maps, which are keyed by MCU part name or name prefix (`mips32`, `pic32`, `jz4740` → `mips`; `mpc`, `p10`, `ppc` → `generic-powerpc.yaml`); the target right of the arrow does exist in every row. + | MCU | eos board YAML | eboot board port | ebuild MCU_TO_EBOOT_BOARD | ebuild EOS_BOARD_MAP | |-----|---------------|-----------------|--------------------------|---------------------| | nRF52840 | `nrf52840.yaml` ✅ | `nrf52/` ✅ | `nrf52→nrf52` ✅ | `nrf52→nrf52840.yaml` ✅ | @@ -53,6 +175,8 @@ This document tracks alignment between all three EoS components to ensure they r ## Product Profile Alignment (41 profiles) +> Kept from the original audit. On 2026-09-14 all 41 rows still hold, but `products/` has 48 headers; the 7 with no `PRODUCT_MAP` entry are listed in the status table. + | eos Profile (`products/*.h`) | In `eos_config.h` #elif chain | In ebuild `PRODUCT_MAP` | |-----|------|------| | `adapter` | ✅ | ✅ | @@ -152,31 +276,33 @@ To verify alignment is maintained after any change: ```bash # 1. Count eos boards -ls eos/boards/*.yaml | wc -l # Should be 25 +ls eos/boards/*.yaml | wc -l # 84 on 2026-09-14 # 2. Count eboot boards -ls eboot/boards/*/board_*.h | wc -l # Should be 25 +ls eboot/boards/*/board_*.h | wc -l # 83 on 2026-09-14 # 3. Count eos product profiles -ls eos/products/*.h | wc -l # Should be 41 +ls eos/products/*.h | wc -l # 48 on 2026-09-14 # 4. Count eos_config.h #elif entries -grep -c "EOS_PRODUCT_" eos/include/eos/eos_config.h # Should be 41 +grep -c "EOS_PRODUCT_" eos/include/eos/eos_config.h # 42 on 2026-09-14 # 5. Count ebuild PRODUCT_MAP entries -grep -c '":' ebuild/ebuild/eos_ai/eos_project_generator.py | head -1 +grep -c '":' ebuild/ebuild/eos_ai/eos_project_generator.py | head -1 # counts every quoted key in the file; use the ast script above (41) # 6. Count platform enum entries -grep -c "EOS_PLATFORM_" eboot/include/eos_hal.h # Should be 24 +grep -c "EOS_PLATFORM_" eboot/include/eos_hal.h # 24 on 2026-09-14 # 7. Run ebuild test -cd ebuild && python test_full_pipeline.py +cd ebuild && python test_full_pipeline.py # test_full_pipeline.py does not exist on ebuild origin/master (2026-09-14) ``` --- ## Build System Alignment +> Kept from the original audit; the counts in this table are the original audit's, and the status table at the top supersedes them. On 2026-09-14 eboot's `CMakeLists.txt` has 86 `EBLDR_BOARD STREQUAL ""` branches naming 83 distinct boards (the sentinel `"none"` appears three times; `grep -c 'EBLDR_BOARD STREQUAL' CMakeLists.txt` → 86, distinct quoted literals → 84 including `"none"`), not 25, and eos's `EOS_PRODUCT` is a free-form cache string whose accepted values are listed in its help text, not enumerated in CMake. + ### CMake Variables Cross-Reference | Variable | eos `CMakeLists.txt` | eboot `CMakeLists.txt` | ebuild CLI | @@ -217,6 +343,8 @@ python3 sign_image.py --key key.pem --input ../../eos/build/app.bin --output sig ### eboot CMakeLists.txt Board Coverage (25/25) +> Kept from the original audit as a 25-board subset; the full eBoot inventory is 83 board directories (status table above), and 27 of them are never targeted by `MCU_TO_EBOOT_BOARD`. The 25/25 below is true of these 25 rows only. + | Board | In `if/elseif` chain | In FATAL_ERROR help string | |-------|---------------------|---------------------------| | stm32f4 | ✅ | ✅ | @@ -259,8 +387,8 @@ python3 sign_image.py --key key.pem --input ../../eos/build/app.bin --output sig | `eos/README.md` | ✅ full API, profiles | ✅ related project link | ✅ related project link | | `eboot/README.md` | ✅ related project link | ✅ full boot API | ✅ related project link | | `ebuild/README.md` | ✅ builds eos | ✅ builds eboot | ✅ full CLI reference | -| `docs/hardware-alignment.md` | ✅ 25 board YAMLs | ✅ 25 board ports | ✅ MCU maps | -| `docs/three-way-alignment.md` | ✅ profiles, boards | ✅ boards, platform enum | ✅ all maps audited | +| `docs/hardware-alignment.md` | ⚠️ 25 of 84 board YAMLs | ⚠️ 25 of 83 board ports | ✅ MCU maps | +| `docs/three-way-alignment.md` | ⚠️ measured 2026-09-14; see the status table | ⚠️ measured 2026-09-14; see the status table | ⚠️ measured 2026-09-14; see the status table | | `docs/adding-hardware.md` | ✅ HAL backends, profiles | ✅ EBOOT_REGISTER_BOARD | ✅ — | | `docs/api-release-process.md` | ✅ Doxyfile, headers | ✅ Doxyfile, headers | ✅ CLI versioning | | `eos/docs/api-reference.md` | ✅ all modules | — | — | diff --git a/kernel/include/eos/kernel.h b/kernel/include/eos/kernel.h index cf1a3412..3a09f15f 100644 --- a/kernel/include/eos/kernel.h +++ b/kernel/include/eos/kernel.h @@ -100,7 +100,27 @@ bool eos_kernel_is_running(void); int eos_task_create(const char *name, eos_task_func_t entry, void *arg, uint8_t priority, uint32_t stack_size); +/** + * @brief Delete a task; its slot becomes available to eos_task_create(). + * @param handle Task handle (slot index) returned by eos_task_create(). + * @return EOS_KERN_OK on success; EOS_KERN_INVALID for handle 0, an + * out-of-range handle, or a slot that holds no task. + * + * Handle 0 is the idle task, and it is permanent: it is the task the + * scheduler falls back to when nothing else is runnable, so it can be + * neither deleted nor suspended. + */ int eos_task_delete(eos_task_handle_t handle); + +/** + * @brief Suspend a task until eos_task_resume() is called on it. + * @param handle Task handle (slot index) returned by eos_task_create(). + * @return EOS_KERN_OK on success; EOS_KERN_INVALID for handle 0, an + * out-of-range handle, or a slot whose entry is NULL (never + * created, or deleted through eos_task_delete()). + * + * Handle 0 is the idle task and is permanent; see eos_task_delete(). + */ int eos_task_suspend(eos_task_handle_t handle); int eos_task_resume(eos_task_handle_t handle); void eos_task_yield(void); diff --git a/kernel/src/task.c b/kernel/src/task.c index 8d93b836..167e5f27 100644 --- a/kernel/src/task.c +++ b/kernel/src/task.c @@ -125,8 +125,8 @@ static int find_next_task(void) for (int i = 0; i < EOS_MAX_TASKS; i++) { int idx = (start + i) % EOS_MAX_TASKS; eos_task_t *t = &g_tasks[idx]; - if (t->state != EOS_TASK_READY) continue; - if (t->priority < best_prio) { + if (t->entry == NULL || t->state != EOS_TASK_READY) continue; + if (best < 0 || t->priority < best_prio) { best_prio = t->priority; best = idx; } @@ -141,15 +141,7 @@ static int find_next_task(void) */ void eos_schedule(void) { - int next = find_next_task(); - if (next < 0) return; /* No ready tasks — stay on current */ - - /* Mark current task as ready (if it was running) */ - if (g_current >= 0 && g_tasks[g_current].state == EOS_TASK_RUNNING) { - g_tasks[g_current].state = EOS_TASK_READY; - } - - /* Check stack canary on current task */ + /* Validate the outgoing task before making it eligible for selection. */ if (g_current >= 0 && g_tasks[g_current].stack_base) { if (g_tasks[g_current].stack_base[0] != EOS_STACK_CANARY) { /* Stack overflow detected! */ @@ -158,12 +150,38 @@ void eos_schedule(void) } } - /* Switch to next task */ - g_current_sp = &g_tasks[g_current >= 0 ? g_current : next].stack_ptr; + /* A preempted task remains runnable and competes at its normal priority. */ + if (g_current >= 0 && g_tasks[g_current].state == EOS_TASK_RUNNING) { + g_tasks[g_current].state = EOS_TASK_READY; + } + + int next = find_next_task(); + /* + * Unreachable by construction. Slot 0 is the idle task, which + * eos_task_delete() and eos_task_suspend() refuse, and idle_task_func() + * only spins or waits for an interrupt, so it never blocks: the idle + * task is always READY (or was RUNNING and was made READY just above), + * and the "best < 0 ||" term in find_next_task() lets it be chosen at + * priority 255 when nothing else is. The only way to reach it is the + * canary check above marking the idle task DELETED, which is a fault, + * not a schedule. find_next_task() scans every slot including the + * outgoing one, so next < 0 means nothing at all is READY; the return + * leaves g_current_sp and g_next_sp naming the outgoing task and its + * state as the canary check left it. Nothing here recovers from that, + * and nothing should: an overflowed idle stack is a fault handler's job. + */ + if (next < 0) return; /* No valid task is runnable. */ + + /* + * PendSV has already saved the outgoing PSP through g_current_sp. + * Point both assembly-facing slots at the task that is now current, so + * the next PendSV saves that task's PSP into its own control block. + */ g_current = next; g_tasks[next].state = EOS_TASK_RUNNING; g_tasks[next].run_count++; - g_next_sp = &g_tasks[next].stack_ptr; + g_current_sp = &g_tasks[next].stack_ptr; + g_next_sp = g_current_sp; } /** @@ -283,6 +301,8 @@ int eos_task_create(const char *name, eos_task_func_t entry, void *arg, if (!entry) return EOS_KERN_INVALID; if (stack_size == 0) stack_size = 1024; + uint32_t crit = eos_port_enter_critical(); + /* Find free slot (skip slot 0 = idle) */ int slot = -1; for (int i = 1; i < EOS_MAX_TASKS; i++) { @@ -292,30 +312,41 @@ int eos_task_create(const char *name, eos_task_func_t entry, void *arg, break; } } - if (slot < 0) return EOS_KERN_NO_MEMORY; + if (slot < 0) { + eos_port_exit_critical(crit); + return EOS_KERN_NO_MEMORY; + } /* Allocate stack */ uint32_t *stack_base = alloc_task_stack(stack_size); - if (!stack_base) return EOS_KERN_NO_MEMORY; + if (!stack_base) { + eos_port_exit_critical(crit); + return EOS_KERN_NO_MEMORY; + } uint32_t *stack_top = stack_base + stack_size / sizeof(uint32_t); memset(&g_tasks[slot], 0, sizeof(eos_task_t)); + g_tasks[slot].state = EOS_TASK_DELETED; g_tasks[slot].id = (uint8_t)slot; g_tasks[slot].name = name; - g_tasks[slot].entry = entry; g_tasks[slot].arg = arg; g_tasks[slot].priority = priority; g_tasks[slot].base_priority = priority; g_tasks[slot].stack_size = stack_size; g_tasks[slot].stack_base = stack_base; g_tasks[slot].stack_ptr = eos_port_init_stack(stack_top, entry, arg); + + /* Publish the entry and READY state only after the TCB is complete. */ + g_tasks[slot].entry = entry; g_tasks[slot].state = EOS_TASK_READY; g_task_count++; /* If kernel is running and new task has higher priority, yield */ - if (g_running && g_current >= 0 && - priority < g_tasks[g_current].priority) { + bool should_preempt = g_running && g_current >= 0 && + priority < g_tasks[g_current].priority; + eos_port_exit_critical(crit); + if (should_preempt) { eos_port_yield(); } @@ -324,7 +355,7 @@ int eos_task_create(const char *name, eos_task_func_t entry, void *arg, int eos_task_delete(eos_task_handle_t h) { - if (h >= EOS_MAX_TASKS) return EOS_KERN_INVALID; + if (h == 0 || h >= EOS_MAX_TASKS) return EOS_KERN_INVALID; if (g_tasks[h].state == EOS_TASK_DELETED || g_tasks[h].entry == NULL) return EOS_KERN_INVALID; g_tasks[h].state = EOS_TASK_DELETED; @@ -339,7 +370,8 @@ int eos_task_delete(eos_task_handle_t h) int eos_task_suspend(eos_task_handle_t h) { - if (h >= EOS_MAX_TASKS || g_tasks[h].entry == NULL) return EOS_KERN_INVALID; + if (h == 0 || h >= EOS_MAX_TASKS || g_tasks[h].entry == NULL) + return EOS_KERN_INVALID; g_tasks[h].state = EOS_TASK_SUSPENDED; if (h == (eos_task_handle_t)g_current) { eos_port_yield(); @@ -574,5 +606,3 @@ int eos_task_get_all_stats(eos_task_stats_t *out, int max_entries, int *count) *count = n; return EOS_KERN_OK; } - - diff --git a/services/linux/src/linux_security.c b/services/linux/src/linux_security.c index 2f86ed59..9a1eb57a 100644 --- a/services/linux/src/linux_security.c +++ b/services/linux/src/linux_security.c @@ -670,6 +670,7 @@ int eos_busybox_build(EosBusybox *bb) { } int eos_busybox_install_to_rootfs(const EosBusybox *bb, const char *rootfs_dir) { + if (!bb || !rootfs_dir) return -1; if (!is_path_safe(bb->source_dir) || !is_path_safe(rootfs_dir)) return -1; #ifndef _WIN32 char cmd[2048]; @@ -692,8 +693,6 @@ int eos_busybox_install_to_rootfs(const EosBusybox *bb, const char *rootfs_dir) "would have no busybox."); return -1; } -#else - (void)bb; #endif /* Create /init symlink for initramfs boot */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a9ec2f8b..28746170 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -350,6 +350,26 @@ add_executable(test_stress test_stress.c) target_link_libraries(test_stress PRIVATE eos_kernel) add_test(NAME test_stress COMMAND test_stress) +# --- test_linux_security_paths: the inputs linux_security must not shell out --- +# Registered by #119, replaced by #115's block in a three-way replay, then +# #118 replaced that; both suites compiled against nothing until +# test_cmake_test_registration.py named them. +# POSIX only: the suite proves refused inputs never reach a shell by probing +# with access()/open(O_CREAT)/rmdir, and the MSVC leg (#132) has no unistd.h. +# On Windows the shell-building bodies are compiled out (#ifndef _WIN32), so +# a path that passes is_path_safe() still reaches no shell there; the +# predicate itself is portable and is exercised by the POSIX legs. +if(UNIX) + add_executable(test_linux_security_paths test_linux_security_paths.c) + target_link_libraries(test_linux_security_paths PRIVATE eos_linux_security) + add_test(NAME test_linux_security_paths COMMAND test_linux_security_paths) +endif() + +# --- test_pkg_fetch: source fetch refuses an unverifiable download --- +add_executable(test_pkg_fetch test_pkg_fetch.c) +target_link_libraries(test_pkg_fetch PRIVATE eos_pkg eos_core) +add_test(NAME test_pkg_fetch COMMAND test_pkg_fetch) + # --- Fuzz targets --- # Opt-in (-DEOS_ENABLE_FUZZING=ON, Clang only); the subdirectory guards itself. add_subdirectory(fuzz) diff --git a/tests/test_firmware.c b/tests/test_firmware.c index 7134a51d..b931ba40 100644 --- a/tests/test_firmware.c +++ b/tests/test_firmware.c @@ -75,7 +75,7 @@ static void test_arm_cortex_arch(void) { } static void test_firmware_init(void) { - EosConfig cfg; + static EosConfig cfg; /* ~4 MB: MSVC's 1 MB default stack cannot hold it */ memset(&cfg, 0, sizeof(cfg)); strncpy(cfg.workspace.build_dir, ".eos/build", EOS_MAX_PATH - 1); strncpy(cfg.toolchain.rtos_target, "arm-none-eabi", EOS_MAX_NAME - 1); @@ -100,7 +100,7 @@ static void test_firmware_init(void) { } static void test_firmware_dry_run(void) { - EosConfig cfg; + static EosConfig cfg; /* ~4 MB: MSVC's 1 MB default stack cannot hold it */ memset(&cfg, 0, sizeof(cfg)); strncpy(cfg.workspace.build_dir, ".eos/build", EOS_MAX_PATH - 1); @@ -119,7 +119,7 @@ static void test_firmware_dry_run(void) { } static void test_hybrid_init(void) { - EosConfig cfg; + static EosConfig cfg; /* ~4 MB: MSVC's 1 MB default stack cannot hold it */ memset(&cfg, 0, sizeof(cfg)); strncpy(cfg.workspace.build_dir, ".eos/build", EOS_MAX_PATH - 1); cfg.system.kind = EOS_SYSTEM_HYBRID; @@ -154,7 +154,7 @@ static void test_firmware_graph_node(void) { } static void test_config_load_rtos(void) { - EosConfig cfg; + static EosConfig cfg; /* ~4 MB: MSVC's 1 MB default stack cannot hold it */ EosResult res = eos_config_load(&cfg, "examples/motor-controller/eos.yaml"); if (res != EOS_OK) { printf("(skip - example file not found) "); @@ -168,7 +168,7 @@ static void test_config_load_rtos(void) { } static void test_config_load_hybrid(void) { - EosConfig cfg; + static EosConfig cfg; /* ~4 MB: MSVC's 1 MB default stack cannot hold it */ EosResult res = eos_config_load(&cfg, "examples/industrial-gateway/eos.yaml"); if (res != EOS_OK) { printf("(skip - example file not found) "); diff --git a/tests/test_kernel.c b/tests/test_kernel.c index aa96de71..1ccc2de6 100644 --- a/tests/test_kernel.c +++ b/tests/test_kernel.c @@ -497,6 +497,8 @@ static int kernel_tests_run = 0; int main(void) { printf("=== EoS Kernel Tests ===\n"); RUN(test_kernel_init); + RUN(test_idle_task_is_permanent); + RUN(test_scheduler_selects_valid_task_and_tracks_stack); RUN(test_task_create); RUN(test_task_create_invalid); RUN(test_task_delete); @@ -507,8 +509,10 @@ int main(void) { RUN(test_queue); RUN(test_queue_full); RUN(test_queue_send_waiter_overflow); + RUN(test_queue_no_stale_send_waiter); RUN(test_task_stats); RUN(test_tick_overflow); + RUN(test_wake_tick_zero); /* Counted rather than written out: the literal said 12/12 while a * thirteenth test sat in this file uncalled, so the number agreed * with the omission instead of exposing it. */ diff --git a/tests/test_linux_security_paths.c b/tests/test_linux_security_paths.c index 7eb9e1d1..1016f3df 100644 --- a/tests/test_linux_security_paths.c +++ b/tests/test_linux_security_paths.c @@ -730,6 +730,31 @@ static void test_busybox_install_reports_an_init_it_could_not_write(void) { PASS_IF_CLEAN(f0, "busybox_install_to_rootfs reports an /init it could not write"); } +/* 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 was a crash + * rather than a -1. Both arguments are now refused before either is + * dereferenced -- and, as everywhere in this file, refused means nothing + * was written: the rootfs gets no /init. */ +static void test_busybox_install_refuses_null_arguments(void) { + int f0 = failures; + char rootfs[] = "/tmp/eos_lsp_null_XXXXXX"; + char init[128]; + EosBusybox bb; + + if (!mkdtemp(rootfs)) { fprintf(stderr, "[SKIP] mkdtemp failed\n"); return; } + snprintf(init, sizeof init, "%s/init", rootfs); + + CHECK(eos_busybox_install_to_rootfs(NULL, rootfs) == -1); + CHECK(access(init, F_OK) != 0); + + eos_busybox_init(&bb); + CHECK(eos_busybox_install_to_rootfs(&bb, NULL) == -1); + + remove(init); + rmdir(rootfs); + PASS_IF_CLEAN(f0, "busybox_install_to_rootfs refuses NULL arguments"); +} + int main(void) { test_dmverity_verify_refuses_hostile_paths(); test_dmverity_verify_refuses_hostile_hash_device(); @@ -747,6 +772,7 @@ int main(void) { test_selinux_install_reports_a_policy_it_did_not_copy(); test_selinux_install_guard_stops_a_reachable_injection(); test_busybox_install_reports_an_init_it_could_not_write(); + test_busybox_install_refuses_null_arguments(); test_ordinary_paths_still_reach_the_shell(); if (failures) { diff --git a/tests/unit/test_ci_gate.py b/tests/unit/test_ci_gate.py index d484b875..d49290e9 100644 --- a/tests/unit/test_ci_gate.py +++ b/tests/unit/test_ci_gate.py @@ -72,6 +72,13 @@ "documentation build; its pull_request trigger is filtered to " "paths: ['docs/book/**'], so on a pull request that touches nothing " "under that path it reports no status at all", + "eosim-sanity.yml": + "nightly EoSim install check; its pull_request trigger is filtered to " + "the two simulation workflow files themselves (#129), so a pull " + "request that touches neither reports no status at all", + "simulation-test.yml": + "nightly EoSim simulation; same paths filter as eosim-sanity.yml " + "(#129), so it reports no status on almost every pull request", } # (workflow, job) pairs excluded from their gate's `needs`, with the reason.