03: FDIR and Adversarial Spoofing Strategy¶
Document ID: PM-FDIR-003
Status: Architecture whitepaper for review
Applies to: navkit.fdir: chi-square gate (ADR-0005) and NIS monitor (ADR-0006)
Date: 2026-09
0. Scope and a naming correction¶
This paper describes the fault detection, isolation and recovery architecture as implemented. Where the programme brief and the code disagree, the code wins and the disagreement is recorded here rather than smoothed over.
Naming. The brief asks this paper to document a PERSISTENT_DIVERGENCE
status. No such status exists. The five implemented statuses are:
| Status constant | Value | Meaning |
|---|---|---|
STATUS_ACCEPTED |
ACCEPTED |
Innovation is consistent. Update fused. |
STATUS_REJECTED_SPOOF |
REJECTED_SPOOF |
Single failure, no relief sought. Update discarded. |
STATUS_REJECTED_PERSISTENT |
REJECTED_PERSISTENT |
A run of failures, or a channel held out by a declared fault. |
STATUS_SENSOR_FAULT |
SENSOR_FAULT |
Channel excluded after max_consecutive_rejections. |
STATUS_REACCEPTED_WITH_INFLATION |
REACCEPTED_WITH_INFLATION |
Re-gated under an inflated covariance and re-fused. |
REJECTED_PERSISTENT is the operational counterpart of the brief's
PERSISTENT_DIVERGENCE and this paper uses the real name throughout. The
distinction matters to an integrator reading a datalink: a status string that
does not exist in the source cannot be parsed, and a paper that documented one
would have sent them looking for it.
REJECTED_SPOOF is a policy label, not a diagnosis. The gate cannot
distinguish multipath from spoofing from a miscalibrated filter: all three
present as a large innovation, and ADR-0005 says so explicitly. The name
records what the filter did: an unproven channel gets no relief. A ground
station that renders this as "SPOOFING DETECTED" would be making a claim the
architecture cannot support, and on a multipath-afflicted route that claim would
be wrong most of the time.
1. Threat taxonomy¶
Four threat classes are distinguished by the signal each leaves behind. This section states, for each, what is implemented, what is architecturally identified but not built, and what is not addressed.
1.1 GNSS jamming¶
| Vector | RF denial. No spoofed signal is produced; the receiver goes quiet, or reports a fix that is not a fix. |
| Signal | The channel's update rate collapses. Longest inter-sample gap grows. |
| Status | Implemented, verified. |
| Mechanism | Tracked per channel as longest_silence_s and min_gap_s. A gap wider than 2 × min_gap_s is a material silence and resets the health counters, because a run of accepted updates accumulated before the gap says nothing about the first update after it. |
This is the one threat the architecture is explicitly built around, and the reason is structural: silence is observable from the filter's own state and requires no assumption about the threat model. Every other threat class needs an assumption.
Jamming is also the only class that is a benign failure. A jammed receiver is telling the truth by being silent, and the correct response is to coast and then re-acquire. This is what OUN-01 covers and it is measured: 2.322 m ATE through a 15 s denial.
1.2 Step-bias spoofing¶
| Vector | Attacker emits a false signal with a constant offset from truth, usually after suppressing the genuine one. |
| Signal | A large, abrupt, sustained innovation against a covariance that was healthy moments earlier. |
| Status | Implemented, verified across 1–100 m. |
| Mechanism | Stage 1 rejects on the first failure. Stage 2 refuses relief, because a continuously streaming channel has no silence to spend. |
Measured, from SRS OUN-02: zero re-acquisition grants at 1, 2, 3, 5, 10, 20, 40 and 100 m. The property that matters is the flatness: magnitude buys nothing.
Documented cost. At 1 m and 2 m the channel escalates to SENSOR_FAULT and
the solution coasts, reaching 3.430 m and 5.441 m of error. The filter refuses a
plausible-looking receiver and pays for it in drift. This is the correct trade
and it is a large transient that operators must be told about in advance; see
§4.3.
Untested variant. Jam for 15 s, then inject. After a 15 s silence the drift bound admits 7.5 m of 1σ, and on the arithmetic in PM-SWAPC-002 §2.5 a consumer MEMS platform provably cannot have drifted 7 m in 15 s. The filter has no way to know that. This attack is not verified against the current build and is the highest-priority probe to add (SRS open item 8).
1.3 Gradual false-lock pulling¶
| Vector | Attacker walks the receiver's reported position smoothly from truth toward an offset, over minutes, keeping every individual innovation inside the gate. |
| Signal | None per-update. Every innovation is plausible. This is the defining property. |
| Status | Architecturally identified, NOT implemented, NOT verified. |
This is the most dangerous class in the taxonomy and the one the current
architecture cannot see. A slow ramp that keeps d² under the threshold at
α = 0.001 never trips Stage 1, never accumulates a trailing run of failures for
Stage 2, and therefore never reaches the re-acquisition logic at all.
The NIS monitor keeps mean_nis per channel and the architecture is pointed
at exactly this: a channel whose mean NIS sits persistently near its degrees of
freedom while the solution translates is the signature. That statistic is
computed and logged today, and nothing acts on it. It is reported in
fdir_<channel>_mean_nis for exactly this reason.
Turning it into a detector is open ROADMAP work and is a genuine research problem, not a threshold. A false-lock pull is, in the limit, indistinguishable from the platform genuinely moving. The discriminator has to be a second, independent sensor, which is why a good inertial grade or an independent odometry source is a security property and not just an accuracy one (PM-SWAPC-002 §2.2).
1.4 Visual tracking corruption¶
| Vector | Adversarial or incidental degradation of the optical front end: a step in the relative pose, a drifting anchor, a rolling-shutter artefact, a textural degenerate scene. |
| Signal | An innovation inconsistent with the IMU-predicted motion, on one of the two anchor blocks. |
| Status | Implemented for detection, deliberately NOT implemented for relief. |
| Mechanism | Stage 1 gates the rotation and translation blocks separately, so a broken translation cannot be vouched for by a healthy rotation. Stage 2 is switched off for these channels. |
The asymmetry is a decision, and it is worth stating plainly because a reviewer will notice it. ADR-0006 enables adaptive inflation for the GNSS position block only. Two reasons:
- Dimensional. The drift bound is metres per second of translational drift. Applying a metre-derived variance to a radian block is a dimensional error that still produces a valid covariance, so nothing downstream would object and no test would catch it.
- Evidential. The visual channels are where relaxing protection is most expensive, and the relief mechanism has not been shown to be safe there.
Verification: a 10 m step on the translation block is rejected with zero
inflation grants
(test_a_visual_step_still_faults_the_channel, asserting
fdir_vision_trans_rejected > 0 and fdir_inflations == 0). The test asserts
rejection and non-inflation, not fault escalation, and this document does not
claim more than the test does.
1.5 Summary¶
| Threat | Detected | Isolated | Relieved | Verified |
|---|---|---|---|---|
| Jamming | Yes | Yes | Yes: re-acquire on return | 1–100 m, 15 s case |
| Step bias 3–100 m | Yes | Yes | n/a: no relief earned | Yes |
| Step bias 1–2 m | Yes | Yes | No, by design | Yes |
| Step bias after suppression | Unknown | Unknown | - | No |
| Gradual false-lock | No | No | - | No |
| Visual step / anchor drift | Yes | Yes | No, by design | Yes |
2. Two-stage defence architecture¶
2.1 Stage 1: chi-square innovation gate (ADR-0005)¶
Every aided update is tested before the Kalman gain is formed.
| Parameter | Value |
|---|---|
| α | 0.001 |
| dof covered exactly | 1–8 |
| dof 1 / 2 / 3 / 6 thresholds | 10.828 / 13.816 / 16.266 / 22.458 |
| dof > 8 | Wilson-Hilferty, relative error −0.33% at m=4 to −0.005% at m=60 |
| Rejected update cost | returns immediately; P⁺ = P⁻, state untouched |
| False alarms, 1212 healthy fixes | 0: recorded in fdir/gating.py, not test-enforced |
The thresholds are tabulated rather than computed, and the reason is specific: this code is on the inner loop, evaluated once per sensor per epoch, and it must answer for any dof without a bisection loop whose iteration count is not obviously bounded. That is a real-time argument, not a style preference, and it is worth stating because it is the kind of decision that otherwise looks like premature optimisation.
Why a fixed gate cannot be enough. The gate is a correct implementation
of a test whose premise is a calibrated S. In the benchmark's vision-aided
outage case that premise is false: after 15 s of denial the position covariance
has collapsed, so S is far too small, and the gate read a returning fix as an
outlier, mahalanobis_sq 37.4 against a threshold of 16.266. It rejected 51
of the 76 GNSS updates and the case degraded from 3.428 m to 5.059 m.
The arithmetic was never wrong. The input was. And the tempting fix (widen the threshold) is precisely the move that discards the false-alarm rate the whole subsystem exists to provide, while admitting the sustained spoof that Stage 2 is built to catch. The premise must be tested, not relaxed.
2.2 Stage 2: NIS window monitor (ADR-0006)¶
A rejection is re-tested only when all of the following hold. Each condition closes a hole the others leave.
| # | Condition | Requirement | What it prevents |
|---|---|---|---|
| 1 | A trailing run of reacq_consecutive_rejections = 3 failures |
TR-20 | A 90% rejection rate being mistaken for a run; an old burst staying eligible |
| 2 | The sample is a genuine outlier | TR-26 | A declared fault being overturned: see §2.4 |
| 3 | The channel was actually silent | TR-23 | A spoof earning trust it did not earn |
| 4 | P and H supplied, block named |
TR-27 | Inflating a covariance the caller cannot see; inflating state the measurement does not constrain |
| 5 | The inflated re-gate passes with headroom (≤ 25% of threshold) | TR-24 | A marginal pass, which is exactly the large-gain case |
The velocity × silence cap is condition 3, and it is the requirement that carries the whole design:
| T | Admitted 1σ | Situation |
|---|---|---|
| 0.2 s | 0.10 m | 5 Hz healthy channel: effectively no budget |
| 1 s | 0.50 m | brief dropout |
| 15 s | 7.50 m | the validated denial; admits the 3.4 m displacement that occurred |
| 60 s | 30.00 m | outside the validated domain: see PM-SWAPC-002 §2.5 |
A constant budget cannot draw that line, because the correct limit depends on how long the filter was blind. That dependence is the design, and it is what separates a genuine outage from a sustained spoof using no threat-model assumption at all: a spoof that never stops transmitting never earns silence.
Also bounded: a factor cap of 100× (TR-22, so one grant cannot add more than 30 m of 1σ), and one grant per divergence episode (TR-25, because a filter that can re-inflate indefinitely is a filter walking itself onto whatever the measurements say).
2.3 The measured result¶
outage_visual, 15 s denial, 76 GNSS updates (71 accepted, 5 rejected):
| Before ADR-0006 | After | Change | |
|---|---|---|---|
| Fixes rejected | 51 | 5 | −90% |
| ATE RMSE | 5.059 m | 2.541 m | −50% |
| Mean NEES | 1996.5 | 419.4 | −79% |
| 2σ coverage | 16.0% | 20.0% | +4 pts |
A single grant at t = 20.4 s carried the recovery:
{"t_s": 20.4, "sensor": "gnss", "status": "REACCEPTED_WITH_INFLATION",
"mahalanobis_sq": 37.378, "threshold": 16.266,
"inflation_variance": 20.681, "mahalanobis_sq_inflated": 1.158}
The distance collapsed from 37.4 (an outlier) to 1.16 (comfortably inside). The log records how much trust was surrendered (20.681 m²) not merely that something happened, which is the difference between an auditable event and a number an operator has to trust.
2.4 The failure this design nearly shipped¶
Stage 2's first implementation was inverted over 1–3 m. It granted relief to 1 m and 2 m step biases and refused 3 m and above.
The cause was a real ambiguity, not a typo. A channel is excluded for one of two
reasons and both return accepted = False:
- the innovation was an outlier, or
- the numbers were fine and a declared fault is holding the channel out.
Stage 2 could not tell them apart. A 1 m spoof does not stay a 1 m innovation: the filter is pulled toward it until the residual falls inside the threshold, at which point the gate is entirely happy and the exclusion is the fault hold. Stage 2 then spent the drift budget buying back a channel it had correctly condemned.
The fix is GatingDecision.outlier, and the requirement it encodes is TR-26.
It is called out here because the benchmark never caught it: the benchmark
has no spoof case, and the failing range was below the offsets the security
test exercised. The defect was found by a probe, not by regression.
The transferable lesson, and the reason this section exists in a strategy paper rather than only in the ADR: for a security mechanism, the test suite must include the case where the mechanism is inverted, and "no false positives" is not the same claim as "no false negatives." SRS keeps AC-05 and AC-06 as separate criteria for exactly this reason.
3. Operator telemetry¶
3.1 What the system emits today¶
Transitions, not transients: a filter that silently drops 40% of its fixes is indistinguishable from one that has degraded gracefully until you read a counter.
Event record (trajectory.metadata["fdir_events"]), one per transition:
| Field | Type | Meaning |
|---|---|---|
t_s |
float | Time of the transition |
sensor |
str | gnss, vision_rot, vision_trans |
status |
str | One of the five statuses in §0 |
mahalanobis_sq |
float | The distance the innovation actually had |
threshold |
float | The gate threshold that was applied |
consecutive_rejections |
int | Rejection count at the transition |
inflation_variance |
float | m² or rad² surrendered; 0.0 unless re-accepted |
mahalanobis_sq_inflated |
float | The distance under the inflated covariance |
Per-channel counters (stats), for fdir_<channel>_<metric>:
accepted, rejected, reaccepted, faulted, consecutive_rejections,
inflation_variance, mean_nis; plus fdir_inflations and fdir_events
globally.
mean_nis is the statistic for §1.3, computed and logged, not yet acted on.
Do not present it to an operator as a spoof indicator until something acts on
it. A number on a display that implies a conclusion the system has not drawn
is worse than an absent number.
3.2 Proposed ground-station presentation¶
Not implemented. This is a specification for a datalink and a UI, and it carries the same honesty constraints as the estimator.
| Status | Operator display | Colour | Action required |
|---|---|---|---|
ACCEPTED |
- | none | None. Not transmitted unless verbose. |
REJECTED_SPOOF |
REJECTED + d²/threshold ratio |
amber | None. A single failure is normal at α=0.001. |
REJECTED_PERSISTENT |
CHANNEL EXCLUDED + duration + rejection count |
amber | Watch. Auto-clears on 10 consecutive accepts. |
SENSOR_FAULT |
SENSOR FAULT + channel + onset time |
red | Investigate. Do not override. |
REACCEPTED_WITH_INFLATION |
RE-ACQUIRED (COVARIANCE WIDENED) + variance in m² |
blue | None, but log it for the mission record. |
Three rules the UI must follow:
- Never render
REJECTED_SPOOFas "spoofing". Render it as a rejection. The label is a policy, not a diagnosis (§0), and on a multipath route a confident "SPOOF" is wrong most of the time, which trains the operator to ignore the field, which is exactly the failure mode the whole subsystem exists to prevent. - Make the variance visible on the inflation event. It is the only record of how much the filter's confidence was reduced on the operator's behalf. A mission review should be able to reconstruct the trust the system spent.
SENSOR_FAULTis a stop condition, not a warning. A faulted channel is coasting on inertial integration. The measured cost of doing that with a spoofed 1 m signal is 3.4 m of error (SRS OUN-02), and it is larger than the 1 m the operator was trying to avoid. An override should be a deliberate, logged action.
3.3 Human-in-the-loop: what the system should not do¶
A contested-navigation filter with a human override has a specific failure mode: the operator learns to override the alarm, and the override becomes the attack surface.
- No auto-recovery override. Recovery is already hysteretic
(
auto_recovery_count= 10 consecutive accepts) and requires no human input. Adding one would let a single decision defeat a mechanism whose entire value is that it is not immediately reversible. - Threshold changes are not a ground-station control.
alpha,reacq_consecutive_rejectionsandmax_drift_sigma_mpsare flight-configuration, not operator settings. A datalink that can raisemax_drift_sigma_mpscan turn off spoof resistance, and it can do so without the estimator emitting anything unusual. - Inflation is logged, not prompted. The operator is told the filter widened its covariance; they are not asked to approve it. Approval on a 200 Hz loop is not a human task.
4. Known limitations¶
Stated as a register, because a strategy paper that ends on a summary of success is not useful to the person deciding whether to rely on it.
| # | Limitation | Impact | Status |
|---|---|---|---|
| L-1 | Calibration fails under visual aiding: NEES 286.2, 2σ coverage 20.5% against 95% required | The filter is confidently wrong through an outage. Not an FDIR defect: B1, needs a pose graph. | Open |
| L-2 | Gradual false-lock pulling is not detected | The most dangerous threat class is invisible to the current architecture | Open, ROADMAP |
| L-3 | Jam-then-inject spoof is untested; after 15 s silence the bound admits 7.5 m against ~0.2 m of real platform drift | A provably-fake offset could be admitted | Open, highest priority probe |
| L-4 | max_drift_sigma_mps = 0.5 is a single default, 33× loose for consumer MEMS and 7 500× for FOG |
Spoof resistance is not matched to the installed hardware | Open, PM-SWAPC-002 action 2 |
| L-5 | No benchmark case exceeds a 15 s denial | Everything beyond 15 s is extrapolation | Open, PM-SWAPC-002 action 3 |
| L-6 | 1–2 m step bias costs a fault and 3.4–5.4 m of coast | Correct, but a large transient operators must expect | Documented, §1.2 |
| L-7 | No target-port timing, WCET or jitter measurement | 200 Hz deadline unverified on hardware | Open, OUN-03 |
| L-8 | The visual channels have no relief path | Detection without recovery | By design, §1.4 |
| L-9 | All evidence is synthetic; no flight or bench hardware | No accredited performance claim is possible | Fundamental, §0 of PM-SRS-001 |
The single most important sentence in this paper: L-1 is not an FDIR problem. The fault detection and isolation architecture works, and it is measurably why the outage case improved by 50%. But the filter is still overconfident by a factor of ~140 under visual aiding, no threshold in this subsystem will move that, and upgrading the platform hardware will make it harder to notice rather than fixing it. Anyone reading a green OUN-01 ATE number and concluding the platform is safe to fly under contested conditions has read one of the two columns.