Skip to content

Merge the .NET (Mono) managed runtime into Meadow.OS - #731

Merged
NevynUK merged 49 commits into
mainfrom
MergeManagedCode
Oct 18, 2026
Merged

NevynUK merged 49 commits into
mainfrom
MergeManagedCode

Conversation

@NevynUK

@NevynUK NevynUK commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Brings the Mono/.NET managed runtime onto Rebase (NuttX 12.12): the work of ManagedPlan.md phases 0-6, each of which was reviewed, hardware-tested on a Meadow F7 CCM (ProjectLab v3) and merged into MergeManagedCode.

PR Phase
#693 0: pin the Mono runtime and build MergeManagedCode in CI
#694 1: build the runtime against NuttX 12 with build-mono.sh
#695 2: reserve the start of SDRAM for the runtime image
#696 3: runtime glue (apps/system/mono), Kconfig and SDRAM placement
#697 4: first Mono boot (JIT and interpreter)
#699 5: managed assemblies (build-managed-assemblies.sh) and the 3.0 CLI deploy flow
#700 OS version 2.999.50 (kept apart from Legacy's 2.999.0)
#710 6: networking and TLS (espcp, NuttX poll() fix, TLS 1.2 cap)
#719 documentation fixes

Runtime

The runtime is built from WildernessLabs/runtime, pinned in MONO_RUNTIME_COMMIT at MergeManagedCode 300006899fb (tree e5888e3c96f). That is the latest commit on that branch and includes runtime #4, #5 and #6:

  • Thumb-2 JIT fixes;
  • NetworkInterface support on NuttX.

The runtime repository itself is not changed by this PR.

Companion firmware

The ESP32 changes are in WildernessLabs/Meadow-ESP32 MergeManagedCode (PR into develop):

  • NuttX 12 errno, socket-level and address-family values;
  • non-blocking sockets and SO_ERROR;
  • the Shutdown handler.

The NuttX 12 value mapping in that firmware is needed by this OS.

Tested

On a Meadow F7 CCM, OS/runtime 2.999.50.17, ESP32 2.5.7.568 with the Meadow-ESP32 changes:

  • Hello World and Blinky through meadow app run;
  • GC stress;
  • network tests 10/10;
  • TLS soak against https://www.google.co.uk (keep-alive 50/50, full handshakes 100/100, full pages 20/20).

Known issues and follow-ups (filed)

🤖 Generated with Claude Code

NevynUK and others added 30 commits October 14, 2026 13:46
Add MONO_RUNTIME_COMMIT, which records the WildernessLabs/runtime commit
(meadow-3.0, f980b4c169b) and tree the Mono runtime is built from.  The
runtime build added in phase 1 will refuse to run against any other
checkout.  The tree hash is recorded because the runtime branch history
has been rewritten once.

Run the build workflow for pushes to, and pull requests into, the
MergeManagedCode integration branch as well as main, so each phase's pull
request is built and uploads its artifacts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 0: pin the Mono runtime and build MergeManagedCode in CI
Add build-mono.sh, which builds the .NET Mono runtime archives against
this repository's NuttX headers.  It is standalone for now; build.sh and
CI will call it once the archives are linked into the OS (phase 3).

- The runtime checkout comes from MEADOW_RUNTIME_DIR, or ../runtime beside
  this repository when that is not set, and must be at the commit and tree
  recorded in MONO_RUNTIME_COMMIT with no local changes.
  MEADOW_RUNTIME_ALLOW_UNPINNED=1 relaxes this for runtime development.
- The compiler must be arm-none-eabi-gcc 15, the same as the rest of the
  OS.  NuttX must be configured first.
- Release by default; --debug builds Debug, --clean starts again and
  --verbose shows the runtime build output.
- Output goes to monoruntime/<release|debug>/ (ignored by git), so several
  checkouts can build one runtime checkout without clashing.  A build stamp
  records the runtime commit and local changes, the compiler version and a
  hash of the NuttX configuration; when any of them changes the build
  directory is discarded and CMake configures again.
- The script checks that every archive the OS link needs was produced.

Move MONO_RUNTIME_COMMIT to WildernessLabs/runtime Phase1 (a76e66e60c1),
which builds cleanly against NuttX 12 with GCC 15.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WildernessLabs/runtime#2 merged Phase1 into the runtime's MergeManagedCode
as c668e4a3aea; the tree is unchanged (5cf48371256).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 1: build the Mono runtime against NuttX 12 with build-mono.sh
generate_build_info used to restore user-space.ld from HEAD and then sed
the version placeholders into it in place, and restore_versioned_files
restored it again on exit.  Any uncommitted edit to the linker script was
therefore silently discarded by every build.

Copy user-space.ld to user-space.stamped.ld (git-ignored), stamp the copy,
and link the user image with it (boards/.../kernel/Makefile).  The tracked
user-space.ld is no longer rewritten or restored.

hcom_nuttx_shared.h and the ESP32 version files are still restored from
HEAD and stamped in place.

Hardware-tested on F7CoreComputeV2 (CCM) with the SDRAM layout change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Mono runtime image is linked to the start of SDRAM and copied there
from QSPI before the runtime starts, but the user heap covered all 32 MB
from 0xC0000000, so the copy would have overwritten live heap memory.

- defconfig: start the user heap after the 3 MB image (CONFIG_HEAP2_BASE
  0xc0300000, CONFIG_HEAP2_SIZE 29 MB to the end of SDRAM).  This is done
  by configuration; NuttX's stm32_allocateheap.c is unchanged.
- hcom_shared_common.h: add HCOM_NX_MONO_RUNTIME_SDRAM_ADDR (0xC0000000)
  and make the HCOM_NX_UPD_COPY_RUNTIME_TO_RAM ioctl copy there instead of
  to CONFIG_HEAP2_BASE.
- stm32_boot.c: map the runtime image area for user space (read, write,
  execute) explicitly.  The heap's MPU region covered it only because MPU
  regions round to 4 MB subregions.
- user-space.ld: place the .NET runtime archives in .mono (text),
  .mono_data and .mono_bss (NOLOAD) after .mono_signature, and discard
  their unwind tables (R_ARM_PREL31 cannot reach SDRAM from uflash).  The
  sections come first in SECTIONS, so first-match placement replaces the
  EXCLUDE_FILE lists; the commented-out Mono 6.9 sections are removed.
- build.sh: check_mono_sdram_layout cross-checks the image address and
  size, the memory.ld sdram region, the heap configuration and SDRAM_SIZE,
  and stops the build if they disagree.  Meadow.OS.Runtime.bin is cut from
  nuttx_user.bin at offsets derived from them (unchanged: 3014400 KB in,
  3072 KB long).
- hcom: log the user heap size once at startup.
- Documentation/Memory.md: describe the SDRAM layout and the raw runtime
  partition; correct the uflash/kflash table.

Hardware-tested on F7CoreComputeV2 (CCM): clean boot, user heap
30,507,004 bytes (3 MB less than before), CLI device info, a 200 KB file
write/read/delete round trip, no faults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 2: reserve the start of SDRAM for the Mono runtime image
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The configuration manager treated a runtime version as packed when any of
bits 8-23 of the build field were set, so a build number above 255 (for
example 1.99.0.19735) was misdecoded, the board reported "Mono version:
(null)" and hcom disabled Mono.  Test only the top byte instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Configuration-dependent test symbols (hello_world_results,
meadow_kt_dispatcher, tensorflow_tests_hello_world) broke the user-space
link when the tests were not built.  Make each entry .weak, and move the
table from .data to .rodata, which saves 62 KB of user SRAM; the loader only
searches it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Mono TLS shim runs in user space and needs
meadow_client_cert_retrieve_credentials and
meadow_client_cert_release_credentials, which are implemented in the kernel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 3 of the managed runtime plan.  apps/system/mono holds the glue ported
from Legacy meadow-3.0: the monovm host (mono_main.c), the P/Invoke tables,
the mbedTLS 3.6.2 TLS shim and root CA table, and support functions the
runtime needs in a protected NuttX 12 build (I-cache maintenance through
up_coherent_dcache, an atomic 64-bit compare-and-swap, readlink, mprotect).

The runtime archives, libSystem.Native.a and the TLS shim are placed in the
SDRAM runtime image; mbedTLS and SQLite stay in user flash.  hcom starts
meadow_mono_main again.  build.sh runs build-mono.sh when CONFIG_SYSTEM_MONO
is set.  The defconfig enables SYSTEM_MONO, LIBC_LOCALE, FS_RAMMAP (file
mmap for assemblies) and mbedTLS threading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fetch the runtime commit recorded in MONO_RUNTIME_COMMIT into runtime-src/,
pass it to the build container as MEADOW_RUNTIME_DIR, and install CMake and
Ninja in the build image for the runtime build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WildernessLabs/runtime#3 adds the NuttX 12 fixes the phase 3 glue needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 3: Mono runtime glue, Kconfig and SDRAM placement
meadow_mono_main ignored the options hcom builds from meadow.config.yaml
(MonoControl: Options), so --interp, --debug and the others had no effect.
monovm_execute_assembly puts the entry assembly first on the mono_main()
command line, where runtime options are no longer read, so join the options
into MONO_ENV_OPTIONS, which mono_main() reads first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Turn on CONFIG_HCOM_MONO_STDERR_STDOUT so the Mono app's stdout and stderr
reach the host (meadow listen), and fix two faults in
hcom_stdxxx_redirect.c that had not been exercised under NuttX 12:

- The readers opened the FIFOs O_RDONLY.  NuttX reports POLLHUP on every
  poll of an empty FIFO with no writer, so while Mono was not running the
  read loop spun and starved hcom (the CLI could not connect).  Open them
  O_RDWR so the reader also counts as a writer.

- hcom_mono_open_mono_fifo() ignored its fd argument and dup2'd both FIFOs
  onto STDOUT_FILENO, so stdout reached the host labelled stderr and
  stderr was lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WildernessLabs/runtime#4: the NuttX 12 stack bounds and the Thumb-2 code
buffer alignment.  To be moved to the MergeManagedCode merge commit once
that pull request is merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WildernessLabs/runtime#4 is merged; the tree is the same as the Phase4
commit pinned before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WildernessLabs/runtime#5: the 32-bit Thumb-2 LDR/STR encoding for negative
offsets, which fixes the interpreter (--interp) crash.  To be moved to the
MergeManagedCode merge commit once that pull request is merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WildernessLabs/runtime#5 is merged; the tree is the same as the Phase4b
commit pinned before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 4: first Mono boot - Hello World on CCM (JIT and interpreter)
The 3.0 Meadow CLI uses its 3.x deploy path (trimmed publish against the
meadow_assemblies package) only when the device reports an OS version of
2.999 or later (MeadowVersion.V3Preview).  Mark this build as a 3.0
preview so the CLI's app deploy works against it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build-managed-assemblies.sh needs the same runtime location and
MONO_RUNTIME_COMMIT check as build-mono.sh.  Move it into a shared
locate_runtime_checkout function; build-mono.sh behaves as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builds the meadow_assemblies package that Meadow apps are trimmed against
and deployed with, ported from Legacy meadow-3.0:

- System.Private.CoreLib from the pinned runtime (mono.corelib subset,
  linux-arm, FeaturePerfTracing=false);
- the framework assemblies of the runtime's .NET SDK;
- System.Security.Cryptography and six System.Net assemblies rebuilt from
  the runtime sources for Unix/Linux, so they use the P/Invokes Meadow.OS
  implements rather than the host OS's;
- the CoreLib ILLink descriptor, which lets the Meadow CLI trim CoreLib;
- without the assemblies that are never used on Meadow.

Unlike the Legacy script it uses the MONO_RUNTIME_COMMIT pin check instead
of cloning and re-branching the runtime, fails if any library build fails,
and records the runtime commit and framework in build-info.txt.  The output
matches the 3.0.0 package's file list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the disabled bcl job, which ran a Legacy Mono BCL script that is
not in this repository, with an assemblies job.  It fetches the pinned
runtime as the OS build does, runs build-managed-assemblies.sh and
uploads meadow_assemblies (the DLLs, the CoreLib ILLink descriptor and
build-info.txt) as an artifact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 5: managed assemblies and the 3.0 CLI deploy flow
2.999.0 is used by the Legacy 3.0 work.  Use 2.999.50 for this (Rebase)
OS so the two can be told apart, while staying at the 2.999 Meadow CLI
3.x threshold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NevynUK and others added 18 commits October 15, 2026 11:26
On NuttX 12, fcntl(F_SETFL) passes O_NONBLOCK to the socket interface as
FIONBIO, and netdev_ioctl() stops at any error other than -ENOTTY.  espcp
forwarded FIONBIO to the ESP32, which rejected it with EINVAL, so
fcntl(O_NONBLOCK) on every WiFi socket failed.

Map FIONBIO to SIOCSESPNONBLOCK, which carries the ESP socket and the
requested state ([command][ESP socket][nonblock]) so the ESP32 can set
O_NONBLOCK on the lwip socket (Meadow-ESP32 d1890583).  If the ESP32
rejects it (older firmware), return -ENOTTY so that the local flag is still
set and fcntl() succeeds, with the ESP32 socket left blocking.  Adapted
from Legacy f79817ac67e, which hooked net_vfcntl() on NuttX 7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
espcp refused SO_ERROR locally with -EPFNOSUPPORT, which is not
-ENOPROTOOPT, so NuttX did not fall back to its own socket-level answer
and .NET reported "Unknown socket error" when it completed a
non-blocking connect.  Send it to the ESP32, which returns and clears the
pending socket error (Meadow-ESP32 b3102532), and decode the reply like
SO_RCVBUF.  From Legacy f35f586ce71.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The poll interrupt handler only set revents on the caller's pollfd.  In
NuttX 12 a driver reports events with poll_notify(), which sets revents and
calls the pollfd's callback to wake the waiting poll(); without it the
waiter slept until its timeout (the NuttX 7 code posted fds->sem).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runtime's NuttX socket engine (WaitForSocketEventsInner in
pal_networking.c) takes events only from a 0-timeout poll of every socket;
its blocking poll only waits for a change and then samples again.  A poll
sent to the ESP32 cannot be answered inside a 0-timeout poll, so non-blocking
connects never completed and async I/O depended on timing.

Keep a readiness cache in espcp_usrsock_conn_s (ESPCP_SF_*) and answer
polls from it in poll_setup without a round trip to the ESP32:
- POLLOUT is answered locally unless writability is unknown (connect in
  progress, or the last send returned EAGAIN); while it is unknown, a
  POLLOUT reported by the ESP32 is cached (WR_READY).
- POLLIN, POLLHUP and POLLERR come from the ESP32's poll interrupts; the
  readable bit is cleared when a receive returns EAGAIN.
- connect, sendmsg, recvmsg and getsockopt(SO_ERROR) keep the write-side
  bits up to date.
- A late interrupt only updates the cache if the connection still has the
  ESP socket the poll was set up on.

Poll teardown now removes its request before checking whether the ESP32 is
responding, so the request can no longer outlive the caller's pollfd.

