From f02bf0763042e120da69d5c4e1c779467dff203a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2026 13:17:24 -1000 Subject: [PATCH 1/4] [core] Use SplitMix32 PRNG for random_uint32() across platforms Replace platform-specific random_uint32() implementations with a unified SplitMix32 PRNG seeded from random_bytes(). This provides consistent, fast, non-cryptographic random numbers on all platforms. ESP8266 retains os_random() as it is already faster on that platform. Benchmarked on real hardware (ns/call, before -> after): - RP2040: 2815 -> 333 (8.5x faster) - ESP32: 871 -> 354 (2.5x faster) - BK72xx: 1073 -> 882 (1.2x faster, replaces weak rand()) - RTL87xx: 330 -> 283 (1.2x faster, replaces weak rand()) - ESP8266: 763 (unchanged, keeps os_random()) random_bytes() remains platform-specific hardware RNG for crypto use. --- esphome/components/esp32/helpers.cpp | 1 - esphome/components/host/helpers.cpp | 9 --------- esphome/components/libretiny/helpers.cpp | 2 -- esphome/components/rp2040/helpers.cpp | 9 --------- esphome/components/zephyr/core.cpp | 1 - esphome/core/helpers.cpp | 19 +++++++++++++++++++ esphome/core/helpers.h | 7 ++++++- 7 files changed, 25 insertions(+), 23 deletions(-) diff --git a/esphome/components/esp32/helpers.cpp b/esphome/components/esp32/helpers.cpp index 76f1c59c73..654a35b473 100644 --- a/esphome/components/esp32/helpers.cpp +++ b/esphome/components/esp32/helpers.cpp @@ -14,7 +14,6 @@ namespace esphome { -uint32_t random_uint32() { return esp_random(); } bool random_bytes(uint8_t *data, size_t len) { esp_fill_random(data, len); return true; diff --git a/esphome/components/host/helpers.cpp b/esphome/components/host/helpers.cpp index fdad4f5cb6..7e8849b3e1 100644 --- a/esphome/components/host/helpers.cpp +++ b/esphome/components/host/helpers.cpp @@ -8,8 +8,6 @@ #include #endif #include -#include -#include #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -18,13 +16,6 @@ namespace esphome { static const char *const TAG = "helpers.host"; -uint32_t random_uint32() { - std::random_device dev; - std::mt19937 rng(dev()); - std::uniform_int_distribution dist(0, std::numeric_limits::max()); - return dist(rng); -} - bool random_bytes(uint8_t *data, size_t len) { FILE *fp = fopen("/dev/urandom", "r"); if (fp == nullptr) { diff --git a/esphome/components/libretiny/helpers.cpp b/esphome/components/libretiny/helpers.cpp index ffbd181c54..52332ef53d 100644 --- a/esphome/components/libretiny/helpers.cpp +++ b/esphome/components/libretiny/helpers.cpp @@ -8,8 +8,6 @@ namespace esphome { -uint32_t random_uint32() { return rand(); } - bool random_bytes(uint8_t *data, size_t len) { lt_rand_bytes(data, len); return true; diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp index ad69192af9..36267619a5 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2040/helpers.cpp @@ -16,15 +16,6 @@ namespace esphome { -uint32_t random_uint32() { - uint32_t result = 0; - for (uint8_t i = 0; i < 32; i++) { - result <<= 1; - result |= rosc_hw->randombit; - } - return result; -} - bool random_bytes(uint8_t *data, size_t len) { while (len-- != 0) { uint8_t result = 0; diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index d7c77fdd2c..a3b0471ebc 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -75,7 +75,6 @@ IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); } // Zephyr LwIPLock is defined inline as a no-op in helpers.h -uint32_t random_uint32() { return rand(); } // NOLINT(cert-msc30-c, cert-msc50-cpp) bool random_bytes(uint8_t *data, size_t len) { sys_rand_get(data, len); return true; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 00b447ebf2..be400bfef2 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -156,6 +156,25 @@ uint32_t fnv1_hash(const char *str) { return hash; } +// SplitMix32 PRNG — MurmurHash3 finalizer with golden-ratio increment. +// Provides fast, uniform, non-cryptographic random numbers. +// Seeded lazily from the platform's hardware RNG via random_bytes(). +// ESP8266 uses os_random() instead (defined in esp8266/helpers.cpp). +#ifndef USE_ESP8266 +static uint32_t splitmix32_state; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +uint32_t random_uint32() { + if (splitmix32_state == 0) { + random_bytes(reinterpret_cast(&splitmix32_state), sizeof(splitmix32_state)); + } + splitmix32_state += 0x9e3779b9u; + uint32_t z = splitmix32_state; + z = (z ^ (z >> 16)) * 0x85ebca6bu; + z = (z ^ (z >> 13)) * 0xc2b2ae35u; + return z ^ (z >> 16); +} +#endif + float random_float() { return static_cast(random_uint32()) / static_cast(UINT32_MAX); } // Strings diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index a703b5a5f3..6bf8371f58 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -842,10 +842,15 @@ template inline constexpr ESPHOME_ALWAYS_INLINE Ret } /// Return a random 32-bit unsigned integer. +/// Not thread-safe. Must only be called from the main loop. +/// Not suitable for cryptographic use; use random_bytes() instead. uint32_t random_uint32(); /// Return a random float between 0 and 1. +/// Not thread-safe. Must only be called from the main loop. +/// Not suitable for cryptographic use; use random_bytes() instead. float random_float(); -/// Generate \p len number of random bytes. +/// Generate \p len number of random bytes from the platform's hardware RNG. +/// Thread-safe. Suitable for cryptographic use. bool random_bytes(uint8_t *data, size_t len); ///@} From 1b090e6b95fda2cf5039e8a0f134fc11fd221049 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2026 13:35:43 -1000 Subject: [PATCH 2/4] Address review: use 'secure RNG' instead of 'hardware RNG' in comments --- esphome/core/helpers.cpp | 2 +- esphome/core/helpers.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index be400bfef2..f01c97e359 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -158,7 +158,7 @@ uint32_t fnv1_hash(const char *str) { // SplitMix32 PRNG — MurmurHash3 finalizer with golden-ratio increment. // Provides fast, uniform, non-cryptographic random numbers. -// Seeded lazily from the platform's hardware RNG via random_bytes(). +// Seeded lazily from the platform's secure RNG via random_bytes(). // ESP8266 uses os_random() instead (defined in esp8266/helpers.cpp). #ifndef USE_ESP8266 static uint32_t splitmix32_state; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 6bf8371f58..43431299de 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -849,7 +849,7 @@ uint32_t random_uint32(); /// Not thread-safe. Must only be called from the main loop. /// Not suitable for cryptographic use; use random_bytes() instead. float random_float(); -/// Generate \p len number of random bytes from the platform's hardware RNG. +/// Generate \p len random bytes using the platform's secure RNG (hardware RNG or OS CSPRNG). /// Thread-safe. Suitable for cryptographic use. bool random_bytes(uint8_t *data, size_t len); From 2af50329f42f049f4ebbaacdaf38ee042e8cebed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2026 13:36:08 -1000 Subject: [PATCH 3/4] Document design trade-off for state==0 reseed --- esphome/core/helpers.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index f01c97e359..dc8e865d78 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -164,6 +164,9 @@ uint32_t fnv1_hash(const char *str) { static uint32_t splitmix32_state; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) uint32_t random_uint32() { + // State of 0 means unseeded. The state will wrap back to 0 after 2^32 calls, + // triggering one extra random_bytes() call — an acceptable trade-off vs. adding + // a separate bool flag (4 bytes BSS + branch on every call). if (splitmix32_state == 0) { random_bytes(reinterpret_cast(&splitmix32_state), sizeof(splitmix32_state)); } From e2a59daabc352c741790c9d1b481289daea80bd1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2026 13:37:26 -1000 Subject: [PATCH 4/4] Add SplitMix32 references for reviewers --- esphome/core/helpers.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index dc8e865d78..1b6f8caf71 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -156,8 +156,11 @@ uint32_t fnv1_hash(const char *str) { return hash; } -// SplitMix32 PRNG — MurmurHash3 finalizer with golden-ratio increment. -// Provides fast, uniform, non-cryptographic random numbers. +// SplitMix32 — a fast, non-cryptographic PRNG from the SplitMix family +// (Steele et al., 2014). Uses a Weyl sequence with golden-ratio increment +// and the MurmurHash3 32-bit finalizer as output mixing function. +// Reference: https://doi.org/10.1145/2714064.2660195 +// Test results: https://lemire.me/blog/2017/08/22/testing-non-cryptographic-random-number-generators-my-results/ // Seeded lazily from the platform's secure RNG via random_bytes(). // ESP8266 uses os_random() instead (defined in esp8266/helpers.cpp). #ifndef USE_ESP8266