Introduction
Fiducia is a language for building embedded applications on seL4 Microkit systems. A Fiducia project describes a system of protection domains (PDs) connected by channels, each PD implemented by external C / Rust code and modelled by a CSP-style process algebra. From this single description the toolchain produces both a bootable Microkit image and a formal model that can be checked for deadlock-freedom.
This manual is the task-oriented guide to the toolchain. For the
syntax and semantics of the language itself, see the
Fiducia Language Reference
— it stays focused on .fis / .fi syntax, while this manual covers
installing, configuring, running, and verifying projects day-to-day.
🚧 Fiducia is under active development. The language and tools are not yet complete; interfaces described here may change between releases.
The toolchain at a glance
| Tool | Role |
|---|---|
fic | The compiler: parses, validates, and type-checks a project; emits ASTs, SDF XML, topology graphs, UPPAAL models, or a bootable Microkit image. |
fire | The project runner: reads fire.toml and drives fic — fire build produces the image, fire run builds and launches it on QEMU or hardware, fire verify model-checks the UPPAAL model with verifyta. |
A Fiducia project
A project is a directory containing:
- one
.fisfile — the system description: memory regions, protection domains, channels, and the system composition; - one
.fifile per protection domain — the PD implementation: imports of C sources, memory mappings, event declarations, and process definitions (the fileeth_outer.fiimplements the PD namedeth_outer); - the imported
.cimplementation files (.rsimports parse but aren’t runnable yet — the C backend rejects them; see--emit=c); - a
fire.tomlmanifest tellingfirehow to build and run it (see The Project Manifest).
The compiler pipeline is:
discover → parse → resolve → validate → typecheck → WF2 → emit
where typecheck enforces information-flow security (no High→Low leaks via channels) and WF2 checks the channel dependency graph for cycles (a structural deadlock-freedom condition).
Where to go next
- Install the tools.
- Write or copy a
fire.toml. - Build and run on QEMU — the
demo/datadiode/project in the repository is a complete worked example. - Verify deadlock-freedom.
- Consult the
ficreference for the individual compiler emit modes.
Installation
Fiducia ships two binaries: fic (the compiler) and fire (the
project runner). They are not yet published to crates.io — install
the pre-built binaries from the website.
Pre-built binaries (recommended)
Pre-built binaries are published monthly (and on manual dispatch) to
fiducia-lang.github.io/releases.
Available platforms: linux-x86-64, macos-arm64.
# Download and extract (replace <version> and <platform> accordingly)
curl -fsSL -o fic.tar.gz \
https://fiducia-lang.github.io/releases/v<version>/fic-<platform>-<version>.tar.gz
tar -xzf fic.tar.gz
# Move to a directory in your PATH (the tarball contains both binaries)
sudo mv fic fire /usr/local/bin/
# Or for user-local installation
mv fic fire ~/.local/bin/
# Verify installation — fic and fire share one toolchain version
fic --version
fire --version
This manual and the language reference are published at fiducia-lang.github.io/docs/manual.
ℹ️
cargo install fic firewill become available once the project is published to crates.io after the language design stabilises.
Building from source
ℹ️ The source repository is private while the language is under development; building from source currently requires collaborator access.
git clone https://github.com/fiducia-lang/fic
cd fic
cargo install --path . # fic
cargo install --path fire # fire
The repository also provides a Nix flake (nix develop) that supplies
the full toolchain — Rust, the Microkit SDK, and the cross-compiler —
with no manual setup. See
CONTRIBUTING.md.
Prerequisites for building images
Compiling a project to a bootable image (fire build, fire run, or
bare fic — image generation is its default mode) needs the seL4 Microkit SDK and an AArch64
bare-metal cross-compiler.
Microkit SDK
# macOS (Apple Silicon):
curl -fsSL -o microkit-sdk.tar.gz \
https://github.com/seL4/microkit/releases/download/2.2.0/microkit-sdk-2.2.0-macos-aarch64.tar.gz
# Linux (x86-64):
curl -fsSL -o microkit-sdk.tar.gz \
https://github.com/seL4/microkit/releases/download/2.2.0/microkit-sdk-2.2.0-linux-x86-64.tar.gz
tar -xzf microkit-sdk.tar.gz
ARM GNU toolchain
# macOS (Apple Silicon):
curl -fsSL -o arm-gnu-toolchain.tar.xz \
https://developer.arm.com/-/media/Files/downloads/gnu/15.2.rel1/binrel/arm-gnu-toolchain-15.2.rel1-darwin-arm64-aarch64-none-elf.tar.xz
# Linux (x86-64):
curl -fsSL -o arm-gnu-toolchain.tar.xz \
https://developer.arm.com/-/media/Files/downloads/gnu/15.2.rel1/binrel/arm-gnu-toolchain-15.2.rel1-x86_64-aarch64-none-elf.tar.xz
tar -xJf arm-gnu-toolchain.tar.xz
QEMU (for fire run in QEMU mode)
Install qemu-system-aarch64 from your package manager
(brew install qemu, apt install qemu-system-arm, …).
UPPAAL (for fire verify)
fire verify runs the UPPAAL command-line
model checker verifyta. Download UPPAAL and either put verifyta
on your PATH, set $VERIFYTA, or point to it from
fire.toml.
UPPAAL requires a (free academic) licence.
Environment variables
| Variable | Needed by | Description |
|---|---|---|
MICROKIT_SDK | fire build / run, fic image generation | Path to the extracted Microkit SDK. Fallback when [run.target] sdk is not set. |
GCC | fire build / run, fic image generation | Full path to the AArch64 cross-compiler binary. Fallback for [run.target] gcc. |
LD | fire build / run, fic image generation | Full path to the AArch64 cross-linker binary. Fallback for [run.target] ld. |
VERIFYTA | fire verify | Path to the verifyta binary (falls back to PATH). |
FIRE_FIC | fire | Override the path to the fic binary. |
# Set once — add to your shell profile
export MICROKIT_SDK=<path-to-microkit-sdk>
export GCC=<path-to-arm-gnu-toolchain>/bin/aarch64-none-elf-gcc
export LD=<path-to-arm-gnu-toolchain>/bin/aarch64-none-elf-ld
Projects can instead pin these per-project in
fire.toml’s [run.target] section.
The Project Manifest (fire.toml)
fire reads a fire.toml manifest from the project directory.
It names the project, locates the sources, and configures how
fire build / fire run build and execute the image and how fire verify invokes
the model checker.
🚧 This page documents the manifest schema introduced with the
fire build/fire run/fire verifysubcommands (PR #174). Until that lands on a release, the previous[target]/[qemu]layout applies — see theCHEATSHEET.mdof your installed version.
Complete example
[project]
name = "datadiode" # output filename stem: build/datadiode.img / .xml
src = "." # directory with the .fis/.fi files, relative to this file
[run]
mode = "qemu" # qemu | hardware (hardware is not yet implemented)
# Arguments for qemu-system-aarch64; {img} is substituted with the
# absolute path to the built image at runtime.
args = [
"-machine", "virt,virtualization=on",
"-cpu", "cortex-a53",
"-serial", "mon:stdio",
"-device", "loader,file={img},addr=0x70000000,cpu-num=0",
"-m", "size=2G",
"-nographic",
]
[run.target]
board = "qemu_virt_aarch64" # Microkit board identifier
profile = "debug" # debug | release
# sdk / gcc / ld — optional, fall back to $MICROKIT_SDK / $GCC / $LD
# allow_wf2_cycles = true # skip WF2 when `fire verify` covers deadlock-freedom
[verify]
mode = "verifyta" # verification engine
# path / args — optional; path falls back to $VERIFYTA / PATH
[project] — identity and sources
| Field | Required | Description |
|---|---|---|
name | yes | Short name used as the output filename stem: fire build / fire run write build/<name>.img, fire verify writes build/<name>.xml and reads the hand-written <src>/<name>.q. |
src | yes | Directory containing the .fis / .fi files, relative to fire.toml. Build artifacts go to <src>/build/. |
[run] — execution mode and arguments
| Field | Required | Description |
|---|---|---|
mode | yes | qemu or hardware. Hardware deployment is not yet implemented. |
args | yes | Arguments passed verbatim to the executor (qemu-system-aarch64 in QEMU mode). The placeholder {img} is substituted with the absolute path to the built .img. |
[run.target] — board and toolchain
| Field | Required | Description |
|---|---|---|
board | yes | Microkit board identifier, e.g. qemu_virt_aarch64, kria_k26. Must exist in the SDK’s board/ directory. |
profile | yes | Microkit build profile: debug or release. |
sdk | no | Path to the Microkit SDK root. Falls back to $MICROKIT_SDK. |
gcc | no | AArch64 cross-compiler binary. Falls back to $GCC. |
ld | no | AArch64 cross-linker binary. Falls back to $LD. |
allow_wf2_cycles | no | Pass --allow-wf2-cycles to fic: skip the structural WF2 acyclicity check. Use when deadlock-freedom is established by fire verify instead — WF2 rejects cyclic topologies even when they are deadlock-free under a fair scheduler. Default false. |
When sdk / gcc / ld are set they are passed to the fic child
process as environment variables, overriding any values already in
your environment — useful for pinning a toolchain per project.
[verify] — formal verification
Optional section; configures fire verify.
| Field | Required | Description |
|---|---|---|
mode | yes | Verification engine. Currently verifyta (UPPAAL); spin, prism, … may follow. |
path | no | Path to the verifier binary. Falls back to $VERIFYTA, then to verifyta on PATH. |
args | no | Extra flags appended after the model and query arguments. |
Worked example
The repository’s demo/datadiode/fire.toml is a complete,
runnable manifest for the data-diode demo — three protection domains
(eth_outer, dd, eth_inner) built into one image and launched on
qemu_virt_aarch64 with two virtio-net devices, driven from the host
(demo/datadiode/inject.py) since a foreign-MAC diode needs raw
frames a NAT can’t deliver.
Building and Running: fire build / fire run
fire build compiles a project to a bootable Microkit image;
fire run builds and then launches it as configured in
fire.toml — mimicking the cargo tools.
🚧 The
build/runsubcommands land with PR #174. On earlier versions the same behaviour is the barefire <PROJECT_DIR>invocation.
Usage
fire build [PROJECT_DIR] [-o <IMG>] # produce the image only
fire run [PROJECT_DIR] [-o <IMG>] # build, then launch
| Argument / flag | Description |
|---|---|
PROJECT_DIR | Directory containing fire.toml. Defaults to the current directory. |
-o, --output <IMG> | Override the output .img path (default: <src>/build/<name>.img). |
What it does
- Reads
fire.tomlfromPROJECT_DIRand resolves the source directory (project.src). - Builds the image — incrementally, like
cargo: whenbuild/<name>.imgis already newer than every source file andfire.toml, the rebuild is skipped (✓ image up to date). Otherwise it invokesfic <src> --board <board> --build-profile <profile>(image generation is fic’s default mode), with the toolchain from[run.target](or theMICROKIT_SDK/GCC/LDenvironment). This compiles each PD’s C file, patches the mapping setvars, and packs the loader, seL4 kernel, and PD ELFs intobuild/<name>.img. The freshness check is mtime-based — coarser than cargo’s fingerprints, but any source edit triggers a rebuild. - (
fire runonly) Inmode = "qemu", launchesqemu-system-aarch64with the[run] args, substituting{img}with the built image path.mode = "hardware"is reserved and not yet implemented.
Walkthrough: the data-diode demo
$ fire run demo/datadiode/
fire: building datadiode for qemu_virt_aarch64 (debug)
✓ Parsed 1 system + 3 program files
✓ Resolved data layer (2 structs, 1 consts)
✓ Resolved names
✓ Resolved mapping vaddrs
✓ Type-checked information flow
✓ Checked channel dependency acyclicity (WF2)
MICROKIT|CAPDL SPEC: number of root objects = 84, spec footprint = 12.44 KiB
MICROKIT|INITIAL TASK: memory size = 412 KiB
MICROKIT|LOADER: image file size = 2.59 MiB
fire: launching QEMU ...
QEMU then boots the image; with -serial mon:stdio the PDs’ serial
output appears in your terminal. Exit QEMU with Ctrl-A x.
To produce the image without launching:
fire build demo/datadiode/
ls demo/datadiode/build/datadiode.img
A Microkit build report is written next to the image at
build/report.txt — useful for inspecting the memory layout and the
setvar patches applied to each PD.
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
could not read .../fire.toml | PROJECT_DIR doesn’t contain a manifest; pass the project directory, not the repo root. |
Could not read MICROKIT_SDK environment variable | Set $MICROKIT_SDK or [run.target] sdk. Same pattern for GCC / LD. |
board path '...' does not exist | [run.target] board/profile doesn’t match a directory under <sdk>/board/. |
fic: command not found | Install fic on PATH or set $FIRE_FIC to the binary. |
could not patch symbol '...' ... no symbol named '...' | A channel declared in the .fis has no matching uint64_t <chan>; symbol in the PD’s C file. Mapping symbols (<mapping>_vaddr …) are emitted only when declared, but channel-ID symbols are required. |
WF2 violation: channel dependency graph contains a cycle | The topology is cyclic. Either restructure the channels, or establish deadlock-freedom with fire verify and set allow_wf2_cycles = true in [run.target]. |
| C compile errors | fic compiles each <pd>.c with -Wall -Werror; fix the warning or build the file out-of-band. |
| QEMU exits immediately | Check the [run] args device/loader address against the board; the demo uses addr=0x70000000 for qemu_virt_aarch64. |
Verifying: fire verify
fire verify checks a project’s process-algebra model with the
UPPAAL model checker. It emits a UPPAAL XTA
model of the system (every PD’s processes as timed automata, channels
as synchronisations), then runs verifyta and pretty-prints
per-query PASS / FAIL results.
🚧 The
verifysubcommand lands with PR #174. On earlier versions the same behaviour isfic <PROJECT_DIR> --verify.
Usage
fire verify [PROJECT_DIR] [-q <QUERY_FILE>]
| Argument / flag | Description |
|---|---|
PROJECT_DIR | Directory containing fire.toml. Defaults to the current directory. |
-q, --query <QUERY_FILE> | Query file to check (defaults to the hand-written <src>/<name>.q next to the sources). |
Writing queries
Queries are user-authored — which properties matter is
application-dependent, so fic emits only the model and you state the
properties. Write a UPPAAL query file
named <name>.q next to your sources (it is part of the project, so
commit it). The customary first line is deadlock-freedom:
/* the system never gets permanently stuck */
A[] not deadlock
/* the diode can complete a forward */
E<> dd.L1
Queries reference the process instance names from the emitted model
(the PD names, snake_case) and template location names — inspect
build/<name>.xml (or open it in the UPPAAL GUI) to find them.
What it does
- Reads
fire.tomland resolves the source directory. - Invokes
fic <src> --emit=xta, writingbuild/<name>.xml(the UPPAAL model). - Locates the query file: the
-qflag, or<src>/<name>.q. Errors if neither exists. - Locates
verifyta—[verify] path→$VERIFYTA→PATH— and runs it with-t 0(request a counterexample / witness trace), the model, the query file, and any[verify] args. - Parses the output and prints one PASS / FAIL line per query. Exits non-zero if any query fails.
Reading the output
$ fire verify demo/datadiode/
✓ Running verifyta: /opt/uppaal/bin/verifyta
A[] not deadlock PASS
E<> EthOuter.Handle_eth PASS
...
A failing query prints FAIL and, when verifyta produces one, the
counterexample trace:
A[] not deadlock FAIL
Trace (12 steps):
pd0.State0 → pd0.State1
...
↯ stuck at: pd0.State1, pd1.State2, ...
The ↯ stuck at line lists each automaton’s location in the deadlocked
state — usually enough to spot the circular wait. A FAIL on a
reachability query with (property unreachable — no counterexample
trace) means the named location can never be entered.
Worked example: ping-pong
demo/pingpong/ ships two PDs (ping/pong) that bounce a bounded
notification ROUNDS times over a genuinely cyclic channel
(ping <-> pong), then go quiescent — a real case where WF2’s
over-approximation would reject a topology that’s actually
deadlock-free. Its hand-written pingpong.q states that, verified
against the real emitted model (see tests/uppaal/pingpong/expected.xml
for a known-good snapshot):
fire verify demo/pingpong/
# A[] (deadlock imply ping.rounds == ROUNDS) PASS
# E<> ping.rounds == ROUNDS PASS
# A[] ping.rounds <= ROUNDS PASS
# E<> pong.L1 PASS
The first query is deadlock-freedom modulo intended termination: the
only reachable quiescent state is the completed rally, so a blanket
A[] not deadlock would itself report a false “deadlock” at the
correct terminal state — see the header comment in
demo/pingpong/pingpong.q for the full reasoning.
Ad-hoc queries
Pass a different query file for a one-off check — a per-invocation
input, like a cargo test filter:
fire verify demo/datadiode -q queries/liveness.q
Relationship to the WF2 check
fic always runs WF2, a structural acyclicity check on the
channel dependency graph, before emitting a deployable image. WF2 is
an over-approximation: it rejects every cyclic topology, including
systems that are deadlock-free under a fair scheduler (demo/pingpong’s
ping <-> pong, for instance). When fire verify establishes
deadlock-freedom, you can build such systems anyway with
fic --allow-wf2-cycles — see the
fic reference.
The fic Compiler
fic is the Fiducia compiler. fire drives it for the common
build-and-run / verify workflows; invoke it directly to check a
project, build an image with explicit flags, or inspect any
intermediate representation.
fic <PROJECT_DIR> --board <BOARD> --build-profile <PROFILE> [-o <IMG>] # image (default)
fic <PROJECT_DIR> --check # front-end only
fic <PROJECT_DIR> --emit=<FORMAT> [-o <PATH>] # inspection emits
PROJECT_DIR is a directory containing exactly one .fis file and
one .fi file per protection domain (bound by filename: eth_outer.fi
implements PD eth_outer).
By default — like rustc, whose default output is a binary — fic
compiles the project to a bootable Microkit image: it runs the full
front-end, lowers to SDF, cross-compiles each PD’s C file, patches the
setvars, and packs the loader, seL4 kernel, and PD ELFs into
<project>.img. This requires --board / --build-profile and the
MICROKIT_SDK / GCC / LD environment.
With --check, fic stops after the front-end — parse, data-layer
resolution, name resolution, vaddr materialization, information-flow
typecheck, WF2 acyclicity — and reports diagnostics without writing
any output (the cargo check of Fiducia):
$ fic demo/datadiode/ --check
✓ Parsed 1 system + 3 program files
✓ Resolved data layer (2 structs, 1 consts)
✓ Resolved names
✓ Resolved mapping vaddrs
✓ Type-checked information flow
✓ Checked channel dependency acyclicity (WF2)
--emit formats
--emit replaces the default image output with an intermediate
representation:
| Format | Output (default path) | Description |
|---|---|---|
rawast | stdout / -o | Parsed AST as JSON, before validation. |
ast | stdout / -o | Validated AST as JSON (vaddrs allocated, names resolved). |
c | <project>/build/gen/ | Generated per-PD C state machines plus <project>_types.h/<pd>_types.h. No image is built. |
sdf | <project>.system | SDF IR as XML — debug bridge for Microkit developers. |
dot | <project>.dot | Graphviz topology graph. Render with dot -Tpng <project>.dot -o topology.png. |
xta | <project>.xml | UPPAAL XTA model. Queries are user-authored — see fire verify. |
The inspection formats (rawast, ast, c, dot) skip the WF2 check —
they have no deployment target. xta exists precisely to analyse
cyclic systems, so it also skips WF2; image generation (the default)
and sdf enforce it.
Options
--check
Run the front-end only; produce no artifact. Conflicts with --emit.
-o, --output <PATH>
Override the default output path for the image or the chosen emit mode.
--board <BOARD>, --build-profile <PROFILE>
Required for image generation (the default mode): the Microkit board
identifier (e.g. qemu_virt_aarch64, kria_k26) and build profile
(debug / release). Must match a directory under
$MICROKIT_SDK/board/.
--allow-wf2-cycles
Skip the WF2 acyclic-channel-dependency check.
WF2 is a structural over-approximation: it rejects any cyclic channel
dependency graph, including programs that are deadlock-free under a
fair scheduler (e.g. the asymmetric dining philosophers, Hoare CSP
§2.5.4). Use this flag when deadlock-freedom is established by an
external authority — typically fire verify on the
emitted UPPAAL model.
Language server
The Language Server Protocol server is a separate binary, fic-lsp
(analogous to rust-analyzer vs rustc), built from the fic-lsp
workspace crate and used by the editor integrations under editors/.
Install it with cargo install --path fic-lsp; it speaks LSP on stdio.
Environment variables
| Variable | Needed by | Description |
|---|---|---|
MICROKIT_SDK | image generation | Path to the extracted Microkit SDK. |
GCC | image generation | AArch64 cross-compiler binary. |
LD | image generation | AArch64 cross-linker binary. |
RUST_LOG | debugging | Log verbosity (debug, info, warn). |
RUST_BACKTRACE | debugging | Show backtraces on panic (1 or full). |
Diagnostics
Errors are rendered as annotated source snippets with the offending
span highlighted — parse errors, duplicate names, undeclared
references, vaddr misalignment / overlaps, information-flow
violations, and WF2 cycles all point back to the .fis / .fi
source location.
Exit status
fic exits 0 on success and non-zero when any pipeline stage
reports diagnostics, making it safe to use in scripts and CI.