Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions infra/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ finite-status-contract:
cd .. && python3 -m unittest discover -s scripts/tests -p 'test_finite_status_brain_scoped.py'
cd .. && python3 -m unittest discover -s scripts/tests -p 'test_finite_status_brain_protocol.py'
cd .. && python3 -m unittest discover -s scripts/tests -p 'test_finite_status_brain_bindings.py'
cd .. && python3 -m unittest discover -s scripts/tests -p 'test_finite_status_tcp_security.py'
cd .. && python3 -m unittest discover -s scripts/tests -p 'test_kata_guest_probe.py'
cd .. && python3 -m unittest discover -s scripts/tests -p 'test_kata_network_probe.py'
cd .. && python3 -m unittest discover -s scripts/tests -p 'test_runtime_route_status.py'
Expand Down
1 change: 1 addition & 0 deletions infra/nixos/hosts/finite-lat-2/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ in
../../modules/monitoring.nix
../../modules/finite-litestream.nix
../../modules/ssh-flood-limits.nix
../../modules/tcp-ip-options-guard.nix
];

networking.hostName = "finite-lat-2";
Expand Down
1 change: 1 addition & 0 deletions infra/nixos/hosts/finite-lat-3/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ in
../../modules/kata-runner-host.nix
../../modules/metrics.nix
../../modules/ssh-flood-limits.nix
../../modules/tcp-ip-options-guard.nix
];

networking.hostName = "finite-lat-3";
Expand Down
1 change: 1 addition & 0 deletions infra/nixos/hosts/finite-lat-4/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ in
../../modules/kata-runner-host.nix
../../modules/metrics.nix
../../modules/ssh-flood-limits.nix
../../modules/tcp-ip-options-guard.nix
];

networking.hostName = "finite-lat-4";
Expand Down
1 change: 1 addition & 0 deletions infra/nixos/hosts/finite-lat-5/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ in
../../modules/kata-runner-host.nix
../../modules/metrics.nix
../../modules/ssh-flood-limits.nix
../../modules/tcp-ip-options-guard.nix
];

networking.hostName = "finite-lat-5";
Expand Down
1 change: 1 addition & 0 deletions infra/nixos/hosts/finite-lat-7/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ in
../../modules/kata-runner-host.nix
../../modules/metrics.nix
../../modules/ssh-flood-limits.nix
../../modules/tcp-ip-options-guard.nix
];

networking.hostName = "finite-lat-7";
Expand Down
42 changes: 42 additions & 0 deletions infra/nixos/modules/tcp-ip-options-guard.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# CVE-2026-98432: protect host listeners and routed Kata traffic while the
# fleet moves to fixed host and guest kernels. No service restart is needed.
{ pkgs, ... }:
let
# First fixed release in the fleet's qualified 6.18 branch. Override only
# the kernel source, keeping the OS, applications, and Kata pins unchanged.
kernelVersion = "6.18.56";
kernel = pkgs.linux_6_18.override {
argsOverride = {
version = kernelVersion;
modDirVersion = kernelVersion;
src = pkgs.fetchurl {
url = "https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${kernelVersion}.tar.xz";
sha256 = "4633bbda2ab95eaf2a7af4041236451d6759c72e6946265531af4b3aacefbf89";
};
};
};
in
{
boot.kernelPackages = pkgs.linuxPackagesFor kernel;
boot.kernel.sysctl."net.mptcp.enabled" = 0;

systemd.services.finite-cve-2026-98432 = {
description = "Block IPv4 TCP IP options (CVE-2026-98432)";
wantedBy = [ "multi-user.target" ];
wants = [ "network-pre.target" ];
before = [ "network-pre.target" ];
after = [ "systemd-modules-load.service" ];
unitConfig.DefaultDependencies = false;
path = [
pkgs.iptables
pkgs.procps
];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
# Deliberately no ExecStop: stopping/replacing this unit must not reopen
# vulnerable listeners during a NixOS switch or rollback.
script = builtins.readFile ../../scripts/mitigate-cve-2026-98432;
};
}
1 change: 1 addition & 0 deletions infra/runbooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ fresh read-only evidence establish observed state.
| Missing conversations | [Chat diagnosis](chats-appear-missing.md) |
| Billing support | [Stripe](stripe-billing.md) |
| Host access | [Break-glass](break-glass.md) |
| IPv4 TCP kernel vulnerability mitigation | [TCP IP-options guard](tcp-ip-options-guard.md) |

