Skip to content

ci(baremetal): Cortex-M7 smoke and size budget gate (#447 Phase 5) - #455

Merged
gg582 merged 1 commit into
devfrom
ci/baremetal-smoke
Oct 27, 2026
Merged

gg582 merged 1 commit into
devfrom
ci/baremetal-smoke

Conversation

@gg582

@gg582 gg582 commented Oct 27, 2026

Copy link
Copy Markdown
Member

Phase 5 of the bare-metal STM32 epic (#447): CI for the bare-metal build. Until now make baremetal-smoke ran only locally, so a change to hosted code could break the Cortex-M build unnoticed.

What runs

.github/workflows/baremetal.yml runs make baremetal-size-gate on every PR and push to main/dev:

  1. Build libcwist_baremetal.a with arm-none-eabi-gcc for Cortex-M7.
  2. Link the three smoke firmware images and run each under qemu-arm -cpu cortex-m7 with semihosting output:
    • hello: in-memory dispatch, no RTC, so no Date header
    • hello-rtc: the same with an RTC, so Date is present
    • hello-lwip: the lwIP adapter over lwIP's loopback netif
  3. Hold each firmware to scripts/ci/baremetal-budget.txt:
    • flash: code, rodata, exidx, init arrays, and the .data load image
    • static RAM: .data + .bss, not counting the linker's .heap region
    • the heap and stack high-water marks the firmware reports

ELFs, linker maps and run logs are uploaded as the baremetal-firmware artifact, so a size regression can be read from the map. The runner is pinned to ubuntu-24.04 (gcc 13.2 / newlib 4.4 / qemu 8.2), so the toolchain and the sizes do not move under the budget.

The workflow checks out top-level submodules only and installs no host libraries. Nothing vendored is built: the build reads headers from boringssl and lsquic (for the Makefile feature probe) and sources from cjson, libttak and lwip.

Budgets

Set about 10% above the measured values and rounded to KiB. I measured with both the CI toolchain (gcc 13.2, extracted from the noble packages) and gcc 14.2; the results agree within 0.6%.

firmware flash static RAM heap stack
hello 44,096 / 49,152 1,792 / 2,048 22,296 / 24,576 8,352 / 9,216
hello-rtc 44,136 / 49,152 1,804 / 2,048 22,296 / 24,576 8,800 / 9,728
hello-lwip 74,964 / 83,968 62,384 / 69,632 55,364 / 61,440 8,320 / 9,216

(measured / budget, bytes, gcc 13.2)

hello-lwip's static RAM comes mostly from the test lwipopts.h (24 KB MEM_SIZE and a 16-entry pbuf pool), not from CWIST. When growth is intended, the limit is raised in the same PR, with a reason.

Changes

  • Makefile:
    • baremetal-smoke saves each run's output to .baremetal-build/<fw>.log, and still stops at the first failing firmware with its log printed.
    • New baremetal-size-gate target.
  • scripts/ci/baremetal_size_gate.py and its unit test scripts/ci/test_baremetal_size_gate.py. The test is picked up by the existing scripts/ci unittest discovery. If the budget has a value for a metric the run did not report, the gate fails instead of passing silently.

Local verification

  • CI toolchain: gcc 13.2 + newlib 4.4 + qemu-arm 8.2 from the ubuntu-24.04 packages ran all three smoke images and the gate. PASS.
  • Local toolchain: gcc 14.2 + qemu 10, including make -j16. PASS.
  • Gate actually fails: with a budget deliberately set below the measurement it fails (FAIL hello stack: 8352 bytes > budget 8000 (+352)).
  • CI-like checkout: a tree with only cjson, libttak, lwip, boringssl and lsquic populated, built with PKG_CONFIG=false. PASS.
  • Gate unit tests: 4/4 pass.
  • Repo checks: check_test_wiring, check_api_baseline and check_env_knobs all OK.

Refs #447

- .github/workflows/baremetal.yml (ubuntu-24.04: gcc-arm-none-eabi 13.2,
  newlib 4.4, qemu-user 8.2) runs `make baremetal-size-gate` on every PR
  and push: the three smoke firmware (in-memory dispatch with and without
  an RTC, and the lwIP adapter over loopback) under qemu-arm, then the
  budget check. ELFs, maps and logs are uploaded as an artifact.
- baremetal-smoke keeps each run's output in .baremetal-build/<fw>.log
  and still fails on the first failing firmware.
- scripts/ci/baremetal_size_gate.py measures flash (code, rodata, exidx,
  init arrays, .data load image), static RAM (.data + .bss, not the
  linker heap region) and the heap/stack high-water marks the firmware
  report, against scripts/ci/baremetal-budget.txt. A budgeted value the
  run did not report fails rather than passing.
- Budgets sit about 10% above values measured with both gcc 13.2 (the CI
  toolchain) and 14.2, which agree within 0.6%.
@gg582
gg582 merged commit 35ed9a0 into dev Oct 27, 2026
41 of 42 checks passed
gg582 added a commit that referenced this pull request Oct 27, 2026
…458)

* ci(baremetal): Cortex-M7 smoke and size budget gate (#447 Phase 5) (#455)

- .github/workflows/baremetal.yml (ubuntu-24.04: gcc-arm-none-eabi 13.2,
  newlib 4.4, qemu-user 8.2) runs `make baremetal-size-gate` on every PR
  and push: the three smoke firmware (in-memory dispatch with and without
  an RTC, and the lwIP adapter over loopback) under qemu-arm, then the
  budget check. ELFs, maps and logs are uploaded as an artifact.
- baremetal-smoke keeps each run's output in .baremetal-build/<fw>.log
  and still fails on the first failing firmware.
- scripts/ci/baremetal_size_gate.py measures flash (code, rodata, exidx,
  init arrays, .data load image), static RAM (.data + .bss, not the
  linker heap region) and the heap/stack high-water marks the firmware
  report, against scripts/ci/baremetal-budget.txt. A budgeted value the
  run did not report fails rather than passing.
- Budgets sit about 10% above values measured with both gcc 13.2 (the CI
  toolchain) and 14.2, which agree within 0.6%.

(cherry picked from commit 35ed9a0)

* feat(port): HTTPS over the lwIP adapter with mbedTLS 3.6 (#447 Phase 4) (#457)

lwIP's altcp_tls_mbedtls only supports mbedTLS 2.x (in 2.2.1 and on
master), so the adapter drives mbedTLS 3.6 itself.

- cwist_lwip_tls_create()/destroy() build a server context (certificate
  chain, key, CTR_DRBG, ssl config); cwist_lwip_config.tls turns HTTPS on
  for a listener. TLS code compiles with -DCWIST_LWIP_TLS; without it,
  create returns NULL and a listener asked for TLS fails instead of
  serving plaintext.
- The DRBG is seeded only from cwist_port_entropy(): a failing hook makes
  create fail, and a failing reseed fails the handshake.
- Per connection: an mbedtls_ssl_context with BIOs over the pcb. Received
  ciphertext queues (bounded, refused back to lwIP when full) and is
  decrypted into the same rx/framing path as plaintext; responses go
  through mbedtls_ssl_write and are copied into lwIP, so tx is freed as
  soon as mbedTLS has taken it. The next pipelined request is picked up
  from tcp_sent, as on the plaintext path. 100 Continue goes through the
  record layer. Connection: close sends close_notify. A failed handshake
  aborts the connection.
- tests/lwip/cwist_test_mbedtls_config.h: TLS 1.2 server profile
  (ECDHE-ECDSA P-256, AES-128-GCM, SHA-256; 16 KB in / 4 KB out records).
  tests/lwip/tls_client.{c,h}: mbedTLS client over a loopback pcb.
  tests/lwip/test_tls_cert.h: test-only self-signed P-256 credentials.
- tests/test_lwip_tls.c (make test): verified handshake, keep-alive and
  pipelining in one session, 100-continue with a split body, 40 KB
  response, 413, close_notify, plaintext client on the TLS port, and
  context creation failures. Under CWIST_TLS=mbedtls it links the
  vendored mbedTLS libcwist already uses.
- tests/baremetal/hello_tls.c joins baremetal-smoke and the size gate:
  Cortex-M7 flash 137 KB, heap high-water 106.6 KB with both sessions,
  ~50 KB held by server + client sessions after the handshake.

(cherry picked from commit eb537f9)
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.

1 participant