Made 🄯 libre (free as in freedom) with ❤️ at Politehnica University of Timișoara — quite possibly the first formally-verified (machine-checked proof of no run-time errors), fountain-coded data-diode transport for protecting critical infrastructures such as power grids, water treatment plants, oil & gas pipelines, railways, and telecommunications networks.
Because we care — and because critical-infrastructure protection should not depend on proprietary / closed-source systems, and should be made FREE.
A high-assurance Ada/SPARK port of lt-diode-pro — a rateless LT (Luby Transform) fountain-code transport for one-way data diodes.
Files are sent over a strictly one-way channel (a hardware data diode, or any lossy UDP link with
no feedback and no retransmission). The sender emits a stream of XOR-combined coding packets over
the Robust Soliton Distribution; the receiver reconstructs the file from any sufficiently large
subset that arrives. This repository is a clean-slate rewrite of both ends in Ada, with the codec
core written in the SPARK subset and proved free of run-time errors by gnatprove.
Status: Phases 0–5 complete. The codec core is proved AoRTE — 175 checks, 0 unproved, 0 justified — and both binaries are feature-complete:
sender_stream, and areceiver_streamdaemon with decoupled receive/decode, the checksum gate, file and--pipemodes, parallel multi-transfer (routed by FILEID, each finalizing independently), lost-trailer eviction, config file,verify.log, syslog, and man pages. Sender → receiver reconstructs byte-for-byte at 0–30 % loss, withrecvmmsgbatching on the capture path, and the trusted shell is adversarially stress-tested. An optional DPDK kernel-bypass transport is available behindWITH_DPDK— off by default, because it enlarges the TCB (docs/ASSURANCE.md§5.1). This is a clean-slate rewrite, not a drop-in replacement for the C binaries.
The goal is high assurance: formal, machine-checked proof that the reconstruction path cannot commit a run-time error (no buffer overrun, no integer overflow, no division by zero), on a codec that must be correct on a link where there is no way to ask for a packet again.
Rewriting both ends (rather than staying wire-compatible with the C binaries) is what makes this tractable, and it unlocked two simplifications the C code could not make:
- The glibc-
rand()mimicry is deleted. The C reference reproduces glibc'srand()bit-for-bit (rbsoliton.c: glibc_srand_r/glibc_rand_r, Schrage seeding, 310-cycle warm-up) only so two independent processes agree on block selection. Owning both ends, we replace it with one clean, well-defined generator (SplitMix64, pure modular arithmetic) that SPARK proves trivially and that carries no global state — resolving the unsynchronised-global-RNG hazard the Crng.hitself warns about. - The soliton distribution is frozen and libm-free. Because
k,c,deltaare compile-time constants, the whole degree distribution is a compile-time constant. Its only transcendental inputs are two scalars (R = c·ln(k/δ)·√kandln(R/δ)), which are frozen as reviewed constants, so the table is built from exact IEEE arithmetic and is bit-identical on every host — which is exactly what lets an independent sender and receiver agree with no feedback.
SPARK cannot prove sockets, tasking or heap, so the design is layered. Everything a packet touches on
its way to being decoded is in the proven column; everything the OS touches is in the small,
enumerable trusted column. The full assurance case — what is proven, what is trusted, the
proof's assumptions, and how the trusted shell is justified — is in
docs/ASSURANCE.md.
| Layer | Mode | Contents |
|---|---|---|
src/codec — the codec core |
SPARK_Mode => On |
RNG + coding-seed mix, robust-soliton PMF/CDF + sampler, index sampling, LT encoder, peeling decoder, XOR checksum, wire (de)serialization |
src — the I/O shell |
SPARK_Mode => Off (trusted Ada) |
sender_stream (UDP send, stdin framing, pacing); receiver_stream (recvmmsg capture, decoupled decode task, hardened file output, checksum gate, --pipe); lt_conf (config file), lt_log (timestamped/levelled logging) |
The core is allocation-free: it operates on caller-supplied buffers with fixed-capacity working
storage, so any heap lives only in the trusted shell and the bounds stay provable. It is also
global-state-free: every generator is threaded explicitly as an in out parameter.
| Unit | Purpose |
|---|---|
lt_types |
Symbol/byte types, group geometry (K = 7375, Data_Len = 1356), Xor_Into |
lt_rng |
SplitMix64 generator; uniform draws; no global state |
lt_soliton |
Robust Soliton cumulative table (frozen constants, spike c=0.015 tuned for low overhead) + inverse-CDF degree sampler |
lt_sample |
Seed → degree + distinct source-index set (partial Fisher–Yates) — the one routine both ends must agree on |
lt_encoder |
Build one XOR coding symbol from a group + seed |
lt_decoder |
Generic peeling (belief-propagation) decoder over a caller-sized incidence store |
lt_decoder_std |
The concrete decoder instance (also the one gnatprove analyses) |
lt_checksum |
Whole-group XOR-fold integrity gate |
lt_wire |
Fixed 1472-byte packet format: Serialize / Parse (proved, so the receiver parses untrusted datagrams safely) |
Toolchain: GNAT 14.2.0 + gprbuild 24 + gnatprove (SPARK). tools/env.sh puts them on PATH
(adjust the paths there if the toolchain moves).
./tools/build.sh # gprbuild -> bin/{test_codec,sender_stream,receiver_stream,udp_decode_sink}
./bin/test_codec # in-memory round-trip test matrix
./tools/prove.sh # gnatprove over the whole SPARK core (incl. lt_wire)
./tools/receiver-test.sh # end-to-end: sender_stream -> receiver_stream (file + pipe + parallel)
./tools/loopback-test.sh # end-to-end: sender_stream -> decode sink, byte-compared
./tools/stress-test.sh # adversarial soak of the trusted shell (attacks + floods + scale)The proven core's entire input contract is a 1472-byte buffer, so it never names a socket — transport lives wholly in the trusted shell, and swapping it re-discharges none of the 175 proof obligations. Two are implemented:
| Build | Run | |
|---|---|---|
| Kernel (default) | ./tools/build.sh |
(nothing — it is the default) |
| DPDK (opt-in) | WITH_DPDK=yes ./tools/build.sh |
--with-dpdk --eal "<EAL args>" |
A default build is DPDK-free in the strict sense: it compiles no C, links no DPDK, and nm finds
zero rte_* symbols. Passing --with-dpdk to such a binary exits 2 with "built without DPDK
support" — you cannot get DPDK into the TCB by accident.
On the DPDK path the LT packet rides raw in an Ethernet frame (EtherType 0x88B6,
14 + 1472 = 1486 ≤ 1500 MTU): no IP, no UDP, no fragmentation. rte_eth_rx_burst replaces
recvmmsg and rte_eth_tx_burst replaces Send_Socket; Handle, Lt_Wire.Parse, the decoder and
the checksum gate are byte-for-byte the same code.
The trade, stated plainly. DPDK moves its EAL, mempool and NIC PMD — a large third-party C body — onto the data path inside the TCB, together with a small mandatory C shim (DPDK's burst API is
static inline, so there is no symbol for Ada to link against). Safety is unaffected: the core is still proved, and the checksum gate still turns any mis-decode into a detected.corrupt. What grows is what you must trust. The kernel path stays the assurance-maximal default. Full ledger:docs/ASSURANCE.md§5.1.
./tools/dpdk-test.sh # exercises the DPDK code path — no root, no hugepages, no NICThat test runs over memif, a shared-memory pipe between two processes on one machine. It genuinely drives EAL bring-up, the C shim and the burst API, and shows byte-exact decode with the checksum gate intact — but memif is not kernel bypass and does not cross a wire. It needs no root precisely because it bypasses nothing.
For real kernel bypass between two physical machines (vfio-pci, a spare Intel NIC, an IOMMU,
hugepages, root to set up), the whole flow is one command per machine:
./tools/bypass.sh doctor # no root, changes nothing
sudo ./tools/bypass.sh receiver eno2 # machine A
sudo ./tools/bypass.sh sender eno2 /path/to/bigfile # machine B
sudo ./tools/bypass.sh teardown eno2 # NIC back to the kernelEverything about kernel bypass — prerequisites, what is not bypass, the setup steps, running non-root, and troubleshooting — is in
KERNEL-BYPASS.TXT. Read it before binding anything: a NIC bound tovfio-pcivanishes from the kernel, and pointing this at your SSH card loses you the machine. (tools/vfio-setup.shrefuses to, but read it anyway.)
cat payload | sender_stream [--progress] [--pace-us N] <IP> <port> <SEED> <name> <loss%>Reads the payload from stdin, splits it into group-local ~10 MB groups, and emits over one UDP
port a pure LT fountain stream — loss-scaled XOR coding packets per group (no systematic clear
channel) — then a 5×-repeated end-of-transfer trailer carrying the exact size, group count and
whole-stream checksum. SEED and loss% must match the receiver. --pace-us throttles the send
(a real diode is paced by network backpressure; loopback is not). Coding packets carry only their
index — both ends derive the packet seed from (SEED, group, index) with Lt_Rng.Coding_Seed.
The degree distribution's spike constant (c=0.015, lt_soliton) was tuned empirically
(tests/test_overhead) so the receiver decodes reliably at ~1.15× K received; the sender
provisions to ~1.25× after loss. Pure coding at this c is markedly more efficient than a
systematic clear+coding scheme, which the tuned distribution pushes to >1.4× — so the clear channel
was dropped, cutting sender traffic ~17% versus the earlier 1.5× target and simplifying both ends.
receiver_stream [--pipe] [--progress] [--config <file>] \
[--max-inflight <n>] [--evict-timeout <s>] [<port> <spool> <SEED> <loss%>]Reads a key = value config file (--config <file>, else /etc/lt-diode/receiver.conf if
present) for port, spool, seed, loss, verify_log; with those set the positional args are
optional (precedence: defaults < config < CLI). Each finalized transfer appends a structured verdict
line to verify.log (<ts> <path> bytes=… verdict=ok|corrupt reason=ok|decode|checksum|size|eviction).
A sandboxed systemd unit and an annotated receiver.conf.example ship in the repo.
A tight capture loop parses each datagram, routes it to its transfer by FILEID, and
accumulates it into a pre-allocated group decoder state — never decoding inline. A separate
decode task reconstructs each completed group, writes it to that transfer's output, and on the
trailer applies the whole-stream checksum gate. Up to Max_Inflight transfers are in flight at
once, each finalizing independently; bounded RAM (a shared pool of group states handed off through a
protected scheduler).
- file mode is a daemon (loops indefinitely) creating
<spool>/<name>withopen(O_CREAT|O_EXCL|O_WRONLY|O_NOFOLLOW)(never overwrites, never follows a planted symlink; numbered.1,.2, … on collision) after sanitizing the FILEID to a safe basename, then writes a.finished(or.corrupt) marker per the gate. --pipestreams the decoded bytes straight to stdout, single-shot; the exit code is the verdict.- A stalled transfer (lost trailer) is evicted after a 10 s idle timeout →
.corrupt, without disturbing the other in-flight transfers.
Verification: tools/receiver-test.sh runs sender → receiver_stream in file mode, --pipe, and
3 concurrent parallel transfers, byte-comparing each (PASS at 3/12/25 MB, 1–3 groups). Sequential
transfers through one daemon and eviction-then-recovery are exercised too.
The capture loop drains the socket in batches with recvmmsg (MSG_WAITFORONE) — up to 64
datagrams per syscall, returning as soon as one is in hand — which is what lets it keep up. The ABI
struct msghdr/mmsghdr layout is hand-bound and guarded by a start-up size check.
Throughput note. With recvmmsg the receiver keeps up an order of magnitude faster than
one-at-a-time (--pace-us 5 is reliable here vs. 40 before). The residual limit is the OS socket
buffer, which is small by default (net.core.rmem_max) and can't be raised without privilege on this
host — under a full no-pacing blast on loopback a group still occasionally drops below the decode
margin. A real deployment raises rmem_max (as the C reference documents) and/or is paced by the
diode link; the tools pace the sender to stay comfortably inside the margin.
test_codec builds a fresh random 10 MB group, emits K clear + N coding packets, drops a fraction
of all of them at random, replays the index set on the receiver side from the packet seed,
peel-decodes, and checks byte-exact reconstruction and the whole-group checksum gate. Redundancy
is scaled with the loss (as the sender's N_send = ceil(N_needed/(1-loss)) does):
loss= 0% received=11062 decoded=TRUE mismatches=0 checksum=OK -> PASS
loss=10% received=11095 decoded=TRUE mismatches=0 checksum=OK -> PASS
loss=20% received=11093 decoded=TRUE mismatches=0 checksum=OK -> PASS
loss=30% received=11052 decoded=TRUE mismatches=0 checksum=OK -> PASS
ALL TRIALS PASS
All 175 checks proved — 0 unproved, 0 justified. The whole codec core — the peeling Decode and
the lt_wire packet parser included — is proved AoRTE-clean (no overflow, no out-of-bounds
indexing, no division by zero), with every functional contract, loop termination and initialization
check discharged and no pragma Assume/justification anywhere.
| Check family | Proved |
|---|---|
| Run-time checks (overflow / index / range / division) | 66 / 66 |
| Assertions & loop invariants | 31 / 31 |
Functional contracts (Valid, pre/post) |
11 / 11 |
| Termination | 9 / 9 |
| Initialization + data dependencies | 30 / 30 |
How the harder obligations were closed:
- The peeling decoder (
Decode, 38 checks). The incidence index (source → packets) is an intrusive singly-linked list over the edge slots (aHead/Nxtpair keyed by a plain edge counter) rather than a counting-sort CSR, so every access is in range from its subtype with no prefix-sum reasoning. Edge-span bounds come from aValidghost predicate carried throughAdd_Packet'sPostandDecode'sPre; the ripple-stack andRemndecrements are protected by capacity guards the algorithm never actually trips. Add_Packet(18 checks). Well-formedness lives inPre/Post(checked at call boundaries) rather than a type predicate — a predicate is re-checked after every component write, and the intermediate state (edge count bumped before packet count) transiently violates it.- The soliton table. The cumulative accumulator's overflow bound falls out of the prover's
loop-bound analysis once each term is asserted to lie in
[0, 2].
- Phase 0 — scaffold ✅ project, build/prove scripts, toolchain wiring
- Phase 1 — proven codec core ✅ AoRTE-clean incl. the peeling decoder
- Phase 2 —
sender_stream✅ proven wire format + stdin framing, single-port emit, UDP, pacing, CLI; verified byte-exact over loopback - Phase 3 —
receiver_stream✅ done: decoupled capture/decode, parallel per-FILEID transfers (runtime--max-inflight),recvmmsgbatching,O_EXCL|O_NOFOLLOWwrites, checksum gate, runtime-tunable--evict-timeouteviction,--pipe, config file +verify.logjournal + systemd unit, timestamped/levelled logging (stderr/file/syslog), byte-exact end-to-end - Phase 4 — integration & hardening ✅
tools/check.sh(build + proof + in-memory matrix + file/pipe/parallel receiver + loopback) andtools/stress-test.sh— an adversarial soak of the trusted shell (path-traversal / symlink attacks, garbage-datagram floods, 40 MB transfers, concurrency + eviction) that asserts the daemon never crashes and never writes outside its spool. It surfaced and fixed three real robustness bugs: aNatural(part_no)overflow crash on hostile packets, a blocking pool-acquire that could deadlock the capture loop, and idle-only eviction; the capture loop now has a defence-in-depth handler so no single datagram can take it down - Phase 5 — docs & assurance ✅ codec core fully proved AoRTE (0 unproved, 0 justified);
man/man1/{sender,receiver}_stream.1man pages; and the written assurance argument,docs/ASSURANCE.md— what is proven, what is trusted, where the boundary sits and why, the proof's assumptions, and how the trusted shell is justified
Ada/SPARK port by Alin-Adrian Anton (alin.anton@upt.ro), Politehnica University of Timișoara,
of lt-diode-pro, itself a fork of
melorian94/robust-soliton-LT-C by Petra — the LT / robust-soliton core, the feedback-free
one-way design and the three-port UDP scheme are hers. As a derivative of an AGPL-3.0 work,
this port is likewise GNU AGPL-3.0. See AUTHORS for details.