## Standing boundaries

Expand Down
93 changes: 93 additions & 0 deletions infra/runbooks/tcp-ip-options-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# IPv4 TCP options guard

The Latitude hosts import `nixos/modules/tcp-ip-options-guard.nix` for
[CVE-2026-98432](https://git.kernel.org/pub/scm/linux/security/vulns.git/plain/cve/published/2026/CVE-2026-98432.mbox).
It pins the fixed 6.18 kernel source without advancing the OS, application,
or Kata inputs. The early boot service runs
`infra/scripts/mitigate-cve-2026-98432`: drop IPv4 TCP packets with IP options
in raw PREROUTING, then disable MPTCP. Ordinary TCP, IPv6, and UDP (including
WireGuard) do not match. IP options are distinct from TCP options such as MSS.

The filter applies before conntrack, host listeners, and routed guest traffic.
It does not establish protection for guest-local or directly bridged traffic
that bypasses the host IPv4 stack. Keep it until the affected host and reachable
guest kernels are fixed. Disabling MPTCP alone does not retire existing
listeners, so do not remove the filter just because the sysctl is zero.

## Observe and verify

Run the current complete `finite-status` bundle as root on each host:

```sh
scripts/finite-status --json
scripts/finite-status --tcp-security
iptables -t raw -L PREROUTING -nvx --line-numbers
```

The TCP probe requires the exact barrier to be the first raw PREROUTING rule,
MPTCP disabled, the guard service active, and its attachment to the boot target.
Green means the mitigation is present, not that the kernel is patched. Compare
`uname -r` after reboot against the reviewed fixed kernel artifact. No reboot
or product-service restart is needed to install the filter itself.

## Emergency service and declarative handoff

The emergency installer uses a writable operator unit because NixOS owns
`/etc/systemd/system` as a read-only store symlink. Its files are:

- `/etc/finite/cve-2026-98432.sh` (the repository mitigation script)
- `/etc/finite/cve-2026-98432.service` (the operator unit source)
- `/etc/systemd/system.control/finite-cve-2026-98432.service` (symlink)
- `/etc/systemd/system.control/multi-user.target.wants/finite-cve-2026-98432.service`
(boot dependency symlink)

The unit is a oneshot ordered before `network-pre.target`, after module loading,
with `DefaultDependencies=no`, `RemainAfterExit=yes`, and no `ExecStop`.
It invokes the script with `/run/current-system/sw/bin/bash` and
`PATH=/run/current-system/sw/bin:/run/wrappers/bin`. The operator unit remains
authoritative across reboot and NixOS activation until explicitly removed.

Deploy the reviewed CI closure through the host's normal runbook. Verify that
the new closure contains the guard unit before removing the operator override:

```sh
test -e /run/current-system/etc/systemd/system/finite-cve-2026-98432.service
rm /etc/systemd/system.control/multi-user.target.wants/finite-cve-2026-98432.service
rm /etc/systemd/system.control/finite-cve-2026-98432.service
systemctl daemon-reload
systemctl restart finite-cve-2026-98432.service
scripts/finite-status --tcp-security
```

Restart only the guard: it has no stop action and its apply operation is
idempotent, so the existing filter stays installed throughout the handoff.
Verify the probe's `unit_fragment` resolves to the declarative unit, then remove
the two obsolete `/etc/finite/cve-2026-98432.*` files. Follow the normal
Runner drain and reboot procedures to replace the running kernel; switching
the closure alone does not replace it. Do not combine this with Runtime
replacement or user-data repair.

## Rollback boundary

The live mitigation changes only packet filtering, the MPTCP sysctl, and its
operator boot service. It does not change chat stores, identity, Runtime
assignments, volumes, or recovery material. No data restore is needed to undo
it. Pre-change firewall, sysctl, service, and canonical status evidence is kept
in `/run/finite-cve-2026-98432/` until reboot; copy required evidence off-host
before rebooting. Never restore the entire old firewall over concurrent changes.

Removing this mitigation reopens the vulnerability on an unpatched kernel.
If separately authorized because it causes an incident, remove only the guard's
unit and exact rule, and restore the recorded pre-change MPTCP value:

```sh
systemctl stop finite-cve-2026-98432.service
# Remove its boot symlink/unit using the operator or declarative owner above.
iptables -w 10 -t raw -D PREROUTING -p tcp -m u32 --u32 '0>>24&0xF=6:15' \
-m comment --comment finite-cve-2026-98432 -j DROP
sysctl -w net.mptcp.enabled="$(cat /run/finite-cve-2026-98432/mptcp-before)"
```

Run both status modes and product probes before and after any rollback. A
binary or boot rollback to the previous vulnerable kernel must retain the
guard and operator boot unit.
27 changes: 27 additions & 0 deletions infra/scripts/mitigate-cve-2026-98432
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Temporary defense for the IPv4 request-socket options UAF. Keep this rule
# until every host and reachable guest kernel carries the upstream fix.
set -euo pipefail

rule=(
-p tcp -m u32 --u32 '0>>24&0xF=6:15'
-m comment --comment finite-cve-2026-98432 -j DROP
)

case "${1:---apply}" in
--apply)
# Raw PREROUTING covers local delivery and routed container traffic before
# conntrack and before any established-connection or port allow rules.
if ! iptables -w 10 -t raw -C PREROUTING "${rule[@]}" 2>/dev/null; then
iptables -w 10 -t raw -I PREROUTING 1 "${rule[@]}"
fi
# Existing MPTCP listeners can survive this sysctl; the packet filter is
# the immediate protection and must be installed first.
sysctl -w net.mptcp.enabled=0
;;
--check)
test "$(sysctl -n net.mptcp.enabled)" = 0
iptables -w 10 -t raw -C PREROUTING "${rule[@]}"
;;
*) echo "usage: $0 [--apply|--check]" >&2; exit 64 ;;
esac
2 changes: 2 additions & 0 deletions scripts/ci/select-harnesses
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,14 @@ FINITE_STATUS_PATHS = {
"scripts/finite_status_hosted_identity.py",
"scripts/finite_status_brain_scoped.py",
"scripts/finite_status_brain_protocol.py",
"scripts/finite_status_tcp_security.py",
"scripts/check_finite_status_contract.py",
"scripts/tests/test_finite_status.py",
"scripts/tests/test_finite_status_brain_roster.py",
"scripts/tests/test_finite_status_hosted_identity.py",
"scripts/tests/test_finite_status_brain_scoped.py",
"scripts/tests/test_finite_status_brain_protocol.py",
"scripts/tests/test_finite_status_tcp_security.py",
"scripts/tests/fixtures/finite_status_aug1.json",
}

