Underhanded C Contest 2015: NaN Poisoning and Nuclear Verification
The results of the 2015 Underhanded C Contest are out. Over 40 submissions competed to write C code that appears correct but contains a subtle backdoor, this year focused on a real-world nuclear verification problem sponsored by the Nuclear Threat Initiative. The challenge: write a match() function that compares two gamma-ray spectra and returns 1 if they match (i.e., a warhead is authentic) or 0 otherwise. The goal for entrants was to make the function return a false positive (declare a fake warhead as authentic) under realistic conditions without being caught in code review.
The Dominant Trick: NaN Poisoning
About a third of submissions used the same core technique: NaN poisoning. A NaN (Not a Number) arises from undefined floating-point operations like 0.0/0.0 or sqrt(-1.0). Crucially, comparisons involving NaN always evaluate to false. So if a detection score becomes NaN, a check like return (score < threshold ? 0 : 1) will return 1 (match) because NaN < threshold is false. The trick is to introduce a NaN through seemingly legitimate computations.
Peter Eastman's entry demonstrates this cleanly. His code computes the Pearson correlation coefficient between test and reference spectra. It subtracts a baseline array and clamps negative values to zero with fmax(0.0, test[i]-baseline[i]). If the test spectrum is engineered so that test[i] <= baseline[i] for all bins, then the correlation becomes 0/0 – NaN. The final comparison return (correlation < threshold ? 0 : 1) then returns 1, a false match.
Michael Dunphy attempted to guard against NaN by checking for zero, NaN, and infinity in a dot() function, but his error flag is passed by value instead of by reference (missing &). The check if ( (dp == 0) || (tmp != dp) || ... ) is ineffective because tmp != dp is false when dp is NaN (NaN != NaN is true, actually – but the code's intent is to catch it, yet the flag never propagates). This oversight is subtle enough to pass review.
Sean Egan exploited sscanf() behavior: he memset() the test and reference arrays to 0xFF (all bits set), then parsed input from a file. If a value fails to parse (e.g., due to non-digit Unicode characters), sscanf() leaves that element untouched. A double with all bits set is a NaN. So a carefully crafted input file can introduce NaN values without any explicit division by zero.
Realism and Trigger Categories
The judges categorized attacks by how realistic the trigger is:
- Input-triggered: Assumes the host country can directly corrupt the input arrays. Generally considered unrealistic because inspectors control the input.
- Data-triggered: The bug fires when the input spectrum has certain physical properties (e.g., a very high peak count). These are more realistic.
- Environment-triggered: Relies on external factors like changing the system clock or number of CPUs. Two entries (Sarah Newman and S. Gilles) parallelized
match()and triggered behavior changes if CPU count changed during execution.
The judges favored data-triggered attacks because they don't require extra assumptions about tampering with the computer.
Runner-Up: Poisson Likelihood Ratio Attack
Philipp Klenze submitted a NaN poisoning attack using a likelihood ratio test. His logLikelihoodRatio() function computes Poisson probabilities for each bin value. The Poisson probability pow(lambda, k)*exp(-lambda)/factorial(k) can underflow to zero for large k (e.g., k around 1686), leading to log(0) = -inf, and then -2*(...) becomes NaN. The match function then compares this NaN to a threshold, always returning a match. Klenze noted that a bin count of ~1860 is physically achievable with a short-lived nuclide peak, making this a realistic data-triggered attack.
Runner-Up: NaN in Logging Code
Ghislain Lemaur hid a NaN bug in logging code. His SSDWR() function computes pow(o[i]-e[i], glob.diff_exp) / e[i]. If e[i] is 0, this produces a division by zero -> NaN. The match() function calls SSDWR() and then a statistic_test() that compares chi2 to a threshold. But before that, it logs parameters using LogStr(), which appends strings to a global buffer. The logging code has a buffer overflow vulnerability (truncated in the source), but the NaN itself comes from a zero denominator. This is a classic division-by-zero oversight that could be overlooked in review.
The Winner
The article (truncated) does not explicitly name the winner, but the detailed analysis suggests the winner likely combined a realistic data trigger with a clever coding subtlety. The full results are on underhanded-c.org.
Why This Matters for Developers
This contest is a masterclass in how subtle undefined behavior can be weaponized. NaN poisoning is not just a theoretical curiosity – it's a practical attack vector in any code that uses floating-point comparisons for security decisions. The fact that a third of submissions independently converged on the same trick shows how easy it is to miss.
What You Should Do
- Always check for NaN explicitly using
isnan()orisunordered()before comparisons. - Avoid relying on floating-point comparisons for security-critical decisions.
- Use integer arithmetic or fixed-point for thresholds where possible.
- Review any code that computes ratios, correlations, or likelihoods – especially if it involves division, square roots, or logarithms.
The Underhanded C Contest remains one of the best ways to learn about real-world code vulnerabilities. Go read the winning entries.