Ported from Legacy 65e257319d5, with WR_READY from f80001fc99c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since NuttX 12.1 (abba05a) getifaddrs() and the other users of
NET_SOCK_TYPE open a SOCK_CTRL socket just to issue interface ioctls.
espcp passed the type to the ESP32 unchanged, and lwIP has no
SOCK_CTRL, so socket() failed with EINVAL and getifaddrs() (and with
it .NET's NetworkInterface.GetAllNetworkInterfaces()) failed.

Ask the ESP32 for SOCK_DGRAM, which is what NET_SOCK_TYPE was before
12.1; the local socket keeps the SOCK_CTRL type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
espcp marks its interface with the Meadow specific IFF_WIFI flag so
that the interface can be identified as WiFi.  It used (1 << 6),
which NuttX 12 defines as IFF_POINTOPOINT.  Use (1 << 14), which
<net/if.h> leaves free.  The runtime reads the flag as
MEADOW_IFF_WIFI (src/mono/cmake/nuttx-compat.h) and the two values
must match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ESP32 answers SIOCGIFFLAGS with IFF_UP when the station has an IP
address and has no separate carrier state, so the interface never
showed IFF_RUNNING.  POSIX callers, .NET's NetworkInterface among
them, need IFF_UP and IFF_RUNNING to report the interface as up, so
set IFF_RUNNING together with IFF_UP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Runtime #6 (merged as 300006899fb) fixes the Thumb-2 JIT encoding of
ASR and ROR by register and makes NetworkInterface work on Meadow.OS:
name servers from /meadow0/dns.conf, the interface type and MAC
address from espcp's flags and SIOCGIFHWADDR, and the gateway from
meadow_os_get_gateway_address().  The merge commit has the same tree
as the runtime that was tested on the board.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
poll_setup() takes a reference on each file with file_get() and keeps it
while poll() waits.  poll_teardown() looked the descriptor up again by
number, tore the poll down on whatever file it found and released two
references from it.

If another thread closes the descriptor while poll() waits, the number
is free and is often reused straight away.  Teardown then acted on the
new file (dropping a reference it did not own) and never released the
reference on the original file, so that file was never closed.  If the
number was not reused, file_get() failed and the reference was again
never released.

The .NET runtime's socket engine keeps every socket in a poll() on its
own thread while other threads close them.  Each HTTPS connection then
left its socket open: sock_file_close() never ran, the usrsock close
never reached the ESP32, and after about 14 connections socket()
failed with ENFILE from the ESP32's lwIP socket table.

poll() now keeps the struct file pointer that setup obtained for each
entry (on the stack for up to 16 descriptors, on the heap above that)
and teardown, including the cancellation cleanup, tears down and
releases exactly that file.  A descriptor closed during the poll is
closed when the poll releases the last reference.  Upstream NuttX has
the same poll_teardown().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With mbedTLS 3.6.2 the TLS 1.3 client handshake fails with
MBEDTLS_ERR_SSL_INTERNAL_ERROR while computing the Finished verify data
(after CertificateVerify has passed), and the failure moves when
diagnostics are added, so it is a race or a stack problem that is not
yet found (issue #704).  Legacy Meadow's mbedTLS 3.2.1 had TLS 1.3
compiled out, so TLS 1.2 is what the shim was validated with.

Cap the shim's configuration at TLS 1.2.  Over TLS 1.2 HTTPS to
www.google.co.uk passes: 50/50 keep-alive requests, 100/100 full
handshakes (about 780 ms each) and 20/20 full pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CryptoNative_SslDoHandshake() kept the mbedTLS error in last_error but
reported only SSL_ERROR_SSL, so a failed handshake showed nothing more
than SslException in managed code.  Print the error code and text, as
CryptoNative_SslRead() already does, and for a certificate verify
failure also the verify flags and their description.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
espcp_usrsock_shutdown() logged "not implemented" and returned
EOPNOTSUPP, and .NET calls shutdown() (or disconnects) before close()
on every socket to wake operations blocked on it, so each HTTPS close
logged an error and nothing ended a poll waiting on the socket.

Send the request to the ESP32 (WiFiFunction Shutdown, socket handle and
how in the NuttX numbering, which the ESP32 translates to lwIP's) and
return its result.  An ESP32 without the handler answers with an error
status, which is reported as EOPNOTSUPP as before.  Needs the matching
Meadow-ESP32 Shutdown handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The .NET runtime's NuttX socket engine leaves every registered socket in
its blocking poll(), and a socket whose read and write readiness have
both been reported is polled with no events at all.  espcp sent each of
these to the ESP32, where it held a poll worker and a wake socket (an
lwIP socket) and cost a setup and a teardown message on every pass of
the engine (at least every 100 ms), yet it could only end on a hang-up
or an error.

Answer a poll that asks for neither POLLIN nor POLLOUT locally: a
hang-up or error already in the readiness cache is still reported, and
one that arrives later is picked up by the next poll that asks for
POLLIN or POLLOUT, which the engine makes as soon as a read or write on
the socket would block.  Teardown already treats a socket with no
request as success.

Tested on the board: the network tests pass (10/10), 15 concurrent
sockets open (the ESP32's lwIP limit; 13-16 before), the burst test
shows no leaked ESP32 sockets, and the TLS soak passes (50/50
keep-alive, 100/100 full handshakes, 20/20 full pages) slightly faster
than the same build without this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p option

Memory.md said DTCM was used for kernel data and stacks; nothing is placed
there and CONFIG_STM32F7_DTCMEXCLUDE keeps it out of every heap.  Record that,
and the option (from Legacy f0b7762b4f7) of adding it to the kernel heap if
espcp runs out of kernel heap, with what to check first: DMA into kmm_malloc()
buffers, adding it to the kernel heap from board code, and keeping the last
256 bytes for the fault black box (#709).  Not done now: no kernel-heap ENOMEM
has been seen on Rebase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 6: networking and TLS for the .NET runtime
…mbers

CLAUDE.md said the ARM toolchain is installed under toolchain/macos/ and
added to PATH by build.sh; the directory is not in the repository, so the
compiler (Arm GNU Toolchain 15.3.Rel1) has to be on PATH, as it is from
Homebrew locally and in the CI image.

Rebasing.md said the notes assume a rebase from 12.10 to 12.11; the tree is
now 12.12 (f962d87), so say what they were written for.  Also replace
the non-ASCII box-drawing characters in its directory diagram with ASCII.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Documentation: toolchain note and Rebasing release numbers
@NevynUK

NevynUK commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Reference: the plan for this work, ManagedPlan.md (revision 3.11, 2026-10-16). It is kept outside the repository; local paths have been removed from this copy. Posted in two parts because of the comment size limit.

ManagedPlan.md, part 1 of 2 (sections 1-3, phases 0-2)

ManagedPlan.md - Adding Mono and the managed libraries to Rebase

Status: phases 0-3 merged into MergeManagedCode (phase 3: #696, 8ebcc0b). Next: phase 4. Updated: 2026-10-14.

Revision history:

  • Rev 1: plan from three read-only research passes (Legacy docs and commits, Legacy NuttX-side diff,
    Rebase tree).
  • Rev 2: reviewed against the cloned runtime repository.
  • Rev 3: every claim re-checked against source, the generated nuttx/.config, the built ELF
    images, and the managed repositories (Meadow.Core, Meadow.CLI). Statements from rev 1/2 that were
    wrong are corrected in place; section 9 lists each check with its evidence.
  • Rev 3.1: toolchain decided by the user: GCC 15 for the runtime too. Added the consequences
    (phase 0, phase 1, R27) and corrected one rev 3 error (section 8).
  • Rev 3.2: the user decided to keep the runtime's -Wno-error=incompatible-pointer-types flag
    (option (b) in phase 1).
  • Rev 3.3 (2026-10-14): working environment set up: a worktree, the integration
    branch and the per-phase branch model (section 4.0).
  • Rev 3.4 (2026-10-14): runtime location via MEADOW_RUNTIME_DIR, default ../runtime.
  • Rev 3.11 (this revision, 2026-10-16): phases 4-6 complete and merged (phase 6: Meadow.OS Phase 6: networking and TLS for the .NET runtime #710,
    Meadow-ESP32 Remove dead stm32_buttons.c (GPIO_BTN_USER undefined, no button hardware) #609); Legacy review (2.3a); housekeeping of stale open items; doc fixes (DocFixes).
  • Rev 3.10 (2026-10-14): phase 3 implemented and boot-tested on CCM; runtime#3
    merged, Meadow.OS PR Phase 3: Mono runtime glue, Kconfig and SDRAM placement #696 open at the time (merged since as 8ebcc0b). Version-decode bug fixed; OS version 1.99.1.
  • Rev 3.9 (2026-10-14): phase 2 implemented, hardware-tested on CCM, merged (Phase 2: reserve the start of SDRAM for the Mono runtime image #695, 4625c4f) (phase 2
    status). Includes the build fix for the destructive user-space.ld checkout.
  • Rev 3.8 (2026-10-14): phase 1 carried out and merged (runtime#2 c668e4a3aea, Meadow.OS Phase 1: build the Mono runtime against NuttX 12 with build-mono.sh #694 50fa130); results,
    warning review, corrections and follow-ups under the phase 1 status.
  • Rev 3.7 (2026-10-14): phase 1 decisions recorded (runtime branch, runtime PRs,
    standalone script, no hardware test, signals, Release default).
  • Rev 3.6 (2026-10-14): phase 0 done; PR Phase 0: pin the Mono runtime and build MergeManagedCode in CI #693 merged into MergeManagedCode (77d4051) (section 4, phase 0 status).
  • Rev 3.5 (2026-10-14): phase 0 questions answered (section 6). MergeManagedCode
    was fast-forwarded to main 94de840 (PR Synchronising build with Legacy #692) and pushed. Added CI and artifact notes and
    the policy of no merges to main (section 4.0).

Sources used (local refs, not re-fetched on 2026-10-09):

Repo Ref Commit
Legacy (../Legacy) origin/meadow-3.0 34125f9cbf1 (2026-09-09)
Runtime (../runtime) origin/meadow-3.0 f980b4c169b (2026-08-23), tree a108891e0a87
Rebase (Rebase/) branch Fix/Issue261; .config and ELFs from the 2026-10-09 08:49 build -
Meadow.Core (WildernessLabs/Meadow.Core) origin/meadow-3.0 f6fc9d1e (2026-08-09)
Meadow.CLI (WildernessLabs/Meadow.CLI) origin/meadow-3.0 64c06e21 (2026-05-27)
Meadow-ESP32 (../Meadow-ESP32) HEAD (Fix/Issue294) and origin/meadow/espcp-* a78f95dd

Path convention: this file lives in the OS/ directory, beside the repositories (moved there from
the Rebase root on 2026-10-10). Unless a path names its repository, paths such as apps/...,
nuttx/..., F7MeadowOS/..., build.sh and DockerFiles/... are relative to the Rebase
repository (OS/Rebase/). A ../<name> path means a sibling of the Rebase repository, which is
the same as OS/<name> next to this file; for example ../runtime is OS/runtime.

1. Summary

Legacy meadow-3.0 replaced the Mono 6.9 fork with the .NET 11-preview Mono runtime from the
WildernessLabs/runtime fork of dotnet/runtime (branch meadow-3.0, built as the sibling repo
../runtime). It is hosted through monovm_initialize / monovm_execute_assembly, runs on the
Thumb2 JIT (the interpreter is only a fallback), uses System.Private.CoreLib plus the .NET BCL,
and reaches the hardware through P/Invoke into NuttX (/dev/upd, ioctl, libc, Meadow syscalls).
Legacy runs on NuttX 7.29 (7.30 in development); Rebase is on NuttX 12.12. Legacy docs report
it validated on F7CoreComputeV2 (Preview 2 shipped as 2.999.4.0; 3.0.0 is a version bump of
2.999.5). AOT is unfinished and is not in 3.0.

Rebase today has the hooks but no runtime:

  • hcom's Mono control code is compiled in, but the start call is #if(0)
    (apps/system/hcom/hcom_startup_manager.c:547-549). With CONFIG_HCOM_MONO_REMOTE_DEBUGGING=y
    (the current .config), Mono would start through mono_main_proxy(), whose call into Mono is
    commented out (//FIXME, hcom_mono_control.c:946). The required-files check still lists the
    Mono 6.9 assemblies (mscorlib.dll, System.Core.dll, System.dll; hcom_mono_control.c:561-564).
  • Kconfig: HCOM_MONO_STDERR_STDOUT and HCOM_MONO_REMOTE_DEBUGGING exist (board Kconfig:16-22);
    CONFIG_HCOM_MONO_STDXXX is referenced but not defined; there is no Mono app symbol.
  • The linker script stamps a .mono_signature at 0xC0000000; the .mono* code/data sections are
    commented out and still name the Mono 6.9 archives.
  • The runtime image path already works end to end, just with an empty image: packaging, the CLI
    write, flashing to QSPI and the copy-to-SDRAM ioctl.
  • SDRAM conflict: the user heap covers all 32 MB from 0xC0000000, so copying the runtime there
    would overwrite the live heap. Legacy starts the heap 3 MB higher; Rebase lost that offset.

Where the work is (re-weighted in rev 3):

  1. Runtime build against NuttX 12 headers. The fork carries compensations for NuttX 7.29
    headers. Some are now redundant and some are wrong: the sys/random.h override will not
    compile, and there are also the errno override, O_CLOEXEC=0, hard-coded signal numbers, the
    compat header and the hand-written pal_config.h. The toolchain is GCC 15 (decided). The
    resulting int32_t = long pointer mismatches stay downgraded to warnings by the runtime's
    existing flag (decided), so they need review rather than fixing (phase 1).
  2. Glue (user space, under apps/). Port mono_main.c and the P/Invoke tables, minus the
    7.x-only parts: the global __errno, 30 errno --wrap wrappers, the boardctl symtab and the libc
    gap stubs. Port the TLS shim onto Rebase's existing mbedTLS 3.6.2.
  3. Memory and limits. Apply the heap offset. Make room in user flash (791 of 1024 KiB already
    used). Raise the WiFi socket cap (6 usrsock connections today).
  4. A few NuttX/board changes: dlopen(NULL) (better fixed in the runtime), the QSPI bounded
    wait, and optionally the DTCM kernel heap and the IWDG/liveness backstop. Several Legacy kernel
    patches are no longer needed on 12.12 (section 2.3).
  5. Build orchestration and pinning: runtime commit and SDK; the toolchain is fixed at GCC 15.3,
    the same compiler as the rest of Rebase.

Recommended approach: a staged port. Boot first with as few changes as possible, then add Legacy
fixes only when a test shows they are needed. Correction from rev 1: the Legacy runtime
binaries cannot be reused. The runtime is compiled against NuttX headers, so it must be rebuilt
from the meadow-3.0 source against Rebase's NuttX 12 headers.

2. What Legacy meadow-3.0 contains

2.1 Runtime (WildernessLabs/runtime, outside the Rebase repo)

  • Branches. origin/main is identical to upstream dotnet/runtime (f1949423fa1, 2026-03-20; 0
    commits ahead). All Meadow work is on origin/meadow-3.0: 42 commits, 102 files, +7797/-320
    over upstream. origin/backup/meadow-3.0-pre-cleanup has the identical tree (a108891e0a87)
    under different commit hashes. Runtime hashes quoted in Legacy commit messages (for example
    8c38ac0442d) exist only on the backup branch.
  • SDK pin: 11.0.100-preview.3.26161.119 (global.json).
  • Local state: the clone is checked out on main and has not been bootstrapped (no .dotnet/,
    no artifacts/).
  • Legacy build-mono.sh branch logic. It clones ../runtime if absent and syncs it to the
    branch with the same name as the Meadow branch, falling back to main. For any Rebase branch name
    that silently builds an unpatched runtime (R19).
  • In-tree mono/. The 540 MB mono/ tree inside Legacy is the old 6.9 source. build-mono.sh
    does not build it; do not import it.
  • Build entry point. runtime/src/mono/build-nuttx.sh <Debug|Release>, with NUTTX_INCLUDE_DIR
    (the default is ../Meadow/nuttx/include, so always pass it). Output goes to
    runtime/src/mono/build-nuttx-<cfg>/. Libraries linked into the user image:
    • libmonosgen-2.0.a
    • four component stubs: marshal-ilgen, hot_reload, debugger, diagnostics_tracing
    • libminipal.a, libaotminipal.a, libsgen_objects.a
    • ../system_native/libSystem.Native.a
  • Cross target. cmake/toolchains/arm-none-eabi-nuttx.cmake sets CMAKE_SYSTEM_NAME Generic
    with variant nuttx, which selects HOST_NUTTX. It also uses a force-included
    cmake/nuttx-compat.h and cmake/nuttx-include-overrides/{errno.h,sys/random.h}.
  • Build flags (build-nuttx.sh):
    • DISABLE_JIT=0 and DISABLE_AOT=1.
    • Disabled: debugger agent, EventPipe, embedded PDB, SIMD, SSA and LLDB; SGen split nursery, debug
      helpers, binary protocol, toggleref and bridge; and concurrent mark-sweep.
    • ENABLE_SMALL_CONFIG, STATIC_COMPONENTS.
    • -mthumb -mcpu=cortex-m7 -mfloat-abi=hard -mfpu=fpv5-d16 -O1.
  • GC suspend is forced cooperative (CMakeLists.txt near line 791), because NuttX 7's armv7-m
    up_schedule_sigaction could not deliver a signal while another handler was active.
  • SGen.
    • CARD_TABLE_BITS 26 under HOST_NUTTX (sgen-cardtable.h:45): a 128 KB card table instead of
      8 MB.
    • A WASM-style configuration (sgen-conf.h).
    • The memalign constraint (sgen-marksweep.c).
    • A nursery-based minimum for the maximum heap (sgen-memory-governor.c).
  • Thumb2 JIT.
    • Code generation: thumb-codegen.h (+4462 lines), mini-arm.c (+383), tramp-arm.c,
      tramp-arm-gsharedvt.c, exceptions-arm.c, cpu-arm.mdesc.
    • I-cache: mono_arch_flush_icache calls up_invalidate_icache(start, end) (mini-arm.c:1084-1086).
      That function is supplied by the firmware glue.
  • Signals. Fixed numbers are used: 20 (suspend), 21 (restart) and 22 (abort)
    (mono-threads-posix-signals.c). The comment says NuttX 7 only supported signals 0-31.
  • dlopen(NULL). mono_dl_open_self() calls dlopen(NULL, RTLD_NOW) (mono-dl.c:86).
  • Stacks. MONO_DEFAULT_STACKSIZE is 262144 on NuttX; mono-internal threads are capped at 64 KB.
  • System.Native PAL. A hand-written pal_config.h (225 lines, no configure step), plus
    pal_io_nuttx.h and edits to pal_io, pal_networking (+501), pal_process, pal_time and others.
  • Managed library edits:
    • System.Console (ConsolePal.Unix).
    • System.Net.Sockets (SocketPal.Unix: re-arm the poll edge on EAGAIN, tolerate EBADF).
    • System.Net.Security (skip managed certificate validation when native TLS has already completed).
    • NetworkInformation and GlobalizationMode.
    • CoreLib (Type.cs; FeaturePerfTracing off in the csproj).
  • Not in the runtime repo: the mbedTLS CryptoNative_* shim is Meadow glue (section 2.2).

2.2 NuttX-side glue (Legacy apps/examples/mono/ and friends)

Item Verified content
mono_main.c (1853 lines, 177 non-ASCII bytes) meadow_mono_main: symtab_initialize(), then hcom_via_nx_copy_mono_runtime_to_ram(), zero _s_mono_bss.._e_mono_bss, setenv (below), TPA built from /meadow0/*.dll, dlopen of *.dll.so AOT images, monovm_initialize with TPA, APP_PATHS and PINVOKE_OVERRIDE, mono_set_optimizations(... GSHAREDVT), hcom_mono_ctrl_mono_appears_to_be_running() (clears the lockup bit, redirects stdio, sets the LED), mbedTLS warm-up thread (priority 50, 16 KB), monovm_execute_assembly of /meadow0/Meadow.dll. Also the meadow_pinvoke_override() resolver (lines 1084-1145) and a 247-entry System.Native table
mono_nuttx_stubs.c (906 lines) __errno() returning one global int (lines 383-387); --wrap errno-sync wrappers for 30 calls (socket connect bind listen accept getsockopt setsockopt getsockname getpeername send sendto recv recvfrom close read write open fcntl ioctl lseek fstat stat ftruncate poll select pipe2 sem_wait sem_trywait sem_timedwait sem_post); a full replacement for mmap/munmap (SDRAM copy); libc gap stubs; up_invalidate_icache() implemented with cacheflush() (lines 731-734)
mono_mbedtls.c (482) + pal_ssl_mbedtls.c (984) + root-ca-der.h An OpenSSL-shaped CryptoNative_* shim (84 entries) over mbedTLS. It uses only the public API (45 mbedtls_* functions, no MBEDTLS_PRIVATE) and never calls psa_crypto_init(). Root CAs: 147 arrays holding 153,298 DER bytes
mappings-meadow.h DllImport("nuttx") table with 70 live entries. Lines 117-1085 are a commented-out table of about 970 entries (kernel internals). The file starts with a UTF-8 BOM
mappings-sqlite.h 133 live entries, names stored without the sqlite3_ prefix (the stated reason: "uflash is within bytes of full")
mappings-mbedtls.h 7 entries, but unreachable: the resolver has no "mbedtls" branch (see 3.2)
symtab.c, symtab.inc (502 lines), generate-symtab.sh Registers the table with boardctl(BOARDIOC_OS_SYMTAB) (CONFIG_BOARDCTL_OS_SYMTAB), which serves the kernel-side loader of NuttX 7
Make.defs Links the runtime archives from $(TOPDIR)/../../runtime/src/mono/build-nuttx-$(MONO_CONFIG)/mono and a separate mbedTLS from $(TOPDIR)/../mbedtls (the wildernesslabs fork; the docs say 3.2.1). Adds the 32 --wrap flags to the whole user-image link
user-space.ld .mono (SDRAM) holds the runtime archives plus the SQLite amalgamation object. libSystem.Native.a, mbedTLS and the glue stay in uflash. A global /DISCARD/ drops .ARM.exidx (R_ARM_PREL31 range workaround)
stm32_allocateheap.c heap2_base = CONFIG_HEAP2_BASE + HCOM_NX_FS_MONO_RAW_PARTITION_SIZE (lines 417-418); the MPU still maps the full 32 MB
hcom_mono_control.c delta 10 insertions, 11 deletions: rename to meadow_mono_main, bypass the lockup bit, required file System.Private.CoreLib.dll
meadow_liveness.c, SysTick IWDG kick, meadow_blackbox.h, fault hooks MEADOW_IWDG_BACKSTOP is defined in board.h; /dev/liveness (a write of 'D' disarms it)

Settings made in mono_main.c:

  • MONO_GC_PARAMS=max-heap-size=8m,nursery-size=512k,soft-heap-limit=4m,major=marksweep. The same
    string is also passed directly to mono_gc_params_set(), because the code comment says getenv()
    may not see setenv() in protected mode.
  • DOTNET_ThreadPool_ForceMinWorkerThreads=4, ForceMax...=16, DOTNET_TieredCompilation=0.
  • DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1, DOTNET_SYSTEM_RESOURCES_USESYSTEMRESOURCEKEYS=true,
    DOTNET_SYSTEM_CONSOLE_SKIP_TERMINAL_INIT=1, TMPDIR=/meadow0/Temp.
  • MONO_LOG_LEVEL=info, MONO_LOG_MASK=aot (diagnostic; revisit for production).

2.3a Legacy review after Phase 6 (2026-10-16)

Legacy meadow-3.0 and main commits compared with Rebase after Phase 6. Not in Rebase, filed
as "Investigation Needed":

2.3b Remaining plan items filed as issues (2026-10-16)

Everything left open in this plan outside phase 7 is now an issue (all assigned to NevynUK):
#724 Q8 remote debugging option (question); #725 MONO_LOG_LEVEL/MONO_LOG_MASK for production
(question); #726 Mono mini test suite; #720 atan(+-inf) workaround check; #727 GC under blocking
and network load, and commit the phase 6 test apps (R8, R18, R22); #728 P/Invoke startup self-test
(R26); #721 ConvertFileStatus zeros (runtime); #722 configure.cmake NuttX 12 features (runtime);
#723 stale __wrap_close comments (runtime); #729 offer the fs_poll fix upstream. Doc fixes
merged as #719 (fe3f929).

2.3 Legacy kernel/board patches: status against Rebase NuttX 12.12

# Legacy patch Rebase 12.12 status (verified unless noted) Action
1 tcp_connect.c/tcp.h: connect cancel, -EINPROGRESS Affects only NuttX's own TCP stack (Ethernet; PPP is not enabled in Rebase). WiFi sockets go through espcp usrsock. 12.12 code not compared Only if Ethernet socket tests fail
2 net_close.c/inet_close.c: always release the slot The 12.12 socket layer is rewritten (sockets are files). Not compared On demand
3 SIOCSESPNONBLOCK (non-blocking bridge) Absent from Rebase espcp, from Rebase esp32/, and from Meadow-ESP32 HEAD. It exists only on the unmerged branches meadow/espcp-nonblock-ioctl (798 commits off main) and meadow/espcp-worker-hardening (532) Decide (Q4). Without it the ESP side stays blocking, which was Legacy's fallback
4 ESP readiness cache in net.h Absent Goes with #3
5 Fake SO_LINGER, SO_RCVBUF/SO_SNDBUF NET_SOLINGER not set, NET_RECV_BUFSIZE=0; espcp option forwarding not checked Test with MQTTnet
6 FD_CLOEXEC kept in f_oflags Native in 12.12 (F_GETFD/F_SETFD/F_DUPFD_CLOEXEC, O_CLOEXEC) Drop
7 dlopen(NULL) sentinel Still needed. dlopen() -> dlinsert() -> strdup(NULL) (lib_dlopen.c:88; the DEBUGASSERT is compiled out by CONFIG_NDEBUG=y), and Mono calls dlopen(NULL) Fix in the runtime (HOST_NUTTX in mono_dl_open_self) or the glue, not in NuttX libc
8 ramlog overwrites the oldest data Native in 12.12 (ramlog_copybuf writes modulo the buffer size) Drop
9 cdcacm initial SerialState (macOS open) 12.12 sends DSR/DCD SerialState from its SET_CONTROL_LINE_STATE path when input flow control is off. Not Mono-specific Drop unless macOS open problems reappear
10 DTCM added to the kernel heap (128 -> 256 KB) Not present (CONFIG_STM32F7_DTCMEXCLUDE=y; kernel heap 128 KB) Port if espcp ENOMEM appears under TLS/MQTT churn
11 QSPI bounded BUSY waits, ABORT recovery, retries Ported to main by PR #702 (with the AN4838 strongly ordered MPU change from Legacy 21c33c52169) Done
12-13 IWDG kick in SysTick, liveness gate, DTCM black box Not present (CONFIG_STM32F7_IWDG not set) Optional, phase 6. Meadow.Core pets /dev/liveness only if the device exists
14 NET_NACTIVESOCKETS=32 The symbol does not exist in 12.12. The equivalent caps are NET_USRSOCK_PREALLOC_CONNS=6 with NET_USRSOCK_ALLOC_CONNS=0 (6 WiFi sockets in total) and NET_TCP_PREALLOC_CONNS/ALLOC_CONNS=8/8 (Ethernet) Raise the usrsock limit (R24)
15 espcp fixes (UAF, leaks, bounded waits, poll) Rebase espcp was reworked separately (for example the file_mq_* conversion 0728a9b and the review fixes). No fix-by-fix comparison done Compare before the networking phase

3. Rebase current state and gaps (verified in rev 3)

Area Rebase today Gap
HCOM Mono control apps/system/hcom/mono/* is built (Makefile:66-68). The start call is #if(0) (hcom_startup_manager.c:547-549). task_create at hcom_mono_control.c:454-461 uses mono_main_proxy when CONFIG_HCOM_MONO_REMOTE_DEBUGGING=y (the current .config), and that proxy's Mono call is //FIXME (line 946); (main_t)NULL is the other branch. A monitor thread waitpid()s and resets the board when Mono exits (lines 364-390) Provide meadow_mono_main; re-enable; update the required-files list to System.Private.CoreLib.dll
Kconfig Board Kconfig:16-22 defines HCOM_MONO_STDERR_STDOUT (default y; .config not set) and HCOM_MONO_REMOTE_DEBUGGING (default y; .config y). CONFIG_HCOM_MONO_STDXXX is referenced but undefined (RebaseNotes item 7). CONFIG_EXAMPLES_MONO survives only as a comment in the defconfig Define the Mono app symbol; settle the STDXXX rename
Linker .mono_signature sits at 0xC0000000 (user-space.ld:121-142) and is stamped with version, build date, hash and MEADOW_GIT_REF (32 bytes) by scripts/version_methods.sh:97-109. The .mono* sections (lines 144-219) are commented out and list Mono 6.9 archive names Rewrite with the .NET archive names
SDRAM stm32_allocateheap.c registers all 32 MB from 0xC0000000 as user heap. The MPU region (stm32_mpu_uheap, stm32_mpuinit.c:103-119) is `TEX_SO C
Image packaging build.sh:528-530 uses dd ... bs=1024 skip=3014400 count=3072. That is exact: (0xC0000000 - 0x08040000) / 1024 = 3014400. The current Meadow.OS.Runtime.bin holds a valid signature (aabbccdd, version, "Fix-Issue261"). Side effect: nuttx_user.bin is 3,086,745,643 bytes Works. Optionally switch to objcopy -j to avoid the 2.9 GB file; add a size check
Runtime write and copy The CLI writes /meadow0/Meadow.OS.Runtime.bin, which must be exactly 3 MB (hcom_nx_fs_flash.c:681). hcom flashes it to the raw QSPI partition at offset 0 and deletes the file (:1111-1122). HCOM_NX_UPD_COPY_RUNTIME_TO_RAM copies 3 MB to CONFIG_HEAP2_BASE (meadow-upd.c:370-377). The config manager reads the signature from block 0 (hcom_nx_config_manager.c:3228-3249) Call it from meadow_mono_main (as Legacy does)
Partitions 3 MB raw runtime partition at QSPI offset 0 (hcom_shared_common.h:90) Keep the image under 3 MB (runtime archives plus anything moved there, see 3.1)
dlopen LIBC_DLFCN, ELF, LIBC_ELF on. Symbols resolve through dlsymtab() with an explicit export table (apps/system/hcom/tests/sotest_tests.c:110); CONFIG_LIBC_ELF_HAVE_SYMTAB is not needed. nuttx/libs/libc/elf/elf_globals.S (4316 lines, committed; generated by nuttx/tools/build-globals.sh). ET_DYN relocation fixes are merged (PR #232) Needed for AOT only; use dlsymtab(), not boardctl
FDs and IPC EVENT_FD, TIMER_FD, SIGNAL_FD, FS_NOTIFY off; epoll with 8 descriptors; FIFO and pipe 1024 bytes; FS_ANONMAP=y, FS_RAMMAP off; PSEUDOFS_SOFTLINKS off (no readlink()) The PAL uses poll and pipe (as in Legacy); decide mmap and readlink (phase 3)
Sockets espcp usrsock sockif with poll and SOCKCAP_NONBLOCKING; NET_USRSOCK_NPOLLWAITERS=1; 6 usrsock connections maximum; DNS through the NuttX client (/meadow0/dns.conf); the sockif getaddrinfo path is not wired; SO_* forwarding not checked Raise the cap; test SocketAsyncEngine
Scheduler and threads PRIORITY_INHERITANCE, CANCELLATION_POINTS, SCHED_HAVE_PARENT off; SCHED_WAITPID=y; PTHREAD_MUTEX_TYPES and ROBUST on; TLS_NELEM=32; TLS_NCLEANUP=8; RR_INTERVAL=200; 1 ms tick; PTHREAD_STACK_DEFAULT=65536; FDCLONE_STDIO=y (the Mono task inherits only stdio) Section 4, phase 4
Signals MAX_SIGNO=63, SIGRTMIN=32; SIG_DEFAULT off; SIG_PREALLOC_ACTIONS=4 Mono's 20/21/22 are SIGTSTP/SIGTTIN/SIGTTOU here (phase 1)
libm CONFIG_LIBM_TOOLCHAIN=y: newlib's libm is whole-archived into the user link; there is no nuttx/include/math.h The runtime's NuttX-libm atan workaround may be moot
TLS mbedTLS 3.6.2 in user space (about 1050 mbedtls_* symbols in nuttx_user.elf, none in the kernel). MEADOW_CLOUD=y selects CRYPTO_MBEDTLS. TLS 1.2 and 1.3, PSA crypto, ALPN and session tickets on; MBEDTLS_THREADING_C off Port the shim to this copy; no second mbedTLS
SQLite CONFIG_LIB_SQLITE=y (3.45.1) in user space. All 133 live Legacy SQLite P/Invoke entries resolve None for the bindings
Watchdog IWDG and WWDG off; software meadow_watchdog slots only; Mono liveness is waitpid plus the BBR lockup bit (BK31R bits 0x800, 0x1000, 0x2000; hcom_bbreg_defn.h:106-110) Optional IWDG and liveness (phase 6)
/dev/upd Has open, close, read and ioctl (meadow-upd.c:159-165), so an O_RDONLY open works (Legacy's EACCES note does not apply). The ioctl numbers match Meadow.Core (3.2) Keep the numbers; ConsolidateUpdDrivers.md must preserve them
GPIO from user mode mpu_user_peripheral(STM32_GPIOA_BASE, ...) (stm32_boot.c:257); stm32_configgpio/gpiowrite/unconfiggpio are syscalls None
Filesystem LittleFS v2.5.1 (Legacy: v1.7, disk format 1.1) Expected re-deploy of all assemblies when moving a device from Legacy to Rebase

Note: CodeReviewTracking.md lists items "Blocked (runtime)" (#325, #326, #327, #393; B43-B45,
C123). They become testable once Mono runs. B44 (#326, remote VS debug) stays untestable, because
the runtime is built without the debugger agent (R29).

3.1 Flash and RAM budget (new in rev 3)

Measured from the ELFs of the 2026-10-09 build:

Region Used Size Free
uflash (user code, 0x08040000) 791 KiB (.text 809,448 bytes) 1024 KiB about 233 KiB
kflash (kernel code, 0x08140000) 320 KiB 768 KiB about 448 KiB
SDRAM image region (0xC0000000) 43 bytes (signature only) 3072 KiB -
  • Symbol sizes in the user image: SQLite about 181 KiB, mbedTLS about 182 KiB.
  • With the Legacy layout, uflash would also receive libSystem.Native.a (size unknown until
    built), the glue (mono_main.c with its tables, the stubs and the TLS shim) and the 153 KB root-CA
    table. Legacy, which had already moved SQLite into SDRAM, reported uflash "within bytes of full".
  • Options, which can be combined:
    • (a) Move SQLite into the SDRAM image, as Legacy did.
    • (b) Move libSystem.Native.a and the root-CA table into the SDRAM image.
    • (c) Load the root CAs from a file in /meadow0. This also lets the CAs be updated without a
      reflash.
    • (d) Move the uflash/kflash boundary inside the fixed 1792 KiB OS slot, since kflash has about
      448 KiB spare. This means changing memory.ld and checking build.sh packing and the
      bootloader before relying on it.
  • SDRAM heap after the offset: about 29 MB. Legacy found that a bigger GC heap hurts, because SGen
    and mbedTLS share it, and that about 6 MB per TLS connection is native memory outside SGen.
  • The kernel heap is 128 KB (ksram) and the espcp bridge allocates from it (Legacy patch Added boardctl.h to Rebase, it may not be required #10).

3.2 Managed ABI: Meadow.Core meadow-3.0 against Rebase (new in rev 3)

  • Target framework: all 16 csproj files target net10.0; they run on the .NET 11-preview Mono
    runtime, which was the Legacy combination.
  • "nuttx" imports: Meadow.Core declares 50 entry points (LibraryImport(LIBRARY_NAME),
    Interop.cs:7). 46 resolve in Rebase's user libraries. The 4 missing are
    meadow_cell_change_state, meadow_cell_is_connected, meadow_get_cell_at_cmds_output and
    meadow_get_cell_error; cellular support is disabled in Rebase. Provide "not supported" stubs or
    port the cell work.
  • "mbedtls" import: mono_mbedtls_set_server_cert_authmode (Interop.mbedtls.cs:9), called
    from the public API in F7PlatformOS.cs:179. Legacy's resolver has no "mbedtls" branch, so this
    appears to fail on Legacy 3.0 as well. Add the branch; the 7-entry mappings-mbedtls.h table
    already exists.
  • Legacy table entries absent in Rebase: besides the 4 cell functions,
    meadow_rotary_encoder_read_count. Rebase replaced it with the struct-based
    meadow_rotary_encoder_read_data() and added a trailing delayMs field to the config struct.
    Meadow.Core meadow-3.0 does not call the rotary encoder, so drop or update the table entry.
  • /dev/upd: Meadow.Core UpdIoctlFn (Interop.upd.cs:142-171) matches Rebase MUPD_* exactly
    for codes 1-72, 81 and 82. Codes 74 and 75 (PowerWDSet/PowerWDPet) exist only in Meadow.Core
    and are not implemented in Legacy or Rebase. Code 73 (PWR_SLEEP2) exists only in Rebase.
  • Liveness: /dev/liveness is optional on the managed side; it is only written when
    File.Exists("/dev/liveness") (MeadowCloudConnectionService.cs:941-944).
  • Meadow.CLI meadow-3.0: writes Meadow.OS.Runtime.bin through HCOM and trims apps with
    ILLink.Descriptors.xml from MeadowAssembliesPath (BuildManager.cs:418-421). The local
    checkouts of Meadow.Core and Meadow.CLI are on develop, so use their meadow-3.0 branches.
  • Coordinated meadow-3.0 branches exist in Meadow.Core, Meadow.CLI, Meadow.Contracts,
    Meadow.Foundation, Meadow.Logging, Meadow.Units and Meadow.Modbus. MQTTnet, Meadow.Samples and
    Meadow.TestSuite have none.

4. Plan

Principle: every phase ends with something observable on hardware. Per project rules, do not commit
until the user has tested on hardware ("a clean build is not enough"), and fix -Werror root
causes rather than suppressing warnings.

4.0 Working environment and branch model (set up 2026-10-14)

  • Worktree: OS/Rebase.worktrees/MergeManagedCode, a git worktree of the Rebase repository
    (OS/Rebase). All work on this plan happens there; the main checkout (OS/Rebase) stays free
    for other work. git worktree list from either checkout shows both.

  • Integration branch: MergeManagedCode, created from main and fast-forwarded on
    2026-10-14 to 94de840 ("Merge pull request Synchronising build with Legacy #692 from WildernessLabs/UpdateBuildToMatchLegacy").
    Pushed to origin and tracking origin/MergeManagedCode.

  • Merge policy (user, 2026-10-14): Claude may push branches and create PRs, but must not merge
    anything into main. Merging MergeManagedCode into main is the user's decision.

  • Phase branches: one per phase, named Phase0, Phase1, ... Phase7. Each is created from
    MergeManagedCode and merged back into MergeManagedCode, never into main.
    MergeManagedCode goes to main once the work is complete.

    main --- MergeManagedCode ---+-------------+---------- ... --> main
                                  \           /  \          /
                                   Phase0 ---+    Phase1 --+
    
    • Start a phase: git switch MergeManagedCode && git switch -c PhaseN in the worktree, after
      merging the previous phase.
    • Open each phase as a GitHub PR from PhaseN with base MergeManagedCode.
    • Keep MergeManagedCode current with main by merging main into it (it is shared and pushed,
      so do not rebase it).
    • Per project rules, commit only after the user confirms a hardware test, and confirm before
      pushing or opening a PR.
  • Build state: ./build.sh --configure succeeds in the worktree (2026-10-14) and leaves the tree
    clean: build.sh restores the files it stamps (user-space.ld, hcom_nuttx_shared.h,
    build_info.template). The worktree has its own nuttx/.config and build outputs, separate from
    OS/Rebase. For a first full build use ./build.sh --clean --force.

  • Sibling-path caution: from the worktree, ../runtime resolves to
    OS/Rebase.worktrees/runtime, which does not exist. The runtime clone is at OS/runtime (still
    on main; phase 0 switches it to meadow-3.0).

  • Runtime location: MEADOW_RUNTIME_DIR (decided 2026-10-14). The Mono build scripts written
    in phase 1 read the runtime location from the environment variable MEADOW_RUNTIME_DIR.

    • If it is not set, the default is ../runtime, resolved against the Rebase checkout's root (the
      directory holding build.sh), not against the current directory. In OS/Rebase this gives
      OS/runtime, so the main checkout needs no setup.
    • In the worktree the default resolves to OS/Rebase.worktrees/runtime, which does not exist,
      so set the variable there: export MEADOW_RUNTIME_DIR=<OS>/runtime (the runtime checkout beside the Rebase checkout).
    • The scripts fail with a clear message if the directory is missing, rather than cloning a copy
      as Legacy's build-mono.sh does. They also fail if its HEAD is not the pinned commit (phase 0).
    • The scripts print the resolved path and commit at the start of every build.
    • The runtime's own build-nuttx.sh defaults NUTTX_INCLUDE_DIR to ../Meadow/nuttx/include;
      always pass that explicitly as well.
    • ../Legacy (reference only) and ../Meadow-ESP32 have the same worktree caveat.
  • CI and artifacts: .github/workflows/build-system.yml ("Build OS and RT") runs
    ./build.sh --force --clean in the DockerFiles/Build image. Since PR Synchronising build with Legacy #692 it uploads the
    artifacts Meadow.OS (Meadow.OS.bin, Meadow.OS.Update.bin, Meadow.OS.Runtime.bin,
    build-info.json) and Meadow.OS Debug Symbols (both ELFs and maps); run ENTER_DFU_MODE request writes a DFU magic value that nothing reads, so the board only restarts #818 kept them for 90
    days. A bcl job exists but is disabled (if: false) and still calls Legacy's
    build-mono-bcl.sh and the Legacy mono/external submodules. The submodule step is disabled too.

    • The workflow only triggers on pushes to main and PRs targeting main. PRs into
      MergeManagedCode get no build or artifacts until MergeManagedCode is added to both branch
      lists. This is a phase 0 change.
    • Prebuilt runtime archives and the BCL should come from this CI as artifacts (user, 2026-10-14);
      replace the bcl job in phase 5.
  • This document lives in OS/, outside both checkouts, and is not under git.

Phase 0 - Decisions, inputs and repository plumbing

Branch Phase0, merged into MergeManagedCode without a hardware test, since it changes nothing on
the board (user, 2026-10-14). Contents:

  • The pinned-commit file MONO_RUNTIME_COMMIT at the repo root (commit f980b4c169b and tree
    a108891e0a87).
  • Add MergeManagedCode to the workflow's push and pull_request branch lists, so phase PRs
    build and upload artifacts.
  • Outside git: switch OS/runtime to meadow-3.0 and check that the runtime's SDK bootstrap works
    on this Mac.

Status (2026-10-14): done and merged. PR #693 (Phase0 -> MergeManagedCode) passed CI run #819 and was merged as 77d4051.

Decisions and inputs:

  • Runtime: check out origin/meadow-3.0 (f980b4c169b, tree a108891e0a87) in ../runtime.
    main is plain upstream. Record the pin in Rebase (for example a MONO_RUNTIME_COMMIT file) and
    make the build fail on a mismatch instead of falling back to main. Do not use Legacy's
    branch-name matching.
  • SDK: the runtime's own bootstrap installs 11.0.100-preview.3.26161.119 into
    runtime/.dotnet. The machine has SDKs 8.0.419 and 10.0.201, which are not used for this. Legacy
    found that installing the SDK by hand left targeting packs missing.
  • Toolchain (decided 2026-10-09): GCC 15. Build the runtime with the same compiler as the rest
    of Rebase:
    • Locally: Homebrew arm-none-eabi-gcc 15.3.1.
    • CI: Arm GNU Toolchain 15.3.rel1 in the build image (DockerFiles/Build/Dockerfile:66).
    • Why this is the only consistent choice: Rebase cannot move to an older compiler anyway. The
      committed dlopen export table nuttx/libs/libc/elf/elf_globals.S is generated by
      nuttx/tools/build-globals.sh against GCC 15.3's newlib, and an older toolchain fails the
      user-space link (Dockerfile:52-65). One compiler for NuttX, newlib, libgcc and the runtime also
      avoids mixing support libraries.
    • Legacy CI used GCC 10.3-2021.10 and CMake 3.30.5 on Linux x86_64 (setup-ci-host.sh). That
      was a requirement of Legacy's NuttX 7 tree (its CLAUDE.md); no runtime-specific reason is
      recorded, and the runtime fork already contains a GCC 14+ accommodation (phase 1).
    • GCC 15's C23 default does not affect the runtime, which pins C11 and C++17
      (eng/native/configurecompiler.cmake:6-9, src/mono/CMakeLists.txt:10-11).
    • Host tools: CMake 4.4.4 and ninja. The toolchain/ directory that CLAUDE.md describes does not
      exist.
    • Consequences: the int32_t pointer warnings in phase 1 (kept as warnings by decision), and R27.
  • Where the runtime lives: MEADOW_RUNTIME_DIR (default ../runtime, section 4.0) plus the
    pinned-commit file. Prebuilt runtime archives come from CI artifacts (section 4.0).
  • Managed side: use the meadow-3.0 branches listed in 3.2.
  • ESP32 firmware: non-blocking bridge deferred; decided later, before phase 6 (Q4).
  • First-milestone scope (decided): JIT only. No AOT, cloud, MQTT or cell; SQLite optional.
  • Sign-off hardware (decided): CCM only (F7CoreComputeV2). F7FeatherV2 is similar enough that
    it is not part of the test matrix.
  • Renode emulator: nice to have, deferred.

Phase 1 - Build the runtime against Rebase's NuttX 12.12 headers

Decisions (user, 2026-10-14):

  • Runtime changes go on a new branch in WildernessLabs/runtime, MergeManagedCode, created
    from meadow-3.0 (f980b4c169b). meadow-3.0 itself is never changed: Legacy's build-mono.sh
    checks it out by name, so changing it would change what Legacy 3.0 builds.
  • Runtime changes are reviewed as PRs in the runtime repository, from a per-phase branch
    (Phase1, ...) into the runtime's MergeManagedCode, mirroring the Meadow.OS branch model.
    After a runtime PR merges, the Meadow.OS PhaseN PR moves MONO_RUNTIME_COMMIT to the new
    runtime commit and tree. The user's push/PR permission covers the runtime repository too (public,
    admin access); never change its main or meadow-3.0.
  • Standalone script: phase 1 adds build-mono.sh to Meadow.OS, which builds only the runtime
    archives. build.sh and CI call it from phase 3, when the archives are first linked. CI then
    needs a shallow fetch of the pinned runtime commit (the full history is 1.3 GB).
  • No hardware test for phase 1; nothing reaches the board. It merges on a clean build and CI.
  • Signals: move Mono's suspend/restart/abort signals from 20/21/22 to SIGRTMIN-based
    numbers.
  • Build type: Release by default; Debug as an option (--debug, as Legacy).

Status (2026-10-14): done and merged, no hardware test (as agreed). runtime#2 merged as c668e4a3aea (tree unchanged, 5cf48371256). MONO_RUNTIME_COMMIT was moved to it (branch=MergeManagedCode, commit 69100f2; CI run #822 passed). #694 merged as 50fa130.

  • Runtime PR: Phase 1: build the NuttX runtime against NuttX 12 headers with GCC 15 runtime#2, Phase1 -> runtime MergeManagedCode (which was
    created from meadow-3.0 f980b4c169b and pushed). Five commits, ending at a76e66e60c1
    (tree 5cf48371256):

    1. 05460ac745a: definition/declaration mismatches exposed by int32_t = long
      (sgen_client_binary_protocol_sweep_begin/end,
      ves_icall_RuntimeType_GetFunctionPointerTypeModifiers, mono_regression_test_step,
      SystemNative_Select). Also skips the h_errno static asserts on NuttX (NuttX swaps
      NO_DATA/TRY_AGAIN; the values are never passed to managed code) and removes the
      sys/random.h override.
    2. c7dd57f3807: retires the NuttX 7 workarounds: errno override, -DO_CLOEXEC=0, most of
      nuttx-compat.h, pal_config.h flags (14 now enabled), HAVE_MKDTEMP.
    3. 535eae386ce: dlopen(NULL) sentinel in mono-dl-posix.c.
    4. ae2e81b67f0: suspend/restart/abort signals become SIGRTMIN + 1/2/3.
    5. a76e66e60c1: MONO_BUILD_DIR in build-nuttx.sh; nxsock_close_notify prototype.
  • Meadow.OS PR: Phase 1: build the Mono runtime against NuttX 12 with build-mono.sh #694, Phase1 -> MergeManagedCode (84ace53): adds build-mono.sh,
    ignores monoruntime/, and points MONO_RUNTIME_COMMIT at the runtime Phase1 commit.
    Merge order: runtime#2 first; then move MONO_RUNTIME_COMMIT (commit, tree,
    branch=MergeManagedCode) to the runtime merge commit in Phase 1: build the Mono runtime against NuttX 12 with build-mono.sh #694; then merge Phase 1: build the Mono runtime against NuttX 12 with build-mono.sh #694.

  • Results:

    • The baseline (unmodified runtime against NuttX 12 with GCC 15) failed with 24 errors from one
      mismatch.
    • After the fixes, clean Release and Debug builds pass, 358/358 targets, in about 15 s each on
      this Mac. All nine archives the OS link needs are produced (libmonosgen-2.0.a is 23 MB
      Release, 24.6 MB Debug; libSystem.Native.a 309 KB).
    • The Meadow.OS worktree build (./build.sh --clean --force) still passes.
    • The build-mono.sh guards were exercised: no runtime at the default path, an unpinned
      commit and a dirty checkout each fail with a message, and the stamp forces a rebuild after
      a source change.
  • Warning review (decision follow-up): all 86 -Wincompatible-pointer-types warnings are
    32-bit int/long aliasing; none is a real pointer mismatch (Debug reports one site twice).
    The other warnings remain as warnings, not errors:

    • 368 -Wformat, the same int32_t = long effect in format strings.
    • Upstream-code warnings that GCC 15 is stricter about: -Wreturn-local-addr
      (metadata.c:2273), -Warray-bounds (interp/transform.h:435, mini-arm.c:3059),
      -Waddress (driver.c), -Wmaybe-uninitialized (w32handle.c:635,
      pal_networking.c:3957), -Wmisleading-indentation (mini-exceptions.c:2565).
  • Corrections to earlier revisions:

    • The dlopen(NULL) call that matters is mono_dl_open_self() -> mono_dl_open(NULL)
      (mono-dl.c:91) -> mono_dl_open_file() -> dlopen(NULL, flags) (mono-dl-posix.c:99).
      The mono-dl.c:86 call cited before is Android-only.
    • HAVE_DLFCN_H stays 0 in configure.cmake; that only affects eglib's gmodule.
  • Follow-ups (later phases):

    • The NuttX branch of ConvertFileStatus (pal_io.c, about line 220) still hard-codes uid,
      gid and the nanosecond times to 0, although NuttX 12's struct stat now has them. Revisit
      when the file API is tested (phase 5).
    • Still-disabled configure.cmake entries that NuttX 12 now supports: HAVE_GETRLIMIT,
      HAVE_PTHREAD_SETNAME_NP/GETNAME_NP, HAVE_STRERROR_R, HAVE_OPENLOG. They are
      behaviour changes (for example named threads), so they are left for a deliberate change.
    • Test the atan(+-inf) workaround on hardware (phase 4).
  • Port build-mono.sh: locate the runtime through MEADOW_RUNTIME_DIR (default ../runtime
    relative to the Rebase root; section 4.0), drop the clone and branch sync, check the pinned
    commit, and pass NUTTX_INCLUDE_DIR explicitly.

  • Re-evaluate the NuttX 7.29 compensations against Rebase nuttx/include (all checked):

    • nuttx-include-overrides/sys/random.h: delete. It assumes a 2-argument getrandom(); NuttX
      12.12 declares the POSIX ssize_t getrandom(void *, size_t, unsigned int), so the wrapper's
      2-argument call will not compile.
    • nuttx-include-overrides/errno.h: NuttX 12 already defines errno as *__errno(), a
      per-thread lvalue, so the override is redundant. The real hazard is in the glue (phase 3).
    • nuttx-compat.h: UINT32_C/INT32_C, realpath, mkdtemp, readlink (declaration only),
      lstat, posix_memalign and _SC_PAGE_SIZE all exist in NuttX 12, so most of the file can go.
      Re-check malloc_getpagesize and CONFIG_C99_BOOL8.
    • -DO_CLOEXEC=0: remove it. NuttX 12 supports O_CLOEXEC (1<<10).
    • Signals: 20/21/22 collide with SIGTSTP/SIGTTIN/SIGTTOU in NuttX 12. They would still
      work, because SIG_DEFAULT is off, but real-time signals 32-63 are now available. Prefer
      SIGRTMIN-based numbers.
    • Cooperative suspend: keep it. NuttX 12 queues signals while tcb->sigdeliver is set instead
      of refusing them. Whether a restart signal can reach a thread parked in sigsuspend() inside a
      handler is untested.
    • pal_config.h: regenerate it by review against NuttX 12 (struct stat fields, getifaddrs,
      sendmsg/recvmsg, pipe2, epoll, sem_clockwait, pthread_attr_getstack, all of which are
      present in user space now).
    • libm: with LIBM_TOOLCHAIN, math.h comes from newlib. Test whether the atan(+-inf)
      workaround is still needed.
    • int32_t is long on NuttX 12, and GCC 15 makes the mismatch an error.
      • NuttX 12 takes _int32_t from the compiler's __INT32_TYPE__
        (arch/arm/include/types.h:61-70), which is long int for arm-none-eabi GCC 15.3. NuttX 7
        used signed int, and the runtime's toolchain file still assumes that.
      • Mono code that mixes int * with int32_t * or gint32 * therefore hits
        incompatible-pointer-types, which GCC 14 and later treat as an error. The fork currently
        downgrades it with -Wno-error=incompatible-pointer-types under HOST_NUTTX.
      • Options considered: (a) fix the mismatches in the runtime; (b) keep the downgrade; (c) build
        the runtime with int32_t as int.
      • Decision (2026-10-09): (b), keep the flag in the runtime. Scope of the exception:
        • It covers only the existing -Wno-error=incompatible-pointer-types, under HOST_NUTTX,
          in the runtime's src/mono/CMakeLists.txt.
        • The no-suppression rule still applies to everything in Rebase, including the new Mono glue
          under apps/.
        • Any other GCC 14+ error in the runtime is fixed, not suppressed.
      • Follow-up: the flag also hides genuine pointer-type bugs, such as mismatched struct pointers.
        Once the first runtime build is clean, review its incompatible-pointer-types warnings and
        confirm they are all 32-bit int/long aliasing, which is harmless on ARM. Keep that list
        in the build notes so new entries stand out.
    • Other GCC 14+ hard errors: implicit function declarations and int-conversion. Any NuttX 12
      header gap left after removing the compat header shows up as an error, not a warning, which
      helps the pal_config.h review.
    • dlopen(NULL): return a sentinel under HOST_NUTTX in mono_dl_open_self(), instead of
      patching NuttX libc.
  • Deliverable: libmonosgen-2.0.a and the companion archives built for Rebase. No linking yet.

Phase 2 - Memory map, linker and image packaging

Status (2026-10-14): done and merged. PR #695 (Phase2 -> MergeManagedCode) passed CI run #824 and was merged as 4625c4f.
Commits: 78ddd75 (stamped copy of user-space.ld) and 453e60f (SDRAM layout). The
hardware test on CCM passed (below).

  • Heap, by configuration, not by patching NuttX:

    • The defconfig sets CONFIG_HEAP2_BASE=0xc0300000 and CONFIG_HEAP2_SIZE=30408704
      (29 MB, to the end of SDRAM).
    • stm32_allocateheap.c (upstream NuttX) is unchanged. Legacy patched it instead.
  • Runtime copy destination: the copy ioctl in meadow-upd.c now copies to the new
    HCOM_NX_MONO_RUNTIME_SDRAM_ADDR (0xC0000000, defined in hcom_shared_common.h) instead of
    CONFIG_HEAP2_BASE.

  • MPU: stm32_boot.c maps the 3 MB runtime area explicitly, with the same attributes as the
    user heap. The heap's own region already covers it through 4 MB subregion rounding, but only by
    accident of the sizes.

  • user-space.ld: the .mono* output sections come first in SECTIONS:

    • /DISCARD/ of the runtime archives' unwind tables.
    • .mono_signature.
    • .mono (text), .mono_data (rodata and data) and .mono_bss (NOLOAD).

    All use the nine .NET archive names except libSystem.Native.a, which stays in uflash as in
    Legacy; its placement is decided in phase 3. First-match placement removes Legacy's
    EXCLUDE_FILE lists, and the old Mono 6.9 comment block is gone.

  • build.sh: check_mono_sdram_layout cross-checks the four descriptions of the layout
    (header address and size, memory.ld sdram, the heap config, SDRAM_SIZE) and stops the
    build on any mismatch; tested with two deliberate mismatches. The Meadow.OS.Runtime.bin
    dd now uses offsets derived from memory.ld (still 3014400 KB in, 3072 KB long).

  • Build fix found on the way: generate_build_info used to git checkout HEAD
    user-space.ld and stamp the version into it in place. That silently discarded uncommitted
    linker-script edits; it happened during this phase, and Legacy had the same trap. The version is
    now stamped into an untracked copy, user-space.stamped.ld, which the user link uses
    (kernel/Makefile). restore_versioned_files no longer touches user-space.ld.

  • Heap report: hcom logs User heap: <total> bytes in total, <free> bytes free (LOG_NOTICE)
    once at startup, to verify this phase and to track memory use later (R7).

  • Docs: Documentation/Memory.md SDRAM section rewritten, the QSPI raw runtime partition
    added, and the uflash/kflash table corrected.

  • Hardware test (2026-10-14, run by Claude on the attached CCM, F7CoreComputeV2):

    • Flash: flash.sh over the ST-Link. The board was in DFU mode, and SWD flashing works
      regardless. verify_image matched all 2,097,152 bytes, including the CRC at 0x081FFFFC.
    • Boot: clean, three times. Logs: p2boot.stm.log / p2boot.esp.log, captured with
      meadow_capture.py.
      • User heap: 30507004 bytes in total, 30434992 bytes free: the 29 MB SDRAM region
        (30,408,704) plus about 98 KB of internal SRAM, 3,145,728 bytes less than the old layout.
      • No fault, assert or stack dump.
    • Third boot explained: the runtime already in QSPI (Mono version: 2.5.7.4, so the
      signature read works) does not match OS 1.99.0.19732. hcom disabled Mono and reset
      (Mono:Enabled -> Mono:Disabled), which is the intended safety behaviour (user, 2026-10-14).
      There were no further resets for the rest of the capture.
    • CLI: meadow device info reports OS 1.99.0.19732 and the coprocessor version, so the
      ESP32 link works.
    • Files: a 200 KB random file written, read back (SHA-1 identical) and deleted.
    • Not tested: WiFi (no network configured on this board), and writing this build's
      Meadow.OS.Runtime.bin, which would replace the runtime in QSPI.
  • Original test list for the user (CCM): flash nuttx/Meadow.OS.bin from the worktree; check
    that it boots and that the log shows the user heap at about 3 MB less than before (the SDRAM
    part is now 30,408,704 bytes); check normal operation (CLI, file write, WiFi). Optionally write
    this build's Meadow.OS.Runtime.bin and check that the runtime version is reported, which
    exercises the partition write and the signature read.

  • Heap: in stm32_allocateheap.c, add the heap2 region from
    CONFIG_HEAP2_BASE + HCOM_NX_FS_MONO_RAW_PARTITION_SIZE, as Legacy does. Keep the MPU mapping of
    the full 32 MB (the attributes already match Legacy). Add a boot-time assert that the heap starts
    at or above _e_mono_bss.

  • Linker: rewrite the .mono, .mono_data, .mono_bss and .mono_comment sections, the
    EXCLUDE_FILE lists and the PHDRS for the .NET archive names. Decide the flash placement from 3.1
    at the same time.

  • Packaging: keep the dd extraction, which is correct. Add a check that the SDRAM sections fit
    in 3 MB. Optionally use objcopy -j to avoid the 2.9 GB intermediate file.

  • Signature: already stamped by scripts/version_methods.sh; no work. (Rev 1 said this was
    missing; it is not.)

  • Deliverable: the link succeeds and Meadow.OS.Runtime.bin holds the runtime. The OS and runtime
    images must come from the same build (R3).

🤖 Generated with Claude Code

@NevynUK

NevynUK commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author
ManagedPlan.md, part 2 of 2 (phases 3-7, risks, questions, notes, change and verification logs)

Phase 3 - Glue code and Kconfig

  • Location: user space, under apps/ (for example next to hcom in apps/system/hcom/mono/, or a
    new apps/system/mono/). Correction: rev 1/2 said F7MeadowOS/, but that tree is compiled
    into the kernel through the board Make.defs. Follow the CLAUDE.md rules for Meadow code: no
    NuttX style, no checkpatch, ASCII only. The Legacy files contain non-ASCII text (mono_main.c has
    177 bytes; mappings-meadow.h and mappings-crypto-native.h have a BOM).
  • Do not port:
    • The global __errno() (mono_nuttx_stubs.c:383-387). In NuttX 12 libc owns a per-thread
      __errno(); the Legacy version would either clash at link time or silently give every
      user-space thread, hcom included, one shared errno.
    • The 30 errno --wrap wrappers. NuttX 12 libc sets errno in user space after each call, and the
      wraps would apply to the whole user image.
    • The stubs for getrlimit, setrlimit, gettid, posix_memalign, lrand48, realpath,
      mkdtemp and lstat, which all exist in Rebase's user libraries now.
    • The boardctl symtab. Use dlsymtab() when AOT arrives.
  • Still needed:
    • up_invalidate_icache(start, end) implemented as up_coherent_dcache(start, end - start).
      That user-callable syscall cleans the D-cache range and invalidates the whole I-cache
      (arch/arm/src/armv7-m/arm_cache.c, syscall.csv:255), so no new syscall is needed.
    • A readlink() stub, or turn on PSEUDOFS_SOFTLINKS.
    • mmap: either keep Legacy's SDRAM-copy replacement, or rely on NuttX 12 mmap with
      FS_ANONMAP and enable FS_RAMMAP for file mappings on LittleFS. Measure the RAM cost either way.
    • Check whether Legacy's __wrap_close socket-close notify is needed with Rebase espcp. It pairs
      with runtime commit a615ddb7c74 (EBADF storm). (Answered: the runtime's SystemNative_Close()
      calls nxsock_close_notify() itself, a phase 3 runtime change; no firmware wrapper needed.)
  • TLS:
    • Link the shim against Rebase's mbedTLS 3.6.2 (apps/crypto/mbedtls); do not add a second copy.
    • Call psa_crypto_init() before the first handshake, since TLS 1.3 and PSA are enabled.
    • Either enable CONFIG_MBEDTLS_THREADING_C (pthread) or serialize access. The shim shares one
      CTR-DRBG across threads, and Legacy's fork had threading on.
    • Check that Meadow Cloud's use of the same library still works.
  • P/Invoke tables:
    • Add the "mbedtls" library branch.
    • Add "not supported" stubs for the 4 cell functions.
    • Drop the stale meadow_rotary_encoder_read_count entry.
    • Keep mappings-sqlite.h; it resolves fully.
  • Kconfig and build:
    • Define the Mono app symbol and HCOM_MONO_STDXXX (RebaseNotes item 7).
    • Register the new files in Make.defs (project rule).
    • Link the runtime archives into the user image only.
  • hcom:
    • Re-enable the start call.
    • Point mono_main_proxy() and the non-debug branch at meadow_mono_main.
    • Update the required-files list.
    • Decide whether to keep CONFIG_HCOM_MONO_REMOTE_DEBUGGING=y. Its --debugger-agent arguments
      are not used by the monovm hosting path, and the runtime is built without the agent (R29).
  • Sockets: raise NET_USRSOCK_PREALLOC_CONNS and/or NET_USRSOCK_ALLOC_CONNS (R24).
  • Deliverable: the image links with the whole runtime and fits in flash.

Phase 3 status (2026-10-14): implemented and boot-tested on CCM. Runtime#3 merged (520e78f6c99,
tree 4814fb133ef); Meadow.OS PR #696 (branch Phase3, pin moved to 520e78f6c99 in 5f959ab)
CI green (run 38608329148), merged into MergeManagedCode as 8ebcc0b.

  • Placement (decided): as Legacy main. The runtime archives, libSystem.Native.a, the TLS shim
    and the root CA table (mono_mbedtls.c, pal_ssl_mbedtls.c) are in the SDRAM image; mbedTLS
    and SQLite stay in uflash. meadow_mono_main stays in uflash.
  • Glue: new apps/system/mono/ (Kconfig SYSTEM_MONO, depends on SYSTEM_HCOM,
    BUILD_PROTECTED and CRYPTO_MBEDTLS; Make.defs; Makefile with -Werror).
    • mono_main.c ported; symtab, ota.h, the dead SDRAM cache and the AOT dlopen block dropped;
      sqlite guard is CONFIG_LIB_SQLITE; "mbedtls" P/Invoke branch added.
    • mono_nuttx_support.c: crash/signal stubs, up_invalidate_icache through
      up_coherent_dcache, mprotect/posix_madvise no-ops, readlink returning EINVAL, and a
      strong __sync_val_compare_and_swap_8 using sched_lock(). New finding: NuttX's weak
      version (libs/libc/machine/arch_atomic.c) masks interrupts, which has no effect in
      unprivileged thread mode, so it is not atomic in a protected build.
    • mmap: CONFIG_FS_RAMMAP=y replaces Legacy's --wrap mmap (file read into a heap buffer).
      Not yet exercised; phase 4.
    • TLS shim against mbedTLS 3.6.2: psa_crypto_init() under MBEDTLS_PSA_CRYPTO_C,
      CONFIG_MBEDTLS_THREADING_C=y with pthread. Meadow Cloud still builds.
    • Client certificates: two new syscalls, meadow_client_cert_release_credentials (3) and
      meadow_client_cert_retrieve_credentials (6), under CONFIG_ARCH_BOARD_MEADOW_F7MICRO.
    • P/Invoke tables: 69 live entries in mappings-meadow.h; cell stubs; rotary
      read_count dropped; BOMs and non-ASCII removed.
    • CONFIG_LIBC_LOCALE=y for setlocale().
  • hcom: start call re-enabled under CONFIG_SYSTEM_MONO; proxy and non-debug branch call
    meadow_mono_main; required files are System.Private.CoreLib.dll and Meadow.dll.
  • Build: build.sh runs build-mono.sh after the bootloader when CONFIG_SYSTEM_MONO=y
    (--debug for MONO_BUILD_TYPE=debug, --clean with a clean build). CI fetches the pinned
    runtime commit shallowly into runtime-src/ and passes MEADOW_RUNTIME_DIR; the Docker image
    installs cmake==3.31.6 and ninja==1.11.1.4 with pip. (CI wiring has run since: the build
    and assemblies jobs pass on Phase 6: networking and TLS for the .NET runtime #710.)
  • elf_globals.S: entries are now .weak and the table is in .rodata (62 KB of usram
    saved; the loader only uses bsearch). Test-only symbols (hello_world_results,
    meadow_kt_dispatcher, tensorflow_tests_hello_world) had broken the link. Caveat for phase
    7:
    a libc symbol referenced only by a dlopen'd module is NULL in the table unless something in
    the image links it.
  • Runtime changes (runtime Phase3): mono-filemap.c drops the sdram_cache_track_fd hook;
    SystemNative_Close calls nxsock_close_notify() before close() (replaces the firmware
    __wrap_close); minipal_get_current_thread_id avoids _Thread_local, because
    __aeabi_read_tp is only provided in the kernel.
  • Bug fix, version decode (F7MeadowOS/hcom_nx/hcom_nx_config_manager.c): the packed-version
    test was (build & 0x00ffff00) != 0, which treats any build number above 255 as packed. With OS
    1.99.0.19735 the board reported "Mono version: (null)" and disabled Mono. Now (build & 0xff000000) != 0. Legacy has the same latent bug, hidden by its small build numbers.
  • Version: version.txt bumped to 1.99.1 at the user's request (commit c310c04 on
    Phase3). The build resets version.txt to HEAD, so a version change must be committed.
  • Sizes: SDRAM image 1568 KiB of 3072 (.mono 0x1165d0, .mono_data 0x67a88, .mono_bss
    0xa190); uflash about 835 KiB of 1024; usram .data 0x1d18.
  • Hardware (CCM): OS flashed and verified over SWD; runtime written with meadow firmware write runtime. meadow device info reports OS 1.99.1.0 and Runtime 1.99.1.0. Boot log: "Mono
    version: 1.99.1.0", user heap 30507004 total / 30434480 free, "Mono is disabled" (expected;
    the earlier mismatch set the disable bit, and running Mono is phase 4).
  • Deferred: the usrsock limit (R24) moves to phase 6, before the networking tests.
    CONFIG_HCOM_MONO_STDERR_STDOUT already exists in hcom. Q8 (remote debugging) is still open.

Phase 4 - Config alignment and first boot (JIT, console only)

  • Symbol diff. Diff the Legacy mono/defconfig (297 symbols) against Rebase's .config with an
    offline kconfig expansion (kconfig-conf --olddefconfig, never make savedefconfig).

  • Legacy symbols that no longer exist in 12.12:

    • NET_NACTIVESOCKETS, NSOCKET_DESCRIPTORS, NFILE_DESCRIPTORS: fds are dynamic now.
    • CLOCK_MONOTONIC: always on.
    • NPTHREAD_KEYS: now TLS_NELEM=32.
    • PTHREAD_CLEANUP: now TLS_NCLEANUP=8.
    • TIME_EXTENDED, ARMV7M_LAZYFPU, USERMAIN_STACKSIZE: the last is now INIT_STACKSIZE=65536.
  • Legacy values that differ and may matter:

    Symbol Legacy Rebase
    FS_RAMMAP y n
    LIBC_LOCALE y n
    NET_TCP_WRITE_BUFFERS y n
    NET_USRSOCK_TCP/UDP y n (Rebase uses its own sockif)
    libm NuttX LIBM=y LIBM_TOOLCHAIN
    FDCLONE_DISABLE y n, with FDCLONE_STDIO=y
    LIB_ENVPATH y n (dlopen needs absolute paths)
    PREALLOC_MQ_MSGS 4 8
  • Bring-up order:

    1. meadow_mono_main reaches monovm_initialize.
    2. CoreLib loads.
    3. Hello World in /meadow0 prints via /dev/monostdout.
    4. GC stress.
    5. Blinky through /dev/upd.
  • I-cache: use up_coherent_dcache from day one. Legacy's non-deterministic UNDEFINSTR faults
    came from an I-cache "invalidate" that only did DSB/ISB.

  • Use the embedded-debugger workflow (OpenOCD/GDB) for faults.

  • Deliverable: Hello World and Blinky on hardware.

Phase 4 status (2026-10-14): Hello World and GC stress pass on CCM (JIT and interpreter); merged (Meadow.OS #697 -> 5b650ff; runtime #4 ff60710f833, #5 fafd81e8032). Blinky passes (2026-10-15).
Meadow.OS branch Phase4, runtime branch Phase4. Board 1.99.1.7.

  • Managed set: CoreLib built from the runtime (./build.sh -subset mono.corelib -arch arm -os linux -c Release /p:FeaturePerfTracing=false); 11 framework assemblies from the 3.0.0
    meadow_assemblies package; a net11.0 console Meadow.dll. The committed Legacy
    hello-world/out/Meadow.dll is stale: it only returns 42 and never calls Console.
  • Fix, stack bounds (runtime mono-threads-nuttx.c): since NuttX 10.2.0 (upstream
    2335b69) pthread_get_stackaddr_np returns stack_base_ptr, the lowest address. The port
    still assumed the NuttX 7 initial SP, so mono_thread_info_get_stack_bounds asserted in GC init.
  • Fix, Thumb-2 JIT alignment (runtime thumb-codegen.h, mini-arm.c, tramp-arm.c): code
    buffer words were accessed through 4-byte aligned types although Thumb code is only halfword
    aligned; GCC 15 merged two stores into an STRD, a UsageFault (UNALIGNED) in
    get_throw_trampoline. arminstr_t/armword_t are now aligned(2) and the 36 32-bit
    instruction structs packed, aligned(2); sizes checked unchanged. Legacy's older Ubuntu GCC
    did not merge these stores.
  • Fix, MonoControl options (mono_main.c): the hcom options were ignored (in Legacy too);
    they now go to MONO_ENV_OPTIONS, because monovm_execute_assembly puts the assembly first.
    --interp and --debug tested.
  • Fix, stdout/stderr redirect: CONFIG_HCOM_MONO_STDERR_STDOUT=y. In
    hcom_stdxxx_redirect.c the readers open the FIFOs O_RDWR (NuttX reports POLLHUP on an
    empty FIFO with no writer, so the loop spun and starved hcom), and hcom_mono_open_mono_fifo
    dup2'd both FIFOs onto fd 1 (stdout arrived as stderr, stderr was lost). Legacy has the dup2 bug.
  • Results: mono_exec returned: exit_code=42 (JIT about 5.8 s, interpreter about 3.5 s);
    meadow listen shows stdout> Hello from Meadow! and stderr> Hello from Meadow on stderr.
  • Bench notes: writing a file disables Mono and reboots first (re-enable afterwards); an
    exiting app makes hcom reset the board, so a short app loops. GDB batch scripts must end with
    monitor resume / detach or the target stays halted.
  • GC stress (JIT, --debug, 2026-10-14): passes. A net11.0 test app (scratchpad gcstress/),
    run for 5 passes in 110 s with 408 gen0 and 153 gen1 collections and live memory 0.55-2.9 MB under
    max-heap-size=8m. Each pass covers: nursery churn (300k objects), a 65535-node retained tree
    across forced collections, 64-68 KB large objects, string building, 3 allocating threads with the
    main thread forcing collections (7-10 per pass), finalizers (200 per pass) and weak references.
    Needs System.Threading.Thread.dll in addition to the hello-world set.
    • A single weak reference to a dead object survived once: SGen scans stacks conservatively, so
      this is expected; the test now checks 64 and allows a few.
    • Follow-up (now measured: 128 vs 80; issue Managed threads run at a higher priority than the Mono task and some hcom tasks #698): managed Threads run above the Mono main
      task. NuttX gives a pthread PTHREAD_DEFAULT_PRIORITY (100) unless one is set, and
      MONO_TASK_PRIORITY is 80; until the workers slept, the main thread did not run at all. Check
      the priorities SystemNative_CreateThread uses, against hcom's.
  • GC stress (--interp, 2026-10-14): failed, then fixed, then passes.
    • First run: no test completed. The Mono main thread's SP became 0 and the core locked up in a
      double fault in the IRQ entry path (MSTKERR; arm_hardfault never ran, so the board did not
      reset). A watchpoint near the base of the stack ruled out an overflow, and the exception and
      unwind paths never ran.
    • Cause: the interp-to-native trampoline (used for every P/Invoke and icall from interpreted
      code) stored its CallContext* and target address with ARM_STR_IMM (code, rN, r7, -4/-8).
      arm_str_imm and arm_ldr_imm in thumb-codegen.h chose the 16-bit encoding without testing
      imm >= 0, so -4 became str r1, [r7, #124]. The trampoline then overwrote 120-124 bytes into
      its caller's frame (saved registers and frame pointers), and read uninitialised slots back.
      Seen by dumping the generated trampoline from SDRAM and disassembling it.
    • Fix: imm >= 0 added to both 16-bit tests (runtime#5, merged as fafd81e8032).
    • Result: 5 passes in 229 s, no failures (407 gen0, 143 gen1). The JIT regression run with the
      fix also passes: 30/30 tests in 121 s.
  • Blinky (2026-10-15): passes on a Project Lab v3 (CCM2 = F7CoreComputeV2), JIT --debug.
    • App: MeadowApp : App<F7CoreComputeV2> (library App.dll) cycling the RGB LED on D09/D10/D11
      (PC6 red, PC7 green, PC9 blue; confirmed in the Meadow.Core F7CoreCompute pinout).
    • Meadow stack from meadow-3.0 worktrees (a local meadow-3.0/ directory) (Meadow.Core,
      Meadow.Contracts, Meadow.Logging, Meadow.Units, Meadow.Foundation; siblings because of the
      relative ProjectReferences); net10.0 builds with no warnings using the runtime's .NET 11 SDK.
    • Deploy: the 3.0 CLI's trimmed publish reproduced by hand (its MeadowTrimmingTargets from
      BuildManager.cs, dotnet publish -c Release --self-contained -r linux-arm, with
      MeadowAssembliesPath = the 3.0.0 meadow_assemblies plus our CoreLib and the runtime's
      ILLink.Descriptors.xml): 67 DLLs, 7.6 MB, CoreLib trimmed to 2.6 MB. The untrimmed reference
      closure was 113 files / 61 MB, which does not fit the ~61 MB file system. Stale DLLs deleted
      so the board matches the deployment, as AppManagerV3 does. The installed CLI 2.5.1 has no
      3.x path, and the 3.0 CLI only takes it when the OS reports >= 2.999 (this OS is 1.99.1.x).
      app.build.yaml NoLink lists apply only to the 2.x trimming path; the 3.x targets root all
      non-BCL dependencies.
    • The board's meadow.config.yaml reserved C6;C7 (COM6), so CreateDigitalOutputPort(D09)
      threw PortInUseException: Pin is defined as System Reserved; the reservation is commented
      out on the board (the user keeps local copies).
    • Meadow.Core 3.0 boots cleanly (Initializing OS..., logging, cloud disabled); it logs
      Could not find a part of the path '/etc/resolv.conf' (networking is phase 6).
    • Result: meadow listen shows BLINKY n red/green/blue/off; the user saw the LED cycle; GPIOC
      MODER has PC6/PC7/PC9 as outputs and ODR bits 6/7/9 step R -> G -> B -> off.
  • Phase 4 deliverable met (Hello World + Blinky on hardware). Thread priorities (Mono-created
    threads 128 vs the Mono task 80) recorded as Meadow.OS issue Managed threads run at a higher priority than the Mono task and some hcom tasks #698, assigned to the user and
    closed for this plan unless it causes problems. (/etc/resolv.conf: done in phase 6, the runtime
    reads /meadow0/dns.conf.)

Phase 5 - Managed libraries and deployment

  • Build the assemblies. Port build-managed-assemblies.sh / build-mono-bcl.sh. CoreLib is
    built with runtime/build.sh -subset mono.corelib -arch arm -os linux -c Release /p:FeaturePerfTracing=false. The seven platform-specific libraries are rebuilt in their unix or
    linux flavour, and the blacklist files apply. Publish the output as a pinned CI artifact.
  • Device layout: a flat /meadow0/*.dll with a non-recursive scan. Confirm that the Rebase file
    write path through HCOM handles 40 MB or more of assemblies, on LittleFS v2.5.1.
  • Trimming: ship ILLink.Descriptors.xml for the Meadow.CLI trimmer; untrimmed CoreLib caused
    OOM in Legacy. Use Meadow.CLI meadow-3.0.
  • Tests: run Mono's mini test suite. Legacy reports 735 tests with 732 passing (99.6%) in
    tracks.md; a handoff reports 733 run, 2 skipped and 1 failed. Known exclusion:
    arm64_vtype_stack_args.
  • Deliverable: a Meadow.Core app runs end to end with the real Meadow.CLI deploy flow.

Phase 5 status (2026-10-15): deliverable met on Project Lab v3; merged (#699 -> 6bcad8a).

  • Version (user, 2026-10-15): 2.999.50 for Rebase, to keep it apart from Legacy, which uses 2.999.0
    (PR Set the OS version to 2.999.50 #700). Earlier decision (user): OS version 2.999.0 (the CLI's V3Preview threshold, so its 3.x deploy path
    is used); Mono's mini test suite moved to a later phase; one commit per task.
  • Commits: version 2.999.0; scripts/runtime_methods.sh (shared pin check);
    build-managed-assemblies.sh (171 assemblies, same file list as the 3.0.0 package, plus
    build-info.txt); CI assemblies job replacing the dead bcl job (uploads meadow_assemblies).
  • Real CLI flow: Meadow.CLI meadow-3.0 (3.0.0-beta5) built in
    a local meadow-3.0 Meadow.CLI checkout, run as dotnet .../bin/Release/meadow.dll. A
    local firmware package 2.999.0.3 was added to the CLI store (OS images + our
    meadow_assemblies); the CLI picks the newest package with the same major.minor and a version
    <= the device's. meadow app run Blinky.csproj published, deployed, enabled and ran Blinky.
  • CLI behaviour to know: the 3.x deploy deletes every device file not in the deployment, so apps
    must ship meadow.config.yaml. dns.conf is deleted too, but the OS recreates it on reset
    (user), so it needs no deploying. One app run failed with an empty "Publish failed" message
    and did not reproduce.

Phase 6 - Networking, TLS, liveness (the long tail)

Each item is an independent change with its own hardware test.

Final PRs (2026-10-16): Meadow.OS #731 (MergeManagedCode -> main) and Meadow-ESP32 #610 (MergeManagedCode -> develop), both mergeable and awaiting review. The runtime stays as is, pinned at its latest MergeManagedCode commit 300006899fb.

Phase 6 status: complete (2026-10-16). Merged into MergeManagedCode: Meadow.OS #710 (a9f2faa) and Meadow-ESP32 #609 (d1cebd137b0). Remaining work is in issues: #703, #704, #706, #707 (managed port), #708 and #709 (deferred enhancements), and the ESPCP code-review issues (#478, #481, #482, #497, #501 and others), which stay out of the merge work. Earlier status notes follow.

Phase 6 status (2026-10-15): in progress. Meadow.OS branch Phase6; Meadow-ESP32 branch
MergeManagedCode (new, from develop c0141f40, pushed) and Phase6 (worktree
OS/Meadow-ESP32.worktrees/Phase6). User decisions: WiFi provisioned once from
Meadow Configuration Files/wifi.config.yaml (deleted by the OS on reset, verified); port the ESP
non-blocking bridge in phase 6 (Q4); sockets first; local test server 192.168.1.65 (HTTP on 80);
ESP-IDF 5.5.3; flash the ESP32 over the ESP-Prog. The esp32/ folder in Rebase is old;
Meadow-ESP32 is the ESP source.

  • usrsock cap (R24) does not apply: espcp registers its own g_usrsock_sockif and allocates its
    connections with zalloc; NuttX's usrsock_alloc is not linked, so
    NET_USRSOCK_PREALLOC_CONNS does not limit WiFi sockets. The limit is espcp/ESP32 (lwIP
    CONFIG_LWIP_MAX_SOCKETS=16); the socket test hit ENOBUFS, then ENFILE, at about 10.
  • ESP32 mapping tables were NuttX 7 numbers (verified in NuttX history): errno renumbered in
    NuttX 12.0 (153060d, "Update errno to match asm-generic"), 41 of 86 entries wrong, including
    most network errnos; socket levels changed in NuttX 10.0 (b296adc): SOL_SOCKET 0 -> 1,
    SOL_TCP 3 -> 6; AF_UNSPEC/PF_UNSPEC mapped as 1 but are 0 (older quirk). Measured on the
    board: System.Native received errno 109 (NuttX 7 ENOPROTOOPT, NuttX 12 ETOOMANYREFS), shown as
    "Unknown socket error". Fixed on Meadow-ESP32 Phase6 (uncommitted); with the fix raw TCP and DNS
    pass.
  • Open: SPI handshake deadlock (intermittent). Seen once: the ESP32's SPI task waits in
    spi_slave_get_trans_result, and the STM32's EspcpMainThread waits on espcp_spi_lock for
    "+++SIR". After start-up the ESP32 sends "+++SIR" once per transaction (portMAX_DELAY, no
    resend), so a lost UART line deadlocks both sides. A plain resend is unsafe because each "+++SIR"
    posts the counting semaphore espcp_spi_lock.
  • Non-blocking bridge, ported (2026-10-15); all network tests pass on the board.
    • Engine: the runtime's NuttX socket engine is the custom poll port in pal_networking.c (no
      epoll is linked). It takes events only from a 0-timeout "Phase A" poll; Phase B blocks for
      up to 100 ms and then re-samples. Readiness therefore has to be answerable locally.
    • ESP32 (Meadow-ESP32 Phase6, pushed): SIOCSESPNONBLOCK (d1890583) and
      getsockopt(SO_ERROR) (b3102532), cherry-picked from meadow/espcp-nonblock-ioctl and
      adapted to develop's handlers. The persistent poll monitor (4cf61e9d) was not needed: develop's
      poll rework conflicts with it and its thread-per-poll copes with the tests.
    • STM32 (Rebase espcp, uncommitted):
      • NuttX 12 change: fcntl(F_SETFL) sends FIONBIO to the socket interface first, and
        espcp forwarded it to the ESP32, which rejected it (EINVAL), so fcntl(O_NONBLOCK) failed.
        espcp now maps FIONBIO to SIOCSESPNONBLOCK, returning -ENOTTY if old firmware rejects
        it so the local flag is still set.
      • getsockopt(SO_ERROR) was refused locally (-EPFNOSUPPORT, shown as "Unknown socket
        error"); it is now sent to the ESP32 (Legacy f35f586ce71).
      • Rebase regression: the poll interrupt handler set revents but never woke the waiter;
        it now calls poll_notify() (NuttX 12 API).
      • Readiness cache in espcp_usrsock_conn_s (ESPCP_SF_*; Legacy 65e257319d5 plus
        WR_READY from f80001fc99c), answered locally in poll_setup; poll teardown removes its
        request before the not-responding check.
    • Results: raw TCP, async send/receive, NetworkStream, async connect (0.6-1.1 s), HttpClient
      (first request 31 s including JIT, then 0.9 s), DNS, 14-16 concurrent sockets (then the ESP32
      lwIP limit), 50/50 repeated HTTP in 4.8 s with memory flat (730 -> 745 KB).
  • Seen once: QSPI hang. The Mono main thread spun in qspi_waitstatusflags (from
    qspi_memory_dma): QUADSPI SR 0x2026 (BUSY, FIFO full), DLR 0x3ff, while DMA2 stream 7 was
    disabled with NDTR 0. This is Legacy fix A few more rebase changes #11 (bounded QSPI waits, ABORT recovery). Fixed on
    main by PR Recover from a stuck QSPI BUSY flag #702 (branch Fix/QSPIStuckBusy): the post-DMA TCF/BUSY waits are bounded at 500 ms,
    a timeout ABORTs, stops the DMA and returns -EIO, qspi_memory() retries twice and logs via
    syslog; the 0x90000000 MPU region is now strongly ordered (C cleared; it was Normal WT).
    Board test: boot, /meadow0 mounts, 4.1 MB read back with matching SHA-256, three resets.
  • Not yet ported from f80001fc99c (deferred, enhancement espcp: port the remaining Legacy f80001fc99c hardening (bounded waits, SO_SNDTIMEO, poll timeouts, teardown retries) #708): bounded espcp waits (30 s response timeout), SO_SNDTIMEO
    on new sockets, poll request timeouts and teardown retries.
  • WiFi IP 255.255.255.255 fixed (2026-10-15). Meadow.Core takes IP, mask and gateway from
    NetworkInterface.GetAllNetworkInterfaces() and returns IPAddress.None when no Wireless80211
    interface is found. Four faults, one commit each:
    • runtime 2f38333bddc: the Thumb-2 JIT encoded ASR/ROR by register wrongly (rd == rm swapped
      the operands; otherwise it emitted EOR/ORN). Every JIT int >> variable could be wrong, e.g.
      a /24 mask became 135.135.135.0. Found with a probe app; the interpreter was unaffected.
    • runtime e504b687660: NetworkFiles.EtcResolvConfFile is /meadow0/dns.conf (no /etc; the
      read threw DirectoryNotFoundException in LinuxNetworkInterfaceSystemProperties).
    • Rebase 99f6577: espcp asks the ESP32 for SOCK_DGRAM when NuttX opens SOCK_CTRL (NuttX
      12.1 abba05a changed NET_SOCK_TYPE; lwIP rejected type 6 with EINVAL, so getifaddrs()
      failed).
    • runtime ac986f43b8d / 8d8eb4ee022 with Rebase df848c3 / 1ad8d93: the Legacy
      Nuttx* NetworkInformation logic in the .NET runtime: type from IFF_WIFI (moved to bit 14,
      MEADOW_IFF_WIFI; bit 6 is NuttX 12's IFF_POINTOPOINT), MAC from ifa_data, lo by name,
      IFF_RUNNING with IFF_UP, gateway from meadow_os_get_gateway_address() when there is no
      /proc/net/route.
    • Runtime PR Added alphaTooling to merge-source-rebase-point-2 branch #6 merged as 300006899fb; Rebase pin moved in 23b07d1 (Phase6 builds pinned).
    • Board: wlan0 Wireless80211 Up, MAC, 192.168.1.76/24, DNS from dns.conf; nettest 10/10.
  • Gateway is 0.0.0.0 over WiFi (issue WiFi gateway is never known to the OS: the connected event carries gateway 0 #703, kept for now): the OS only learns the gateway from
    the ESP32 NetworkConnectedEvent, which Meadow-ESP32 raises on association, before DHCP, with
    zeroed IP information; NetworkGotIpEvent carries only DNS. Legacy has the same gap. Proposed:
    a live "get gateway" ioctl answered from esp_netif_get_ip_info().
  • Follow-ups (user, 2026-10-15):
  • TLS soak (2026-10-15/16, https://www.google.co.uk): committed on Rebase Phase6 087fd11 (fs_poll), a67f445 (TLS 1.2 cap), 2379734 (handshake error log), edfa03a (espcp shutdown); Meadow-ESP32 Phase6 0df260c8 (Shutdown handler).
    • TLS 1.3 fails in mbedTLS 3.6.2: mbedtls_ssl_tls13_calculate_verify_data() returns
      INTERNAL_ERROR (-0x6c00) after CertificateVerify passed; adding syslog inside
      ssl_tls13_calc_finished_core() made it pass (race or stack). Legacy's 3.2.1 had TLS 1.3
      compiled out and threading off. Shim capped at TLS 1.2 (mono_mbedtls.c); issue TLS 1.3 client handshake fails with MBEDTLS_ERR_SSL_INTERNAL_ERROR (mbedTLS 3.6.2) #704.
    • pal_ssl_mbedtls.c now logs the handshake error (and certificate verify flags).
    • "File table overflow" = ENFILE from the ESP32 lwIP socket table (16, about 15 usable), not
      NuttX (OPEN_MAX 256). Cause: NuttX poll_teardown() looked the fd up again by number; the
      runtime socket engine polls on its own thread while sockets are closed elsewhere, the fd is
      reused, so the old file was never closed and espcp never sent close. Fixed in
      nuttx/fs/vfs/fs_poll.c (keep the setup struct file *); upstream has the same bug.
    • Result on 2.999.50.13: keepalive 50/50 (52 ms warm), full handshake 100/100 (avg 763 ms),
      full page 33.5 KB 20/20; the first request fails (about 30 s of JIT, server reset; issue First HTTPS request after app start is reset by the server (connection idle after TLS handshake) #706).
    • shutdown() implemented both sides (NuttX how 1/2/3 mapped to lwIP 0/1/2 on the ESP32); returns
      0, a second call gives ENOTCONN. Close timing unchanged in the burst test (about 30 ms after the
      response either way, as the server FIN already wakes the poll). Final soak: 50/50, 100/100,
      20/20, no "not implemented" lines.
    • The engine's Phase B polled idle sockets with events=0; espcp sent each to the ESP32 (poll
      worker + wake socket + 2 SPI messages per pass). espcp now answers them locally (2d299bc):
      concurrent sockets 15 (the lwIP limit; was 13-16), soak passes, slightly faster in an A/B.
    • Open: offer the poll fix upstream.
  • Open: the SPI handshake deadlock is issue ESP32/STM32 SPI handshake deadlocks if a +++SIR signal is lost #701.

Phase 7 - AOT and dlopen (optional, not needed for parity)

Filed as enhancement #730 (2026-10-16); not started.

  • Unresolved Legacy failure. CoreLib AOT took a MemManage fault about 370 ms after start, with
    MMFAR "an unrelocated .text vaddr". That signature matches the ET_DYN sh_addr/st_value bug
    class already fixed in Rebase's libelf (PR Get tensorflow tests working #232). Legacy's NuttX 7 loader lacks those fixes, so
    AOT may behave better on Rebase. This is a hypothesis, not tested.
  • Remaining known instance: one case of that class is still unfixed (up_init_exidx, dormant
    unless LIBC_ELF_EXIDX_SECTNAME is set).
  • Prerequisites:
    • DISABLE_AOT=0, which is not in build-nuttx.sh.
    • The cross-AOT compiler and aot-support/.
    • The dlsymtab() export table, including Mono runtime symbols. Regenerate elf_globals.S with
      nuttx/tools/build-globals.sh once the runtime is linked, and keep it in step with the
      toolchain version (DockerFiles/Build/Dockerfile:64-65).

5. Risks and mitigations

# Risk Likelihood / impact Mitigation
R1 Runtime fork drifts or the wrong branch is used (the work is only on meadow-3.0, 4197 commits behind upstream) Med / Critical Pin f980b4c169b and its tree; build it standalone first; rebasing onto upstream is a separate project
R2 SDRAM collision: the heap covers the runtime image (confirmed) Certain until fixed / Critical Phase 2 heap offset; boot-time assert; zero .mono_bss every boot (Legacy bug)
R3 OS and runtime images from different builds (the signature size shifts SDRAM code) High / High One build produces both; signature version check; CI checks the pairing
R4 JIT I-cache not invalidated -> non-deterministic UNDEFINSTR Low (mechanism exists) / High up_coherent_dcache syscall (verified); test with caches on
R5 NuttX 7.29 assumptions in the glue: global __errno, --wrap set, libc stubs, boardctl symtab High if ported as-is / High Do-not-port list in phase 3. (The rev 1 mqd_t item is resolved: kernel code uses file_mq_* since 0728a9b, 2026-08-08)
R6 Legacy kernel patches do not map onto 12.12 Med / Med Status table 2.3; add patches only with a failing test; keep NuttX libc untouched (Legacy saw layout-sensitive failures from 72 extra bytes in libm)
R7 Memory: 32 MB SDRAM shared by SGen, mbedTLS and CoreLib; kernel heap 128 KB High / High Legacy GC parameters; trimmed CoreLib; DTCM kernel heap on demand; track free heap per milestone
R8 Threading: no priority inheritance or cancellation points; ThreadPool deadlock (max 4 deadlocked MQTT); sched_lock deadlock with concurrent GC Med / High Concurrent mark-sweep off; ThreadPool 4/16; evaluate priority inheritance; GC plus network stress
R9 /dev/upd ABI changes during ConsolidateUpdDrivers Med / High Numbers verified against Meadow.Core; freeze them; regression test with a real app
R10 espcp semantics vs SocketAsyncEngine (single poll waiter, wake pipe, SO_*, getaddrinfo unwired) High / Med Early socket tests; port Legacy fixes only as needed
R11 QSPI stuck-BUSY hang (Legacy captured a 14 h freeze) while assemblies stream from flash Med / High Bounded-wait patch and strongly ordered MPU region ported (PR #702)
R12 TLS: unexplained intermittent reboot; memory BIO truncation; misleading DllNotFound for unmapped P/Invokes Med / Med Keep the Legacy fixes; soak; log unmapped P/Invokes loudly
R13 Silent managed wedge (timers die while the OS stays healthy) Med / Med Liveness plus IWDG backstop (phase 6)
R14 Build complexity: SDK, CMake, cross toolchain, sibling repo; stale runtime archives in CI High / Med One-command flow; pinned prebuilt artifacts; always --force in CI
R15 Coding rules: Meadow style, -Werror root causes, ASCII only; the Legacy glue has non-ASCII text and BOMs Certain / Low Clean up while porting; check with grep -P '[^\x00-\x7F]'
R16 Rebase docs are stale (Memory.md, Rebasing.md, CLAUDE.md toolchain section) Certain / Low Treat memory.ld, .config and git history as the truth; fix the docs in the same PRs
R17 Scope creep: AOT, SQLite, cloud, MQTT, cell, emulator Med / Med Milestone scope fixed in phase 0
R18 Hardware validation bottleneck (commits need a user hardware test) Certain / Med Phase-sized PRs; a written hardware test matrix per phase
R19 Legacy build-mono.sh falls back to runtime main (unpatched) for any non-matching branch name Certain / High Pinned commit; hard failure on mismatch
R20 The runtime is compiled against NuttX headers; a header or config change after the runtime build silently breaks struct layouts High / High Rebuild the runtime when NuttX headers or config change (hash them into the stamp); pair with R3
R21 (Rev 2 I-cache syscall concern) Resolved up_coherent_dcache is user-callable and invalidates the whole I-cache
R22 Cooperative GC suspend: a thread in a tight loop without safepoints, or blocked in native code, stalls GC Med / Med Keep Legacy behaviour; long-loop and blocking-I/O GC tests
R23 User flash overflow: 233 KiB free before System.Native, the glue and 153 KB of root CAs High / High Section 3.1 options; measure after phase 3
R24 6-socket WiFi cap (NET_USRSOCK_PREALLOC_CONNS=6, ALLOC_CONNS=0); Legacy needed 32 active sockets High / High Raise the limits before the networking tests; watch kernel heap use
R25 TLS shim moves from mbedTLS 3.2.1 to 3.6.2: PSA initialisation, threading, TLS 1.3 now on Med / High psa_crypto_init(); threading decision; HTTPS and MQTT soak
R26 Managed ABI drift: cell functions missing, unreachable "mbedtls" library, rotary API change, UPD 74/75 Med / Med Section 3.2 actions; a startup self-test that resolves every table entry
R27 GCC 15 (decided) has never built this runtime against NuttX 12 headers: int32_t is long, and GCC 14+ hard errors (incompatible pointer types, implicit declarations, int-conversion); Legacy's runtime was built with 10.3 High / Med Same compiler as Rebase and CI (15.3.1 local, 15.3.rel1 in the CI image). incompatible-pointer-types stays a warning by decision; review it once and fix the other hard errors in phase 1. The CI image gives a reproducible Linux build to compare against macOS
R28 Runtime history rewrite: hashes in Legacy commit messages point at backup/meadow-3.0-pre-cleanup Low / Low Pin by tree hash too; map hashes when reading Legacy history
R29 No managed debugging: runtime built with DISABLE_DEBUGGER_AGENT=1 Certain / Low Accept for 3.0 parity; revisit separately

6. Open questions for the user

  1. (Answered) The runtime is at ../runtime, branch meadow-3.0. f980b4c169b is almost certainly
    what 3.0.0 was built from: it was committed 2026-08-31 18:12 (the history rewrite), 40 minutes
    before Legacy's "bump to 3.0.0" (b72abcb2c37, 18:52), and the branch has nothing later. (Its
    author date is 2026-08-23, which earlier revisions quoted.)
  2. (Answered 2026-10-14) First milestone: JIT only.
  3. (Answered 2026-10-14) Renode: nice to have, deferred for now.
  4. (Answered 2026-10-15: port it now, in phase 6) ESP32 non-blocking bridge from the unmerged Meadow-ESP32 meadow/espcp-* branches:
    decide before phase 6.
  5. (Answered 2026-10-14) Sign-off on CCM only; FeatherV2 is similar enough.
  6. (Answered 2026-10-09) Toolchain: GCC 15 for the runtime, the same as Rebase (15.3.1 locally,
    15.3.rel1 in CI).
  7. (Answered in phase 3, 2026-10-14) Which user-flash option from 3.1: move SQLite and others into the SDRAM image, load CAs
    from a file, or move the uflash/kflash boundary? Decided: placement as Legacy main - the runtime
    archives, libSystem.Native.a, the TLS shim and the root CA table in the SDRAM image; mbedTLS and
    SQLite stay in uflash (about 835 KiB of 1024 used after phase 3).
  8. (New) Keep CONFIG_HCOM_MONO_REMOTE_DEBUGGING=y when there is no debugger agent?
  9. (Answered 2026-10-14) Phase 0 content as listed in section 4; it merges without a hardware test.
  10. (Answered 2026-10-14) Claude may push and open PRs, but must not merge into main.
  11. (Answered 2026-10-14) Prebuilt runtime and BCL archives: CI artifacts, as in PR Synchronising build with Legacy #692 (run ENTER_DFU_MODE request writes a DFU magic value that nothing reads, so the board only restarts #818).

7. Notes and caveats

  • Hardware results, soak numbers and test pass rates are as reported in the Legacy docs and commit
    messages; they were not reproduced. tracks.md lists a "golden-3.0-release" track whose directory
    is absent, and some conductor text is stale (GC heap and interpreter stack values), so prefer the
    commit messages.
  • Not verified:
    • A fix-by-fix comparison of Legacy and Rebase espcp.
    • Legacy patches 1 and 2 against the 12.12 code.
    • The size of libSystem.Native.a.
    • The runtime build itself (not attempted).
    • Legacy's mbedTLS fork version beyond the docs.
  • Doc inaccuracies found in Rebase (status 2026-10-16: Memory.md flash and SDRAM text corrected in
    earlier phases and its DTCM section in ecfd135; Rebasing.md and CLAUDE.md fixed on branch
    DocFixes):
    • Documentation/Memory.md swaps the kflash/uflash addresses, says "3 MB visible to NuttX heap"
      (it is 32 MB), and says 64 MB QSPI while meadow_os_partitions.h has QSPI_FLASH_SIZE 32 MB
      (the config is 64 MB).
    • Documentation/Rebasing.md talks about 12.10 -> 12.11, but the tree is 12.12 (commit
      f962d87).
    • CLAUDE.md describes toolchain/macos/, which does not exist.
  • ../Task.md says the Legacy NuttX is "7.3"; the Legacy ChangeLog shows 7.29, with 7.30 dated
    "2019-xx-xx".
  • Project notes that point to Research.md, DynamicLoadingIssues.md and ImplmentCloud.md: none
    of those files exist in the workspace or in git history.

8. Change log

  • Rev 2:
    • The runtime branch is meadow-3.0 (main is plain upstream).
    • The SDK is .NET 11 preview 3.
    • Legacy NuttX is 7.x, not 9.x.
    • GC suspend is cooperative and DISABLE_AOT=1.
    • Added R19-R22.
    • Rev 2 also claimed CARD_TABLE_BITS 26 was absent from the runtime. That was wrong (the grep
      output was truncated); it is at sgen-cardtable.h:45.
  • Rev 3: the corrections and additions listed in section 9.
  • Rev 3.1:
    • Toolchain decided: GCC 15 for the runtime as well (phase 0, R27, Q6).
    • Added the int32_t = long finding and its options (phase 1).
    • Corrected rev 3's claim that the elf_globals.S generator is missing. It is
      nuttx/tools/build-globals.sh; a listing cut short by head hid it.
  • Rev 3.2: decided to keep the runtime's -Wno-error=incompatible-pointer-types flag. Phase 1
    records the decision, its scope (the runtime only; Rebase code still follows the no-suppression
    rule) and a one-off review of the resulting warnings. R27 updated.
  • Rev 3.3: added section 4.0: the worktree OS/Rebase.worktrees/MergeManagedCode, the
    integration branch MergeManagedCode (from main 2ff7d9a), the PhaseN branch model, and
    the sibling-path caution for build scripts.
  • Rev 3.4: runtime location decided: MEADOW_RUNTIME_DIR, defaulting to ../runtime relative
    to the Rebase root when unset (section 4.0, phase 1).
  • Rev 3.5: phase 0 answers recorded (JIT only, CCM-only sign-off, phase 0 content and no
    hardware test, push and PR allowed but no merge to main, ESP and Renode deferred, artifacts
    from CI). MergeManagedCode fast-forwarded to 94de840 and pushed. Found that the workflow
    ignores PRs not targeting main.
  • Rev 3.6: phase 0 carried out. Commit 198b989, PR Phase 0: pin the Mono runtime and build MergeManagedCode in CI #693 into MergeManagedCode, CI run
    hcom_host_receive_takesem() and hcom_host_send_transmit_takesem() cannot retry after EINTR (wrong errno test) #819. Runtime switched to meadow-3.0; SDK bootstrap verified. Merged as 77d4051.
  • Rev 3.7: phase 1 decisions. Runtime work goes on a new runtime-repo branch
    MergeManagedCode (from meadow-3.0) through per-phase PRs. Phase 1 adds a standalone
    build-mono.sh (build.sh and CI wiring in phase 3). No hardware test. Signals move to
    SIGRTMIN-based numbers. Release by default.
  • Rev 3.8: phase 1 done (runtime#2, Meadow.OS Phase 1: build the Mono runtime against NuttX 12 with build-mono.sh #694). Corrected the dlopen(NULL) call path
    (mono-dl-posix.c:99, not the Android-only mono-dl.c:86). Found the IFF_ALLMULTI/IFF_NAT
    alias bug. Recorded the warning review and follow-ups.
  • Rev 3.9: phase 2 done (Phase 2: reserve the start of SDRAM for the Mono runtime image #695).
  • Rev 3.10: phase 3 implemented (phase 3 status). New findings: the non-atomic weak
    __sync_val_compare_and_swap_8 in protected builds, the elf_globals.S weak/rodata change, the
    version-decode bug, and __aeabi_read_tp being kernel-only.

9. Verification log (rev 3)

# Claim (source) Verdict Evidence
1 Runtime main is upstream; work on meadow-3.0, 42 commits (rev 2) Confirmed git rev-list counts; merge base f1949423fa1
2 Runtime is .NET 11 preview (rev 2) Confirmed global.json
3 CARD_TABLE_BITS 26 absent (rev 2) Wrong sgen-cardtable.h:45; only MEADOW_AOT_NUTTX_CARDS (from a Legacy doc) is absent
4 Signals 20/21/22 (Legacy docs) Confirmed; NuttX 12 meaning added mono-threads-posix-signals.c; Rebase signal.h:160-162
5 Cooperative suspend; DISABLE_AOT=1 (rev 2) Confirmed runtime CMakeLists.txt, build-nuttx.sh
6 No CONFIG_HCOM_MONO_* in any Kconfig (rev 1) Wrong board Kconfig:16-22; .config:733-734
7 Start path is task_create(... (main_t)NULL) (rev 1) Partly wrong REMOTE_DEBUGGING=y selects mono_main_proxy, whose call is //FIXME (hcom_mono_control.c:454-461,946)
8 Required-files list New finding hcom_mono_control.c:561-564 still lists Mono 6.9 assemblies
9 Memory map regions Confirmed memory.ld:97-118 (also an ITCM region)
10 Heap overlaps the runtime image Confirmed; Legacy fix located Rebase stm32_allocateheap.c HEAP2 block; Legacy lines 417-418
11 MPU attributes for SDRAM Confirmed identical to Legacy stm32_mpuinit.c:103-119; Legacy mpu.h:422-431
12 dd skip=3014400 is invalid or brittle (rev 1) Wrong The arithmetic is exact; Meadow.OS.Runtime.bin has a valid signature
13 ###MONO_GIT_REF### not substituted (rev 1) Wrong scripts/version_methods.sh:97-109
14 Runtime copied from the /meadow0 file (rev 1 agent) Corrected The file is staged, flashed to the raw partition at offset 0, then deleted (hcom_nx_fs_flash.c:657-1122); the ioctl copies the raw partition (meadow-upd.c:370-377)
15 Needs LIBC_ELF_HAVE_SYMTAB for dlopen (rev 1) Wrong dlsymtab() is used (sotest_tests.c:110); elf_globals.S is committed
16 Defconfig items (EVENT_FD, PI, cancellation, NPOLLWAITERS, IWDG and others) Confirmed nuttx/.config
17 Usrsock connection cap New finding .config:1722-1723 (6 prealloc, 0 dynamic)
18 Memory.md vs memory.ld; QSPI 32 vs 64 MB Confirmed, plus the "3 MB heap" error Memory.md:18-19,99,104; .config:380
19 BBR bits 0x800/0x1000/0x2000 Confirmed hcom_bbreg_defn.h:106-110
20 mqd_t task-group defect (R5) Resolved Commit 0728a9b (2026-08-08); F7MeadowOS kernel code uses file_mq_*; Research.md absent
21 Legacy NuttX version Refined ChangeLog 7.29 / 7.30-dev; Rebase is 12.12 (commit f962d87)
22 mappings-meadow.h 1089 lines of libc/ioctl (rev 1) Corrected 70 live entries; lines 117-1085 commented out; 65 of 70 resolve in Rebase
23 SQLite table has 141 exports (rev 1) Corrected 133 live entries, all present in Rebase
24 Legacy __errno and the --wrap list Confirmed; hazard identified mono_nuttx_stubs.c:383-387; Legacy Make.defs (30 errno wraps plus mmap/munmap)
25 Legacy symtab mechanism New finding symtab.c:65-74 uses boardctl(BOARDIOC_OS_SYMTAB)
26 sys/random.h override on NuttX 12 New finding NuttX 12 getrandom() takes 3 arguments; the override calls it with 2
27 dlopen(NULL) patch still needed New finding lib_dlopen.c:88, CONFIG_NDEBUG=y; runtime mono-dl.c:86
28 I-cache syscall gap (R21) Resolved up_coherent_dcache in syscall.csv:255 and libproxies; arm_cache.c
29 Rebase toolchain under toolchain/ (CLAUDE.md) Wrong No such directory; arm-none-eabi-gcc 15.3.1 from Homebrew
30 Legacy toolchain GCC 10.3 Confirmed Legacy setup-ci-host.sh:18-23
31 "Check whether Rebase has mbedTLS" (rev 1) Answered 3.6.2, user space only, TLS 1.3 and PSA on, threading off
32 mono_main.c settings (GC, ThreadPool, env, warm-up thread) Confirmed mono_main.c:1316-1386, 1792-1794
33 hcom_mono_control.c change is small Confirmed 10+/11- diff
34 Kernel patch statuses 6, 7, 8, 9, 14 Verified as in 2.3 fs_fcntl.c, lib_dlopen.c, ramlog.c, cdcacm.c, .config
35 Managed ABI against Meadow.Core meadow-3.0 New 50 "nuttx" imports, 46 resolve; "mbedtls" gap; UPD numbers match; liveness optional
36 ESP non-blocking bridge availability New Only on unmerged Meadow-ESP32 branches
37 User and kernel flash budget New Section 3.1, from nuttx_user.elf / nuttx.elf
38 LittleFS versions New Legacy lfs.h 0x00010007; Rebase CONFIG_FS_LITTLEFS_VERSION="v2.5.1"
39 Runtime history rewrite New Identical tree hash on meadow-3.0 and backup/meadow-3.0-pre-cleanup
40 Hardware validation, test counts, soak results Not verified (docs only) tracks.md and the handoffs disagree on test counts (735/732 vs 733/2/1)
41 elf_globals.S generator not in the tree (rev 3) Wrong nuttx/tools/build-globals.sh (tracked); DockerFiles/Build/Dockerfile:56-65
42 Toolchain for the runtime Decided: GCC 15 Local 15.3.1; CI ARM_TOOLCHAIN_VERSION=15.3.rel1 (Dockerfile:66); the runtime pins C11/C++17
43 int32_t on NuttX 12 New finding arch/arm/include/types.h:61-70; GCC 15.3 __INT32_TYPE__ is long int
44 Runtime -Wno-error=incompatible-pointer-types Decided: keep runtime src/mono/CMakeLists.txt (HOST_NUTTX block near line 630); exception limited to the runtime

🤖 Generated with Claude Code

@NevynUK NevynUK left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Approved.

@NevynUK
NevynUK merged commit 99be1ed into main Oct 18, 2026
6 of 8 checks passed
@NevynUK
NevynUK deleted the MergeManagedCode branch October 18, 2026 12:28
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