Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ jobs:
(cd /tmp/otp-28.5.0.3 && ./Install -minimal "$PWD" > /dev/null)
TRINITY_BELOW_FLOOR_OTP=/tmp/otp-28.5.0.3/bin mix test --only below_floor_otp test/trinity/toolchain_node_test.exs

# Slice 141: the headless release's floor is OTP 28.5.0.8. OTP 28.5.0.7 is the release just
# before it: a headless node on it must be refused, naming erts and ssl, while a desktop node
# on the same runtime boots. Prebuilt by Hex for this runner's OS,
# pinned by the sha256 in builds.txt (matched a download on 2026-10-20). The untagged case
# beside it runs in `mix gate` on this job's own runtime, where a headless node refuses.
- name: A headless boot on OTP 28.5.0.7, below the headless floor, refuses
run: |
curl -fsSL -o /tmp/otp-28.5.0.7.tar.gz https://builds.hex.pm/builds/otp/ubuntu-24.04/OTP-28.5.0.7.tar.gz
echo "90f1dfa231e1536069e445e0d212a8b8fb57f2e8601c55062f500b2a42699a3d /tmp/otp-28.5.0.7.tar.gz" | sha256sum -c -
mkdir -p /tmp/otp-28.5.0.7
tar -xzf /tmp/otp-28.5.0.7.tar.gz -C /tmp/otp-28.5.0.7 --strip-components=1
(cd /tmp/otp-28.5.0.7 && ./Install -minimal "$PWD" > /dev/null)
TRINITY_BELOW_HEADLESS_FLOOR_OTP=/tmp/otp-28.5.0.7/bin mix test --only below_headless_floor_otp test/trinity/toolchain_node_test.exs

# Slice 120 AC1. `reuse lint` is a Python tool and this is the only leg that installs one,
# deliberately: `mix gate` has to stay runnable inside the FIPS container and on a developer's
# machine without a Python environment, so the REUSE check is a step here rather than a row in
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,25 @@ algorithm, Ed25519 admitted at `nss`, any `typ` admitted in the carrier, a refus
apart from a bad key by the error alone, a key's registered algorithm ignored) each turned their
own tests red.

### `slice/141`: the headless release asks for OTP 28.5.0.8

The headless release, the one deployed as a server, now refuses to boot below OTP 28.5.0.8 and on
any OTP line but 28. 28.5.0.8 fixes CVE-2026-94200 and CVE-2026-105236 in ssl, CVE-2026-42789 in
public_key and CVE-2026-48856 in inets. Those fixes are in the applications, and 28.5.0.8's erts
can be installed without them, so the check compares every loaded application with 28.5.0.8's row
of erlang/otp's `otp_versions.table`, checked in verbatim. The desktop keeps the slice 137 floor, because its runtime is the one Burrito can
fetch; ADR-0014 carries a proposed amendment that says so. Every node now also asks its decoder
directly whether it refuses the oversize tuple CVE-2026-55737 is about, instead of trusting the
version alone. The container images build OTP 28.5.0.8, and the FIPS leg's image now installs the
Hex and rebar3 the hardened image pins rather than the newest, which changed under it when it was
rebuilt.

Verified by the test suite: the floor at each boundary for both releases; a real headless boot on
the suite's own runtime, refused on 28.5.0.6 and booting on 28.5.0.8, with the expectation read from
the runtime's `OTP_VERSION` file; a real boot on OTP 28.5.0.7, refused as headless and booting
as desktop; a runtime with 28.5.0.8's erts and an older ssl refused, naming ssl; and the decoder probe catching the defect on OTP 28.1.1 and
28.5.0.3.

## 2026-10-08

### `slice/136`: the web pages ask who is there
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,10 @@ demonstrate, each tied to the slice that proves it.
## Running from source

