diff --git a/src/helpers/nrf52/BleDebugTrace.h b/src/helpers/nrf52/BleDebugTrace.h index 2c868a214..29b937bde 100644 --- a/src/helpers/nrf52/BleDebugTrace.h +++ b/src/helpers/nrf52/BleDebugTrace.h @@ -31,6 +31,7 @@ struct MeshBleTraceBuffer { extern "C" { extern MeshBleTraceBuffer mesh_ble_trace; } void meshBleTraceState(char* reply, size_t capacity); void meshBleTraceIdentity(char* reply, size_t capacity); +void meshBleTraceAdvertising(char* reply, size_t capacity); static inline void meshBleTraceMax(std::atomic& value, uint32_t sample) { uint32_t old = value.load(std::memory_order_relaxed); while (sample > old && !value.compare_exchange_weak(old, sample, @@ -78,6 +79,11 @@ static inline void meshBleTraceDispatchExit(uint32_t event, uint32_t handle) { } static inline bool meshBleTraceCommand(const char* command, char* reply, size_t capacity) { + if (!strcmp(command, "get bluetooth.trace.advertising") + || !strcmp(command, "get ble.trace.advertising")) { + meshBleTraceAdvertising(reply, capacity); + return true; + } if (!strcmp(command, "get bluetooth.trace.identity") || !strcmp(command, "get ble.trace.identity")) { meshBleTraceIdentity(reply, capacity); diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index 971815b46..efa2e8f9e 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -94,6 +94,33 @@ void meshBleTraceIdentity(char* reply, size_t capacity) { name_hex, (unsigned)Bluefruit.Advertising.count(), (unsigned)Bluefruit.ScanResponse.count()); } + +void meshBleTraceAdvertising(char* reply, size_t capacity) { + if (!instance) { + snprintf(reply, capacity, "> Bluetooth not initialized"); + return; + } + // Legacy advertising/scan-response bytes are public. Read the configured + // payloads themselves: the GAP name getter cannot prove what addName copied. + // Retain a hard 31-byte bound even if an invalid count is ever observed. + const auto encode = [](BLEAdvertisingData& payload, char* output) -> unsigned { + static const char hex[] = "0123456789abcdef"; + const unsigned count = payload.count(); + const uint8_t* bytes = payload.getData(); + const unsigned copied = bytes ? (count < 31u ? count : 31u) : 0u; + for (unsigned i = 0; i < copied; ++i) { + output[2 * i] = hex[bytes[i] >> 4]; + output[2 * i + 1] = hex[bytes[i] & 15]; + } + output[2 * copied] = 0; + return count; + }; + char advertising_hex[63], scan_hex[63]; + const unsigned advertising_length = encode(Bluefruit.Advertising, advertising_hex); + const unsigned scan_length = encode(Bluefruit.ScanResponse, scan_hex); + snprintf(reply, capacity, "> adv_len=%u,adv_hex=%s,scan_len=%u,scan_hex=%s", + advertising_length, advertising_hex, scan_length, scan_hex); +} #endif static bool formatDeviceName(char* dest, size_t capacity, diff --git a/test/test_nrf52_ble_trace.py b/test/test_nrf52_ble_trace.py index 73ccddf0d..426e09769 100644 --- a/test/test_nrf52_ble_trace.py +++ b/test/test_nrf52_ble_trace.py @@ -27,6 +27,9 @@ void meshBleTraceState(char* reply, size_t capacity) { void meshBleTraceIdentity(char* reply, size_t capacity) { snprintf(reply,capacity,"> identity"); } +void meshBleTraceAdvertising(char* reply, size_t capacity) { + snprintf(reply,capacity,"> advertising"); +} int main() { char reply[160]; assert(!meshBleTraceCommand("get radio",reply,sizeof(reply))); @@ -49,6 +52,11 @@ int main() { assert(meshBleTraceCommand(command,reply,sizeof(reply))); assert(!strcmp(reply,"> identity")); } + for (const char* command : {"get bluetooth.trace.advertising", "get ble.trace.advertising"}) { + assert(meshBleTraceCommand(command,reply,sizeof(reply))); + assert(!strcmp(reply,"> advertising")); + } + assert(!meshBleTraceCommand("get ble.trace.advertising extra",reply,sizeof(reply))); meshBleTraceDispatchEnter(0x10,0); assert(mesh_ble_trace.dispatch_event.load()==0x10); meshBleTraceDispatchExit(0x10,0); @@ -150,6 +158,71 @@ int main() { result = subprocess.run([str(binary)], capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_real_advertising_query_reads_public_payloads_with_31_byte_bounds(self): + source = (ROOT / "src/helpers/nrf52/SerialBLEInterface.cpp").read_text() + start = source.index("void meshBleTraceAdvertising(") + implementation = source[start:source.index("\n}\n", start) + 3] + with tempfile.TemporaryDirectory() as directory: + work = Path(directory) + cpp = work / "advertising.cpp" + cpp.write_text(r''' +#include +#include +#include +#include +static void* instance; +struct BLEAdvertisingData { + uint8_t length = 0; + uint8_t bytes[31] = {}; + uint8_t count() { return length; } + uint8_t* getData() { return bytes; } +}; +static struct { BLEAdvertisingData Advertising, ScanResponse; } Bluefruit; +@IMPLEMENTATION@ +int main() { + char reply[172]; + meshBleTraceAdvertising(reply, sizeof(reply)); + assert(!strcmp(reply, "> Bluetooth not initialized")); + instance = reply; + const uint8_t advertising[] = {2, 1, 6, 2, 10, 4}; + memcpy(Bluefruit.Advertising.bytes, advertising, sizeof(advertising)); + Bluefruit.Advertising.length = sizeof(advertising); + // This is the copied advertising payload, independent of the GAP getter. + const uint8_t scan[] = {5, 9, 'R', 0, 0xc3, 0xa9}; + memcpy(Bluefruit.ScanResponse.bytes, scan, sizeof(scan)); + Bluefruit.ScanResponse.length = sizeof(scan); + meshBleTraceAdvertising(reply, sizeof(reply)); + assert(!strcmp(reply, "> adv_len=6,adv_hex=020106020a04,scan_len=6,scan_hex=05095200c3a9")); + for (unsigned i = 0; i < 31; ++i) { + Bluefruit.Advertising.bytes[i] = i; + Bluefruit.ScanResponse.bytes[i] = 255 - i; + } + Bluefruit.Advertising.length = Bluefruit.ScanResponse.length = 255; + meshBleTraceAdvertising(reply, sizeof(reply)); + assert(strlen(reply) < sizeof(reply) - 1); + const char* adv_hex = strstr(reply, "adv_hex=") + 8; + const char* scan_hex = strstr(reply, "scan_hex=") + 9; + assert(strchr(adv_hex, ',') - adv_hex == 62 && strlen(scan_hex) == 62); + assert(!strncmp(reply, "> adv_len=255,", 14)); + assert(strstr(reply, ",scan_len=255,")); + char tiny[3] = {}; + meshBleTraceAdvertising(tiny, sizeof(tiny)); + assert(tiny[2] == 0); + char untouched = 'X'; + meshBleTraceAdvertising(&untouched, 0); + assert(untouched == 'X'); +} +'''.replace("@IMPLEMENTATION@", implementation)) + binary = work / "advertising" + compiled = subprocess.run([ + "c++", "-std=c++17", "-O2", "-flto", "-Wall", "-Wextra", "-Werror", + "-fsanitize=address,undefined", "-fno-omit-frame-pointer", + "-fno-pie", "-no-pie", str(cpp), "-o", str(binary), + ], capture_output=True, text=True) + self.assertEqual(compiled.returncode, 0, compiled.stderr) + result = subprocess.run([str(binary)], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_concurrent_reader_never_accepts_mixed_records(self): with tempfile.TemporaryDirectory() as directory: work = Path(directory)