Expand Down
8 changes: 8 additions & 0 deletions scripts/finite_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@
"finite_status_brain_bindings.py",
"finite_status_brain_protocol.py",
"finite_tinfoil_status.py",
"finite_status_tcp_security.py",
"kata_guest_probe.py",
"kata_network_probe.py",
)
Expand Down Expand Up @@ -4896,6 +4897,8 @@ def parse_args(arguments: list[str]) -> argparse.Namespace:
help="read one Project's exact Core recovery inputs without credential values; emits JSON")
mode.add_argument("--kata-recovery-host", action="store_true",
help="read host prerequisites for an isolated Kata recovery rehearsal; emits JSON")
mode.add_argument("--tcp-security", action="store_true",
help="read CVE-2026-98432 packet-filter, MPTCP, and boot-persistence evidence; emits JSON")
mode.add_argument("--runtime-route", metavar="SOURCE_MACHINE_ID",
help="compare one owned Kata Runtime's direct and published identity routes")
mode.add_argument("--runtime-lifecycle", nargs=4,
Expand Down Expand Up @@ -5003,6 +5006,10 @@ def main(arguments: list[str] | None = None) -> None:
elif options.runtime_lifecycle:
report = collect_runtime_lifecycle(*options.runtime_lifecycle,
guest_agent_probe=options.guest_agent_probe)
elif options.tcp_security:
from finite_status_tcp_security import collect

report = collect()
elif options.tinfoil:
from finite_tinfoil_status import collect

Expand Down Expand Up @@ -5049,6 +5056,7 @@ def main(arguments: list[str] | None = None) -> None:
}
if (
options.json
or options.tcp_security
or options.tinfoil
or options.finite_private_usage
or options.brain_identity
Expand Down
61 changes: 61 additions & 0 deletions scripts/finite_status_tcp_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Read-only CVE-2026-98432 mitigation evidence for the Latitude fleet."""

from __future__ import annotations

import platform
import shlex
import subprocess
from datetime import datetime, timezone


RULE = [
"-A", "PREROUTING", "-p", "tcp", "-m", "u32", "--u32",
"0x0>>0x18&0xf=0x6:0xf", "-m", "comment", "--comment",
"finite-cve-2026-98432", "-j", "DROP",
]


def verdict(mptcp: str, rules: str, unit: dict[str, str]) -> dict:
"""Require the early packet barrier, not just a disabled MPTCP sysctl."""
ordered = [shlex.split(line) for line in rules.splitlines() if line.startswith("-A ")]
barrier = bool(ordered and ordered[0] == RULE)
boot_attached = "multi-user.target" in unit.get("WantedBy", "").split()
active = unit.get("ActiveState") == "active"
mitigated = barrier and mptcp.strip() == "0" and active and boot_attached
return {
"status": "green" if mitigated else "red",
"state": "mitigated" if mitigated else "mitigation-incomplete",
"mptcp_disabled": mptcp.strip() == "0",
"ipv4_tcp_options_drop_first": barrier,
"unit_active": active,
"unit_attached_to_boot": boot_attached,
"unit_fragment": unit.get("FragmentPath"),
"scope": "host IPv4 PREROUTING, including routed guest traffic; not a guest-kernel patch audit",
"kernel_fix": "6.18.56 is the first fixed upstream 6.18 release; mitigation is not a kernel patch",
}


def collect() -> dict:
def run(*args: str) -> str:
result = subprocess.run(args, capture_output=True, text=True, timeout=15, check=True)
return result.stdout

try:
mptcp = run("sysctl", "-n", "net.mptcp.enabled")
rules = run("iptables", "-w", "10", "-t", "raw", "-S", "PREROUTING")
properties = run(
"systemctl", "show", "finite-cve-2026-98432.service",
"-p", "ActiveState", "-p", "WantedBy", "-p", "FragmentPath",
)
unit = dict(line.split("=", 1) for line in properties.splitlines() if "=" in line)
section = verdict(mptcp, rules, unit)
except (OSError, subprocess.SubprocessError, ValueError) as error:
section = {"status": "unknown", "state": "unavailable", "error": str(error)}
section["running_kernel"] = platform.release()
return {
"schema_version": "finite.status.v1",
"generated_at": datetime.now(timezone.utc).isoformat(),
"overall_status": section["status"],
"exit_code": {"green": 0, "red": 1, "unknown": 2}[section["status"]],
"sections": {"tcp_security": section},
}
35 changes: 35 additions & 0 deletions scripts/tests/test_finite_status_tcp_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Avoid reporting green for incomplete emergency mitigations."""

import importlib.util
import shlex
import unittest
from pathlib import Path

spec = importlib.util.spec_from_file_location(
"tcp_security", Path(__file__).resolve().parents[1] / "finite_status_tcp_security.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)


class TcpSecurityTests(unittest.TestCase):
def setUp(self):
self.rule = shlex.join(module.RULE)
self.unit = {"ActiveState": "active", "WantedBy": "multi-user.target"}

def test_complete_mitigation(self):
self.assertEqual(module.verdict("0\n", self.rule, self.unit)["state"], "mitigated")

def test_sysctl_alone_cannot_protect_existing_listeners(self):
self.assertEqual(module.verdict("0", "", self.unit)["status"], "red")

def test_earlier_accept_rule_cannot_bypass_barrier(self):
rules = "-A PREROUTING -j ACCEPT\n" + self.rule
self.assertEqual(module.verdict("0", rules, self.unit)["status"], "red")

def test_unrelated_drop_is_not_evidence(self):
rules = self.rule.replace("6:0xf", "7:0xf")
self.assertEqual(module.verdict("0", rules, self.unit)["status"], "red")

def test_runtime_only_unit_is_not_persistent(self):
self.assertEqual(module.verdict("0", self.rule, {"ActiveState": "active"})["status"], "red")
Loading