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
co-authored by GitHub
parent 510e9796f9
commit b4dd76a4db
14 changed files with 945 additions and 20 deletions
@@ -1,7 +1,9 @@
#include "Channels.h"
#include "GeoCoord.h"
#include "PositionPrecision.h"
#include "TestUtil.h"
#include "mesh-pb-constants.h"
#include <cstdint>
#include <cstring>
#include <unity.h>
@@ -207,6 +209,26 @@ static void test_cryptoKeyIsPublic_invalidKeyIsNotPublic()
TEST_ASSERT_FALSE(cryptoKeyIsPublic(makeCryptoKey(nullptr, -1)));
}
// Regression for out-of-bounds indexing in GeoCoord's UTM/MGRS conversion on extreme
// latitude_i/longitude_i that arrive in a received Position (raw int32, unvalidated on decode).
// Pre-fix, latitude_i = INT32_MAX made latLongToUTM read latBands[36] on a 21-char string
// (stack-buffer-overflow at GeoCoord.cpp:128, an AddressSanitizer abort); extreme longitude produced
// a negative UTM zone feeding the MGRS letter tables. The fix clamps the zone/band/col/row indices.
// This exercises the fix under the coverage env's ASan.
static void test_geocoord_extreme_coords_no_oob()
{
const int32_t vals[] = {INT32_MIN, INT32_MAX, INT32_MIN + 1, INT32_MAX - 1, 0, 1, -1, 900000000, -900000000, // +/-90 deg
1800000000, -1800000000, // +/-180 deg
2000000000, -2000000000, 123456789, -123456789};
const size_t n = sizeof(vals) / sizeof(vals[0]);
for (size_t i = 0; i < n; i++)
for (size_t j = 0; j < n; j++) {
GeoCoord g(vals[i], vals[j], 0); // ctor -> setCoords() -> UTM/MGRS/OSGR/OLC
// Surviving every extreme pair (no ASan fault) means the index clamps hold.
TEST_ASSERT_EQUAL_INT32(vals[i], g.getLatitude());
}
}
void setUp(void) {}
void tearDown(void) {}
@@ -232,6 +254,7 @@ void setup()
RUN_TEST(test_cryptoKeyIsPublic_strongKeyIsPrivate);
RUN_TEST(test_cryptoKeyIsPublic_aes256KeyIsPrivate);
RUN_TEST(test_cryptoKeyIsPublic_invalidKeyIsNotPublic);
RUN_TEST(test_geocoord_extreme_coords_no_oob);
exit(UNITY_END());
}