Requires the pinned toolchain in `.tool-versions` (Erlang 28.5.0.6, Elixir 1.20.4, Zig 0.16.0),
installed with `asdf install`. The container images build Erlang 28.5.0.7 from source
(`ci/container.tool-versions`); the desktop's pin waits for Burrito's builds of it and, until then,
holds every outbound TLS connection to TLS 1.2 (`docs/adr/0014-otp-pins-split.md`). Rust 1.92.0 is pinned separately in `rust-toolchain.toml` and is
installed with `asdf install`. The container images build Erlang 28.5.0.8 from source
(`ci/container.tool-versions`), the floor the headless release refuses to boot below (Slice 141);
the desktop's pin waits for Burrito's builds and, until then, holds every outbound TLS connection
to TLS 1.2 (`docs/adr/0014-otp-pins-split.md`). A headless node run from source needs 28.5.0.8 too. Rust 1.92.0 is pinned separately in `rust-toolchain.toml` and is
only needed for the desktop shell.

```
Expand Down
2 changes: 1 addition & 1 deletion VERSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ never pin a version hex marks as retired or vulnerable.
| Name | Pin | Verified | Note |
|---|---|---|---|
| `Erlang/OTP` | **28.5.0.6** | ✅ `.tool-versions` | The desktop build and the gate. Pinned to the newest OTP whose ERTS Burrito 1.6.0 can fetch for all three targets (macOS universal, Linux x86_64, Windows), measured by probing Burrito's artifact sources, not read from a README (ADR-0005). Was 28.5.0.5 from Slice 000. 28.5.0.6 since 2026-10-08 (owner decision D2, ADR-0014): 200 on all three; 28.5.0.7, which fixes CVE-2026-89422, is 404 on Burrito's CDN for Linux and macOS, so this runtime carries that CVE and `Trinity.TLS` holds every outbound TLS client to TLS 1.2 while it runs. `.github/workflows/otp-canary.yml` probes the CDN daily and opens the pull request that moves this pin to the container's. ⚠️ Windows tracks OTP releases immediately while the other two lag a third-party CDN's build queue. |
| `Erlang/OTP (container images)` | **28.5.0.7** | ✅ `ci/container.tool-versions` | Added 2026-10-08 (owner decision D2, ADR-0014): the OTP the hardened headless image (`ci/ironbank/`) and the FIPS leg's image (`ci/fips/Containerfile`) build from source, so it does not wait for Burrito. 28.5.0.7 (released 2026-09-22) fixes CVE-2026-89422 (ssl, Critical) and CVE-2026-65634 (asn1, High). The source archive's SHA-256, `ddf17db6d3e9b7a7cfac0d72238ddc8ea040fedc5e3dfad82fc90675319d6c93`, was read from the release's SHA256.txt and its asset digest and matched a download. `mix trinity.ironbank.lint` holds the manifest to this file, and this pin to the desktop's major and to no older than it. |
| `Erlang/OTP (container images)` | **28.5.0.8** | ✅ `ci/container.tool-versions` | Added 2026-10-08 (owner decision D2, ADR-0014): the OTP the hardened headless image (`ci/ironbank/`) and the FIPS leg's image (`ci/fips/Containerfile`) build from source, so it does not wait for Burrito. Moved from 28.5.0.7 to 28.5.0.8 on 2026-10-20 (slice 141), the headless release's floor (`Trinity.Toolchain`): 28.5.0.8 (released 2026-10-14) fixes CVE-2026-94200 and CVE-2026-105236 (ssl), CVE-2026-42789 (public_key) and CVE-2026-48856 (inets). The source archive's SHA-256, `8a696e15bcbe5d57197b1f4c03c92d415f9fd9af70f9222a05911c8a5463cba6`, was read from the release's SHA256.txt and its asset digest and matched a download. `mix trinity.ironbank.lint` holds the manifest to this file, and this pin to the desktop's major and to no older than it. |
| `Elixir` | **1.20.4-otp-28** | ✅ `.tool-versions` | Confirmed at Slice 000: `elixir --version` reports Elixir 1.20.4 on Erlang/OTP 28, erts-16.4.0.5. Built-in type checker is part of the gate. `boundary` 0.10.4 compiles and enforces on this pair, measured at Slice 000 (H7). |
| `asdf` | v0.18.0 | 📐 `asdf --version` | `.tool-versions` committed in Slice 000. `mise` is absent on the build machine; measured at Slice 000 G1 with `which mise asdf`. asdf cannot pin itself, so this row is a command, not a file. ⚠️ Measured at Slice 001 line 3: asdf does **not** fail on a tool it has no plugin for, a `rust 1.92.0` line is omitted from `asdf current` and `asdf install` still exits 0. A pin file entry is only a pin where a plugin exists. |
| `Rust` | **1.92.0** | ✅ `rust-toolchain.toml` | Measured at Slice 001 line 3: `rustc --version` reports 1.92.0 (ded5c06cf 2025-12-08), exit 0. Pinned in `rust-toolchain.toml`, **not** `.tool-versions`: `asdf` here has no rust plugin and silently ignores a rust line, whereas `rustup show active-toolchain` reports this file as an override. See NOTES.md deviation D1. Corrected 2026-09-06: this row previously read `Rust + Tauri CLI | stable | ✅ .tool-versions`, which named a file carrying neither. |
Expand Down
2 changes: 1 addition & 1 deletion ci/container.tool-versions
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
# comes from Burrito's ERTS builds and a container's does not (ADR-0014).
# `mix trinity.ironbank.lint` holds the manifest's OTP resource to this line, the two pins to one
# OTP major, and this pin to no older than the desktop's.
erlang 28.5.0.7
erlang 28.5.0.8
24 changes: 20 additions & 4 deletions ci/fips/Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
# ci/container.tool-versions, the container images' OTP, and Elixir from .tool-versions (ADR-0014:
# the container's OTP can lead the desktop's). The two archives are checked against digests pinned
# here; a new OTP or Elixir pin is a new digest in this file, read from the release page.
# OTP 28.5.0.7's digest was read from the release's SHA256.txt and the release API's asset digest,
# and matched a download, on 2026-10-08.
# OTP 28.5.0.8's digest was read from the release's SHA256.txt and the release API's asset digest,
# and matched a download, on 2026-10-20 (slice 141; 28.5.0.7 on 2026-10-08).
#
# Base pinned by digest, not tag: registry.access.redhat.com/ubi9/ubi:latest resolved on
# 2026-09-20 (Red Hat Enterprise Linux release 9.8, openssl-libs 3.5.8-1.el9_8, FIPS provider
Expand All @@ -20,7 +20,7 @@ FROM registry.access.redhat.com/ubi9/ubi@sha256:9295c5c688f487fa5cf27a734fa55ecd

ARG OTP_VERSION
ARG ELIXIR_VERSION
ARG OTP_SHA256=ddf17db6d3e9b7a7cfac0d72238ddc8ea040fedc5e3dfad82fc90675319d6c93
ARG OTP_SHA256=8a696e15bcbe5d57197b1f4c03c92d415f9fd9af70f9222a05911c8a5463cba6
ARG ELIXIR_SHA256=ea7ff98bc1ed76c663a5d034c863c6fd37e65c9c855b6bbf6ccb2034d806673c

# The toolchain OTP's configure needs, plus what the gate's steps call from the shell: git
Expand Down Expand Up @@ -60,11 +60,27 @@ RUN set -eu; \

# Hex and rebar in a path that does not depend on HOME: a GitHub container job sets HOME to
# /github/home, and an install under /root would be invisible there.
#
# Both pinned, as ci/ironbank/hardening_manifest.yaml pins them, by the SHA-512 builds.hex.pm
# publishes (installs/hex.csv, installs/rebar.csv). This line was `mix local.hex --force && mix
# local.rebar --force`, which installs the newest of each when the image is built: the image built
# on 2026-10-08 got Hex 2.5.1, the one rebuilt for OTP 28.5.0.8 on 2026-10-20 got 2.5.3, and the
# bill of materials names the running Hex, so the gate's THIRD_PARTY_LICENSES.md check failed on
# the new image (slice 141). test/ci_pins_test.exs holds these lines to the manifest.
ENV PATH=/usr/local/elixir/bin:/usr/local/bin:$PATH \
LANG=C.UTF-8 \
MIX_HOME=/opt/mix \
HEX_HOME=/opt/hex
RUN mix local.hex --force && mix local.rebar --force
ARG HEX_SHA512=690f12f6933088bb32f1d7125485fb13426318c03cfeabcbd3cce55824cddf2a701104b09b7f4cd7092fe7f76d5b669e391a072428ec4526879b36f62820ae5d
ARG REBAR3_SHA512=992fd755b7926fae455e5e07d9d195f4d3e7f181609eed1b9cabfe548624df10d148cd4b59bda40bebb185d3d68f9a9fd68a70b294101c8ad9cf0fadcc683d24
RUN set -eu; \
curl -fsSLo /tmp/hex-2.5.1-otp-28.ez https://builds.hex.pm/installs/1.20.0/hex-2.5.1-otp-28.ez; \
echo "${HEX_SHA512} /tmp/hex-2.5.1-otp-28.ez" | sha512sum -c -; \
curl -fsSLo /tmp/rebar3-3.25.1-otp-28 https://builds.hex.pm/installs/1.18.4/rebar3-3.25.1-otp-28; \
echo "${REBAR3_SHA512} /tmp/rebar3-3.25.1-otp-28" | sha512sum -c -; \
mix archive.install /tmp/hex-2.5.1-otp-28.ez --force; \
mix local.rebar rebar3 /tmp/rebar3-3.25.1-otp-28 --force; \
rm -f /tmp/hex-2.5.1-otp-28.ez /tmp/rebar3-3.25.1-otp-28

