<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Thomas Gazagnaire]]></title>
    <description><![CDATA[Building Functional Systems from Cloud to Orbit.]]></description>
    <link>https://gazagnaire.org</link>
    <lastBuildDate>Thu, 14 May 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://gazagnaire.org/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title><![CDATA[O(x)Caml in Space]]></title>
      <description><![CDATA[<p>On 23 April, our pure-OCaml <a href="https://ccsds.org">CCSDS</a> protocol
stack booted up in low Earth orbit! The project, codename
<em>Borealis</em>, is running inside <a href="https://dphi.space">DPhi Space</a>'s
ClusterGate-2 <a href="https://software.dphispace.com/">payload module</a>
on the host satellite, with end-to-end-encrypted command and
control and post-quantum key rotation, all implemented in safe
OCaml.</p>
<p>Why does OCaml matter here? Untrusted code running on a satellite
is a <a href="https://gazagnaire.org/blog/2026-02-25-satellite-software.html">huge security risk</a>,
and OCaml is an ideal safe language to run in space. In his
<a href="https://kcsrk.info/slides/icfp22_keynote.pdf">ICFP 2022 keynote</a>,
<a href="https://kcsrk.info/">KC Sivaramakrishnan</a> looked back on the
<a href="https://tarides.com/blog/2023-03-02-the-journey-to-ocaml-multicore-bringing-big-ideas-to-life/">decade-long engineering effort</a>
that produced OCaml 5, the release that put safe and performant
multi-threading into the OCaml runtime.</p>
<p>KC ended his talk speculating that OCaml 5.0 would go to the
moon, due to its language features that would deliver C/Rust-like
performance on demand while keeping the mathematical rigour of
classic ML. Here at Parsimoni, we took his words a bit too
literally :-)</p>
<div class="my-8 not-prose grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
  <figure class="cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/kc-icfp22-roadmap.png"><img class="w-full rounded-lg" src="https://gazagnaire.org/blog/images/kc-icfp22-roadmap.png" alt="KC Sivaramakrishnan's ICFP 2022 keynote slide titled Where do we go from here, showing an arrow from OCaml 5.0 to the moon"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight text-center">Closing slide of KC Sivaramakrishnan's <a href="https://kcsrk.info/slides/icfp22_keynote.pdf">ICFP 2022 keynote</a>: the arrow from OCaml 5.0 to the moon, and the metaphor that gave this post its title.</figcaption>
  </figure>
  <figure class="cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/borealis-clustergate2-boot.png"><img class="w-full rounded-lg" src="https://gazagnaire.org/blog/images/borealis-clustergate2-boot.png" alt="Screenshot of DPhi Space's mission ops dashboard showing the Borealis commissioning execution completed, with the satellite.log window open over it"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight text-center">Borealis's first boot on DPhi Space's mission-ops dashboard, 23 April 2026. The first time a pure-OCaml CCSDS stack ran in <strong>space</strong>!</figcaption>
  </figure>
</div>
<p>The host satellite circles the Earth every ninety minutes or so. A few
months after <a href="https://firobe.fr/">Virgile Robles</a> and I hacked on
this over Christmas, we (virtually) jumped around when we saw
this:</p>
<pre><code>2026-04-23 18:48:06  SpaceOS/Borealis (BPv7, BPSec, OTAR) by Parsimoni
2026-04-23 18:48:06  ClusterGate-2 proxy [single iteration]
2026-04-23 18:48:06  Config: scid=100, tm_vcid=0, tc_vcid=4, tm_spi=1, tc_spi=2, tm_frame_len=1115
2026-04-23 18:48:06  Session keys: EK=0x0100 AK=0x0101 active
2026-04-23 18:48:09  Telemetry health: { ... &quot;status&quot;: &quot;healthy&quot; }
</code></pre>
<h2>What is actually running</h2>
<p>Borealis is a daemon. On both the ground and the satellite it
speaks a normal client-server protocol (telemetry queries,
commands and responses, OTAR rekey requests), the same shape as
any production server. What is unusual is the wire underneath.</p>
<p>The protocol stack is a pure-OCaml implementation of
<a href="https://gazagnaire.org/blog/2026-04-15-ccsds-protocol-stack.html">CCSDS</a>, the protocol
family that links spacecraft to the ground. It covers every
layer from radio framing up through Bundle Protocol and the
security extensions on top; the binary formats are described as
<a href="https://gazagnaire.org/blog/2026-03-31-ocaml-wire.html"><code>ocaml-wire</code></a> codecs.</p>
<p>On ClusterGate-2, only the upper layers of that stack are
exercised. The satellite has no network connectivity from
outside. The only ground link is filesystem upload and download
via DPhi's API: a file written to the uplink directory is
forwarded by DPhi on the next pass, and downlink works the same
way. Borealis treats that filesystem as a delay-tolerant network.
Every command, response, telemetry sample and image chunk is
serialised into a
<a href="https://datatracker.ietf.org/doc/html/rfc9171">BPv7</a> bundle and
written to disk; DPhi forwards the file as opaque bytes.</p>
<p><a href="https://datatracker.ietf.org/doc/html/rfc9172">BPSec</a> wraps each
bundle in two extension blocks: one encrypts the payload, the
other authenticates it. Sequence numbers
reject replays, and the pre-shared keys (rotated by OTAR, below)
keep the routing path out of the trust path. The satellite operator sees only opaque bundle bytes; nothing in
the routing path can read, modify, forge or substitute the
contents.</p>
<p>This matters because we are tenants on someone else's hardware.
On a <a href="https://gazagnaire.org/blog/2026-02-25-satellite-software.html">hosted-payload
satellite</a> multiple tenants
share a single bus, and container isolation alone would not
suffice. A shared Linux kernel means kernel-level CVEs regularly
break the tenant boundary, and the same primitives keep
resurfacing in new forms: <a href="https://dirtyfrag.io/">Dirty
Frag</a> (a universal Linux LPE published
this year),
<a href="https://github.com/v12-security/pocs/tree/main/fragnesia">Fragnesia</a>
(a close cousin in the same family), and
<a href="https://cert.europa.eu/publications/security-advisories/2026-005/">&quot;Copy Fail&quot;</a>,
a Linux kernel privilege escalation disclosed in late April that
hit every major distribution at once. Earlier rounds
(<a href="https://dirtypipe.cm4all.com/">Dirty Pipe</a> in 2022, the
<a href="https://nvd.nist.gov/vuln/detail/CVE-2024-1086">nf_tables use-after-free</a>
exploited for container escape in 2024) suggest there will be
more. On a ground server you can run
the package manager and reboot; in orbit, kernel patching is its
own delivery problem with its own delay, and is sometimes not
possible at all. The cryptographic
envelope around each bundle is the only durable guarantee.</p>
<p>Beyond confidentiality and authenticity, the long-mission threat
model needs key rotation. Borealis supports OTAR (Over-The-Air
Rekeying) for its post-quantum signing keys
(<a href="https://csrc.nist.gov/pubs/fips/204/final">ML-DSA-65</a>). Those keys live for the life of the satellite (ten
to fifteen years), which is why NASA's
<a href="https://standards.nasa.gov/standard/NASA/NASA-STD-1006">Space System Protection Standard (NASA-STD-1006A)</a>
treats post-quantum command authentication as a requirement
rather than a future option. OTAR lets us rotate the post-quantum
keys without re-flashing the satellite. To our knowledge this will be
<strong>the first public in-orbit demonstration of post-quantum OTAR</strong>;
we plan to exercise the rotation on a later pass.</p>
<p>Borealis runs as a guest on DPhi's hosted-payload module: an Arm
SoC (four Cortex-A53 cores, 4 GB RAM) running Linux. The
flight binary is 5-10 MB, statically linked, shipped as a
<code>FROM scratch</code> Docker image. It polls the bus for telemetry
(angle, speed, position) and an onboard camera (a low-quality
fish-eye, good for demos only). The core of the satellite-side
loop looks like this:</p>
<pre><code class="language-ocaml">let send_telemetry t ~prefix payload =
  let bundle =
    make_bundle t ~source:Eid.sat_telem
      ~destination:Eid.ground_telem ~payload
  in
  match protect_bundle t bundle with
  | Ok protected -&gt;
      ignore (write_bundle t ~dir:(downlink_dir t.config) ~prefix protected)
  | Error _ -&gt; log t &quot;Failed to protect telemetry bundle&quot;
</code></pre>
<p><code>protect_bundle</code> applies BPSec with keys from the SDLS Security
Association, the cryptographic parameters ground and satellite
agreed on at provisioning. If any of that fails, no bundle leaves
the satellite.</p>
<p>The uplink path mirrors the downlink path. The satellite reads
bundles from <code>/data/uplink/</code>, unprotects each with BPSec, and
routes by destination at the bundle layer:</p>
<pre><code class="language-ocaml">if dest = Eid.sat_cmd  then handle_command t payload
else if dest = Eid.sat_otar then handle_otar t payload
else log t &quot;Unknown destination&quot;
</code></pre>
<p>Text commands are short UTF-8 strings parsed into an ADT; the
typechecker enforces exhaustive dispatch:</p>
<pre><code class="language-ocaml">type cmd = Ping | Check | Capture | Halt

let dispatch t = function
  | Ping    -&gt; send_response t ~prefix:&quot;pong&quot; &quot;PONG&quot;
  | Check   -&gt; run_self_check t
  | Capture -&gt; capture_and_send t
  | Halt    -&gt; t.shutdown_requested &lt;- true
</code></pre>
<p>Adding a new command means adding a constructor; the compiler
then flags every place it is not yet handled.</p>
<p>OTAR rekey messages take the other branch. The payload is binary
rather than text, encrypted under a master key that was loaded
onto the satellite at integration time and lives in process
memory on the module (the module has no TPM or secure element,
because building a radiation-tolerant one is still an open
hardware problem). The satellite decrypts the new keys, holds them in a
staging slot, and activates them. The current flight loop
activates on receipt; the protocol also supports a separate
ground-driven activation step where the operator verifies the
install before committing, and switching to that path is a
flight-loop change, not new code.</p>
<p>The master key itself has no rotation path. It was installed on
the payload before the satellite was mated to the launcher, and
there is no more-trusted channel to deliver a new one once the
spacecraft is in orbit. If the master key is lost, this stack is
unreachable. That is the honest failure mode for a long mission
with no hardware-backed key storage.</p>
<h2>What is coming next: OxCaml</h2>
<p><a href="https://oxcaml.org/">OxCaml</a> is Jane Street's compiler branch on
top of OCaml. Its mode system matters on the satellite hot path.
Locality lets us mark allocations stack-bound, so they never
reach the heap and never reach the GC. Uniqueness and
capabilities track shared mutable state in the type system,
turning data races into compile-time errors on the parallel parts
of the stack.</p>
<p>The hot path on the hosted-payload module is CCSDS dispatch:
every CFDP segment, every COP-1 frame, every camera packet flows
through a Space Packet header decode and an APID-based routing
step before the payload reaches the application logic.
Real-time on-board dispatch with hard scheduling deadlines on
every pass is exactly the workload the EU
<a href="https://orchide-project.eu/">ORCHIDE</a> Horizon Europe project was
set up to address (the consortium where this on-board work first
started inside <a href="https://tarides.com/">Tarides</a>, and which
eventually led us to spin <a href="https://parsimoni.co/">Parsimoni</a> out
as a dedicated space-software company).</p>
<div class="my-8 not-prose">
  <figure>
    <img class="w-full" src="https://gazagnaire.org/blog/images/borealis-tail-latency.svg" alt="Bar chart of CCSDS packet dispatch latency at mean, p99, p99.9 and max for stock OCaml 5.3.0 and OxCaml 5.2.0+ox with stack-bound allocation. At p99.9: stock OCaml 29 ns, OxCaml stack 9 ns. Stock fires 394 minor GCs over 25 million packets; OxCaml stack fires zero.">
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">
      Per-packet latency on the CCSDS dispatch hot path:
      decoding a Space Packet primary header into a 3-field record
      and routing by APID. Stock OCaml versus OxCaml with the same
      code annotated <code>exclave_ stack_</code>. Measured on a
      laptop, not the flight module.
    </figcaption>
  </figure>
</div>
<p>Switching to OxCaml with <code>exclave_ stack_</code> annotations drops
p99.9 latency from <strong>29 ns to 9 ns per packet</strong> on the dispatch
hot path, and removes GC pressure entirely (<strong>394 minor GCs to
zero</strong> over 25 million packets). Throughput is comparable; the
win is jitter, and on a hosted-payload module with hundreds of
microseconds of jitter budget, that is the whole game.</p>
<p>The recipe is mechanical: turn a per-iteration heap allocation
(<code>{ apid; seq_count; data_len }</code>) into a stack-allocated one
(<code>exclave_ stack_ { apid; seq_count; data_len }</code>), and require
the consumer to take it <code>@ local</code>. The type system proves the
record cannot escape the dispatch scope; the compiler emits no
heap traffic; the GC has nothing to collect.</p>
<p><strong>Setup.</strong> Apple M5 Max, macOS 25.4. Stock OCaml 5.3.0 versus
5.2.0+ox (Jane Street's OxCaml fork). Workload: 100 000 batches
of 256 CCSDS Space Packet header dispatches each (about 25.6 M
packets total), each routed through an <code>[@inline never]</code> handler
so the record genuinely escapes. Median of 10 runs.</p>
<h2>Why OCaml</h2>
<p>Around 70% of severe CVEs in C/C++ codebases trace to memory
corruption (buffer overflows, use-after-free, integer overflows),
based on <a href="https://github.com/microsoft/MSRC-Security-Research/blob/master/presentations/2019_02_BlueHatIL/2019_01%20-%20BlueHatIL%20-%20Trends%2C%20challenge%2C%20and%20shifts%20in%20software%20vulnerability%20mitigation.pdf">Microsoft's MSRC analysis (2019)</a>
and <a href="https://www.chromium.org/Home/chromium-security/memory-safety/">Chromium's 2020 study</a>.
Our security extensions (SDLS, BPSec, and OTAR) all handle
ciphertexts and key material, which is exactly where memory bugs
hurt most. The C-based incumbent in this domain,
<a href="https://github.com/nasa/CryptoLib">NASA CryptoLib</a>, has had its
own such bugs: for instance,
<a href="https://github.com/nasa/CryptoLib/security/advisories/GHSA-q2pc-c3jx-3852">a heap buffer overflow in the TC frame parser</a>,
triggered by an integer underflow on a crafted frame. An OCaml
implementation removes that class of attack surface from the
application logic by construction. The runtime, the kernel
underneath, and the bootloader are still C and still in the TCB:
memory safety helps where it helps, and is not a substitute for a
trusted compute base audit.</p>
<p>Beyond what OCaml gives us today, the language itself keeps
advancing. <a href="https://oxcaml.org/">Jane Street</a> maintains OxCaml, an
experimental branch of OCaml. Its design goal is safe,
predictable control over the performance-critical parts of a
program, opt-in only where you need it, and still in OCaml:
every valid OCaml program is also a valid OxCaml program.
<a href="https://anil.recoil.org/projects/oxcaml">OxCaml Labs</a> (Anil
Madhavapeddy's group at Cambridge) and
<a href="https://fplaunchpad.org/">FP Launchpad</a> (KC Sivaramakrishnan's
lab at IIT Madras) are pushing <strong>OCaml</strong> forward;
<a href="https://tarides.com/blog/2025-07-09-introducing-jane-street-s-oxcaml-branch/">Tarides upstreams the pieces that are ready</a>
into the mainline.</p>
<p>I somehow focused on the moon :-) Going to orbit first meant
prioritising correctness over performance, because protocol bugs
in orbit are expensive to fix. The defence runs through every
layer of the stack: type checking, formally verified
cryptographic primitives
(<a href="https://github.com/cryspen/libcrux">libcrux</a>,
<a href="https://github.com/mit-plv/fiat-crypto">fiat-crypto</a>), interop
testing, and <a href="https://hacksat.dev/">dependency audits</a>.</p>
<p>Take three of those layers as concrete examples. The wire-format
codecs are generated from a typed schema, reject malformed bytes
at decode time, and feed Microsoft's
<a href="https://project-everest.github.io/everparse/">EverParse</a> parser
generator, which produces C validators formally verified in
<a href="https://fstar-lang.org/">F*</a>. The protocol state machines are
encoded as <a href="https://ocaml.org/manual/5.3/gadts.html">GADTs</a>, so
the typechecker rejects invalid transitions at compile time. An
interop pipeline runs against existing reference implementations,
catching what the type system cannot express and surfacing
defects in upstream libraries along the way.</p>
<p>Beyond what these layers catch, the functional core lets us ship
the same code as flight software, ground software, and test
oracle. The <code>protect_bundle</code> above is the same function in all
three roles. We feed recorded traffic from one role to the
others and compare outputs byte-for-byte. The OCaml code is also
the reference implementation that other implementations are
validated against. This is the <a href="https://www.usenix.org/conference/usenixsecurity15/technical-sessions/presentation/kaloper-mersinjak">nqsb-TLS</a>
approach (Kaloper-Mersinjak, Mehnert, Madhavapeddy and Sewell,
USENIX Security 2015), and it has held up in TLS for a decade.
<a href="https://www.nitrokey.com/products/nethsm">Nitrokey's NetHSM</a>
runs the same OCaml TLS stack in shipping hardware security
modules today.</p>
<p>Borealis is no exception. It might look like we wrote a full
CCSDS protocol stack from zero to in-orbit demonstration in a
couple of months. That is not what happened. The core libraries
come from <a href="https://mirageos.org/">MirageOS</a>, and have been
running in production on the ground for the last decade. A library operating system is, by
definition, a large toolset where you pick the pieces you need.</p>
<p>The <a href="https://gazagnaire.org/blog/2026-02-23-asplos-unikernels.html">ASPLOS 2013 unikernels paper</a>
asked whether sealed, single-purpose images could improve cloud
infrastructure. A decade later, the same libraries
<a href="https://gazagnaire.org/blog/2026-04-23-docker-story.html">run in Docker Desktop</a> on hundreds
of millions of laptops. Now they run in space, on ClusterGate-2,
doing the system-level plumbing as a Linux process rather than a
unikernel, in places I did not predict when we first designed
them.</p>
<div class="my-6 rounded-lg border border-gray-200 p-4 bg-gray-50">
<p>Borealis is one binary in orbit. <strong>The next problem is scale:</strong>
deploying and managing a fleet of specialised payload binaries
across many satellites with the same one-command ease that Docker
brought to Linux on the ground. The harder half is doing that
safely: signed updates from the ground, isolation between
payloads, and attestation of what is actually running. Getting hardware to orbit
is becoming routine; the interesting problems are increasingly in
the software that runs on it, a familiar shift from cloud
computing, where the stack on top of the servers ended up
mattering more than the servers.
That is what we are building next at
<a href="https://parsimoni.co/">Parsimoni</a>, with collaborators at
<a href="https://geotessera.org/">Cambridge</a> and beyond. I sketched the
wider question in <a href="https://gazagnaire.org/blog/2026-02-25-satellite-software.html">Is Running Untrusted Code on a Satellite a
Good Idea?</a>.</p>
</div>
<p>If you are building payload software, considering hosted payloads
on your bus, or want to compare notes on OCaml in flight,
<a href="mailto:thomas@gazagnaire.org">talk to me</a> or write to the
<a href="https://parsimoni.co/">Parsimoni team</a>.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-05-14-borealis.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-05-14-borealis.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[Library Operating Systems for the Desktop]]></title>
      <description><![CDATA[<p>On 15 April, the <a href="https://www.cst.cam.ac.uk/ring">Cambridge Ring</a>,
the alumni society of the University of Cambridge Computer
Laboratory, named our paper
<a href="https://doi.org/10.1145/3747525">&quot;Functional Networking for Millions of Docker Desktops&quot;</a>
its <a href="https://www.cst.cam.ac.uk/news/celebrating-culture-innovation-our-hall-fame-awards">Publication of the Year</a>.
I wrote the paper with my Cambridge colleagues <a href="https://anil.recoil.org/">Anil Madhavapeddy</a>,
<a href="https://dave.recoil.org/">Dave Scott</a>, <a href="https://patrick.sirref.org/">Patrick Ferris</a>, and <a href="https://ryan.freumh.org/">Ryan Gibb</a>. It describes how we
rebuilt Docker Desktop's networking and storage stack: a small VM
and a set of host-side daemons, built on
<a href="https://mirageos.org/">MirageOS</a> libraries, doing the
system-level plumbing that lets Linux containers reach the
outside world on the developer's macOS or Windows laptop. As our
2013 ASPLOS paper was titled
<a href="https://doi.org/10.1145/2451116.2451167">&quot;Unikernels: Library Operating Systems for the Cloud&quot;</a>,
this 2025 ICFP paper could have been titled &quot;Library Operating
Systems for the Desktop&quot;. Same architecture, different vertical,
same low-level libraries written in a high-level language
(<a href="https://ocaml.org/">OCaml</a>)!</p>
<div class="image-grid grid grid-cols-1 sm:grid-cols-3 gap-4 my-8 not-prose">
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/docker-pinata-whiteboard-2015.jpeg"><img class="w-full max-h-64 object-cover rounded-lg" src="https://gazagnaire.org/blog/images/docker-pinata-whiteboard-2015.jpeg" alt="Whiteboard discussion after the first internal Docker Piñata demo in October 2015"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">After the first internal Piñata demo (October 2015). Shipped as Docker for Mac beta three months later. From running mDNS on <code>baguette.local</code> to building vpnkit to manage desktop networking at scale.</figcaption>
  </figure>
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/docker-dockercon-2016.jpeg"><img class="w-full max-h-64 object-cover rounded-lg" src="https://gazagnaire.org/blog/images/docker-dockercon-2016.jpeg" alt="Empty hall before DockerCon 2016 keynote in Seattle"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">Preparation for DockerCon '16, where Docker Desktop would be revealed to the world. Can you spot the small whale in the presenter's menu bar?</figcaption>
  </figure>
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/docker-icfp-prize.jpeg"><img class="w-full max-h-64 object-cover rounded-lg" src="https://gazagnaire.org/blog/images/docker-icfp-prize.jpeg" alt="Cambridge Ring Hall of Fame Publication of the Year award for the Docker ICFP paper"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">The Cambridge Ring Hall of Fame Publication of the Year 2026 trophy. As Anil said: "it's so transparent that we'll never know who has it".</figcaption>
  </figure>
</div>
<p>The library-OS idea has a pedigree at Cambridge.
<a href="https://www.cl.cam.ac.uk/research/srg/netos/projects/archive/nemesis/">Nemesis</a>
came out of the Computer Lab in the 1990s.
<a href="https://xenproject.org/">Xen</a>, the hypervisor that provides the
secure low-level runtime for such designs, followed from the same
lab in the 2000s. In the 2010s, in
<a href="https://www.cl.cam.ac.uk/~jac22/">Jon Crowcroft</a>'s group at
Cambridge, we built MirageOS on top (Anil, Balraj Singh, Richard
Mortier, and others) as the higher-level library OS written
entirely in OCaml. We coined the term &quot;unikernels&quot; for these single-purpose,
sealed images (the paper describing this line of research received
a <a href="https://anil.recoil.org/notes/unikernels-test-of-time">test-of-time award</a>
in 2025 and
<a href="https://gazagnaire.org/blog/2026-02-23-asplos-unikernels.html">I wrote about that part of the story</a>
in February). In 2015, Anil, Balraj, and I spun out
<a href="https://www.cst.cam.ac.uk/news/computer-lab-start-unikernel-systems-acquired-docker-inc">Unikernel Systems</a>
to bring MirageOS to production. We brought
together the MirageOS team at Cambridge with the key maintainers
of <a href="https://github.com/rumpkernel/rumprun">Rumprun</a>, a unikernel
toolchain built on NetBSD's rump kernels. <a href="https://ignorepreviousdirections.com/">Justin Cormack</a> (who
became Docker's CTO a few years later) was among them.
<a href="https://www.mosaicventures.com/blog/the-story-of-our-investment-in-docker-via-unikernel-systems-and-why-enterprises-are-running-their-business-on-their-commercial-platform">Mosaic Ventures</a>,
who supported us very early on, put it this way:
<em>&quot;MirageOS was a great technology, and had a number of
applications. We weren't the only ones who saw its potential.&quot;</em> Docker acquired our company a few months later, and
we became the &quot;Piñata&quot; team inside Docker.</p>
<p>At Docker, we built what nobody was expecting us to build: a
desktop app. I was lucky enough to manage the first releases of
what would become the most-desired developer tool in every
<a href="https://survey.stackoverflow.co/2019/">Stack Overflow Developer Survey</a>
since 2019: Docker for Mac and Docker for Windows. The beta
shipped
<a href="https://web.archive.org/web/20160324150553/https://blog.docker.com/2016/03/docker-for-mac-windows-beta/">on Docker's third birthday, in March 2016</a>.
It has been downloaded hundreds of millions of times since. The
launch post credited
MirageOS directly: <em>&quot;the translator between Linux and Mac OS X
networking uses the MirageOS TCP/IP implementation.&quot;</em> That
translator was <a href="https://github.com/moby/vpnkit">vpnkit</a>, an OCaml
unikernel (running as a userspace service) built on MirageOS
libraries that acts as the network proxy inside Docker Desktop.</p>
<p><a href="https://gazagnaire.org/pub/2025-icfp-docker.pdf">Read the paper</a>
to know more (and find the full team credit in the
acknowledgments)!</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-04-23-docker-story.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-04-23-docker-story.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Thu, 23 Apr 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[Tailwind Without Node]]></title>
      <description><![CDATA[<p><a href="https://tailwindcss.com/">Tailwind CSS</a> introduced a clever way to
manage the HTML/CSS split: encode CSS properties as class names. This
solves a recurrent friction in large codebases: keeping HTML and CSS
in sync. Tailwind's build scans the source and generates <em>exactly</em>
the classes needed to style it. You can (mostly) copy-paste an HTML
snippet anywhere and its stylesheet follows. As in other domains,
referential transparency is a really nice property, and helps
programmers (and designers!) make fewer mistakes, especially when
eagerly refactoring a codebase.</p>
<p>However, scanning HTML snippets comes with downsides. When the HTML
is generated (either statically by a blog engine, or dynamically
with browser apps), the strategy falls short. Tailwind proposes
various strategies to overcome this (like embedding static CSS
classes in its config file), but I don't find them very helpful
when I'm developing in OCaml.</p>
<p>So six months ago I started reimplementing Tailwind in OCaml.
<a href="https://github.com/samoht/tw"><code>tw</code></a> began life targeting v3, where
classes project directly to CSS values; it has since fully migrated
to v4, a substantial architectural change that swaps concrete
values for a variable-based theme layer. It passes the 905 upstream
tests extracted from
<a href="https://github.com/tailwindlabs/tailwindcss/tree/main/packages/tailwindcss/src">Tailwind's own TypeScript suites</a>,
and the <code>tw</code> CLI emits CSS that is byte for byte identical to the
reference
<code>tailwindcss</code> under <code>--optimized --minify</code>. Here is <code>tw</code> compiled
to JavaScript with
<a href="https://ocsigen.org/js_of_ocaml/"><code>js_of_ocaml</code></a>. Paste any HTML
snippet on the left and the CSS regenerates in a sandboxed iframe
on the right:</p>
<figure class="my-6 not-prose">
<div class="rounded-lg bg-white overflow-hidden shadow-sm">
<div class="grid grid-cols-1 md:grid-cols-2">
<div class="p-4">
<div class="flex items-center justify-between mb-2">
<label for="tw-html" class="text-xs font-medium text-gray-500 uppercase tracking-wide">HTML snippet</label>
<span id="tw-status" class="text-xs font-mono text-gray-400"></span>
</div>
<textarea id="tw-html" rows="14" spellcheck="false" class="w-full font-mono text-sm p-3 rounded-md resize-y bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:bg-white transition-colors">&lt;div class="p-8 bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500 rounded-2xl shadow-xl max-w-sm mx-auto mt-6"&gt;
  &lt;div class="flex items-center gap-3 mb-4"&gt;
    &lt;div class="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center text-white font-bold text-lg backdrop-blur-sm"&gt;tw&lt;/div&gt;
    &lt;h2 class="text-xl font-bold text-white tracking-tight"&gt;Tailwind v4, in OCaml&lt;/h2&gt;
  &lt;/div&gt;
  &lt;p class="text-sm text-white/80 mb-6 leading-relaxed"&gt;
    Edit me. The preview updates on every keystroke.
  &lt;/p&gt;
  &lt;button class="group w-full px-5 py-2.5 bg-white/10 hover:bg-white/20 active:scale-95 text-white rounded-lg text-sm font-semibold backdrop-blur-sm border border-white/20 transition-all duration-200"&gt;
    &lt;span class="inline-block group-hover:rotate-12 transition-transform"&gt;✦&lt;/span&gt; Try it
  &lt;/button&gt;
&lt;/div&gt;</textarea>
<div id="tw-errors" class="hidden mt-3 p-3 border border-rose-200 bg-rose-50 rounded-md"></div>
</div>
<div class="p-4">
<label class="text-xs font-medium text-gray-500 uppercase tracking-wide block mb-2">Preview</label>

<details class="mt-3 group">
<summary class="text-xs font-medium text-gray-500 uppercase tracking-wide cursor-pointer hover:text-gray-700 transition-colors select-none">Generated CSS</summary>
<pre id="tw-css" class="mt-2 p-3 bg-slate-900 text-slate-200 rounded-md text-xs leading-relaxed overflow-auto max-h-64 font-mono"></pre>
</details>
</div>
</div>
</div>
<figcaption class="text-xs text-gray-500 mt-2 leading-tight text-center">Every keystroke in the left panel fully regenerates the right iframe.</figcaption>
</figure>

<p>Under the hood, <code>Tw.of_string</code> parses each class and <code>Tw.to_css</code>
compiles the set into a v4 stylesheet. Unknown classes show up in
the red panel. The palette, arbitrary values (<code>bg-[#ff00ff]</code>,
<code>bg-[oklch(60%_0.2_30)]</code>), and every variant combination (<code>hover:</code>,
<code>dark:md:</code>, <code>group-hover:</code>) work. What doesn't is Tailwind's
config-level machinery (<code>@theme</code>, <code>@apply</code>, custom tokens); those
are exposed on the OCaml side via a <code>Tw.Scheme.t</code> argument to
<code>Tw.to_css</code>.</p>
<h2>A Tailwind v4 bonus</h2>
<p>Since Tailwind v4, every utility resolves through a CSS custom
property (<code>var(--color-gray-900)</code>, <code>var(--spacing)</code>, and so on). That
makes the v4 engine substantially more complex than v3, where
utilities were emitted as concrete values, but it also makes the
output tweakable from the browser at runtime: change a token, the
whole page restyles. <code>tw</code> inherits this along with everything else
Tailwind v4 gives for free. Drag a colour or spacing below and look
around the page:</p>
<div class="my-6 not-prose rounded-lg border border-gray-200 p-4 bg-gray-50">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<label class="flex items-center justify-between gap-2"><span class="text-gray-700">Page background (<code class="text-xs text-gray-500">--color-white</code>)</span><input type="color" value="#ffffff"  class="w-10 h-8 border border-gray-300 rounded cursor-pointer"/></label>
<label class="flex items-center justify-between gap-2"><span class="text-gray-700">Nav background (<code class="text-xs text-gray-500">--color-gray-900</code>)</span><input type="color" value="#101828"  class="w-10 h-8 border border-gray-300 rounded cursor-pointer"/></label>
<label class="flex items-center justify-between gap-2"><span class="text-gray-700">Prose text (<code class="text-xs text-gray-500">--tw-prose-body</code>)</span><input type="color" value="#3d4350"  class="w-10 h-8 border border-gray-300 rounded cursor-pointer"/></label>
<label class="flex items-center justify-between gap-2"><span class="text-gray-700">Spacing (<code class="text-xs text-gray-500">--spacing</code>)</span><input type="range" min="0.2" max="0.4" step="0.01" value="0.25"  class="w-32"/></label>
</div>
<button  class="mt-3 px-3 py-1 text-xs bg-white border border-gray-300 rounded hover:bg-gray-50 cursor-pointer transition-colors">Reset</button>
</div>
<h2>Why an OCaml port</h2>
<p>My OCaml web stack had one irreducible dependency on Node.
Generating a Tailwind stylesheet meant invoking the npm package or
bundling the Rust-compiled standalone binary. Nothing else in an
OCaml web project still required a non-OCaml toolchain.</p>
<p>The trigger was <a href="https://github.com/mirage/mirage-www"><code>mirage-www</code></a>,
the <a href="https://mirageos.org/">MirageOS</a> homepage. It is a 1,100-line
OCaml application serving
TLS traffic through an OCaml stack: HTTP, TLS, TCP/IP, and the
ethernet and block-device drivers are all written in the same
language and linked into one self-contained binary. The stylesheet
was the one thing the build could not produce from its own
dependencies. Contributors on FreeBSD and Windows kept hitting
missing binaries and npm drift; Tailwind has improved its platform
coverage since, but for a project whose other inputs are all OCaml,
the gap was awkward. <a href="https://github.com/mirage/mirage-www/pull/860">Pull request
<code>mirage-www#860</code></a>
deletes the <code>tailwindcss</code> script tag and the npm dev-dependency and
replaces them with <code>tw.html</code> directly.
Once the PR lands, the build needs only an OCaml compiler and <code>opam</code>.</p>
<h2>What it covers</h2>
<p><code>tw</code> implements every core Tailwind v4 utility and both official
plugins,
<a href="https://github.com/tailwindlabs/tailwindcss-typography"><code>@tailwindcss/typography</code></a>
and
<a href="https://github.com/tailwindlabs/tailwindcss-forms"><code>@tailwindcss/forms</code></a>.
Responsive variants, state variants, dark mode, group and peer
modifiers, pseudo-elements, OKLCH colours, the <code>color/opacity</code>
syntax, arbitrary values like <code>min-h-[6rem]</code>, and the preflight reset all
behave the way they do in the reference binary. The tests
themselves come straight from the upstream project: each case
pairs an input class with the expected CSS from Tailwind's own
suites. <code>tw</code>'s runner reparses both sides so that whitespace
differences do not matter, and any other difference fails the
test.</p>
<p>The string API sits on top of a typed OCaml AST. <code>Tw.of_string</code>
parses each raw class into that AST, which is then lowered through
several stages into the final stylesheet. Building the AST
directly instead of going through strings catches misspelled
utilities as compile errors, and the combinators reject invalid
combinations:</p>
<pre><code class="language-ocaml">open Tw

let button =
  [ flex; items_center; gap 4; px 6; py 3;
    bg blue; hover [ bg ~shade:600 blue ];
    rounded_lg; shadow_sm;
    dark [ bg ~shade:800 gray; text ~shade:100 gray ] ]
</code></pre>
<p>The same binary produces the utility strings for HTML and the CSS
for the stylesheet, from one source, in one <code>dune build</code>.</p>
<h2>Shipping HTML and CSS together</h2>
<p>OCaml has several mature libraries for composing HTML fragments
type-safely. <a href="https://github.com/ocsigen/tyxml">Tyxml</a> gives you a
W3C-accurate AST; <a href="https://github.com/aantron/dream">Dream</a> has
<code>Dream.html</code>; <a href="https://erratique.ch/software/htmlit">Htmlit</a> is a
small combinator library; <a href="https://ocsigen.org/eliom/">Eliom</a>
crosses the client/server boundary. None of them answer the CSS
question. Styled fragments either carry inline <code>style=&quot;...&quot;</code>
attributes (no caching, no variants, no hover states) or refer to
classes defined in a separate hand-maintained stylesheet.</p>
<p>Classes in <code>tw</code> are first-class OCaml values. A UI fragment carries
the styles it needs as part of its type, and the build collects
every class used across every fragment into one stylesheet:</p>
<pre><code class="language-ocaml">open Tw_html

let card ~title ~body =
  div ~tw:Tw.[ flex; flex_col; p 6; bg white; rounded_lg; shadow_md ]
    [ h2 ~tw:Tw.[ text_lg; font_semibold; mb 2 ] [ txt title ];
      p  ~tw:Tw.[ text_sm; text ~shade:600 gray ] [ txt body ] ]
</code></pre>
<p>Reusing <code>card</code> from another page adds the eleven utilities it needs
to the generated CSS, deduplicated with the rest of the site. The
contract between the HTML tree and the stylesheet is two functions:</p>
<pre><code class="language-ocaml">Tw_html.to_tw : Tw_html.t -&gt; Tw.t list
Tw.to_css     : Tw.t list -&gt; Cascade.Css.t
</code></pre>
<p>No source scanner, no safelist, no <code>content: [...]</code> configuration.
Reusable components can be packaged as opam libraries and exchanged
between projects without a CSS-sharing protocol.</p>
<h2>Matching byte for byte</h2>
<p>Tailwind v4's engine is a CSS generator, so verifying the port means
comparing two stylesheets structurally, not as byte streams. A
string diff over 50,000 lines of minified CSS is unreadable; a
byte-for-byte target is only useful if you can see <em>which rule</em>
differs and <em>how</em> when it breaks.</p>
<p>Existing CSS diff tools either predate Tailwind v4 or are limited
to CSS Syntax Level 2/3, so they do not handle <code>@layer</code>,
<code>@container</code>, nesting, or modern colour spaces. So, as I have
discussed before, I had to write one:
<a href="https://gazagnaire.org/blog/2026-04-02-cascade.html">Cascade</a>, whose <code>cssdiff</code> CLI parses
both stylesheets into a typed AST (CSS Syntax Level 3 through
Selectors Level 5) and reports rule-level differences: this selector's <code>margin-top</code>
changed from <code>1.25em</code> to <code>1em</code>; this <code>@layer</code> has a new
declaration; this <code>@container</code> rule moved from position 10 to
position 200. The <code>tw</code> CLI wraps both halves. <code>tw -s &lt;class&gt;</code> asks
the basic question, what CSS is generated for this class, and
<code>tw -s &lt;class&gt; --diff</code> compares that output against the reference
<code>tailwindcss</code> using <code>cssdiff</code> (forcing <code>--optimized --minify</code> on
both sides).
Porting Tailwind became a tight loop around that second one-liner:
run, diff, fix, repeat. The same one-liner now drives a big chunk
of the test suite: differential testing against <code>tailwindcss</code>, one
class at a time.</p>
<p>The kind of bug <code>cssdiff</code> caught is the kind I would not have found
by reading the spec. A few examples from the commit log:</p>
<ul>
<li><strong>Width precision.</strong> <code>w-1/3</code> should print as <code>33.3333%</code>, not
<code>33.333333%</code>. Tailwind rounds fractional widths to four decimal
places. CSS does not require this; Tailwind does it for stable
diffs.</li>
<li><strong>Hex shortening.</strong> Tailwind compresses 8-digit hex colours to
4-digit form when each pair is repeated: <code>#ffffffcc</code> becomes
<code>#fffc</code>, but only inside opacity-modifier output.</li>
<li><strong>OKLCH out-of-gamut.</strong> Some OKLCH colours fall outside the sRGB
triangle; Tailwind's fallback renders the gamut-mapped sRGB
approximation per CSS Color Level 4. Implementing the mapping
algorithm was a small detour through colour science.</li>
<li><strong>Variant ordering.</strong> Doubly-nested
<code>hover:group-[&amp;_p]:hover:flex</code> must sort <em>after</em> both
single-nested forms, otherwise the optimiser cannot merge their
selectors. The comparator distinguishes three nesting levels.</li>
</ul>
<p>None of these are wrong; they are choices the reference binary
makes that only a strict diff would surface.</p>
<h2>Getting it</h2>
<pre><code class="language-sh">opam pin add tw https://github.com/samoht/tw.git
</code></pre>
<p>The code is on <a href="https://github.com/samoht/tw">GitHub</a> under the ISC
licence. An <code>opam</code> release will follow once the API stabilises.
Try it, and tell me what breaks.</p>
<p>The <a href="https://tailwindcss.com/">Tailwind Labs</a> team built the
framework this library targets; the reference <code>tailwindcss</code> binary
is the oracle every <code>tw</code> output is checked against. If you ship
anything built with <code>tw</code>,
<a href="https://github.com/sponsors/tailwindlabs">sponsor them</a>.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-04-21-tailwind-ocaml.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-04-21-tailwind-ocaml.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Tue, 21 Apr 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[Reimplementing the Space Protocol Stack from Scratch]]></title>
      <description><![CDATA[<p>A satellite link is a radio signal between a ground station and a
spacecraft moving at 7.5 km/s, visible for 10 minutes at a time,
over a channel measured in kilobits per second. If you have used
TCP/IP, the protocol structure will look familiar: there is a
transport layer that handles reliability, a network layer that
handles addressing, and an application layer where the actual data
lives. The difference is the link.</p>
<p>The protocol suite that handles this is
<a href="https://ccsds.org/">CCSDS</a> (Consultative
Committee for Space Data Systems), a set of standards maintained since
1982 by NASA, ESA, JAXA, and most other space agencies. Nearly every
satellite launched in the last 30 years speaks some subset of CCSDS.
If you want to talk to a satellite, this is the stack you need to
understand.</p>
<p>Most implementations are proprietary C libraries inside ground
segment frameworks. I wanted something I could test in a browser.</p>
<p>So I have been implementing these protocols in OCaml using the
<a href="https://gazagnaire.org/blog/2026-03-31-ocaml-wire.html">typed binary codec</a> approach I described
in the wire formats post. The demo below runs the real parser in the
browser, so you can play with the encodings directly. Type a
message and watch it get wrapped into a telemetry frame (Space
Packet inside a TM frame, exactly as a satellite would transmit
it). Hover any byte in the hex dump to see which protocol field it
belongs to.</p>
<div class="my-6 not-prose">
<div class="grid grid-cols-3 gap-3 items-end">
<div class="col-span-2">
<label class="text-xs text-gray-500 block mb-1">Message from the satellite</label>
<input id="ccsds-msg" type="text" value="Hello from orbit!" class="w-full text-sm p-2 border border-gray-300 rounded-md" placeholder="Type a message">
</div>
<div>
<button id="ccsds-encode-tm" class="w-full px-4 py-1.5 text-sm bg-gray-800 text-white rounded-md hover:bg-gray-700 transition-colors cursor-pointer">Transmit</button>
</div>
</div>
<div class="flex gap-4 mt-3 items-center flex-wrap text-xs text-gray-600">
<label title="Transfer frame type. TM=telemetry (downlink), TC=telecommand (uplink), AOS=advanced orbiting systems, USLP=unified space link protocol.">Frame
<select id="ccsds-frame-type" class="ml-1 px-1 py-0.5 border border-gray-300 rounded text-xs">
<option value="tm" selected>TM (132.0-B-3)</option>
<option value="tc">TC (232.0-B-4)</option>
<option value="aos">AOS (732.0-B-4)</option>
<option value="uslp">USLP (732.1-B-2)</option>
</select></label>
<label title="Spacecraft Identifier. Identifies which satellite sent this frame.">SCID <input id="ccsds-scid" type="number" min="0" max="1023" value="42" class="w-16 ml-1 px-1 py-0.5 border border-gray-300 rounded text-xs"></label>
<label title="Virtual Channel Identifier. Multiplexes different data streams on the same link.">VCID <input id="ccsds-vcid" type="number" min="0" max="7" value="0" class="w-12 ml-1 px-1 py-0.5 border border-gray-300 rounded text-xs"></label>
<label title="Application Process Identifier (11 bits). Identifies the application that produced this packet.">APID <input id="ccsds-apid" type="number" min="0" max="2047" value="1" class="w-16 ml-1 px-1 py-0.5 border border-gray-300 rounded text-xs"></label>
<label title="Sequence Count (14 bits). Increments with each packet from this APID.">Seq# <input id="ccsds-seq" type="number" min="0" max="16383" value="0" class="w-16 ml-1 px-1 py-0.5 border border-gray-300 rounded text-xs"></label>
<label class="flex items-center gap-1" title="Space Data Link Security (CCSDS 355.0-B-2). Encrypts and authenticates the frame payload."><input id="ccsds-sdls" type="checkbox"> SDLS</label>
<label class="flex items-center gap-1" title="Command Link Control Word (CCSDS 232.1-B-2). COP-1 retransmission feedback."><input id="ccsds-clcw" type="checkbox"> CLCW</label>
<label class="flex items-center gap-1" title="Frame Error Control Field. CRC-16 checksum for error detection."><input id="ccsds-fecf" type="checkbox" checked> FECF</label>
</div>
<input id="ccsds-hex" type="hidden">
<div class="grid grid-cols-2 gap-3 mt-3">
<div>
<div class="text-[10px] text-gray-500 uppercase tracking-wider mb-1">Hex dump <span class="normal-case tracking-normal">(editable)</span></div>
<pre id="ccsds-hex-dump" contenteditable="true" spellcheck="false" class="p-3 bg-slate-900 text-slate-200 rounded-md text-xs font-mono leading-relaxed overflow-auto min-h-[6rem] max-h-80 outline-none focus:ring-1 focus:ring-cyan-500/30"></pre>
</div>
<div>
<div class="text-[10px] text-gray-500 uppercase tracking-wider mb-1">Decoded layers <span class="normal-case tracking-normal">(hover to highlight bytes)</span></div>
<div id="ccsds-tree" class="p-3 bg-slate-900 rounded-md overflow-auto min-h-[6rem] max-h-80"></div>
</div>
</div>
</div>

<h2>The layers</h2>
<p>CCSDS protocols were designed to run on spacecraft with kilobytes of
RAM, fixed-point processors, and no operating system. The formats
are deliberately simple: small headers, fixed fields, no optional
extensions, no negotiation. A flight computer from the 1990s can
parse them. The layers that matter for most missions are described in the
Blue Books (the ratified standards). The newer stuff (recommended
practices like delay-tolerant networking) lives in the Magenta
Books, and the really new stuff (experimental specifications like
post-quantum crypto) in the Orange Books.</p>
<figure class="m-0 mx-auto max-w-sm cursor-pointer">
  <a href="https://gazagnaire.org/blog/images/ccsds-reference-model.png"><img class="w-full rounded-lg" src="https://gazagnaire.org/blog/images/ccsds-reference-model.png" alt="Space Communications Protocols Reference Model"></a>
  <figcaption class="text-xs text-gray-500 mt-2 leading-tight text-center">The CCSDS protocol reference model (Figure 2-1 from CCSDS 130.0-G-4), suggested by <a href="https://firobe.fr/">Virgile Robles</a>.</figcaption>
</figure>
<p>From top to bottom:</p>
<p><strong>Space Packets</strong> are the application-layer unit
(<a href="https://ccsds.org/Pubs/133x0b2e2.pdf">CCSDS 133.0-B-2</a>).
A Space Packet is a 6-byte header followed by up to 65,536 bytes of
data. The header contains a version number, an application identifier
(APID, 11 bits), a sequence counter (14 bits), and the data length.
That is it. No checksums, no encryption, no retry logic. Space Packets
are deliberately simple because everything else is handled by the
layers below.</p>
<p><strong>Transfer Frames</strong> carry Space Packets over the radio link. There
are two classic flavours: TM frames
(<a href="https://ccsds.org/Pubs/132x0b3.pdf">CCSDS 132.0-B-3</a>) for
telemetry (satellite to ground) and TC frames
(<a href="https://ccsds.org/Pubs/232x0b4e1c1.pdf">CCSDS 232.0-B-4</a>) for
telecommand (ground to satellite). A TM frame has a 6-byte header
with a spacecraft ID (10 bits), a virtual channel ID (3 bits), and
two frame counters. TC frames are similar but smaller, designed for
low-bandwidth uplinks. Both carry Space Packets as payload.</p>
<p>Two newer frame types exist for missions that need more flexibility:
AOS (<a href="https://ccsds.org/Pubs/732x0b4.pdf">CCSDS 732.0-B-4</a>)
adds an 8-bit spacecraft ID and 6-bit virtual channel ID with a
24-bit frame counter, and USLP
(<a href="https://ccsds.org/Pubs/732x1b3e1.pdf">CCSDS 732.1-B-2</a>)
unifies all four frame types into a single format with a 16-bit
spacecraft ID.</p>
<p><strong>Security</strong> sits between the frame header and the frame data.
<a href="https://ccsds.org/Pubs/355x0b2.pdf">SDLS</a> (CCSDS 355.0-B-2)
adds authentication (MAC), encryption (AES-GCM), and anti-replay
protection. Without SDLS, anyone with a dish and the right frequency
can send commands to a spacecraft. With SDLS, every command is
authenticated and every telemetry frame is encrypted.</p>
<p><strong>Reliability</strong> is handled by COP-1 (part of
<a href="https://ccsds.org/Pubs/232x1b2e2c1.pdf">CCSDS 232.1-B-2</a>), a
retransmission protocol for telecommand. The ground sends a command,
the spacecraft acknowledges receipt via a CLCW (Command Link Control
Word) embedded in the next telemetry frame. If the acknowledgement
is missing, the ground retransmits. This is where the 10-minute pass
window hurts most: every missed acknowledgement costs seconds that
cannot be recovered.</p>
<h2>How they compose</h2>
<p>A typical downlink looks like this: the application generates data,
wraps it in a Space Packet (6-byte header + payload), the packet is
placed inside a TM frame (6-byte frame header + packet + optional
error-correction trailer), SDLS encrypts and authenticates the frame,
and the frame is transmitted over the radio link. The ground station
receives the signal, decrypts the frame, extracts the packet, and
delivers the data to the mission control system.</p>
<p>A typical uplink is the reverse: the operator creates a command,
wraps it in a Space Packet, places it in a TC frame, SDLS
authenticates it, and the frame is transmitted during the next pass
window. COP-1 handles retransmission if the spacecraft does not
acknowledge.</p>
<p>The key insight is that each layer is independent. The Space Packet
does not know whether it is riding inside a TM frame or a USLP frame.
The security layer does not know what the packet contains. This is
the same separation that makes TCP/IP work: you can replace Ethernet
with Wi-Fi without changing HTTP.</p>
<h2>The OCaml implementation</h2>
<p>Each layer described above has its own library (one per frame type,
plus security and the feedback word):</p>
<div role="region"><table>
<tr>
<th>Protocol</th>
<th>Library</th>
<th>CCSDS spec</th>
</tr>
<tr>
<td>Space Packets</td>
<td><a href="https://tangled.org/gazagnaire.org/ocaml-space-packet">space-packet</a></td>
<td>133.0-B-2</td>
</tr>
<tr>
<td>TM frames</td>
<td><a href="https://tangled.org/gazagnaire.org/ocaml-tm">tm</a></td>
<td>132.0-B-3</td>
</tr>
<tr>
<td>TC frames</td>
<td><a href="https://tangled.org/gazagnaire.org/ocaml-tc">tc</a></td>
<td>232.0-B-4</td>
</tr>
<tr>
<td>AOS frames</td>
<td><a href="https://tangled.org/gazagnaire.org/ocaml-aos">aos</a></td>
<td>732.0-B-4</td>
</tr>
<tr>
<td>USLP frames</td>
<td><a href="https://tangled.org/gazagnaire.org/ocaml-uslp">uslp</a></td>
<td>732.1-B-2</td>
</tr>
<tr>
<td>Security (SDLS)</td>
<td><a href="https://tangled.org/gazagnaire.org/ocaml-sdls">sdls</a></td>
<td>355.0-B-2</td>
</tr>
<tr>
<td>Feedback word (CLCW)</td>
<td><a href="https://tangled.org/gazagnaire.org/ocaml-clcw">clcw</a></td>
<td>232.1-B-2</td>
</tr>
</table></div><p>I also have implementations of the file delivery protocol
(<a href="https://tangled.org/gazagnaire.org/ocaml-cfdp">CFDP</a>), the ground
station interface
(<a href="https://tangled.org/gazagnaire.org/ocaml-sle">SLE</a>), the
delay-tolerant networking stack for deep-space missions
(<a href="https://tangled.org/gazagnaire.org/ocaml-bundle">Bundle Protocol</a>,
<a href="https://tangled.org/gazagnaire.org/ocaml-ltp">LTP</a>,
<a href="https://tangled.org/gazagnaire.org/ocaml-cgr">Contact Graph Routing</a>),
and <a href="https://tangled.org/gazagnaire.org/ocaml-ccsds">most of the other Blue Book protocols</a>. I
will write more about them in the coming months and release the code
properly on <a href="https://github.com/parsimoni-labs">GitHub</a> once the
APIs stabilise.</p>
<p>All the libraries in the table use
<a href="https://github.com/parsimoni-labs/ocaml-wire">ocaml-wire</a> to
generate parsers and serialisers from a single typed definition.
The wire description <em>is</em> the specification: a typo in a field width
is a type error, not a runtime bug.</p>
<p>The parser descriptions are pure OCaml with no system dependencies.
The same definitions compile to JavaScript via
<a href="https://ocsigen.org/js_of_ocaml/">js_of_ocaml</a> and to verified C
via <a href="https://project-everest.github.io/everparse/">EverParse</a>. The
demo above runs the real <code>space-packet</code> parser in your browser, and
the SDLS crypto! The same code runs in the test harness on my laptop. The EverParse
path is still work in progress, but the generated C is zero-copy
and allocation-free, so there is no obvious blocker to embedding it
on a constrained target once the right glue code is written.
Building for
the browser first meant I could encode a frame, tweak a field, and
watch the hex change before writing a single test. Same compilation
story as the <a href="https://gazagnaire.org/blog/2026-04-02-cascade.html">CSS engine</a> and the
<a href="https://gazagnaire.org/blog/2026-04-07-ssa.html">conjunction assessment</a> dashboard.</p>
<p>Each library has an
<a href="https://github.com/mirage/alcotest">alcotest</a> suite with hand-written
cases drawn from the blue book examples, plus round-trip fuzz tests
using <a href="https://github.com/samoht/alcobar">Alcobar</a> (a Crowbar fork
with an Alcotest-compatible API). The fuzz tests caught real bugs
(an OCF/FECF masking error in the AOS encoder, for instance).</p>
<p>Several libraries have interoperability suites that round-trip frames
against external implementations: SDLS against NASA CryptoLib 1.5.1
(byte-identical TC frames with AES-256-GCM), USLP against
spacepackets-py (18 reference frames), LTP against NASA JPL's ION
4.1.4 (SDNV encoding), and CFDP against both spacepackets-py and
dariol83/ccsds (Java). The CFDP interop tests
<a href="https://github.com/us-irs/spacepackets-py/issues/143">found a byte-order bug</a>
in spacepackets-py 0.31.0, which is
<a href="https://github.com/us-irs/spacepackets-py/issues/143">already fixed</a>:
<code>KeepAlivePdu.pack()</code> was using native endian instead of big-endian for
the progress field, producing wrong bytes on every x86 and ARM machine. The CryptoLib interop tests also
surfaced <a href="https://github.com/nasa/CryptoLib/issues/500">questions about IV handling in AOS</a>
that may be related to a <a href="https://github.com/nasa/CryptoLib/issues/508">confirmed bug</a>
in SA Create. More interop suites are in progress.</p>
<p>The <a href="https://gazagnaire.org/blog/ocaml-wire">wire approach</a> I described in an earlier post
is what keeps these parsers compact and correct across every
layer. The whole stack is open source and being developed at
<a href="https://parsimoni.co/">Parsimoni</a> as part of a larger effort to
bring <a href="https://gazagnaire.org/blog/satellite-software">modern tooling to satellite
software</a>. If any of this is useful to you,
do <a href="mailto:thomas@parsimoni.co">drop me a line</a> -- I would love to
hear about it.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-04-15-ccsds-protocol-stack.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-04-15-ccsds-protocol-stack.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[Predicting Satellite Collisions in OCaml]]></title>
      <description><![CDATA[<p>In January 2026, two things happened that changed how satellite
operators think about collision risk. The Office of Space Commerce
published the
<a href="https://space.commerce.gov/dataset-for-conjunction-assessment-verification/">TraCSS verification dataset</a>:
open test data, with an answer key, for anyone who wants to build
software that predicts whether two satellites will collide. A few days
later, SpaceX unveiled <a href="https://starlink.com/updates/stargaze">Stargaze</a>,
a free collision-screening service powered by 30,000 star trackers
across the Starlink constellation. In Europe,
<a href="https://www.eusst.eu/">EUSST</a> already provides conjunction screening
for over 600 satellites. All three signal the same shift: tracking
what is in orbit is becoming shared infrastructure, not a proprietary
advantage.</p>
<p>I wanted to see what it takes to build a conjunction assessment
system from scratch. I wrote an orbit propagator, a screening
pipeline, and a 3D globe to visualise the results. The screening algorithm matches the TraCSS answer key
(spherical hard-body test cases), and the whole thing runs in the
browser.</p>
<h2>Cosmos 1408</h2>
<p>On November 15, 2021, Russia destroyed the Cosmos 1408 satellite with
a missile. The debris cloud (over 1,500 trackable fragments) passed
through the ISS orbit repeatedly. The crew sheltered in their escape
capsules.</p>
<div class="my-6 not-prose" >
<canvas id="globe-ssa" ></canvas>
</div>
<p class="text-xs text-gray-500 mt-1">ISS (blue) and Cosmos 1408 debris (red) on November 15, 2021. Orbital elements are approximate (for illustration, not operational use). The animation starts 30 minutes before closest approach and runs at 60x speed. Drag to rotate, scroll to zoom.</p>

<p>This is the kind of event conjunction assessment exists to predict.
Two objects in low Earth orbit approach each other at 10-15 km/s. Ground
stations track both, but no position estimate is exact. Each one comes
with an uncertainty envelope: not &quot;the satellite is here&quot;, but &quot;the
satellite is probably within this region&quot;. Collision prediction takes
those two uncertain positions at the moment of closest approach and
computes a probability: how likely is it that the objects actually
overlap? If the probability exceeds a threshold (typically 1 in
10,000), the operator fires thrusters to move out of the way.</p>
<p>The maths for computing that probability are well understood (a 2D
integral over projected uncertainties, first published by Foster and Estes in
1992). The hard part is not the algorithm. It is knowing whether your
implementation is correct. Getting a number is easy. Trusting the
number requires open specifications you can read, and test data you
can run your code against. Until recently, neither was easy to get.</p>
<h2>Five components, one pattern</h2>
<p>A conjunction assessment system needs five things:</p>
<p><strong>Orbit propagation.</strong> Given a satellite's orbital elements (published
as Two-Line Element sets on <a href="https://www.space-track.org/">Space-Track</a>
and other catalogues), predict where it will be at any future time. The standard algorithm is
SGP4 (Spacetrack Report #3),
designed by NORAD in the 1980s and still the baseline for all
catalogued objects. Vallado's
<a href="https://arc.aiaa.org/doi/10.2514/6.2006-6753">Revisiting Spacetrack Report #3</a>
is the modern reference; the
TraCSS verification dataset
provides test vectors.</p>
<p><strong>Coordinate transforms.</strong> The propagator outputs positions in one
reference frame, the collision geometry needs another, and the
visualisation needs a third. Each conversion involves Earth rotation
models and is easy to get subtly wrong. The IAU SOFA library
documents the <a href="https://www.iausofa.org/">standard algorithms</a>;
Vallado's <em>Fundamentals of Astrodynamics and Applications</em> covers
the practical implementation.</p>
<p><strong>Message parsing.</strong> Collision warnings arrive as Conjunction Data
Messages (CDMs), defined in
<a href="https://ccsds.org/Pubs/508x0b1e2c2.pdf">CCSDS 508.0-B-1</a>.
A CDM contains the predicted time of closest approach, the miss
distance, the relative speed, the uncertainty envelopes for both
objects, and metadata about the data source. The CDM format is another
CCSDS standard, similar to the ones I described in the
<a href="https://gazagnaire.org/blog/2026-03-31-ocaml-wire.html">wire formats</a> post. There are at least
three serialisation variants in active use (CSV from TraCSS, JSON from
Space-Track, and the CCSDS text standard).</p>
<p><strong>Probability computation.</strong> Project the two 3D uncertainty envelopes
onto the plane where collisions happen (perpendicular to the relative
velocity), then integrate the overlapping region. The result is a
single number: the probability of collision.
<a href="https://stacks.stanford.edu/file/druid:dg552pb6632/Foster-estes-parametric_analysis_of_orbital_debris_collision_probability.pdf">Foster and Estes (1992)</a>
describes the 2D method; the TraCSS dataset includes spherical test
cases with expected results for validation.</p>
<p><strong>Visualisation.</strong> Render the orbital geometry so an operator can see
what is happening. Two satellites converging at 14 km/s look identical
in a spreadsheet; on a globe, the crossing geometry is obvious. For
an operator deciding whether to manoeuvre in a window of minutes,
visual context matters.</p>
<p>The five components are standalone: I can test each one against its
own reference data before connecting them. Without Vallado's test
vectors I would not know if my propagator is correct; without the
TraCSS dataset I would not know if my probability code gives the
right answer. We learnt this in the MirageOS community when we
<a href="https://tarides.com/blog/2022-03-08-secure-virtual-messages-in-a-bottle-with-scop/">built an email parser</a>
and had to release a corpus of a million messages because nobody had
public test data. Open standards need open test data.</p>
<h2>ssa.space</h2>
<p>The result is <a href="https://ssa.space">ssa.space</a>, a conjunction assessment
dashboard. It loads the TraCSS verification dataset, propagates every
tracked object, computes the collision probability for each close
approach, and renders the results on a 3D globe. Close approaches are
colour-coded by risk: red for dangerous, orange for watch-list, green
for low. Click one to zoom in and see the orbital geometry at the
moment of closest approach.</p>
<p>This runs on test data today (the TraCSS dataset includes difficult
cases but is not live operational data). The entire stack is pure
OCaml with no system dependencies, so it runs in the browser (via
js_of_ocaml), on a server, and could run as a
<a href="https://mirageos.org/">MirageOS</a> unikernel on a satellite (the same
architecture I described in
<a href="https://gazagnaire.org/blog/2026-02-23-asplos-unikernels.html">the ASPLOS post</a>). That is the
direction we are pursuing with <a href="https://parsimoni.co/">SpaceOS</a>.</p>
<p>Today, collision avoidance runs through ground stations. A conjunction
warning arrives 24-72 hours before closest approach, but the ground
loop (downlink, screening, decision, command uplink) can consume most
of that window. If the satellite runs the same screening algorithm
on board, it can flag close approaches and pre-compute candidate
manoeuvres before the next ground contact. The ground still approves,
but time to first assessment drops to seconds.</p>
<h2>What comes next</h2>
<p>The application is live at <a href="https://ssa.space">ssa.space</a>. The
underlying libraries
(<a href="https://tangled.org/gazagnaire.org/ocaml-sgp4">sgp4</a>,
<a href="https://tangled.org/gazagnaire.org/ocaml-coordinate">coordinate</a>,
<a href="https://tangled.org/gazagnaire.org/ocaml-cdm">cdm</a>,
<a href="https://tangled.org/gazagnaire.org/ocaml-collision">collision</a>,
<a href="https://tangled.org/gazagnaire.org/ocaml-globe">globe</a>,
<a href="https://tangled.org/gazagnaire.org/ocaml-cam">cam</a>) are work in
progress and will be properly open-sourced when a full review is
completed (currently validating against
<a href="https://etd.gsfc.nasa.gov/capabilities/capabilities-listing/general-mission-analysis-tool-gmat/">GMAT</a>,
NASA's General Mission Analysis Tool). The immediate goal is live
data from TraCSS, Stargaze, or <a href="https://www.eusst.eu/">EUSST</a>. If
you have ephemeris data or screening results,
<a href="mailto:thomas@gazagnaire.org">get in touch</a>.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-04-07-ssa.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-04-07-ssa.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[A CSS Engine in OCaml]]></title>
      <description><![CDATA[<p>I have rewritten my website in OCaml more times than I can count
(from <a href="https://github.com/mirage/ocaml-cow">ocaml-cow</a> to
<a href="https://github.com/Engil/Canopy">Canopy</a> to custom
<a href="https://gazagnaire.org/blog/2026-02-23-asplos-unikernels.html">MirageOS unikernels</a>). This time,
the styling was the problem: I use
<a href="https://tailwindcss.com/">Tailwind CSS</a>, and I wanted the entire
pipeline (Markdown to styled HTML to CSS) to be a single <code>dune build</code>
with no Node.js dependency.</p>
<p>That meant porting Tailwind's CSS generation to OCaml. To make sure
the port was correct, I needed to compare its CSS output against the
JavaScript original. The existing CSS diff tools are either abandoned or limited to CSS
2/3, and none of them handle <code>@layer</code>, container queries, nesting, or
the modern colour spaces that Tailwind v4 generates. So I started writing one. Which needed a
complete parser. Which grew into a typed AST and an optimiser, and ended up being
useful on its own.</p>
<p>The result is <a href="https://github.com/samoht/cascade">Cascade</a>, a
30,000-line OCaml library for parsing, generating, optimising, and
diffing CSS. One of the tools it ships is <code>cssdiff</code>, a structural
CSS comparison tool. Since Cascade is pure OCaml, it can be compiled to
JavaScript via <a href="https://ocsigen.org/js_of_ocaml/">js_of_ocaml</a> and
run in the browser. Try it, or pick one of the examples below:</p>
<div class="my-6 not-prose">
<div class="grid grid-cols-2 gap-3">
<div>
<label class="text-xs text-gray-500 block mb-1">Left</label>
<textarea id="css-left" rows="8" class="w-full font-mono text-sm p-2 border border-gray-300 rounded-md resize-y" placeholder=".btn { color: red; }"></textarea>
</div>
<div>
<label class="text-xs text-gray-500 block mb-1">Right</label>
<textarea id="css-right" rows="8" class="w-full font-mono text-sm p-2 border border-gray-300 rounded-md resize-y" placeholder=".btn { color: blue; }"></textarea>
</div>
</div>
<div class="flex gap-2 mt-3 items-center flex-wrap">
<button id="cssdiff-compare" class="px-4 py-1.5 text-sm bg-gray-800 text-white rounded-md hover:bg-gray-700 transition-colors cursor-pointer">Compare</button>
<span class="text-xs text-gray-500">Examples:</span>
<button  class="px-2 py-0.5 text-xs bg-gray-100 text-gray-700 border border-gray-300 rounded cursor-pointer hover:bg-gray-200 transition-colors">@layer reorder</button>
<button  class="px-2 py-0.5 text-xs bg-gray-100 text-gray-700 border border-gray-300 rounded cursor-pointer hover:bg-gray-200 transition-colors">Nesting</button>
<button  class="px-2 py-0.5 text-xs bg-gray-100 text-gray-700 border border-gray-300 rounded cursor-pointer hover:bg-gray-200 transition-colors">Container queries</button>
<button  class="px-2 py-0.5 text-xs bg-gray-100 text-gray-700 border border-gray-300 rounded cursor-pointer hover:bg-gray-200 transition-colors">color-mix()</button>
</div>
<pre id="css-output" class="mt-3 p-3 bg-slate-800 text-slate-200 rounded-md text-xs leading-relaxed overflow-auto max-h-96 min-h-8 hidden"></pre>
</div>


<h2>The toolkit</h2>
<p>Cascade parses CSS strings into a typed AST, and provides a small eDSL
to build the same AST from OCaml code. From that AST, it can render
(pretty-print or minify), optimise (deduplicate and merge rules), and
structurally diff two stylesheets. The parser uses hand-written recursive descent
(does <a href="https://claude.ai/code">Claude</a> have hands?) covering
<a href="https://www.w3.org/TR/css-syntax-3/">CSS Syntax Level 3</a> through
<a href="https://www.w3.org/TR/selectors-4/">Level 4 and 5</a> (modern
selectors, colour spaces, <code>@layer</code>, container queries, nesting).</p>
<div >
<figure >
<pre><code class="language-ocaml">let css = Cascade.Css.of_string {|
  .btn {
    display: inline-block;
    background-color: #3b82f6;
    color: white;
    padding: 0.5rem;
  }
  .btn {
    background-color: #2563eb;
  }
|}
</code></pre>
<figcaption>
<p><strong>Parse.</strong> The input has a duplicate <code>.btn</code> rule (common in generated
CSS). The parser preserves both and returns a typed AST.</p>
</figcaption>
</figure>
<figure >
<pre><code class="language-ocaml">open Cascade.Css

let btn = Selector.class_ &quot;btn&quot;

let rules =
  [ rule ~selector:btn
      [ display Inline_block
      ; background_color (hex &quot;#3b82f6&quot;)
      ; color (hex &quot;#ffffff&quot;)
      ; padding (Rem 0.5) ]
  ; rule ~selector:btn
      [ background_color (hex &quot;#2563eb&quot;) ] ]
</code></pre>
<figcaption>
<p><strong>Generate.</strong> The same two rules, built from OCaml values. A typo like
<code>dsiplay</code> is a compile error. Passing a colour to <code>padding</code> is a type
error.</p>
</figcaption>
</figure>
</div>
<div >
<figure >
<pre><code class="language-css">.btn {
  display: inline-block;
  background-color: #3b82f6;
  color: #fff;
  padding: 0.5rem;
}
.btn {
  background-color: #2563eb;
}
</code></pre>
<figcaption>
<p><strong>Print.</strong> <code>Css.to_string (Css.v rules)</code> renders the AST back to CSS.
Both rules are preserved. The printer shortens <code>white</code> to <code>#fff</code>.</p>
</figcaption>
</figure>
<figure >
<pre><code class="language-css">.btn {
  display: inline-block;
  background-color: #2563eb;
  color: #fff;
  padding: 0.5rem;
}
</code></pre>
<figcaption>
<p><strong>Optimise.</strong> <code>Css.to_string ~optimize:true (Css.v rules)</code> merges the
two <code>.btn</code> rules, keeping the last <code>background-color</code> (cascade order).
It respects <code>!important</code> and preserves intentional patterns like
<code>content</code> fallbacks.</p>
</figcaption>
</figure>
</div>
<p>The <code>.btn</code> example uses a few properties, but the library has 100+
typed constructors covering layout (box model, flexbox, grid), visual
(typography, transforms, animations, filters), and logical properties.</p>
<h2>Testing against Tailwind</h2>
<p>How do you know a CSS parser is correct? The usual answer is &quot;read the
spec&quot;, and the <a href="https://github.com/web-platform-tests/wpt">W3C test suite</a>
covers parsing for individual features. But those tests check that browsers <em>apply</em> CSS correctly, not that a
tool can safely <em>transform</em> it. How do you know that merging two rules
across cascade layers or deduplicating a property does not change what
the page looks like?</p>
<p>One approach that I found works well in practice is differential
comparison against a reference implementation. For instance, to port
Tailwind to OCaml, I run the JavaScript Tailwind CLI on the same input,
then run the OCaml port, and compare the two CSS outputs byte for
byte. The comparison is strict: same characters, same order, same
whitespace. Not &quot;structurally equivalent&quot; (that would hide bugs in the
comparison tool itself).</p>
<p>When the outputs diverge (and they do, constantly, during
development), I need to know <em>which rule</em> changed and <em>how</em>. A
50,000-line string diff is unreadable. That is what <code>cssdiff</code> is
for: a structural diff that says &quot;rule <code>.mt-4</code> changed <code>margin-top</code>
from <code>1rem</code> to <code>16px</code>&quot; is actionable. It works at the AST level, so
a rule that moved from line 10 to line 200 shows as reordered, not as
a deletion plus an addition. It handles <code>@media</code>, <code>@layer</code>,
<code>@supports</code>, and <code>@container</code> blocks. This is the same tool running
in the demo above.</p>
<p>The byte-for-byte target also keeps the optimiser honest. It would be
easy to produce &quot;correct&quot; CSS that differs in harmless ways (different
property order, different hex capitalisation). But allowing those
differences means the diff tool would need to know which differences
are harmless, which is exactly the kind of subtle bug that hides
semantic errors. Matching the reference output exactly eliminates that
class of problems.</p>
<h2>Getting started</h2>
<p>Two CLI tools ship with the library: <code>cascade</code> (format, minify,
optimise CSS files) and <code>cssdiff</code> (structural comparison).</p>
<pre><code class="language-sh">brew install samoht/tap/cascade
</code></pre>
<p>Or via opam:</p>
<pre><code class="language-sh">opam pin add cascade https://github.com/samoht/cascade.git
</code></pre>
<p>The library requires OCaml 4.14 or later. The
<a href="https://github.com/samoht/cascade">README</a> has the full API overview
and CSS specification coverage table.</p>
<p>I wrote Cascade because I needed it for my blog. It turned out to be
useful beyond that: any OCaml project that generates, parses, or
compares CSS now has a typed alternative to string manipulation. The
source is on GitHub.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-04-02-cascade.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-04-02-cascade.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[Describing Binary Formats in OCaml]]></title>
      <description><![CDATA[<p><a href="https://gazagnaire.org/blog/2026-02-25-satellite-software.html">Satellites are becoming software platforms</a>.
More software means more bugs, and bugs 400 km overhead are not easy
to fix. Binary parsers written in C are a recurring weak spot: the
<a href="https://doi.org/10.2514/6.2022-4380">KA-SAT attack</a> bricked
thousands of satellite modems by exploiting the management interface,
and researchers have since demonstrated
<a href="https://www.usenix.org/conference/usenixsecurity24/presentation/bisping">RF signal injection</a>
against VSAT terminal firmware by crafting inputs that exploit parsing
flaws.</p>
<p><a href="https://project-everest.github.io/everparse/">EverParse</a> has a good
answer to that problem, for on-earth software. It generates C
validators and parsers with proofs of memory safety, arithmetic
safety, double-fetch freedom, and correctness. It is part of
Microsoft's <a href="https://project-everest.github.io/">Project Everest</a>,
which ran from 2016 to 2021 around
<a href="https://fstar-lang.org/">F*</a> (a dependently typed language from the ML family) and produced
software that ended up in the Windows kernel, Hyper-V, Linux,
Firefox, and Python.</p>
<p>The EverParse compiler seems like a perfect fit for embedded satellite
software. I wanted to try it, but I did not want to write
<a href="https://project-everest.github.io/everparse/3d-lang.html"><code>.3d</code></a>
files by hand (how do you maintain those? and how do you write
serialisers from them?). So I
wrote <a href="https://github.com/parsimoni-labs/ocaml-wire">ocaml-wire</a>, a
combinator library that lets me describe a format once in OCaml, automatically
write zero-allocation (when possible) parser and serialiser codecs, and generate the
<code>.3d</code> schema and C glue from the same description.</p>
<p>I will use
<a href="https://newspaceeconomy.ca/wp-content/uploads/2024/05/130x0g4e1.pdf">CCSDS Space Packets</a>
as the running example, because they are small, bitfield-heavy, and
annoying to parse by hand.</p>
<h2>EverParse</h2>
<p>For a CCSDS Space Packet header, the <code>.3d</code> format is already close to
the specification:</p>
<pre><code>module SpacePacket

typedef struct SpacePacket {
  UINT16BE Version:3;
  UINT16BE Type:1;
  UINT16BE SecHdrFlag:1;
  UINT16BE APID:11;
  UINT16BE SeqFlags:2;
  UINT16BE SeqCount:14;
  UINT16BE DataLength;
} SpacePacket;
</code></pre>
<p>You can read this as two 16-bit words of bitfields, followed by
a 16-bit <code>DataLength</code> field. EverParse turns this into a C validator
you could link into flight software. The guarantees are useful: memory safety, arithmetic safety,
double-fetch freedom, and correctness with respect to the <code>.3d</code>
specification. They do not prove that the <code>.3d</code> file matches the blue
book, nor that the C extraction from F* or the C compiler are
bug-free. Still, this is exactly the kind of low-level code where
mistakes hurt.</p>
<p>What <code>.3d</code> does not give me is the OCaml side. I would still need
separate code for decoding, encoding, and field access.</p>
<h2>The Same Header in OCaml</h2>
<p>ocaml-wire lets me
describe the same header once in OCaml, then use it directly in OCaml
and project it to EverParse <code>.3d</code>. It can also generate the OCaml/C
glue to call the verified validator. So, for the Space Packet primary
header at least, I get an OCaml codec and a C validator from the same source.</p>
<p>Define named fields with <code>Field.v</code>, bind them to record projections
with <code>$</code>, and assemble a codec with <code>Codec.v</code>:</p>
<pre><code class="language-ocaml">open Wire

type packet = {
  version : int; type_ : int; sec_hdr : int; apid : int;
  seq_flags : int; seq_count : int; data_len : int;
}

let f_version   = Field.v &quot;Version&quot;    (bits ~width:3  U16be)
let f_type      = Field.v &quot;Type&quot;       (bits ~width:1  U16be)
let f_sec_hdr   = Field.v &quot;SecHdrFlag&quot; (bits ~width:1  U16be)
let f_apid      = Field.v &quot;APID&quot;       (bits ~width:11 U16be)
let f_seq_flags = Field.v &quot;SeqFlags&quot;   (bits ~width:2  U16be)
let f_seq_count = Field.v &quot;SeqCount&quot;   (bits ~width:14 U16be)
let f_data_len  = Field.v &quot;DataLength&quot;  uint16be

(* Bind fields before building the codec. The same bound fields are then
   reused for get/set. *)
let bf_version   = Codec.(f_version   $ fun p -&gt; p.version)
let bf_type      = Codec.(f_type      $ fun p -&gt; p.type_)
let bf_sec_hdr   = Codec.(f_sec_hdr   $ fun p -&gt; p.sec_hdr)
let bf_apid      = Codec.(f_apid      $ fun p -&gt; p.apid)
let bf_seq_flags = Codec.(f_seq_flags $ fun p -&gt; p.seq_flags)
let bf_seq_count = Codec.(f_seq_count $ fun p -&gt; p.seq_count)
let bf_data_len  = Codec.(f_data_len  $ fun p -&gt; p.data_len)

let packet_codec =
  let open Codec in
  v &quot;SpacePacket&quot;
    (fun version type_ sec_hdr apid seq_flags seq_count data_len -&gt;
      { version; type_; sec_hdr; apid; seq_flags; seq_count; data_len })
    [ bf_version;
      bf_type;
      bf_sec_hdr;
      bf_apid;
      bf_seq_flags;
      bf_seq_count;
      bf_data_len ]
</code></pre>
<p>The generated <code>.3d</code> is a projection of that OCaml description. It is not
a second source of truth:</p>
<pre><code>entrypoint
typedef struct _SpacePacket(mutable WireCtx *ctx)
{
   UINT16BE Version : 3 {:act WireSetU16BE(ctx, (UINT32) 0, Version); };
   UINT16BE Type : 1 {:act WireSetU16BE(ctx, (UINT32) 1, Type); };
   UINT16BE SecHdrFlag : 1 {:act WireSetU16BE(ctx, (UINT32) 2, SecHdrFlag); };
   UINT16BE APID : 11 {:act WireSetU16BE(ctx, (UINT32) 3, APID); };
   UINT16BE SeqFlags : 2 {:act WireSetU16BE(ctx, (UINT32) 4, SeqFlags); };
   UINT16BE SeqCount : 14 {:act WireSetU16BE(ctx, (UINT32) 5, SeqCount); };
   UINT16BE DataLength {:on-success WireSetU16BE(ctx, (UINT32) 6, DataLength);
                         return true; };
} SpacePacket;
</code></pre>
<p>The annotations in the output show how EverParse uses the codec at
validation time. <code>:act</code> writes each validated field value into an
output structure (via the <code>WireSet*</code> callbacks); <code>:on-success</code> is the
longer form that can conditionally fail. So the generated C parser
validates and extracts in a single pass.</p>
<p>The same codec also gives zero-copy field access on the OCaml side:</p>
<pre><code class="language-ocaml">(* Stage once, reuse the closure *)
let get_apid = Staged.unstage (Codec.get packet_codec bf_apid)
let set_seq  = Staged.unstage (Codec.set packet_codec bf_seq_count)

let apid = get_apid buf 0        (* zero allocation for int fields *)
let () = set_seq buf 0 42        (* read-modify-write for bitfields *)

(* Full record decode/encode when needed *)
let pkt = Result.get_ok (Codec.decode packet_codec buf 0)
let () = Codec.encode packet_codec pkt buf 0
</code></pre>
<p>The Space Packet header has a fixed layout, but many formats have
variable-length payloads. For those, dependent sizes use <code>Field.ref</code>
to let one field's value determine the size of another:</p>
<pre><code class="language-ocaml">let f_len  = Field.v &quot;Length&quot; uint16be
let f_data = Field.v &quot;Data&quot; (byte_array ~size:(Field.ref f_len))
</code></pre>
<h2>Testing against C</h2>
<p>The same description drives testing against the C path:</p>
<pre><code class="language-ocaml">let schema = Everparse.schema packet_codec
let () = Wire_3d.run ~outdir:&quot;schemas&quot; [ schema ]
let () = Wire_stubs.generate ~schema_dir:&quot;schemas&quot; ~outdir:&quot;.&quot;
           [ C packet_codec ]
</code></pre>
<p><code>Wire_3d.run</code> writes the <code>.3d</code> files, invokes EverParse, and produces
C validators. <code>Wire_stubs.generate</code> writes the OCaml/C FFI glue
(external declarations and C stubs) so OCaml code can call the
verified validators directly.</p>
<p>The <code>fuzz/</code> tests use
<a href="https://github.com/stedolan/crowbar">Crowbar</a> to check that the
OCaml codec does not crash on random input. Separately, <code>test/diff/</code> generates random
schemas, runs EverParse to produce C validators, and checks that OCaml
and C agree on every random input. One OCaml description defines the
codec, the schema, and the test oracle.</p>
<h2>Performance</h2>
<p>Describing a format is one thing; the question is whether the
generated codec stays fast enough for real-time packet processing.
The benchmarks below measure three common operations (routing, frame
reassembly, status polling) and compare the Wire OCaml path with
C loops built on EverParse validators (generated from the same OCaml
codecs). The protocols come from
CCSDS
(Space Packets, TM Frames, CLCW words), which travel over
<a href="https://www.star-dundee.com/spacewire/getting-started/an-overview-of-the-spacewire-standard/">SpaceWire</a>
links at up to 200 Mbit/s.</p>
<div >
<figure >
<pre >  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |Versi|T|S|        APID         |Seq|         SeqCount          |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |          DataLength           |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</pre>
<figcaption>Space Packet (6 bytes)</figcaption>
</figure>
<div>
<p><strong>Packet routing</strong> (10M variable-size packets in a 1.2 GB stream). A
satellite receives interleaved telemetry from different subsystems
(housekeeping, science, diagnostics). Each Space Packet carries an
11-bit Application Process Identifier (APID) that says which subsystem
sent it. The router reads the 6-byte header, extracts the APID, looks
up a handler in a 2048-entry table, and advances to the next packet.
This is the tightest loop in a ground station receiver.</p>
</div>
</div>
<div >
<div>
<p><strong>Status polling</strong> (1M words). The CLCW is a 32-bit status word that
the spacecraft sends back inside every telemetry frame to report the
state of its command link (locked out? waiting? retransmitting?). The
ground system polls it continuously to detect anomalies. The word
packs 13 bitfields, but the polling loop only needs 4 of them.</p>
</div>
<figure >
<pre >  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |C|CLC|Statu|COP|   VCID    |Spare|N|N|L|W|R|FAR|  ReportValue  |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</pre>
<figcaption>CLCW (32 bits)</figcaption>
</figure>
</div>
<div >
<figure >
<pre >  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |Ver|       SCID        |VCID |O|    MCCount    |    VCCount    |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |S|S|P|Seg|     FirstHdrPtr     |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</pre>
<figcaption>TM Frame (6 bytes)</figcaption>
</figure>
<div>
<p><strong>Frame reassembly</strong> (1M frames, 15M packets extracted). Telemetry
travels in fixed-size frames (1115 bytes each), but Space Packets are
variable-size and can span frame boundaries. The TM Frame header has
a FirstHdrPtr field that points to where the first complete packet
starts inside the frame. The reassembler walks each frame, extracts
the embedded packets, and stitches together packets that were split
across two frames. This is the most complex of the three benchmarks.</p>
</div>
</div>
<p>The chart below shows wall-clock time per operation (lower is better).
Each benchmark has two bars: the C baseline (blue, using EverParse
validators) and the Wire OCaml path (orange). The ratio column shows
how much slower (or faster) Wire is compared to C. A ratio below 1.0
means Wire wins.</p>
<div >
<div >
<div ></div>
<div >ns/op</div>
<div >ratio</div>
<div >Packet routing</div>
<div >
<div  title="C: 5.5 ns"></div>
<span >5.5 ns</span>
</div>
<div></div>
<div></div>
<div >
<div  title="Wire: 14.1 ns"></div>
<span >14.1 ns</span>
</div>
<div >2.6x</div>
<div >Frame reassembly</div>
<div >
<div  title="C: 64.2 ns"></div>
<span >64.2 ns</span>
</div>
<div></div>
<div></div>
<div >
<div  title="Wire: 67.4 ns"></div>
<span >67.4 ns</span>
</div>
<div >1.0x</div>
<div >Status polling</div>
<div >
<div  title="C: 6.9 ns"></div>
<span >6.9 ns</span>
</div>
<div></div>
<div></div>
<div >
<div  title="Wire: 5.2 ns"></div>
<span >5.2 ns</span>
</div>
<div >0.8x</div>
</div>
<div >
<span><span ></span> C (EverParse, -O2)</span>
<span><span ></span> Wire (OCaml 5.4.1, --profile=release)</span>
<span>Median, 10 runs, Apple M5 Max</span>
</div>
</div>
<p>Reading the chart from top to bottom: packet routing is 2.6x slower
than C, which sounds like a lot until you note that 14.1 ns per packet
gives 71 Mpkt/s (a SpaceWire link at 200 Mbit/s with 128-byte packets
carries roughly 200 kpkt/s, so Wire has 350x headroom). Frame
reassembly is at parity. And status polling is the surprise: Wire is
<em>faster</em> than C.</p>
<p>One caveat: the C baseline uses EverParse validators that extract
all fields (7 for a Space Packet, 13 for a CLCW word), while the
OCaml path uses <code>Staged.unstage (Codec.get ...)</code> to read only the
fields it needs. That makes the comparison
slightly unfair to C, but it reflects how the two paths would be used
in practice. Wire wins on CLCW precisely because it loads the 4-byte
word once and extracts only the 4 fields the polling loop needs.</p>
<p>Additional per-field micro-benchmarks confirm the pattern. Reading an 11-bit
bitfield like <code>SpacePacket.apid</code> takes 3.4 ns with zero allocation.
Writing a bitfield (read-modify-write) runs at 2–3 ns.</p>
<p>The remaining 2.5x gap in routing comes from three sources of
overhead in OCaml's runtime:</p>
<ul>
<li><strong>Function dispatch.</strong> The staged field readers are closures, not
inlined code. Each call goes through an indirect dispatch (about 11
extra instructions per call).</li>
<li><strong>Thread-safety barriers.</strong> OCaml 5 inserts memory barriers on
every mutable write to support multicore. C writes are plain stores.</li>
<li><strong>Register pressure.</strong> The compiler cannot keep state in registers
across closure calls, so it spills to the stack between reads.</li>
</ul>
<p>None of this is specific to Wire. It is the cost of OCaml 5's runtime
model. <a href="https://blog.janestreet.com/oxidizing-ocaml-locality/">OxCaml</a>
would address all three (unboxed closures, thread-local stores, better
register allocation across calls), but that is the story for another
post.</p>
<h2>Getting started</h2>
<p>To try the examples from this post, clone the repo and build:</p>
<pre><code class="language-sh">git clone https://github.com/parsimoni-labs/ocaml-wire.git
cd ocaml-wire
opam install . --deps-only
dune build
</code></pre>
<p>The library requires OCaml 5.1 or later. The codec definitions from
this post are in
<a href="https://github.com/parsimoni-labs/ocaml-wire/tree/main/examples/space">examples/space</a>
(Space Packet, CLCW, TM Frame);
<a href="https://github.com/parsimoni-labs/ocaml-wire/tree/main/examples/net">examples/net</a>
does the same for Ethernet, IPv4, TCP, and UDP. Generating <code>.3d</code>
files works out of the box; compiling them into verified C needs
<a href="https://project-everest.github.io/everparse/3d-lang.html#installing-3d">EverParse</a>
(<code>3d.exe</code> in <code>PATH</code>).</p>
<p>Satellite protocols have many binary formats: Space Packets, TM
frames, CLCW words, telecommand segments, and more. I do not want to
write C parsers and serialisers for each of them by hand. I want to
describe the format once in OCaml, get a codec I can use and compose the layers directly, and
generate the EverParse <code>.3d</code> and C glue from the same source.
<code>ocaml-wire</code> is still experimental, but I already find it quite useful
(if only to get ASCII art diagrams of packet headers :-). With OxCaml closing the remaining routing gap, the C side might matter
even less (apart from differential testing against a formally verified
parser). If you want to try it,
<a href="https://github.com/parsimoni-labs/ocaml-wire">the repo is here</a>.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-03-31-ocaml-wire.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-03-31-ocaml-wire.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Tue, 31 Mar 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[From Cannes to Los Angeles: Visiting the People Who Build Satellites]]></title>
      <description><![CDATA[<p>Four years ago, Thales Alenia Space invited me into their integration
hall in Cannes. I was there to sketch ideas about running
<a href="https://gazagnaire.org/blog/2026-02-23-asplos-unikernels.html">the unikernel software we had been building at Cambridge</a>
on satellites. <a href="https://www.esa.int/Science_Exploration/Space_Science/Euclid">Euclid</a> was in the corner of the room, being prepared for
its vacuum chamber test. That kind of early trust meant a lot -- a small
team with a whiteboard, in the same building as a spacecraft about to travel
1.5 million kilometres from Earth. It led further than either of us
expected, to
<a href="https://tarides.com/blog/2023-12-29-announcing-the-orchide-project-powering-satellite-innovation/">ORCHIDE</a>
(Horizon Europe) and <a href="https://tarides.com/blog/2025-05-30-ceos-project-kick-off-using-satellites-to-survey-the-earth/">CEOS</a>
(BPI France), two major European consortium projects built around those same ideas.</p>
<p>Last month I spent a week in Southern California visiting the people who
build satellites. One morning I dropped my daughter at pre-school and walked to
the building next door, where fifteen satellites were in simultaneous
integration at different stages of assembly. That is what the LA space
industry looks like: it is just there, woven into the neighbourhood.
Another factory nearby was targeting high-volume production with a
backlog of over forty spacecraft. A space avionics manufacturer <a href="https://parsimoni.co/blog/2025-11-13-parsimoni-and-innoflight-partner-to-expand-secure-space-computing-capabilities-for-u-s-and-european-markets.html">we have since partnered with</a>,
whose hardware has flown on dozens of missions over twenty years. Engineers at <a href="https://gazagnaire.org/blog/2026-02-17-los-angeles.html">NASA/JPL</a> developing
<a href="https://gazagnaire.org/blog/2026-03-10-ocaml-fpp.html">a reusable flight software framework</a>. Teams from <a href="https://parsimoni.co/blog/2025-09-08-parsimoni-joins-techstars-space-accelerator-fall-2025-cohort.html">Techstars Space</a>
building ground segment tooling and application software that will run
on other people's satellites. All of this within an hour's drive of each other. And the
questions were different from 2022 -- not &quot;could this work?&quot; but &quot;how
big is the binary, what RTOS do you replace, how do you handle OTA updates,
what does your <a href="https://gazagnaire.org/blog/2026-02-25-satellite-software.html">security compliance story</a> look like?&quot;</p>
<div class="image-grid grid grid-cols-1 sm:grid-cols-3 gap-4 my-8 not-prose">
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/euclid-tas-cannes.jpg"><img class="w-full aspect-[4/3] object-cover rounded-lg" src="https://gazagnaire.org/blog/images/euclid-tas-cannes.jpg" alt="Euclid being integrated at Thales Alenia Space Cannes"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">Euclid in the TAS Cannes integration hall (2022), about to enter the vacuum chamber. It launched in July 2023 and is now 1.5 million kilometres from Earth. ©ESA/Airbus/Thales Alenia Space.</figcaption>
  </figure>
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/rocket-lab-factory.webp"><img class="w-full aspect-[4/3] object-cover rounded-lg" src="https://gazagnaire.org/blog/images/rocket-lab-factory.webp" alt="Rocket Lab spacecraft production floor in Long Beach"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">A large satellite production line I visited last month. ©Rocket Lab.</figcaption>
  </figure>
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/IMG_0567.jpeg"><img class="w-full aspect-[4/3] object-cover rounded-lg" src="https://gazagnaire.org/blog/images/IMG_0567.jpeg" alt="Skywriting heart over Playa Vista, Los Angeles"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">Playa Vista, LA. Space + LA = ♥</figcaption>
  </figure>
</div>
<p>Launch costs are falling and production volumes are rising, and how you
produce space software at scale has to keep up. Ideas I have spent years on
(library operating systems, <a href="https://tarides.com/blog/2023-12-14-ocaml-memory-safety-and-beyond/">building software that is correct by construction</a>)
are turning out to be well-timed. I am looking forward to sending more OCaml
code to space this year. <a href="https://parsimoni.co/index.html#contact">Ping me</a>
if you are interested ;-)</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-03-12-satellite-factories.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-03-12-satellite-factories.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[Apparently I Have Been Writing Flight Software All Along]]></title>
      <description><![CDATA[<p>Back in 2012, <a href="https://anil.recoil.org/">Anil Madhavapeddy</a> and I
were building MirageOS as a library OS, and we started to accumulate
a lot of libraries to manage manually. So I wrote
<a href="https://opam.ocaml.org/">opam</a> to handle the dependencies and the
<a href="https://github.com/mirage/mirage"><code>mirage</code></a> tool, an embedded DSL to describe device trees and
auto-generate the build and wiring code.
<a href="https://gabriel.radanne.net/">Gabriel Radanne</a> later refined and
<a href="https://arxiv.org/abs/1905.02529">formalised</a> it as Functoria,
which made the correspondence between device graphs and OCaml
functors explicit.
<a href="https://nasa.github.io/fprime/">F Prime</a> does the same thing for
C++ flight software: as I described in a
<a href="https://gazagnaire.org/blog/2026-02-19-nasa-fprime.html">previous post</a>, both frameworks
decompose systems into typed components and generate the plumbing
that wires them together. The difference is that Functoria is an
embedded DSL in OCaml, while
<a href="https://nasa.github.io/fpp/fpp-users-guide.html">FPP</a> (F Prime's
modelling language) is a standalone DSL with a
<a href="https://nasa.github.io/fpp/fpp-spec.html">formal specification</a>.
The interesting thing about a standalone DSL is that it can target
multiple languages: the same <code>.fpp</code> files can generate C++ and OCaml.</p>
<p>So I wrote <a href="https://github.com/parsimoni-labs/ocaml-fpp">ofpp</a> to test
this idea. It parses the complete FPP grammar and generates MirageOS
wiring code from FPP topology files. The generated code compiles
against the real MirageOS libraries, and NASA's reference <code>fpp-check</code>
tool accepts the same <code>.fpp</code> files unchanged. FPP's dependency graph
maps directly to OCaml functors, so the rest of the post walks
through that mapping.</p>
<p>ofpp is a prototype, not a replacement for the <code>mirage</code> tool or for
Functoria -- I built it to explore what a shared architecture
language between F Prime and MirageOS could look like.</p>
<h2>The mapping</h2>
<p>Here is <code>UnixHelloKey</code>, a unikernel that takes a <code>--hello</code>
command-line flag
(<a href="https://github.com/parsimoni-labs/ocaml-fpp/tree/main/examples/mirage/tutorial/hello-key">source</a>):</p>
<div >
<div >
<p><strong>FPP topology</strong></p>
<pre><code class="language-fpp">passive component Unikernel {
  async input port start: serial
  param hello: string default &quot;Hello World!&quot;
}

instance unikernel: Unikernel base id 0

topology UnixHelloKey {
  instance unikernel
}
</code></pre>
</div>
<div >
<p><strong>Generated <code>main.ml</code></strong></p>
<pre><code class="language-ocaml">let hello_0 =
  let doc = Cmdliner.Arg.info ~doc:&quot;hello&quot; [&quot;hello&quot;] in
  Mirage_runtime.register_arg
    Cmdliner.Arg.(value &amp; opt string &quot;Hello World!&quot; doc)

let start = lazy (Unikernel.start ~hello:(hello_0 ()))

let () =
  let t =
    let open Lwt.Syntax in
    (* Mirage_runtime init ... *)
    let* _ = Lazy.force start in
    Lwt.return ()
  in
  Unix_os.Main.run t; exit 0
</code></pre>
</div>
</div>
<p >The FPP instance <code>unikernel</code> resolves to the OCaml module <code>Unikernel</code> by convention (capitalised). The <code>param hello</code> declaration generates a Cmdliner term (<code>hello_0</code>) that becomes the <code>--hello</code> command-line flag, passed as a labelled argument to <code>Unikernel.start</code>.</p>
<pre><code>$ dune exec examples/mirage/tutorial/hello-key/main.exe
2026-03-04T13:52:57-08:00: [INFO] [application] Hello World!
...
$ dune exec examples/mirage/tutorial/hello-key/main.exe -- \
    --hello='Bonjour\ MirageOS!'
2026-03-04T13:53:08-08:00: [INFO] [application] Bonjour MirageOS!
...
</code></pre>
<p>ofpp handles wiring only. Type safety comes from the OCaml module
system: the generated functor applications must type-check against
the MirageOS library signatures, so a miswired topology is a compile
error.</p>
<h2>config.ml vs config.fpp</h2>
<p>In MirageOS, each unikernel has a <code>config.ml</code> that describes its
device dependencies using Functoria combinators. In FPP, each
unikernel has a <code>config.fpp</code> that describes the same dependencies
as a connection graph. Here is the <code>network</code> example -- a unikernel
that needs a TCP/IP stack:</p>
<div >
<div >
<p><strong>Functoria</strong> (<code>config.ml</code>)</p>
<pre><code class="language-ocaml">open Mirage

let main =
  main &quot;Unikernel.Main&quot; (stackv4v6 @-&gt; job)
let stack = generic_stackv4v6 default_network
let () = register &quot;network&quot; [ main $ stack ]
</code></pre>
</div>
<div >
<p><strong>FPP</strong> (<code>config.fpp</code>)</p>
<pre><code class="language-fpp">module Unikernel {
  passive component Main {
    async input port start: serial
    output port stack: serial
  }
}

instance unikernel: Unikernel.Main base id 0

topology UnixNetwork {
  import SocketStack
  instance stackv4v6
  instance unikernel

  connections Start {
    unikernel.stack -&gt; stackv4v6.connect
  }
}
</code></pre>
</div>
</div>
<p >The Functoria DSL hides the stack implementation behind <code>generic_stackv4v6</code>. The FPP topology makes it explicit: <code>import SocketStack</code> pulls in a sub-topology that wires <code>Udpv4v6_socket</code> and <code>Tcpv4v6_socket</code> into <code>Stackv4v6.Make</code>. The connection <code>unikernel.stack -> stackv4v6.connect</code> declares that the unikernel depends on the stack.</p>
<p>The FPP version is more verbose. Functoria's combinators are more
concise for pure OCaml projects, and <code>generic_stackv4v6</code> picks the
right implementation for each target automatically (FPP makes you
spell that out).
The payoff is that the topology is a standalone graph, not embedded
in OCaml. The same <code>.fpp</code> files can drive C++ code generation, and
the connection graph is available for visualisation and tooling.</p>
<h2>The device catalogue</h2>
<p>Device components live in a shared
<a href="https://github.com/parsimoni-labs/ocaml-fpp/blob/main/examples/mirage/mirage.fpp"><code>mirage.fpp</code></a>.
Here are three of the devices used in the <code>UnixPing6</code> topology
shown below:</p>
<pre><code class="language-fpp">module Vnetif {
  passive component Make {
    import Mirage_net.S
    output port backend: serial
  }
}

module Ethernet {
  passive component Make {
    import Ethernet.S
    output port net: serial
  }
}

module Ipv6 {
  passive component Make {
    import Tcpip.Ip.S
    async input port connect: Ipv6Connect
    output port net: serial
    output port eth: serial
  }
}
</code></pre>
<p>Each FPP construct maps to a specific OCaml concept.</p>
<p><strong>Output ports are functor arguments.</strong> <code>Vnetif.Make</code> has one
output port (<code>backend</code>), so its generated functor takes one
argument: <code>Vnetif.Make(Backend)</code>. <code>Ipv6.Make</code> has two (<code>net</code> and
<code>eth</code>), so its functor takes two: <code>Ipv6.Make(Net)(Ethernet)</code>. The
topology connections decide which instance fills each argument.</p>
<p><strong><code>import</code> declarations are module type constraints.</strong> <code>import Mirage_net.S</code> on <code>Vnetif.Make</code> tells ofpp to generate <code>module type Net = Mirage_net.S</code> in the output. The OCaml compiler then checks
that the concrete module satisfies that signature.</p>
<p><strong>Port types encode <code>connect</code> signatures.</strong> <code>Ipv6Connect</code> is
defined elsewhere in <code>mirage.fpp</code> as <code>port Ipv6Connect(conf: Ipv6Conf)</code>, so <code>Ipv6.connect</code> takes a labeled <code>~conf</code> argument.
Positional parameters (like <code>_0: string</code>) generate positional
arguments instead.</p>
<p>The generated code for <code>UnixPing6</code> uses all three:</p>
<pre><code class="language-ocaml">(* from import declarations *)
module type Backend = Vnetif.BACKEND
module type Net = Mirage_net.S
module type Ethernet = Ethernet.S
module type Ipv6 = Tcpip.Ip.S

(* from topology connections + output ports *)
module Net = Vnetif.Make(Backend)
module Ethernet = Ethernet.Make(Net)
module Ipv6 = Ipv6.Make(Net)(Ethernet)

(* from connect port types + connection order *)
let* backend = Backend.connect () in
let* net = Net.connect backend in
let* ethernet = Ethernet.connect net in
let* ipv6 = Ipv6.connect net ethernet in
</code></pre>
<p>If a connection is missing, it does not compile.</p>
<h2>A larger example</h2>
<p>Here is <code>UnixPing6</code>, an IPv6 ping unikernel wired through a
virtual network
(<a href="https://github.com/parsimoni-labs/ocaml-fpp/tree/main/examples/mirage/device-usage/ping6">source</a>):</p>
<div class="my-8 not-prose">
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/UnixPing6.png"><img class="w-full rounded-lg" src="https://gazagnaire.org/blog/images/UnixPing6.png" alt="UnixPing6 topology"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight"><b>Figure 1.</b> The <code>UnixPing6</code> topology, rendered by <a href="https://github.com/fprime-community/fprime-visual">fprime-visual</a> from the FPP connection graph.</figcaption>
  </figure>
</div>
<div >
<div >
<p><strong>FPP topology</strong> (<code>config.fpp</code>)</p>
<pre><code class="language-fpp">module Unikernel {
  passive component Main {
    async input port start: serial
    output port net: serial
    output port eth: serial
    output port ipv6: serial
  }
}

instance unikernel: Unikernel.Main base id 0

topology UnixPing6 {
  instance backend
  instance net
  instance ethernet
  instance ipv6
  instance unikernel

  connections Connect {
    net.backend -&gt; backend.connect
    ethernet.net -&gt; net.connect
    ipv6.net -&gt; net.connect
    ipv6.eth -&gt; ethernet.connect
  }
  connections Start {
    unikernel.net -&gt; net.connect
    unikernel.eth -&gt; ethernet.connect
    unikernel.ipv6 -&gt; ipv6.connect
  }
}
</code></pre>
<p >The <code>backend</code> and <code>net</code> instances come from the shared <code>mirage.fpp</code> device catalogue (<code>Backend</code> is a Vnetif in-memory switch, <code>Vnetif.Make</code> creates a virtual Ethernet interface from it). The two connection groups (<code>Connect</code>, <code>Start</code>) become separate lazy bindings.</p>
</div>
<div >
<p><strong>Generated <code>main.ml</code></strong></p>
<pre><code class="language-ocaml">module type Backend = Vnetif.BACKEND
module type Net = Mirage_net.S
module type Ethernet = Ethernet.S
module type Ipv6 = Tcpip.Ip.S

module Net = Vnetif.Make(Backend)
module Ethernet = Ethernet.Make(Net)
module Ipv6 = Ipv6.Make(Net)(Ethernet)
module Unikernel = Unikernel.Main(Net)(Ethernet)(Ipv6)

open Lwt.Syntax

let connect = lazy (
  let* backend = Backend.connect () in
  let* net = Net.connect backend in
  let* ethernet = Ethernet.connect net in
  let* ipv6 = Ipv6.connect net ethernet in
  Lwt.return (net, ethernet, ipv6))

let start = lazy (
  let* (net, ethernet, ipv6) = Lazy.force connect in
  Unikernel.start net ethernet ipv6)

(* Mirage_runtime init, then: *)
let* _ = Lazy.force start in ...
</code></pre>
<p >ofpp reads the connection graph, sorts the dependencies, and emits the functor applications in the right order. The <code>Backend</code> → <code>Net</code> → <code>Ethernet</code> → <code>Ipv6</code> chain mirrors the physical network stack.</p>
</div>
</div>
<h2>Sub-topology composition</h2>
<p>Sub-topologies are shared via <code>import</code>. The DNS example imports the
socket stack and wires additional layers on top:</p>
<pre><code class="language-fpp">topology UnixDns {
  import SocketStack          @ pulls in Udpv4v6_socket, Tcpv4v6_socket, Stackv4v6
  instance stackv4v6
  instance happy_eyeballs_mirage
  instance dns_client
  instance unikernel

  connections Connect_device {
    happy_eyeballs_mirage.stack -&gt; stackv4v6.connect
  }
  connections Start {
    dns_client.stack -&gt; stackv4v6.connect
    dns_client.happy_eyeballs -&gt; happy_eyeballs_mirage.connect_device
    unikernel.dns -&gt; dns_client.start
  }
}
</code></pre>
<p>The generated <code>main.ml</code> chains the socket stack <code>connect</code>, then
<code>connect_device</code> for Happy Eyeballs, then <code>start</code> for the DNS
resolver -- each connection group becomes a lazy binding that
forces its dependencies:</p>
<pre><code class="language-ocaml">module type Udpv4v6_socket = Tcpip.Udp.S
module type Tcpv4v6_socket = Tcpip.Tcp.S
module type Stackv4v6 = Tcpip.Stack.V4V6
module type Happy_eyeballs_mirage = Happy_eyeballs_mirage.S
module type Dns_client = Dns_client_mirage.S

module Stackv4v6 = Stackv4v6.Make(Udpv4v6_socket)(Tcpv4v6_socket)
module Happy_eyeballs_mirage = Happy_eyeballs_mirage.Make(Stackv4v6)
module Dns_client = Dns_resolver.Make(Stackv4v6)(Happy_eyeballs_mirage)
module Unikernel = Unikernel.Make(Dns_client)

let connect = lazy (
  let* udpv4v6_socket = Udpv4v6_socket.connect ~ipv4_only:false ... in
  let* tcpv4v6_socket = Tcpv4v6_socket.connect ~ipv4_only:false ... in
  Stackv4v6.connect udpv4v6_socket tcpv4v6_socket)

let connect_device = lazy (
  let* stackv4v6 = Lazy.force connect in
  let* happy_eyeballs_mirage = Happy_eyeballs_mirage.connect_device stackv4v6 in
  Lwt.return (stackv4v6, happy_eyeballs_mirage))

let start = lazy (
  let* (stackv4v6, happy_eyeballs_mirage) = Lazy.force connect_device in
  let* dns_client = Dns_client.start stackv4v6 happy_eyeballs_mirage in
  Unikernel.start dns_client)
</code></pre>
<p >This is something Functoria's flat combinator model does not express cleanly: named sub-topologies that can be imported and reused across unikernels, with cross-boundary connections wired by the parent topology.</p>
<h2>Target switching</h2>
<p>In MirageOS, you write your application once and compile it for
different platforms without changing application code.
<code>mirage configure -t unix</code> wires in the kernel's TCP/IP stack;
<code>-t hvt</code> wires in MirageOS's own protocol implementations running on
a virtual network device. In FPP, each target is simply a different
topology.</p>
<p>Shared sub-topologies (<code>TcpipStack</code>, <code>SocketStack</code>, <code>DnsStack</code>) are
imported and reused across variants. The caller picks the target by
passing it to ofpp:</p>
<pre><code class="language-sh">ofpp to-ml --topologies UnixHello mirage.fpp tutorial/hello/config.fpp
ofpp to-ml --topologies UnixDns --target unix mirage.fpp applications/dns/config.fpp
</code></pre>
<p>The <code>--target</code> flag selects the OS main loop: <code>unix</code> (default),
<code>xen</code>, <code>solo5</code>, or <code>unikraft</code>. The codegen emits the appropriate
entry point -- <code>Unix_os.Main.run</code>, <code>Xen_os.Main.run</code>,
<code>Solo5_os.Main.run</code>, or <code>Unikraft_os.Main.run</code>. Target switching
in ofpp is very experimental for now: only the Unix target has
been tested end-to-end.</p>
<h2>Mixing C++ and OCaml components</h2>
<p>ofpp already works for building MirageOS unikernels on the Unix
backend: describe the topology in FPP, run <code>ofpp to-ml</code>, compile.
The interesting next step is mixing C++ F Prime components with
OCaml MirageOS components in the same topology.</p>
<p>Three pieces are missing. First, <strong>binary serialisation</strong>: ofpp
already generates OCaml types from FPP enums, structs, and arrays,
but matching C++'s wire format for command, telemetry, and event
dispatch is not done yet. Second, <strong>FFI stubs</strong>: I have early
experiments with OCaml components running inside F Prime over
<code>caml_callback</code>, but they are not ready to open-source. Third,
<strong>build integration</strong>: I have a working (if ugly) CMake + dune
bridge, though a cleaner solution might come from
<a href="https://ryan.freumh.org/papers/2026-package-calculus.pdf">Package Managers à la Carte</a>
(pre-print).</p>
<p>The end goal: run OCaml components on isolated MirageOS platforms
(<a href="https://github.com/Solo5/solo5">Solo5</a>,
<a href="https://muen.codelabs.ch/">Muen</a>,
<a href="https://github.com/mirage/mirage-unikraft">Unikraft</a>, seL4)
inside F Prime deployments, and let MirageOS unikernels integrate
with existing C++ flight software.</p>
<h2>State machines</h2>
<p>FPP also has state machines. ofpp generates typed OCaml modules
from them: states become a GADT, signals become a variant, and
guards and actions become functor parameters. Here is a toy example:</p>
<div >
<div >
<p><strong>FPP state machine</strong></p>
<pre><code class="language-fpp">state machine Door {
  action lock
  guard locked
  signal open
  signal close
  initial enter Closed
  state Closed {
    on open if locked enter Closed
    on open enter Opened
  }
  state Opened {
    on close do { lock } enter Closed
  }
}
</code></pre>
</div>
<div >
<p><strong>Generated OCaml</strong></p>
<pre><code class="language-ocaml">type closed
type opened

type _ state =
  | Closed : closed state
  | Opened : opened state

type signal = Open | Close

module type ACTIONS = sig
  type ctx
  val lock : ctx -&gt; unit
end

module type GUARDS = sig
  type ctx
  val locked : ctx -&gt; bool
end

module Make
  (A : ACTIONS)
  (G : GUARDS with type ctx = A.ctx) :
sig
  type t
  val create : A.ctx -&gt; t
  val state : t -&gt; any
  val step : t -&gt; signal -&gt; t
end
</code></pre>
</div>
</div>
<p >Phantom types index the GADT, so pattern matching in <code>step</code> is exhaustive. The caller supplies <code>ACTIONS</code> and <code>GUARDS</code> as functor arguments. ofpp also renders state machines as Graphviz diagrams. In F Prime, components can embed <code>state machine instance</code> declarations that connect a state machine to the component's lifecycle; ofpp does not wire those up fully yet.</p>
<h2>ofpp</h2>
<p>The tool is called
<a href="https://github.com/parsimoni-labs/ocaml-fpp">ofpp</a>. I started it
in February at the JPL workshop. Rob Bocchino
<a href="https://rob-bocchino.net/Professional/bocchino-ieee-aero-2022.pdf">designed FPP</a>
with a
formal specification
and an upstream test suite of 670 <code>.fpp</code> files across 35
categories, which I used to validate the parser. The parser covers
the complete FPP grammar (zero conflicts).</p>
<p>The whole thing is about 19k lines of OCaml: 1k for the parser and
lexer, 8k for the checker, 2.7k for the code generator, and the
rest for tests. ofpp also exports topology graphs to
<a href="https://github.com/fprime-community/fprime-visual">fprime-visual</a>.
The <a href="https://github.com/parsimoni-labs/ocaml-fpp/tree/main/examples/mirage">example repo</a>
covers all of
<a href="https://github.com/mirage/mirage-skeleton">mirage-skeleton</a>:
tutorials, device examples, and applications like DNS, DHCP, and
Git. Every example has a cram test that builds the unikernel and
exercises it.</p>
<p>ofpp is not affiliated with NASA or JPL. It is an independent
experiment. If you work with FPP or MirageOS and want to try it:</p>
<pre><code class="language-sh"># macOS
brew tap parsimoni-labs/tap &amp;&amp; brew install ofpp

# from source
opam pin add fpp https://github.com/parsimoni-labs/ocaml-fpp.git
</code></pre>
<pre><code class="language-sh">ofpp check path/to/file.fpp
ofpp to-ml path/to/file.fpp
ofpp dot path/to/file.fpp | dot -Tsvg -o sm.svg
</code></pre>
<p>If you have a MirageOS <code>config.ml</code>, try describing the same topology
in FPP and compare the generated <code>main.ml</code> with what Functoria
produces. File issues on
<a href="https://github.com/parsimoni-labs/ocaml-fpp/issues">GitHub</a>.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-03-10-ocaml-fpp.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-03-10-ocaml-fpp.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Tue, 10 Mar 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[Is Running Untrusted Code on a Satellite a Good Idea?]]></title>
      <description><![CDATA[<p>The same conversation keeps happening. I explain what
<a href="https://parsimoni.co/">Parsimoni</a> does (run third-party software
on someone else's satellite) and the response is always some
variant of: &quot;I would never trust code I have not reviewed to run
on my satellite.&quot;
They are right to worry -- I would say the same thing in their
position. And the security research community has been saying it loudly
for several years now.</p>
<p>To understand where the gaps are, it helps to separate the two
halves of the stack. Bus software (attitude control, power, thermal)
runs on qualified RTOSes and is the satellite manufacturer's domain.
Payload software (imaging, communications, data processing)
increasingly runs on COTS (commercial off-the-shelf)
processors with memory management units (MMUs) perfectly capable of enforcing memory
isolation. For LEO missions on COTS hardware, the hardware is
there. The software running on it does not use it.</p>
<p>I think three things are missing for anyone wanting to run hosted
payloads or patch software in orbit safely:</p>
<ol>
<li>
<p><strong>Hardware-enforced memory isolation</strong> between payloads and
flight software, either through a hypervisor (like Xen or KVM) or
a separation kernel (seL4 has a formal proof of functional
correctness on specific hardware configurations). Both use the
MMU to ensure a compromised payload cannot reach the bus.</p>
</li>
<li>
<p><strong>A standard OTA deployment format</strong> so &quot;install this application&quot;
means the same thing across different satellites: cryptographically
signed, designed for 10-minute LEO pass windows and partial
transfers.</p>
</li>
<li>
<p><strong>A metered runtime</strong> that enforces resource quotas (CPU, memory,
storage, bus bandwidth) so one payload cannot starve the others.
Resource requirements should be declarative, embedded in the
deployment image, and verified before installation.</p>
</li>
</ol>
<p>The tools to address much of this exist in the terrestrial world:
memory-safe languages (<a href="https://msrc.microsoft.com/blog/2019/07/we-need-a-safer-systems-programming-approach/">roughly 70% of CVEs</a>
in large C/C++ codebases are memory-safety issues, a figure consistent
across Microsoft, Google, and Android since 2019), verified cryptography,
minimal single-purpose OS images. These are not theoretical: Google
<a href="https://security.googleblog.com/2024/09/eliminating-memory-safety-vulnerabilities-Android.html">cut Android memory-safety vulnerabilities by 76%</a>
in six years by writing new code in memory-safe languages, and AWS
runs millions of serverless workloads on
<a href="https://www.usenix.org/conference/nsdi20/presentation/agache">Firecracker</a>,
a 50,000-line Rust VMM that boots in under 125 ms. None of these
have made it into flight software yet, even as the number of
spacecraft in orbit (and the attack surface) has grown sharply.
<a href="https://doi.org/10.1109/SP46215.2023.10351029">Space Odyssey</a> examined
three satellites and found vulnerabilities in all of them, and those are
only the ones someone bothered to look for.</p>
<div class="my-6 flex flex-col items-center not-prose">
  <img src="https://gazagnaire.org/blog/images/satellite-cves.svg" alt="Mass launched to orbit versus publicly disclosed satellite software vulnerabilities, 2000-2025" class="max-w-2xl w-full">
  <p class="text-sm text-gray-500 mt-2 max-w-2xl text-center">Mass launched to orbit (blue) versus publicly disclosed satellite software vulnerabilities (red bars, from CVE/NVD and conference disclosures; <a href="https://gazagnaire.org/blog/images/satellite-cves.py" class="underline">data and sources</a>). The gap between the curves is the problem: launch volume has exploded, security scrutiny has barely started.</p>
</div>
<p>The evidence is mounting. None of the Space Odyssey satellites
had ASLR, stack canaries, or DEP; one had no authentication
on its telecommand interface. The Viasat/KA-SAT attack in February
2022 disrupted Ukrainian communications and 5,800 German wind
turbines, making the threat operational (Boschetti, Gordon and Falco,
<a href="https://doi.org/10.2514/6.2022-4380">ASCEND 2022</a>). At Black Hat
2025, a
<a href="https://visionspace.com/nasa-cfs-version-aquila-software-vulnerability-assessment/">vulnerability assessment</a>
of NASA's core Flight System (cFS) found remote code execution,
path traversal, and denial-of-service, all from textbook C memory
bugs. NASA's own
<a href="https://github.com/nasa/CryptoLib/security">CryptoLib</a>, the
reference implementation of CCSDS encryption, has had recurring
buffer overflows in its parsing code
(<a href="https://github.com/nasa/CryptoLib/security/advisories/GHSA-q2pc-c3jx-3852">CVE-2025-29909</a>,
CVSS 9.8). A memory-safe language eliminates these classes of bug
by construction.</p>
<p>The pattern across all of these is consistent: space software
engineering has a safety-versus-security bias (Pavur and Martinovic,
<a href="https://doi.org/10.1093/cybsec/tyac008">J. Cybersecur. 2022</a>;
Falco, <a href="https://doi.org/10.2514/1.I010693">JAIS 2019</a>). Systems are
designed for availability and determinism (watchdogs, radiation
tolerance, FDIR). Integrity and confidentiality are secondary. This
is not irrational: a satellite that loses attitude control is a more
immediate problem than one that leaks telemetry. But it leaves
software stacks undefended against adversarial threats. Jero et al.
(<a href="https://doi.org/10.14722/spacesec.2024.23015">NDSS SpaceSec 2024</a>)
proposed a defence-in-depth taxonomy -- secure boot, memory
protection, authenticated updates, compartmentalisation -- that
overlaps substantially with the three gaps above.</p>
<p>So why does the current stack not provide the three things above?
Most small-sat payload software runs on
<a href="https://www.windriver.com/products/vxworks">VxWorks</a>,
<a href="https://www.rtems.org/">RTEMS</a>, embedded Linux
(<a href="https://www.yoctoproject.org/">Yocto</a>,
<a href="https://buildroot.org/">Buildroot</a>), or bare metal. The RTOS path
typically runs everything in a single address space: the hardware has
an MMU, but most CubeSat missions do not enable memory protection
because it requires rethinking memory management, interrupt handling,
and DMA buffer allocation. This is exactly the architecture Space
Odyssey found vulnerable. Embedded Linux gives you process isolation,
but hardening it for multi-tenancy and certifying it for flight
remains a substantial undertaking. ESA's
<a href="https://www.esa.int/Enabling_Support/Operations/OPS-SAT">OPS-SAT</a>
demonstrated deploying new applications before its re-entry in 2024,
but it remains the exception.</p>
<p>The isolation and metering techniques are not new. Cloud
infrastructure solved analogous problems years ago, and avionics
systems (ARINC 653, DO-178C) have their own qualified solutions. The
gap is in LEO payload software, where neither stack fits: the cloud
stack assumes abundant resources, the avionics stack assumes a single
operator and a multi-year qualification budget. I am not going to
minimise how much work qualification (under ECSS-Q-ST-80C or
equivalent) requires. Calabrese,
Kavallieratos and Falco
(<a href="https://doi.org/10.2514/6.2024-0270">SCITECH 2024</a>) emulated a
cyberattack from a hosted payload on OPS-SAT that compromised the
bus, demonstrating why isolation matters. But a real multi-tenant
satellite also needs FDIR (Fault Detection, Isolation, and Recovery)
integration, thermal management, power budgeting, and link budget
allocation. I am focusing on the software infrastructure, but I do
not want to pretend the problem ends there.</p>
<p>The regulatory pressure is new. The EU Cyber Resilience Act is
pushing toward stronger isolation and update mechanisms.
<a href="https://www.enisa.europa.eu/publications/space-threat-landscape">ENISA</a>
is calling out the space sector. The White House
<a href="https://tarides.com/blog/2024-03-07-a-time-for-change-our-response-to-the-white-house-cybersecurity-press-release/">called for memory-safe languages</a>
in critical infrastructure in 2024, a recommendation that applies
to flight software as much as to anything on the ground. OCaml, the
language Parsimoni and Tarides use, is memory-safe in its default
fragment. And the hardware is finally there:
radiation-tolerant processors with enough compute for on-board
processing are commercially available at CubeSat price points. The
missing piece is the software infrastructure.</p>
<p>These are the problems I co-founded
<a href="https://parsimoni.co/">Parsimoni</a> to work on. Our payload software
platform, SpaceOS, builds on the OCaml ecosystem that
<a href="https://tarides.com/blog/2023-12-14-ocaml-memory-safety-and-beyond/">Tarides maintains</a>.
Our first payload
reached orbit on SpaceX Transporter-13 in March 2025. If you are
building payload software and running into these constraints, or if
you are an operator considering hosted payloads, we would like to
hear from you: <a href="https://parsimoni.co/index.html#contact">get in touch</a>.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-02-25-satellite-software.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-02-25-satellite-software.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[From ASPLOS to Orbit: Unikernels Twelve Years Later]]></title>
      <description><![CDATA[<p>Our 2013 ASPLOS paper,
&quot;<a href="https://doi.org/10.1145/2451116.2451167">Unikernels: Library Operating Systems for the Cloud</a>&quot;,
received a <a href="https://anil.recoil.org/notes/unikernels-test-of-time">test-of-time
award</a> in
March 2025. The title says &quot;for the Cloud.&quot; We wrote it while I was
in <a href="https://www.cl.cam.ac.uk/~jac22/">Jon Crowcroft</a>'s group at the University of Cambridge. The core
idea: instead of running an application on a general-purpose OS,
compile the application and only the OS libraries it needs into a
single sealed image that runs directly on a hypervisor. No kernel, no
shell, no unused drivers. Our DNS server image was 200 KB. The BIND
appliance it replaced was over 400 MB.</p>
<p>Five properties fell out of that architecture. None of them are
unique to unikernels (a careful Yocto configuration can achieve most
of them), but unikernels make them the default. I think they hold up
better now than they did in the cloud:</p>
<ol>
<li><strong>Sealed images.</strong> The binary is fixed at compile time. No runtime
package installation, no configuration files, no shell to log into.</li>
<li><strong>Dead code elimination.</strong> If the application does not use the
filesystem, the filesystem is not in the binary.</li>
<li><strong>Single address space.</strong> One application, one address space (no
internal process isolation, the hypervisor provides isolation
between unikernels).</li>
<li><strong>Configuration is compilation.</strong> Network addresses, TLS
certificates, and firewall rules are baked in at build time. No
configuration parser at runtime means no configuration parser to
exploit.</li>
<li><strong>Small trusted computing base.</strong> The TCB is the hypervisor plus
the application. No general-purpose kernel, no init system, no
sshd.</li>
</ol>
<div  class="my-8 not-prose">
<div >
<div >
  <div >Application</div>
  <div >System libraries</div>
  <div >Unused packages</div>
  <div >Shell, sshd, cron</div>
  <div >OS kernel</div>
  <div >Hypervisor</div>
</div>
<p >Traditional VM</p>
</div>
<div >
<div >
  <div >Application + OS libs</div>
  <div >OCaml runtime</div>
  <div >Hypervisor</div>
</div>
<p >MirageOS unikernel</p>
</div>
</div>
<p>Anil, Balraj and I co-founded Unikernel Systems to bring this to
production.
<a href="https://www.mosaicventures.com/blog/the-story-of-our-investment-in-docker-via-unikernel-systems-and-why-enterprises-are-running-their-business-on-their-commercial-platform">Docker acquired us three months later</a>.
At Docker, we built
<a href="https://www.docker.com/blog/how-docker-desktop-networking-works-under-the-hood/">Docker Desktop</a>
from scratch, adding <a href="https://mirageos.org/">MirageOS</a> unikernel
components to the networking stack
via <a href="https://github.com/moby/vpnkit">vpnkit</a> (we wrote an <a href="https://doi.org/10.1145/3747525">ICFP
paper</a> about how that works).
Millions of developers use it daily. Meanwhile, containers won the
cloud and <a href="https://unikraft.com/">Unikraft</a> took the unikernel
deployment model in a POSIX-compatible direction for serverless. The
unikernel-as-product did not take over (yet!), but the libraries shipped
further than we ever expected.</p>
<p>In 2021, when <a href="https://tarides.com/">Tarides</a> joined Cyber@Station F,
I pitched unikernels to over thirty internal teams at Thales: cyber,
compliance, transport, defence, ... Most were politely interested. The one that
clicked was Régis de Ferluc at
<a href="https://www.thalesaleniaspace.com/">Thales Alenia Space</a>. His
reaction was not &quot;that is interesting&quot; but &quot;that is how we already
build embedded systems, but done in a principled way (and where you
can safely introduce dynamic behaviour).&quot;</p>
<div class="image-grid grid grid-cols-1 sm:grid-cols-2 gap-4 my-8 not-prose">
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/asplos25-award.jpg"><img class="w-full max-h-64 object-cover object-bottom rounded-lg" src="https://gazagnaire.org/blog/images/asplos25-award.jpg" alt="Richard Mortier accepting the ASPLOS test-of-time award in Rotterdam"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">Richard Mortier accepting the ASPLOS test-of-time award in Rotterdam (2025).</figcaption>
  </figure>
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/tas.jpg"><img class="w-full max-h-64 object-cover rounded-lg" src="https://gazagnaire.org/blog/images/tas.jpg" alt="Early whiteboard session with Régis de Ferluc at TAS Cannes"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">Early whiteboard session with Régis de Ferluc at TAS Cannes, evaluating MirageOS components for space (2022).</figcaption>
  </figure>
</div>
<p>He was right. Satellite flight software ships
as fixed binaries, qualified on the ground, no shell access in orbit,
no runtime configuration changes. The five properties above are not
innovations in space, they are requirements. What satellite software
does not have is the type safety, the tooling, and the library
ecosystem that <a href="https://ocaml.org/">OCaml</a> and MirageOS provide.</p>
<p>Régis pushed us to look deeper, introduced us to EU programmes
(ORCHIDE via Horizon Europe, CEOS), and
<a href="https://parsimoni.co/blog/2023-07-31-ocaml-in-space-welcome-spaceos.html">SpaceOS</a>
was born. <a href="https://www.linkedin.com/in/miklostomka/">Miklos</a> and I co-founded
<a href="https://parsimoni.co/">Parsimoni</a> to build it.</p>
<p>The ASPLOS paper asked whether sealed, single-purpose images could
improve cloud infrastructure. Twelve years later, I think the more
interesting question is whether those same properties matter more in
constrained environments (satellites, hardware security modules,
embedded controllers) where every kilobyte counts, you cannot ssh in
after deployment, and &quot;no unused code&quot; is not an optimisation but a
qualification requirement.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-02-23-asplos-unikernels.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-02-23-asplos-unikernels.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Mon, 23 Feb 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[F Prime Looks a Lot Like MirageOS (but in C++)]]></title>
      <description><![CDATA[<p>Last week I attended the
<a href="https://gazagnaire.org/blog/2026-02-17-los-angeles.html">F Prime workshop at JPL</a>, over 100
people, from CubeSat student teams to flagship mission engineers. I
learned a lot about F Prime, and I kept noticing how familiar the
concepts felt. This post is some of my thoughts on why.</p>
<p><a href="https://nasa.github.io/fprime/">F Prime</a> (<a href="https://github.com/nasa/fprime">GitHub</a>) is NASA's open-source
framework for building <em>reusable</em> flight software. Before it, most
missions started from scratch or copy-pasted code from previous
ones. It already flies on
<a href="https://mars.nasa.gov/technology/helicopter/">Ingenuity</a> (Mars
Helicopter),
<a href="https://www.jpl.nasa.gov/missions/cadre">CADRE</a> (autonomous lunar
rovers), and <a href="https://europa.nasa.gov/">Europa Clipper</a>. It is trying to
bring standard software engineering practices (modularity, unit
testing, reusability, continuous integration) to a domain that has
historically resisted them.</p>
<p>The framework has its own modelling language called
<a href="https://nasa.github.io/fpp/fpp-users-guide.html">FPP</a> (F Prime
Prime). You describe your system in FPP (components, ports, state
machines, topologies) and the toolchain generates thousands of lines
of C++ for you (plus a fair amount of CMake glue to wire the build
together). That is the point: you write the architecture, not the
boilerplate. If you have ever worked with MirageOS, all of this should sound
oddly familiar.</p>
<p>For those who do not know,
<a href="https://mirageos.org/">MirageOS</a> (<a href="https://github.com/mirage/mirage">GitHub</a>) is a library operating system where
the entire application, from the network stack to the storage layer,
is built from OCaml modules wired together by functors. The runtime
is minimal (Solo5 provides about ten hypercalls), no general-purpose
OS, no libc, just typed interfaces all the way down. Our
<a href="https://doi.org/10.1145/2451116.2451167">ASPLOS 2013 paper</a>
received the
<a href="https://anil.recoil.org/notes/unikernels-test-of-time">Influential Paper Award</a>
in 2025. I think F Prime and MirageOS share a surprising number of
ideas. Here is how I see the two relate.</p>
<p><strong>Components are module types.</strong> In F Prime, you define a component
by declaring its ports, typed interfaces that connect it to the
rest of the system. The FPP paper puts it nicely: a port &quot;has a type
and a kind; the type is like a function signature.&quot; Components have
no compile- or link-time dependencies on each other: each component
depends only on the types of the ports that it uses.</p>
<p>FPP distinguishes three kinds of components: passive (called
synchronously by the caller), queued (has an internal message queue
but no thread of its own), and active (has its own thread that
drains its queue). This is a concurrency model baked into the
component types:</p>
<div >
<div >
<pre><code class="language-fpp">passive component Thermometer {
  sync input port cmdIn: Fw.Cmd
  output port tlmOut: Fw.Tlm
  telemetry Temperature: F32
}
</code></pre>
<p >FPP: a passive component declares typed ports.</p>
</div>
<div >
<pre><code class="language-ocaml">module type THERMOMETER = sig
  type t
  val cmd_in : t -&gt; Cmd.t -&gt; unit
  val tlm_out : t -&gt; Tlm.t
  val temperature : t -&gt; float
end
</code></pre>
<p >OCaml: a module type declares the same interface.</p>
</div>
</div>
<p>Both say the same thing: anything that calls itself a thermometer
must accept commands, emit telemetry, and report a temperature. The
ports are the function signatures. The component type is the module
type.</p>
<p>The mapping is suggestive, not exact. FPP ports have directionality
(input vs output) and invocation kinds (sync, async, guarded) that a
flat OCaml signature does not enforce. In MirageOS we used phantom
types on device connectors to recover some of this structure (see
<a href="https://arxiv.org/abs/1905.02529">Radanne, Gazagnaire et al., 2019</a>)
but the parallel is clear enough to be useful.</p>
<p>The concurrency story is interesting too. F Prime's
active/passive/queued distinction maps loosely to what OCaml has been
working through for years. A passive component is a plain function
call. An active component has its own thread that drains a message
queue. Compare:</p>
<div >
<div >
<pre><code class="language-fpp">active component Logger {
  async input port logIn: Fw.Log
}
</code></pre>
<p >FPP: an active component has its own thread.</p>
</div>
<div >
<pre><code class="language-ocaml">module type LOGGER = sig
  type t
  val log_in : t -&gt; Log.t -&gt; unit Lwt.t
end
</code></pre>
<p >OCaml: <code>Lwt.t</code> marks the call as asynchronous.</p>
</div>
</div>
<p>In FPP, a component is either passive, queued, or active. In OCaml,
you can mix synchronous and asynchronous functions in the same module
signature: some values return <code>unit</code>, others return <code>unit Lwt.t</code>. The
concurrency model is per-function, not per-component. Queued
components sit in between, an event loop without a dedicated thread.
OCaml has been moving from monadic concurrency (<code>Lwt.t</code>) to
<a href="https://ocaml.org/manual/effects.html">effect handlers</a> in OCaml 5,
which delegate scheduling to a library
like <a href="https://github.com/ocaml-multicore/eio">Eio</a> without colouring
every type signature. F Prime bakes the concurrency model into the
component kind; OCaml delegates it to the application scheduler.</p>
<p><strong>State machines are GADTs.</strong> This one is not specific to MirageOS,
it is about OCaml's type system more broadly. FPP has built-in
state machine support (states, signals, guards, and transitions).
Here is a door controller (from the
<a href="https://nasa.github.io/fpp/fpp-users-guide.html">FPP User's Guide</a>):</p>
<div >
<div >
<pre><code class="language-fpp">state machine Door {
  signal open
  signal close
  guard isLocked

  initial enter Closed

  state Closed {
    on open if isLocked enter Closed
    on open enter Open
  }

  state Open {
    on close enter Closed
  }
}
</code></pre>
<p >FPP: a door with two states and a guard.</p>
</div>
<div >
<img src="https://gazagnaire.org/blog/images/door-sm.svg" alt="Door state machine" >
<p >The generated state machine diagram.</p>
</div>
</div>
<p>In OCaml, you can encode the same transitions as a GADT. The type
parameters track which state you are in and which state a signal
takes you to:</p>
<div >
<div >
<pre><code class="language-ocaml">type closed
type opened

type _ state =
  | Closed : closed state
  | Open : opened state

type (_, _) signal =
  | Open  : (closed, opened) signal
  | Close : (opened, closed) signal

type guard = Locked | Unlocked

let step : type a b. a state -&gt; (a, b) signal -&gt; guard
    -&gt; (b state, a state) result =
  fun state signal guard -&gt;
    match state, signal, guard with
    | Closed, Open, Locked   -&gt; Error state
    | Closed, Open, Unlocked -&gt; Ok Open
    | Open, Close, _         -&gt; Ok Closed
</code></pre>
<p >OCaml: the GADT encodes valid transitions in the types. The guard returns a result: you either advance or stay put.</p>
</div>
</div>
<p>The type <code>(closed, opened) signal</code> means: this signal is only valid
from the <code>closed</code> state, and it produces the <code>opened</code> state. Try to
close an already-closed door and the compiler rejects it. The guard
is a runtime check (just like in FPP), but the result type makes the
self-loop explicit: <code>Error state</code> means the guard blocked and you
stayed where you were. FPP's state machine compiler does the same
checks at the FPP level; the GADT pushes them into OCaml's own type
system.</p>
<p><strong>The wiring is where they diverge.</strong> How you describe the way all
these components connect into a running system is where the two
frameworks take different paths.</p>
<p>In F Prime, you define a <em>topology</em>: a
wiring diagram that connects component instances through their ports,
the way an engineer connects chips on a board. Sub-topologies let you
group and reuse partial wirings (think of them as sub-circuits). The
FPP compiler checks that port types match, then the autocoder
generates C++ and a fair amount of CMake glue. For the final assembly,
users customise init sequences, thread priorities, and buffer sizes
through C++ templates and build rules. There are many knobs to turn.</p>
<div >
<div >
<pre><code class="language-fpp">topology Mission {
  instance sensor: Sensors.Thermometer
  instance dispatcher: Svc.CmdDispatcher

  connections command {
    dispatcher.compCmdSend -&gt; sensor.cmdIn
  }
}
</code></pre>
<p >FPP: a topology wires component instances through their ports.</p>
</div>
<div >
<pre><code class="language-ocaml">module Mission
    (Sensor : THERMOMETER)
    (Dispatcher : CMD_DISPATCHER) = struct
  let dispatch sensor cmd =
    Sensor.cmd_in sensor cmd
  let init sensor =
    Dispatcher.register (dispatch sensor)
end
</code></pre>
<p >OCaml: a functor wires modules through their signatures.</p>
</div>
</div>
<p>In MirageOS, a <em>functor</em> is a function from modules to modules: you
pass a module satisfying some signature, and you get a new module
back. The functor is the sub-topology. People write functor
applications by hand all the time in OCaml, and it works fine. But
in MirageOS we wanted to automate redeployment across different
hypervisors and driver sets (Xen, KVM, Solo5, each with their own
network and storage stacks), so we built
<a href="https://arxiv.org/abs/1905.02529">Functoria</a>, an eDSL that
describes the assemblage and generates the build rules and functor
applications for each target. That is structurally closer to what
F Prime's autocoder does: both take a high-level wiring description
and produce glue code for a specific target.</p>
<p>Both check that the wiring is type-correct, but in different places:
FPP in its own compiler, OCaml in the host language's type system.
FPP gives you a graph-oriented model that maps naturally to how
electronic engineers think. Functoria gives you composability
(functors are first-class language constructs), but the resulting
code can get... involved. (Who said Irmin in the back? :p)</p>
<p>The unit of compute seems settled: small typed components, explicit
async I/O, function types as interfaces. The global wiring is not. I
have seen it spawn build system rules (Makefile spaghetti),
domain-specific languages (FPP, Functoria), and language-specific support (functors).
<a href="https://doi.org/10.1145/361598.361623">Parnas (1972)</a> told us to
hide information behind interfaces. Nobody told us how to wire a
thousand of them together ;-)</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-02-19-nasa-fprime.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-02-19-nasa-fprime.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Thu, 19 Feb 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title><![CDATA[New Year, New Blog]]></title>
      <description><![CDATA[<p>Hello world, finally this is live :-)</p>
<p>The last couple of weeks have been pretty intense as I've been moving
to LA with my family. I am spending six months here, working on
<a href="https://parsimoni.co/">Parsimoni</a> and getting closer to the US space
industry.</p>
<p>Last week I visited NASA's <a href="https://www.jpl.nasa.gov/">Jet Propulsion Laboratory (JPL)</a> and saw
Voyager 2's control room -- still running after 48 years.
<a href="https://mars.nasa.gov/resources/26340/optimism-in-jpls-mars-yard/">Optimism</a>,
Perseverance's ground twin, was rolling over Pasadena sand in a test
enclosure next door. I also attended the
<a href="https://nasa.github.io/fprime/">F Prime</a> workshop and completed two
workshops on flight software engineering. F Prime is NASA's open-source
flight software framework. Over 100 people attended -- university and
student teams, internal JPL product teams, and large projects like
<a href="https://www.nasa.gov/game-changing-development-projects/high-performance-spaceflight-computing-hpsc/">HPSC</a>.
Only a small bunch of us were not from the US.</p>
<div class="image-grid grid grid-cols-1 sm:grid-cols-3 gap-4 my-8 not-prose">
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/IMG_0410.jpeg"><img class="w-full aspect-[4/3] object-cover rounded-lg" src="https://gazagnaire.org/blog/images/IMG_0410.jpeg" alt="Charles Elachi Mission Control Center at JPL"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">Charles Elachi Mission Control Center at JPL. The right screen reads VGR2 +48 YY.</figcaption>
  </figure>
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/IMG_0395.jpeg"><img class="w-full aspect-[4/3] object-cover rounded-lg" src="https://gazagnaire.org/blog/images/IMG_0395.jpeg" alt="Optimism rover at JPL"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">Optimism, Perseverance's ground twin, in the JPL test enclosure.</figcaption>
  </figure>
  <figure class="m-0 cursor-pointer">
    <a href="https://gazagnaire.org/blog/images/IMG_0344.jpeg"><img class="w-full aspect-[4/3] object-cover rounded-lg" src="https://gazagnaire.org/blog/images/IMG_0344.jpeg" alt="F Prime workshop at JPL"></a>
    <figcaption class="text-xs text-gray-500 mt-2 leading-tight">The F Prime workshop at JPL.</figcaption>
  </figure>
</div>
<p>The recurring theme in every conversation: flying a helicopter on Mars
is now routine, but deploying shared payload software to a satellite is still
real science fiction (no pressure!).</p>
<p>On the <a href="https://tarides.com/">Tarides</a> side, the team continues to
maintain and develop the OCaml platform -- opam, Dune, Merlin, odoc
-- while I am on a different timezone. Lots of things are
happening (see the <a href="https://tarides.com/blog/">Tarides blog</a>) and many
more are planned for 2026. In a world where people write apps by
chatting with AI bots, core language tools are even more important to make
sure we keep trust in our software systems (more on this later in this
blog, hopefully).</p>
<p>I intend to write here more regularly this year -- about SpaceOS,
the OCaml platform, and what happens when you try to put unikernels
in orbit. Stay tuned.</p>
]]></description>
      <link>https://gazagnaire.org/blog/2026-02-17-los-angeles.html</link>
      <guid isPermaLink="false">https://gazagnaire.org/blog/2026-02-17-los-angeles.html</guid>
      <dc:creator><![CDATA[Thomas Gazagnaire]]></dc:creator>
      <pubDate>Tue, 17 Feb 2026 00:00:00 GMT</pubDate>
    </item>
  </channel>
</rss>