[READ-ONLY] Mirror of https://github.com/jmrplens/kleidos. Kleidos — Hardware BLE password manager for ESP32-S3 (M5StickC Plus2, StickS3, M5Stack Gray, T-Deck, Cardputer) jmrplens.github.io/kleidos/
0

Configure Feed

Select the types of activity you want to include in your feed.

Fix cert-err33-c, oop57-cpp, and web/harness tail findings

Route every flagged fire-and-forget snprintf through the bounded
str::format/str::copy facades (26 sites: web admin handlers/portal/auth,
BLE+WiFi host harnesses, NimBLE shim, regmap engine), replace memset on
the non-trivially-constructible Credential/TotpEntry with their own
wipe() zeroizer (cert-oop57-cpp x4), apply DeMorgan to the BLE host
delete guard (readability-simplify-boolean-expr), merge the duplicated
500-status branch, and annotate the deliberately unterminated fuzz
payload prefix and the printf-forwarding variadic (rationale comments on
each NOLINT).

Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4

José M. Requena Plens (Jul 10, 2026, 8:10 AM +0200) 9c5cf5ed c8f42212

+67 -50
+3 -4
src/ble/stack/nimble_host_shim.cpp
··· 16 16 #ifdef ESP_PLATFORM 17 17 #include "ble_stack_task.h" 18 18 #include "platform/nvs_store.h" 19 - 20 - #include <cstdio> 19 + #include "platform/safe_string.h" 21 20 #endif // ESP_PLATFORM 22 21 23 22 #include <array> ··· 137 136 uint8_t count = 0; 138 137 for ( uint8_t index = 1; index <= kMaxPeerSecScan; ++index ) { 139 138 std::array<char, 16> key = {}; 140 - std::snprintf( key.data(), key.size(), "%s_%u", kNimblePeerSecPrefix, 141 - static_cast<unsigned>( index ) ); 139 + platform::str::format( key.data(), key.size(), "%s_%u", kNimblePeerSecPrefix, 140 + static_cast<unsigned>( index ) ); 142 141 if ( prefs.blobSize( key.data() ) > 0 ) { 143 142 ++count; 144 143 }
+5 -2
src/drivers/introspection/regmap_engine.cpp
··· 8 8 */ 9 9 #include "drivers/introspection/regmap_engine.h" 10 10 11 + #include "platform/safe_string.h" 12 + 11 13 #include <cstdarg> 12 14 #include <cstdio> 13 15 ··· 45 47 */ 46 48 bool appendLine( char* buf, size_t cap, size_t& len, const char* fmt, ... ) 47 49 __attribute__( ( format( printf, 4, 5 ) ) ); 50 + // NOLINTNEXTLINE(cert-dcl50-cpp) -- C-variadic by necessity: forwards a va_list to vsnprintf; a parameter pack cannot produce one 48 51 bool appendLine( char* buf, size_t cap, size_t& len, const char* fmt, ... ) { 49 52 if ( len >= cap ) { 50 53 return false; ··· 133 136 if ( ok ) { 134 137 formatRegLine( map, reg, value, line, sizeof( line ) ); 135 138 } else { 136 - snprintf( line, sizeof( line ), "REG chip=%s addr=0x%02x name=%s val=ERR", map.chip, 137 - static_cast<unsigned>( reg.addr ), reg.name ); 139 + platform::str::format( line, sizeof( line ), "REG chip=%s addr=0x%02x name=%s val=ERR", 140 + map.chip, static_cast<unsigned>( reg.addr ), reg.name ); 138 141 } 139 142 140 143 sink( line, ctx );
+5 -3
src/tools/ble_host_harness.cpp
··· 19 19 #include "platform/app_loop.h" 20 20 #include "platform/clock.h" 21 21 #include "platform/rtos.h" 22 + #include "platform/safe_string.h" 22 23 #include "platform/serial_transport.h" 23 24 #include "platform/system.h" 24 25 #include "tools/host_mode.h" ··· 82 83 namespace platformClock = kleidos::platform::clock; 83 84 namespace rtos = kleidos::platform::rtos; 84 85 namespace serial = kleidos::platform::serial; 86 + namespace str = kleidos::platform::str; 85 87 86 88 constexpr const char* kHarnessName = "KleidosHost"; 87 89 // Match the whole Kleidos family by prefix: production advertises "Kleidos", ··· 244 246 245 247 /// @brief Render @p addr MSB-first ("aa:bb:cc:dd:ee:ff"), matching host notation. 246 248 void formatAddress( const ble_addr_t& addr, char ( &out )[kAddressTextLen] ) { 247 - std::snprintf( out, sizeof( out ), "%02x:%02x:%02x:%02x:%02x:%02x", addr.val[5], addr.val[4], 248 - addr.val[3], addr.val[2], addr.val[1], addr.val[0] ); 249 + str::format( out, sizeof( out ), "%02x:%02x:%02x:%02x:%02x:%02x", addr.val[5], addr.val[4], 250 + addr.val[3], addr.val[2], addr.val[1], addr.val[0] ); 249 251 } 250 252 251 253 // --- Bond enumeration (raw store) ------------------------------------------- ··· 1516 1518 serial::println( "TARGETADDR ANY" ); 1517 1519 return; 1518 1520 } 1519 - std::snprintf( s_targetAddressFilter, sizeof( s_targetAddressFilter ), "%s", arg ); 1521 + str::copy( s_targetAddressFilter, arg ); 1520 1522 serial::printf( "TARGETADDR %s\n", s_targetAddressFilter ); 1521 1523 } 1522 1524
+23 -16
src/tools/wifi_host_harness.cpp
··· 13 13 #include "platform/logger.h" 14 14 #include "platform/random.h" 15 15 #include "platform/rtos.h" 16 + #include "platform/safe_string.h" 16 17 #include "platform/serial_transport.h" 17 18 #include "platform/system.h" 18 19 #include "tools/host_mode.h" ··· 47 48 48 49 namespace platformClock = kleidos::platform::clock; 49 50 namespace serial = kleidos::platform::serial; 51 + namespace str = kleidos::platform::str; 50 52 51 53 constexpr const char* kHarnessName = "KleidosWifiHost"; 52 54 constexpr const char* kDefaultBaseUrl = "http://192.168.4.1"; ··· 188 190 return; 189 191 } 190 192 for ( size_t index = 0; index < 32; ++index ) { 191 - snprintf( out + ( index * 2 ), 3, "%02x", digest[index] ); 193 + str::format( out + ( index * 2 ), 3, "%02x", digest[index] ); 192 194 } 193 195 out[64] = '\0'; 194 196 } ··· 437 439 void printIpInfo() { 438 440 esp_netif_ip_info_t ipInfo{}; 439 441 if ( s_staNetif != nullptr && esp_netif_get_ip_info( s_staNetif, &ipInfo ) == ESP_OK ) { 442 + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast) -- casts live inside ESP-IDF's IP2STR macro 440 443 serial::printf( " ip=" IPSTR " gw=" IPSTR, IP2STR( &ipInfo.ip ), IP2STR( &ipInfo.gw ) ); 441 444 } else { 442 445 serial::print( " ip=0.0.0.0 gw=0.0.0.0" ); ··· 857 860 const char longSuffix[] = " HTTP/1.1\r\nHost: 192.168.4.1\r\n\r\n"; 858 861 char longPath[384]; 859 862 memset( longPath, 'A', sizeof( longPath ) ); 863 + // Deliberately unterminated: the prefix runs straight into the 'A' 864 + // filler of this oversized-path fuzz payload; the terminating NUL 865 + // arrives with the longSuffix copy below (sizeof includes its NUL). 866 + // NOLINTNEXTLINE(bugprone-not-null-terminated-result) 860 867 memcpy( longPath, "GET /", strlen( "GET /" ) ); 861 868 memcpy( longPath + sizeof( longPath ) - sizeof( longSuffix ), longSuffix, 862 869 sizeof( longSuffix ) ); ··· 1053 1060 } 1054 1061 1055 1062 char url[160]; 1056 - snprintf( url, sizeof( url ), "%s%s", kDefaultBaseUrl, path ); 1063 + str::format( url, sizeof( url ), "%s%s", kDefaultBaseUrl, path ); 1057 1064 1058 1065 HttpCapture capture{}; 1059 1066 capture.captureBody = captureBody; ··· 1184 1191 1185 1192 bool extractJsonInt( const char* json, const char* key, int& value ) { 1186 1193 char marker[48]; 1187 - snprintf( marker, sizeof( marker ), "\"%s\":", key ); 1194 + str::format( marker, sizeof( marker ), "\"%s\":", key ); 1188 1195 const char* pos = strstr( json, marker ); 1189 1196 if ( pos == nullptr ) { 1190 1197 return false; ··· 1204 1211 return false; 1205 1212 } 1206 1213 char marker[48]; 1207 - snprintf( marker, sizeof( marker ), "\"%s\":\"", key ); 1214 + str::format( marker, sizeof( marker ), "\"%s\":\"", key ); 1208 1215 const char* pos = strstr( json, marker ); 1209 1216 if ( pos == nullptr ) { 1210 1217 return false; ··· 1234 1241 1235 1242 bool extractCredentialIdByName( const char* json, const char* name, uint8_t& id ) { 1236 1243 char marker[96]; 1237 - snprintf( marker, sizeof( marker ), "\"name\":\"%s\"", name ); 1244 + str::format( marker, sizeof( marker ), "\"name\":\"%s\"", name ); 1238 1245 const char* namePos = strstr( json, marker ); 1239 1246 if ( namePos == nullptr ) { 1240 1247 return false; ··· 1332 1339 1333 1340 bool deleteCredential( uint8_t id ) { 1334 1341 char path[40]; 1335 - snprintf( path, sizeof( path ), "/api/credentials/%u", static_cast<unsigned>( id ) ); 1342 + str::format( path, sizeof( path ), "/api/credentials/%u", static_cast<unsigned>( id ) ); 1336 1343 return apiStep( "credential_delete", HttpVerb::Delete, path, nullptr, 200, false, nullptr, 0, 1337 1344 nullptr ); 1338 1345 } ··· 1391 1398 recordApiStep( created, steps, passed ); 1392 1399 if ( created ) { 1393 1400 char path[48]; 1394 - snprintf( path, sizeof( path ), "/api/credentials/%u", 1395 - static_cast<unsigned>( credentialId ) ); 1401 + str::format( path, sizeof( path ), "/api/credentials/%u", 1402 + static_cast<unsigned>( credentialId ) ); 1396 1403 recordApiStep( apiStep( "credential_detail", HttpVerb::Get, path, nullptr, 200, true, body, 1397 1404 sizeof( body ), "\"name\"" ), 1398 1405 steps, passed ); 1399 1406 char favoriteBody[48]; 1400 - snprintf( favoriteBody, sizeof( favoriteBody ), "{\"id\":%u,\"favorite\":true}", 1401 - static_cast<unsigned>( credentialId ) ); 1407 + str::format( favoriteBody, sizeof( favoriteBody ), "{\"id\":%u,\"favorite\":true}", 1408 + static_cast<unsigned>( credentialId ) ); 1402 1409 recordApiStep( apiStep( "credential_favorite", HttpVerb::Post, "/api/credentials/favorite", 1403 1410 favoriteBody, 200, false, nullptr, 0, nullptr ), 1404 1411 steps, passed ); 1405 1412 char orderBody[40]; 1406 - snprintf( orderBody, sizeof( orderBody ), "{\"order\":[%u]}", 1407 - static_cast<unsigned>( credentialId ) ); 1413 + str::format( orderBody, sizeof( orderBody ), "{\"order\":[%u]}", 1414 + static_cast<unsigned>( credentialId ) ); 1408 1415 recordApiStep( apiStep( "credential_order", HttpVerb::Post, "/api/credentials/order", 1409 1416 orderBody, 200, false, nullptr, 0, nullptr ), 1410 1417 steps, passed ); ··· 1450 1457 && extractJsonInt( body, "autoLock", autoLock ); 1451 1458 recordApiStep( settingsRead, steps, passed ); 1452 1459 char settingsBody[96]; 1453 - snprintf( settingsBody, sizeof( settingsBody ), "{\"autoLock\":%d}", 1454 - autoLock < 0 ? 0 : autoLock ); 1460 + str::format( settingsBody, sizeof( settingsBody ), "{\"autoLock\":%d}", 1461 + autoLock < 0 ? 0 : autoLock ); 1455 1462 recordApiStep( apiStep( "settings_write", HttpVerb::Post, "/api/settings", settingsBody, 200, 1456 1463 false, nullptr, 0, nullptr ), 1457 1464 steps, passed ); ··· 1462 1469 && extractJsonString( body, "layout", layout, sizeof( layout ) ); 1463 1470 recordApiStep( layoutRead, steps, passed ); 1464 1471 char layoutBody[32]; 1465 - snprintf( layoutBody, sizeof( layoutBody ), "{\"layout\":\"%s\"}", layout ); 1472 + str::format( layoutBody, sizeof( layoutBody ), "{\"layout\":\"%s\"}", layout ); 1466 1473 recordApiStep( apiStep( "layout_write", HttpVerb::Post, "/api/layout", layoutBody, 200, false, 1467 1474 nullptr, 0, nullptr ), 1468 1475 steps, passed ); ··· 1490 1497 recordApiStep( totpCreated, steps, passed ); 1491 1498 if ( totpCreated ) { 1492 1499 char path[40]; 1493 - snprintf( path, sizeof( path ), "/api/totp/%u", static_cast<unsigned>( totpId ) ); 1500 + str::format( path, sizeof( path ), "/api/totp/%u", static_cast<unsigned>( totpId ) ); 1494 1501 recordApiStep( apiStep( "totp_delete", HttpVerb::Delete, path, nullptr, 200, false, nullptr, 1495 1502 0, nullptr ), 1496 1503 steps, passed );
+19 -14
src/web/admin_api_handlers.cpp
··· 202 202 default: 203 203 if ( ch < 0x20U ) { 204 204 std::array<char, 7> escaped = {}; 205 - snprintf( escaped.data(), escaped.size(), "\\u%04x", 206 - static_cast<unsigned>( ch ) ); 205 + kleidos::platform::str::format( escaped.data(), escaped.size(), "\\u%04x", 206 + static_cast<unsigned>( ch ) ); 207 207 output += escaped.data(); 208 208 } else { 209 209 output.push_back( static_cast<char>( ch ) ); ··· 350 350 } 351 351 352 352 Credential cred; 353 - memset( &cred, 0, sizeof( cred ) ); 353 + cred.wipe(); // start from the all-zero state (memset on a non-trivial type is UB) 354 354 kleidos::platform::str::copy( cred.name, doc["name"] | "" ); 355 355 kleidos::platform::str::copy( cred.user, doc["user"] | "" ); 356 356 kleidos::platform::str::copy( cred.pass, doc["pass"] | "" ); ··· 385 385 386 386 if ( ok ) { 387 387 std::array<char, 32> response = {}; 388 - snprintf( response.data(), response.size(), R"({"id":%u,"ok":true})", id ); 388 + kleidos::platform::str::format( response.data(), response.size(), R"({"id":%u,"ok":true})", 389 + id ); 389 390 ctx.req.send( 200, "application/json", response.data() ); 390 391 } else { 391 392 ctx.req.send( 500, "application/json", R"({"error":"save failed"})" ); ··· 560 561 VaultStore::end(); 561 562 562 563 std::array<char, 48> resp = {}; 563 - snprintf( resp.data(), resp.size(), R"({"ok":true,"imported":%u})", imported ); 564 + kleidos::platform::str::format( resp.data(), resp.size(), R"({"ok":true,"imported":%u})", 565 + imported ); 564 566 ctx.req.send( 200, "application/json", resp.data() ); 565 567 } 566 568 ··· 599 601 } 600 602 601 603 Credential cred; 602 - memset( &cred, 0, sizeof( cred ) ); 604 + cred.wipe(); // start from the all-zero state (memset on a non-trivial type is UB) 603 605 kleidos::platform::str::copy( cred.name, name ); 604 606 kleidos::platform::str::copy( cred.user, obj["username"] | obj["user"] | "" ); 605 607 kleidos::platform::str::copy( cred.pass, obj["password"] | obj["pass"] | "" ); ··· 617 619 VaultStore::end(); 618 620 619 621 std::array<char, 48> resp = {}; 620 - snprintf( resp.data(), resp.size(), R"({"ok":true,"imported":%u})", imported ); 622 + kleidos::platform::str::format( resp.data(), resp.size(), R"({"ok":true,"imported":%u})", 623 + imported ); 621 624 ctx.req.send( 200, "application/json", resp.data() ); 622 625 } 623 626 ··· 696 699 } 697 700 698 701 TotpEntry te; 699 - memset( &te, 0, sizeof( te ) ); 702 + te.wipe(); // start from the all-zero state (memset on a non-trivial type is UB) 700 703 kleidos::platform::str::copy( te.name, doc["name"] | "" ); 701 704 te.digits = static_cast<uint8_t>( doc["digits"] | 6 ); 702 705 te.period = doc["period"] | 30u; ··· 741 744 742 745 if ( ok ) { 743 746 std::array<char, 32> response = {}; 744 - snprintf( response.data(), response.size(), R"({"id":%u,"ok":true})", id ); 747 + kleidos::platform::str::format( response.data(), response.size(), R"({"id":%u,"ok":true})", 748 + id ); 745 749 ctx.req.send( 200, "application/json", response.data() ); 746 750 } else { 747 751 ctx.req.send( 500, "application/json", R"({"error":"save failed"})" ); ··· 1096 1100 obj["idx"] = i; 1097 1101 obj["name"] = h.name; 1098 1102 std::array<char, 18> addrStr = {}; 1099 - snprintf( addrStr.data(), addrStr.size(), "%02x:%02x:%02x:%02x:%02x:%02x", h.addr[0], 1100 - h.addr[1], h.addr[2], h.addr[3], h.addr[4], h.addr[5] ); 1103 + kleidos::platform::str::format( addrStr.data(), addrStr.size(), 1104 + "%02x:%02x:%02x:%02x:%02x:%02x", h.addr[0], h.addr[1], 1105 + h.addr[2], h.addr[3], h.addr[4], h.addr[5] ); 1101 1106 obj["addr"] = addrStr; 1102 1107 } 1103 1108 sendJsonDocument( &ctx.req, doc ); ··· 1130 1135 1131 1136 bool tryHandleBleHostDelete( AdminHttpRequest* req ) { 1132 1137 uint8_t hostIndex = 0xFF; 1133 - if ( !( parseUnsignedSuffix( req->url(), "/api/ble/hosts/", kble::Facade::kMaxBonds, hostIndex ) 1134 - && req->method() == platformHttp::Method::Delete ) ) { 1138 + if ( !parseUnsignedSuffix( req->url(), "/api/ble/hosts/", kble::Facade::kMaxBonds, hostIndex ) 1139 + || req->method() != platformHttp::Method::Delete ) { 1135 1140 return false; 1136 1141 } 1137 1142 if ( hostIndex >= kble::Facade::kMaxBonds ) { ··· 1163 1168 std::string response; 1164 1169 response.reserve( kMaxNameLen + kMaxUserLen + kMaxPassLen + kMaxUrlLen + 64U ); 1165 1170 std::array<char, 24> prefix = {}; 1166 - snprintf( prefix.data(), prefix.size(), R"({"id":%u,"name":)", id ); 1171 + kleidos::platform::str::format( prefix.data(), prefix.size(), R"({"id":%u,"name":)", id ); 1167 1172 response += prefix.data(); 1168 1173 appendJsonEscapedString( response, cred.name.c_str() ); 1169 1174 response += ",\"user\":";
+2 -1
src/web/admin_auth_logic.cpp
··· 212 212 213 213 void toHexLower( char* dst, const uint8_t* bytes, size_t byteCount ) { 214 214 for ( size_t i = 0; i < byteCount; ++i ) { 215 - snprintf( dst + static_cast<ptrdiff_t>( i * 2 ), 3, "%02x", bytes[i] ); 215 + kleidos::platform::str::format( dst + static_cast<ptrdiff_t>( i * 2 ), 3, "%02x", 216 + bytes[i] ); 216 217 } 217 218 } 218 219
+1 -3
src/web/admin_http_request.h
··· 134 134 wipe(); 135 135 return; 136 136 } 137 - snprintf( value_.data(), sizeof( value_ ), "%s", value ); 138 - valueLen_ = strnlen( value_.data(), sizeof( value_ ) ); 137 + valueLen_ = kleidos::platform::str::copy( value_.data(), sizeof( value_ ), value ); 139 138 } 140 139 void wipe() { 141 140 mbedtls_platform_zeroize( value_.data(), sizeof( value_ ) ); ··· 437 436 case 429: 438 437 return "429 Too Many Requests"; 439 438 case 500: 440 - return "500 Internal Server Error"; 441 439 default: 442 440 return "500 Internal Server Error"; 443 441 }
+8 -6
src/web/admin_portal.cpp
··· 694 694 auto resp = web::decideIdleResponse( elapsed, kAdminTimeoutMs, kAdminWarnLeadMs ); 695 695 696 696 std::array<char, 96> json = {}; 697 - snprintf( json.data(), json.size(), 698 - R"({"remaining_s":%u,"timeout_s":%u,"warning_threshold_s":%u})", 699 - static_cast<unsigned>( resp.remainingS ), 700 - static_cast<unsigned>( kAdminTimeoutMs / 1000u ), 701 - static_cast<unsigned>( kAdminWarnLeadMs / 1000u ) ); 697 + kleidos::platform::str::format( 698 + json.data(), json.size(), 699 + R"({"remaining_s":%u,"timeout_s":%u,"warning_threshold_s":%u})", 700 + static_cast<unsigned>( resp.remainingS ), 701 + static_cast<unsigned>( kAdminTimeoutMs / 1000u ), 702 + static_cast<unsigned>( kAdminWarnLeadMs / 1000u ) ); 702 703 ctx.req.send( 200, "application/json", json.data() ); 703 704 } ); 704 705 ··· 802 803 std::to_array<const char*>( { "US", "ES", "UK", "FR", "DE", "PT", "IT" } ); 803 804 const char* name = layout < 7 ? names[layout] : "US"; 804 805 std::array<char, 24> response = {}; 805 - snprintf( response.data(), response.size(), R"({"layout":"%s"})", name ); 806 + kleidos::platform::str::format( response.data(), response.size(), R"({"layout":"%s"})", 807 + name ); 806 808 ctx.req.send( 200, "application/json", response.data() ); 807 809 } ); 808 810
+1 -1
src/web/csv_credential_parser.h
··· 161 161 return false; 162 162 } 163 163 164 - memset( &credential, 0, sizeof( credential ) ); 164 + credential.wipe(); // start from the all-zero state (memset on a non-trivial type is UB) 165 165 kleidos::platform::str::copy( credential.name, row.field( 0 ).c_str() ); 166 166 kleidos::platform::str::copy( credential.user, row.field( 1 ).c_str() ); 167 167 kleidos::platform::str::copy( credential.pass, row.field( 2 ).c_str() );