# The image proves its own build: the crypto NIF reports the FIPS provider available, and
# entering FIPS mode is possible. Which mode a container runs in is the leg's decision, made
Expand Down
6 changes: 4 additions & 2 deletions ci/headless/justifications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
# What a justification here must be: facts someone else can check, with the command or file that
# shows them. "Not exploitable" with no evidence is not one. A finding that has a fix available is
# fixed, not justified: the OTP findings fixed in 28.5.0.7 never had rows, and moving the container
# images to 28.5.0.7 fixed them (ADR-0014).
# images to 28.5.0.7 fixed them (ADR-0014). The images moved to 28.5.0.8 on 2026-10-20 (slice 141).
#
# The ERTS facts below were re-read in OTP 28.5.0.7's source on 2026-10-08, when the image moved to
# it: `erts/emulator/pcre/pcre2.h` gives PCRE2_MAJOR 10, PCRE2_MINOR 47; the directory's 39 files
# include no pcre2_jit_*.c and no pcre2_dfa_match.c (`tar -tzf otp_src_28.5.0.7.tar.gz`).
# include no pcre2_jit_*.c and no pcre2_dfa_match.c (`tar -tzf otp_src_28.5.0.7.tar.gz`). On
# 2026-10-20 `erts/emulator/pcre/` in otp_src_28.5.0.8.tar.gz was compared with 28.5.0.7's
# (`diff -r` over both extracted directories): the same files, byte for byte, so the facts hold.
---
justifications:
# Retired 2026-10-19, from the scan of run 39257269071 (grype database built 2026-10-19T06:39Z):
Expand Down
4 changes: 2 additions & 2 deletions ci/ironbank/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ RUN dnf -y install --setopt=install_weak_deps=False --nodocs \
# The toolchain: OTP from source and Elixir.
FROM build-packages AS toolchain

