Harden against crafted-packet crashes + adversarial fuzzing (#10862)

Audit and fuzzing of the RF-packet decode -> dispatch -> display/phone paths for
the "crash a node or phone with a crafted packet" surface, beyond the XEdDSA
authenticity work.

Crash fixes (reproduced under AddressSanitizer / UBSan):
- GeoCoord::latLongToUTM/latLongToMGRS read fixed letter tables out of bounds on
  extreme latitude_i/longitude_i from a received Position, and narrowed
  out-of-range easting/northing doubles to unsigned (float-cast-overflow UB).
  Clamp the UTM zone, the easting/northing narrowing, and the band/col/row
  indices. Regression: test_geocoord_extreme_coords_no_oob.
- EnvironmentTelemetry/AirQualityTelemetry render attacker floats via
  String(float), which on nRF52/RP2040/STM32/portduino formats into a fixed
  char[33] (dtostrf) and overflows near FLT_MAX. Clamp the rendered metrics via
  UnitConversions::displaySafeFloat (finite + magnitude <= 1e9), unit-tested in
  test_type_conversions.

Defense-in-depth + robustness:
- TraceRouteModule::printRoute: fix an snr_back[-1] OOB read (wrong count in the
  guard) and stop formatting the INT8_MIN "unknown SNR" sentinel as a dB value.
- WaypointModule/NodeDB: sanitize untrusted strings before the OLED renderer and
  the phone-facing ClientNotification (belt-and-suspenders vs PB_VALIDATE_UTF8).
- MeshService::sendToPhone: withhold NODEINFO/WAYPOINT packets whose nested string
  won't cleanly decode, protecting strict phone protobuf decoders without
  affecting mesh relay.

Tests: new test_fuzz_decode (protobuf decode + UTF-8 sanitizer fuzz) and
test_fuzz_packets (perhapsDecode / module-handler / traceroute / phone-gate fuzz),
all under AddressSanitizer; native-suite-count 25 -> 27. Full suite 515/515 green.
This commit is contained in:
Ben Meadors
2026-07-02 16:50:49 -05:00
committed by GitHub
parent 510e9796f9
commit b4dd76a4db
14 changed files with 945 additions and 20 deletions
+40 -15
View File
@@ -1,4 +1,14 @@
#include "GeoCoord.h"
#include <cmath>
// Narrow a UTM meter value to its unsigned field, clamping non-finite/negative/oversized inputs: an
// extreme (crafted) lat/lon can drive these out of range, and an overflowing double->unsigned cast is UB.
static uint32_t clampMeters(double m)
{
if (!std::isfinite(m) || m < 0.0)
return 0;
return m > 4.0e9 ? 4000000000u : (uint32_t)m;
}
GeoCoord::GeoCoord()
{
@@ -124,8 +134,13 @@ void GeoCoord::latLongToUTM(const double lat, const double lon, UTM &utm)
{
const std::string latBands = "CDEFGHJKLMNPQRSTUVWXX";
utm.zone = int((lon + 180) / 6 + 1);
utm.band = latBands[int(lat / 8 + 10)];
// A received Position carries raw int32 latitude_i/longitude_i with no range validation, so lat/lon
// here can be far outside real geographic bounds. Clamp the derived UTM zone (valid 1..60) and the
// latitude-band index so the lookups below cannot read out of bounds (GeoCoord.cpp:128 stack over/
// under-read on e.g. latitude_i = INT32_MAX/INT32_MIN).
utm.zone = std::min(std::max(int((lon + 180) / 6 + 1), 1), 60);
int bandIdx = std::min(std::max(int(lat / 8 + 10), 0), int(latBands.length()) - 1);
utm.band = latBands[bandIdx];
double a = 6378137; // WGS84 - equatorial radius
double k0 = 0.9996; // UTM point scale on the central meridian
double eccSquared = 0.00669438; // eccentricity squared
@@ -160,17 +175,22 @@ void GeoCoord::latLongToUTM(const double lat, const double lon, UTM &utm)
sin(2 * latRad) +
(15 * eccSquared * eccSquared / 256 + 45 * eccSquared * eccSquared * eccSquared / 1024) * sin(4 * latRad) -
(35 * eccSquared * eccSquared * eccSquared / 3072) * sin(6 * latRad));
utm.easting = (double)(k0 * N *
(A + (1 - T + C) * pow(A, 3) / 6 +
(5 - 18 * T + T * T + 72 * C - 58 * eccPrimeSquared) * A * A * A * A * A / 120) +
500000.0);
utm.northing =
(double)(k0 * (M + N * tan(latRad) *
(A * A / 2 + (5 - T + 9 * C + 4 * C * C) * A * A * A * A / 24 +
(61 - 58 * T + T * T + 600 * C - 330 * eccPrimeSquared) * A * A * A * A * A * A / 720)));
double eastingMeters =
k0 * N *
(A + (1 - T + C) * pow(A, 3) / 6 + (5 - 18 * T + T * T + 72 * C - 58 * eccPrimeSquared) * A * A * A * A * A / 120) +
500000.0;
double northingMeters =
k0 * (M + N * tan(latRad) *
(A * A / 2 + (5 - T + 9 * C + 4 * C * C) * A * A * A * A / 24 +
(61 - 58 * T + T * T + 600 * C - 330 * eccPrimeSquared) * A * A * A * A * A * A / 720));
if (lat < 0)
utm.northing += 10000000.0; // 10000000 meter offset for southern hemisphere
northingMeters += 10000000.0; // 10000000 meter offset for southern hemisphere
// Clamp before narrowing to the unsigned UTM fields (see clampMeters): extreme lat/lon can drive
// these negative or past UINT32, and the raw double->unsigned cast would be UB.
utm.easting = clampMeters(eastingMeters);
utm.northing = clampMeters(northingMeters);
}
// Converts lat long coordinates to an MGRS.
@@ -182,10 +202,15 @@ void GeoCoord::latLongToMGRS(const double lat, const double lon, MGRS &mgrs)
latLongToUTM(lat, lon, utm);
mgrs.zone = utm.zone;
mgrs.band = utm.band;
double col = floor(utm.easting / 100000);
mgrs.east100k = e100kLetters[(mgrs.zone - 1) % 3][col - 1];
double row = (int32_t)floor(utm.northing / 100000.0) % 20;
mgrs.north100k = n100kLetters[(mgrs.zone - 1) % 2][row];
// utm.zone is clamped to 1..60 above, but guard every index defensively: the column/row derived
// from easting/northing can fall outside the 100km-grid letter tables when lat/lon are extreme.
int zoneIdx3 = ((mgrs.zone - 1) % 3 + 3) % 3;
int zoneIdx2 = ((mgrs.zone - 1) % 2 + 2) % 2;
int colIdx = std::min(std::max(int(floor(utm.easting / 100000)) - 1, 0), int(e100kLetters[zoneIdx3].length()) - 1);
mgrs.east100k = e100kLetters[zoneIdx3][colIdx];
int rowIdx = ((int32_t)floor(utm.northing / 100000.0) % 20 + 20) % 20;
rowIdx = std::min(std::max(rowIdx, 0), int(n100kLetters[zoneIdx2].length()) - 1);
mgrs.north100k = n100kLetters[zoneIdx2][rowIdx];
mgrs.easting = (int32_t)utm.easting % 100000;
mgrs.northing = (int32_t)utm.northing % 100000;
}