COPY otp_src_28.5.0.7.tar.gz elixir-1.20.4-otp-28.zip hex-2.5.1-otp-28.ez rebar3-3.25.1-otp-28 /build/resources/
COPY otp_src_28.5.0.8.tar.gz elixir-1.20.4-otp-28.zip hex-2.5.1-otp-28.ez rebar3-3.25.1-otp-28 /build/resources/

# --enable-fips compiles the crypto NIF's FIPS support, and --with-ssl links UBI's OpenSSL
# dynamically, which is where Red Hat's FIPS provider lives (docs/fips-leg.md). Applications with
# no place in a server are left out of the build.
WORKDIR /build/otp
RUN tar -xzf /build/resources/otp_src_28.5.0.7.tar.gz --strip-components=1 \
RUN tar -xzf /build/resources/otp_src_28.5.0.8.tar.gz --strip-components=1 \
&& ./configure --prefix=/usr/local --enable-fips --with-ssl=/usr \
--without-javac --without-jinterface --without-wx --without-odbc \
--without-debugger --without-observer --without-et --without-megaco \
Expand Down
6 changes: 3 additions & 3 deletions ci/ironbank/hardening_manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ resources:
# Erlang/OTP, built from source in the builder with --enable-fips against UBI's OpenSSL. The
# version is the container's, `ci/container.tool-versions`, which can run ahead of the desktop's
# `.tool-versions` (ADR-0014); the lint holds the two files and this resource together.
- url: "https://github.com/erlang/otp/releases/download/OTP-28.5.0.7/otp_src_28.5.0.7.tar.gz"
filename: "otp_src_28.5.0.7.tar.gz"
- url: "https://github.com/erlang/otp/releases/download/OTP-28.5.0.8/otp_src_28.5.0.8.tar.gz"
filename: "otp_src_28.5.0.8.tar.gz"
validation:
type: "sha256"
value: "ddf17db6d3e9b7a7cfac0d72238ddc8ea040fedc5e3dfad82fc90675319d6c93"
value: "8a696e15bcbe5d57197b1f4c03c92d415f9fd9af70f9222a05911c8a5463cba6"
# Elixir, precompiled for OTP 28 (BEAM files only, no native code).
- url: "https://github.com/elixir-lang/elixir/releases/download/v1.20.4/elixir-otp-28.zip"
filename: "elixir-1.20.4-otp-28.zip"
Expand Down
2 changes: 1 addition & 1 deletion config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ headless? = System.get_env("TRINITY_MODE") == "headless"
# Owner decision D2 (ADR-0014): on an OTP whose `ssl` carries CVE-2026-89422 every outbound TLS
# client is held to TLS 1.2, configured here so it is in place before any application starts
# (ReqLLM reads its pool configuration when it starts). `Trinity.TLS` decides from the `ssl` the
# runtime loads and returns nothing on a fixed runtime, so the container images, on 28.5.0.7, are
# runtime loads and returns nothing on a fixed runtime, so the container images, on 28.5.0.8, are
# configured exactly as before. This calls a project module, which the note on `smoke?` above
# says Mix does not allow; it holds for `mix run`, `mix test` and a release alike, measured
# 2026-10-08 (test/trinity/tls_clamp_test.exs runs against what this sets), and `--smoke` is not
Expand Down
25 changes: 25 additions & 0 deletions docs/07-security-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,31 @@ fail-open. Where it signs today: a runtime linked to OpenSSL 3.5 or later with t
mode, on the image's current FIPS provider, it does not; a provider validated with ML-DSA-87
(CMVP #5523) is what would change that.

## The headless release's floor, and the decoder asked directly (Slice 141, as built)

The headless release, the one deployed as a server, is held to OTP 28.5.0.8 and to the OTP 28 line
only; the desktop keeps the floor above, because its runtime is the one Burrito can fetch
(ADR-0014, amended). 28.5.0.8 fixes CVE-2026-94200 and CVE-2026-105236 in ssl (a TLS 1.3 client's
ticket store; a session resumed under another SNI name), CVE-2026-42789 in public_key (a v1 or v2
certificate accepted as a CA) and CVE-2026-48856 in inets (credentials sent on to a cross-origin
redirect). Those fixes are in the applications, and erts-16.4.0.7 can be installed without them
(erlang.org: it "can be applied independently of other applications on a full OTP 28
installation"), so the check is again every loaded application against that release's row of
`otp_versions.table`, from the same commit as the rows above. A node is held to it when its release name is `headless` or it was
started with `TRINITY_MODE=headless`; either is enough, so leaving one unset does not lower it.
Any other OTP line is refused, above as well as below.

Every node also asks the decoder instead of trusting a version string. `Trinity.Toolchain.check_decoder/1`
decodes a `LARGE_TUPLE_EXT` of arity 2^24, one over erts's tuple limit, which the CVE-2026-55737
fix refuses (`decoded_size()` in erts's `external.c`), and a small tuple of the same encoding, which
every runtime decodes, so a decoder that refuses everything is not taken for a fixed one. Measured
on 2026-10-20: OTP 28.1.1 builds the 16,777,216-element tuple in 82 ms; 28.5.0.5 to 28.5.0.8 and
29.1 refuse it in about 20 microseconds. It runs in a process of its own, so neither the 16 MiB
probe nor what an unfixed runtime builds from it stays on the booting process.

Not covered: both checks run when the application starts, so `bin/headless eval`, which runs code
in the release without starting it, is not held to them.

## Signed statements in one byte form (Slice 138, as built)

The rule: every statement Trinity constructs and signs is deterministic CBOR inside COSE_Sign1,
Expand Down
Loading
Loading