Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions wled00/fcn_declare.h
Original file line number Diff line number Diff line change
Expand Up @@ -295,14 +295,31 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage);
bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply=true);

//udp.cpp
void handleNotifications();

//sync_notifier.cpp
void notify(byte callMode, bool followUp=false);
uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t* buffer, uint8_t bri=255, bool isRGBW=false);
void notifyRetryIfNeeded();
void parseNotifyPacket(const uint8_t *udpIn);

//sync_nodes.cpp
bool parseNodeInfoPacket(const uint8_t *udpIn, unsigned len, bool isSupp, const IPAddress &localIP);
void refreshNodeList();
void sendSysInfoUDP();

//realtime.cpp
void realtimeLock(uint32_t timeoutMs, byte md = REALTIME_MODE_GENERIC);
void exitRealtime();
void handleNotifications();
void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w);
void refreshNodeList();
void sendSysInfoUDP();

//realtime_udp.cpp
bool handleHyperionPacket();
bool handleDirectRealtimePacket(uint8_t *udpIn, size_t packetSize, bool isSupp);

//realtime_broadcast.cpp
uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t* buffer, uint8_t bri=255, bool isRGBW=false);

//espnow_sync.cpp
#ifndef WLED_DISABLE_ESPNOW
void espNowSentCB(uint8_t* address, uint8_t status);
void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rssi, bool broadcast);
Expand Down
109 changes: 109 additions & 0 deletions wled00/sync/espnow_sync.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#include "wled.h"
#include "sync.h"

/*
* ESP-NOW transport for the WLED Notifier sync protocol. Not a protocol of its own -
* reassembles fragmented packets and hands the result to parseNotifyPacket()
* (sync_notifier.cpp), the same decoder used by the UDP transport.
*/

#ifndef WLED_DISABLE_ESPNOW
// ESP-NOW message sent callback function
void espNowSentCB(uint8_t* address, uint8_t status) {
DEBUG_PRINTF_P(PSTR("Message sent to " MACSTR ", status: %d\n"), MAC2STR(address), status);
}

// ESP-NOW message receive callback function
void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rssi, bool broadcast) {
sprintf_P(last_signal_src, PSTR("%02x%02x%02x%02x%02x%02x"), address[0], address[1], address[2], address[3], address[4], address[5]);

#ifdef WLED_DEBUG
DEBUG_PRINT(F("ESP-NOW: ")); DEBUG_PRINT(last_signal_src); DEBUG_PRINT(F(" -> ")); DEBUG_PRINTLN(len);
for (int i=0; i<len; i++) DEBUG_PRINTF_P(PSTR("%02x "), data[i]);
DEBUG_PRINTLN();
#endif

// usermods hook can override processing
if (UsermodManager::onEspNowMessage(address, data, len)) return;

bool knownRemote = false;
for (const auto& mac : linked_remotes) {
if (strlen(mac.data()) == 12 && strcmp(last_signal_src, mac.data()) == 0) {
knownRemote = true;
break;
}
}
if (!knownRemote) {
DEBUG_PRINT(F("ESP Now Message Received from Unlinked Sender: "));
DEBUG_PRINTLN(last_signal_src);
return;
}

// handle WiZ Mote data
if (data[0] == 0x91 || data[0] == 0x81 || data[0] == 0x80) {
handleWiZdata(data, len);
return;
}

partial_packet_t *buffer = reinterpret_cast<partial_packet_t *>(data);
if (len < 3 || !broadcast || buffer->magic != 'W' || !useESPNowSync || WLED_CONNECTED) {
DEBUG_PRINTLN(F("ESP-NOW unexpected packet, not syncing or connected to WiFi."));
return;
}
Comment on lines +43 to +52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate len before reading data[0].

Line 43 reads data[0], and line 22 iterates data[i] in the debug block. The only length check is at line 49. If a peer sends a zero-length ESP-NOW frame, line 43 reads out of bounds. Move a minimum-length guard to the top of the callback.

🛡️ Proposed fix
 void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rssi, bool broadcast) {
+  if (len == 0) return;
   sprintf_P(last_signal_src, PSTR("%02x%02x%02x%02x%02x%02x"), address[0], address[1], address[2], address[3], address[4], address[5]);

This code is moved rather than newly written, so the defect may predate the PR.

As per path instructions, ESP-NOW raw messages input is an untrusted ingress point where bounds checking must be enforced.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (data[0] == 0x91 || data[0] == 0x81 || data[0] == 0x80) {
handleWiZdata(data, len);
return;
}
partial_packet_t *buffer = reinterpret_cast<partial_packet_t *>(data);
if (len < 3 || !broadcast || buffer->magic != 'W' || !useESPNowSync || WLED_CONNECTED) {
DEBUG_PRINTLN(F("ESP-NOW unexpected packet, not syncing or connected to WiFi."));
return;
}
if (len == 0) return;
if (data[0] == 0x91 || data[0] == 0x81 || data[0] == 0x80) {
handleWiZdata(data, len);
return;
}
partial_packet_t *buffer = reinterpret_cast<partial_packet_t *>(data);
if (len < 3 || !broadcast || buffer->magic != 'W' || !useESPNowSync || WLED_CONNECTED) {
DEBUG_PRINTLN(F("ESP-NOW unexpected packet, not syncing or connected to WiFi."));
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/sync/espnow_sync.cpp` around lines 43 - 52, Move the minimum-length
validation to the start of the ESP-NOW receive callback, before any access to
data[0] or debug-loop data[i]. Reject zero- or undersized frames immediately,
while preserving the existing handleWiZdata and partial_packet_t processing for
valid-length packets.

Source: Path instructions


static uint8_t *udpIn = nullptr;
static uint8_t packetsReceived = 0;
static uint8_t segsReceived = 0;
static unsigned long lastProcessed = 0;

if (buffer->packet == 0) {
packetsReceived = 0; // it will increment later (this is to make sure we start counting packets correctly)
if (udpIn == nullptr) {
udpIn = (uint8_t *)malloc(WLEDPACKETSIZE); // we cannot use stack as we are in callback
if (!udpIn) return; // memory alocation failed
DEBUG_PRINTLN(F("ESP-NOW inited UDP buffer."));
}
memcpy(udpIn, buffer->data, len-3); // global data (41 bytes + up to 5 segments)
segsReceived = (len - 3 - 41) / UDP_SEG_SIZE;
Comment on lines +59 to +67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

A short first fragment leaves the reassembly buffer partly uninitialized.

malloc() at line 62 does not zero the buffer. Line 66 copies only len-3 bytes. The notifier header needs 41 bytes. A sender can transmit a valid first fragment with len as low as 3, which passes the check at line 49. segsReceived at line 67 then evaluates (len-3-41)/UDP_SEG_SIZE, which is a negative int truncated to 0, so no wrap occurs. However parseNotifyPacket() later reads bytes 0..40 of the buffer, and most of them hold uninitialized heap contents. The decoded version byte, sync group, and segment stride then come from stale memory.

Reject a first fragment shorter than 44 bytes, and clamp the copy to the destination size.

🛡️ Proposed fix
   if (buffer->packet == 0) {
+    if (len < 44 || (size_t)(len - 3) > WLEDPACKETSIZE) return; // need at least the 41-byte global block
     packetsReceived = 0; // it will increment later (this is to make sure we start counting packets correctly)
     if (udpIn == nullptr) {
       udpIn = (uint8_t *)malloc(WLEDPACKETSIZE); // we cannot use stack as we are in callback
       if (!udpIn) return; // memory alocation failed
       DEBUG_PRINTLN(F("ESP-NOW inited UDP buffer."));
     }
     memcpy(udpIn, buffer->data, len-3); // global data (41 bytes + up to 5 segments)
     segsReceived = (len - 3 - 41) / UDP_SEG_SIZE;

This code is moved rather than newly written, so the defect may predate the PR.

As per path instructions, ESP-NOW raw messages input is an untrusted ingress point where input validation must be enforced.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (buffer->packet == 0) {
packetsReceived = 0; // it will increment later (this is to make sure we start counting packets correctly)
if (udpIn == nullptr) {
udpIn = (uint8_t *)malloc(WLEDPACKETSIZE); // we cannot use stack as we are in callback
if (!udpIn) return; // memory alocation failed
DEBUG_PRINTLN(F("ESP-NOW inited UDP buffer."));
}
memcpy(udpIn, buffer->data, len-3); // global data (41 bytes + up to 5 segments)
segsReceived = (len - 3 - 41) / UDP_SEG_SIZE;
if (buffer->packet == 0) {
if (len < 44 || (size_t)(len - 3) > WLEDPACKETSIZE) return; // need at least the 41-byte global block
packetsReceived = 0; // it will increment later (this is to make sure we start counting packets correctly)
if (udpIn == nullptr) {
udpIn = (uint8_t *)malloc(WLEDPACKETSIZE); // we cannot use stack as we are in callback
if (!udpIn) return; // memory alocation failed
DEBUG_PRINTLN(F("ESP-NOW inited UDP buffer."));
}
memcpy(udpIn, buffer->data, len-3); // global data (41 bytes + up to 5 segments)
segsReceived = (len - 3 - 41) / UDP_SEG_SIZE;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/sync/espnow_sync.cpp` around lines 59 - 67, Validate the first ESP-NOW
fragment in the packet handling branch before allocating or copying: reject any
packet with len less than 44 bytes, then copy only up to the WLEDPACKETSIZE
destination capacity. Keep the existing reassembly initialization and
segsReceived calculation for accepted fragments, using the symbols
buffer->packet, udpIn, and WLEDPACKETSIZE.

Source: Path instructions

} else if (buffer->packet == packetsReceived && udpIn && ((len - 3) / UDP_SEG_SIZE) * UDP_SEG_SIZE == (len-3)) {
// we received a packet full of segments
if (segsReceived >= MAX_NUM_SEGMENTS) {
// we are already past max segments, just ignore
DEBUG_PRINTLN(F("ESP-NOW received segments past maximum."));
len = 3;
} else if ((segsReceived + ((len - 3) / UDP_SEG_SIZE)) >= MAX_NUM_SEGMENTS) {
len = ((MAX_NUM_SEGMENTS - segsReceived) * UDP_SEG_SIZE) + 3; // we have reached max number of segments
}
if (len > 3) {
memcpy(udpIn + 41 + (segsReceived * UDP_SEG_SIZE), buffer->data, len-3);
segsReceived += (len - 3) / UDP_SEG_SIZE;
}
} else {
// any out of order packet or incorrectly sized packet or if we have no UDP buffer will abort
DEBUG_PRINTF_P(PSTR("ESP-NOW incorrect packet: %d (%d) [%d]\n"), (int)buffer->packet, (int)len-3, (int)UDP_SEG_SIZE);
if (udpIn) free(udpIn);
udpIn = nullptr;
packetsReceived = 0;
segsReceived = 0;
return;
}
if (!udpIn) return;

packetsReceived++;
DEBUG_PRINTF_P(PSTR("ESP-NOW packet received: %d (%d/%d) s:[%d/%d]\n"), (int)buffer->packet, (int)packetsReceived, (int)buffer->noOfPackets, (int)segsReceived, MAX_NUM_SEGMENTS);
if (packetsReceived >= buffer->noOfPackets) {
// last packet received
if (millis() - lastProcessed > 250) {
DEBUG_PRINTLN(F("ESP-NOW processing complete message."));
parseNotifyPacket(udpIn);
lastProcessed = millis();
} else {
DEBUG_PRINTLN(F("ESP-NOW ignoring complete message."));
}
free(udpIn);
udpIn = nullptr;
packetsReceived = 0;
segsReceived = 0;
}
}
#endif
61 changes: 61 additions & 0 deletions wled00/sync/realtime.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#include "wled.h"

/*
* Realtime state machine shared by every "external source pushes raw pixel data at us"
* protocol (Hyperion, TPM2.NET, legacy UDP realtime - see realtime_udp.cpp). This is
* infrastructure, not a protocol of its own.
*/

// realtimeLock() is called from UDP notifications, JSON API or serial Ada
void realtimeLock(uint32_t timeoutMs, byte md)
{
if (!realtimeMode && !realtimeOverride) {
if (useMainSegmentOnly) {
Segment& mainseg = strip.getMainSegment();
mainseg.clear(); // clear entire segment (in case sender transmits less pixels)
mainseg.freeze = true;
// if WLED was off and using main segment only, freeze non-main segments so they stay off
if (bri == 0) {
for (size_t s = 0; s < strip.getSegmentsNum(); s++) strip.getSegment(s).freeze = true;
}
} else {
// clear entire strip
strip.fill(BLACK);
}
// if strip is off (bri==0) and not already in RTM
if (briT == 0) {
strip.setBrightness(briLast, true);
}
}

if (realtimeTimeout != UINT32_MAX) {
realtimeTimeout = (timeoutMs == 255001 || timeoutMs == 65000) ? UINT32_MAX : millis() + timeoutMs;
}
realtimeMode = md;

if (realtimeOverride) return;
if (arlsForceMaxBri) strip.setBrightness(255, true);
if (briT > 0 && md == REALTIME_MODE_GENERIC) strip.show();
}

void exitRealtime() {
if (!realtimeMode) return;
if (realtimeOverride == REALTIME_OVERRIDE_ONCE) realtimeOverride = REALTIME_OVERRIDE_NONE;
strip.setBrightness(bri, true);
realtimeTimeout = 0; // cancel realtime mode immediately
realtimeMode = REALTIME_MODE_INACTIVE; // inform UI immediately
realtimeIP[0] = 0;
if (useMainSegmentOnly) { // unfreeze live segment again
strip.getMainSegment().freeze = false;
strip.trigger();
} else {
strip.show(); // possible fix for #3589
}
updateInterfaces(CALL_MODE_WS_SEND);
}

void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w)
{
unsigned pix = i + arlsOffset;
strip.setRealtimePixelColor(pix, RGBW32(r,g,b,w));
}
153 changes: 153 additions & 0 deletions wled00/sync/realtime_broadcast.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#include "wled.h"

/*********************************************************************************************\
* Art-Net, DDP, E131 output - work in progress
* (this is the outbound/sender side; see e131.cpp for the inbound receiver side of these
* same wire protocols)
\*********************************************************************************************/

//
// Send real time UDP updates to the specified client
//
// type - protocol type (0=DDP, 1=E1.31, 2=ArtNet)
// client - the IP address to send to
// length - the number of pixels
// buffer - a buffer of at least length*4 bytes long
// isRGBW - true if the buffer contains 4 components per pixel

static size_t sequenceNumber = 0; // this needs to be shared across all outputs
static const size_t ART_NET_HEADER_SIZE = 12;
static const byte ART_NET_HEADER[] PROGMEM = {0x41,0x72,0x74,0x2d,0x4e,0x65,0x74,0x00,0x00,0x50,0x00,0x0e};

uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t *buffer, uint8_t bri, bool isRGBW) {
if (!(apActive || interfacesInited) || !client[0] || !length) return 1; // network not initialised or dummy/unset IP address 031522 ajn added check for ap

WiFiUDP ddpUdp;

switch (type) {
case 0: // DDP
{
// calculate the number of UDP packets we need to send
size_t channelCount = length * (isRGBW? 4:3); // 1 channel for every R,G,B value
size_t packetCount = ((channelCount-1) / DDP_CHANNELS_PER_PACKET) +1;

// there are 3 channels per RGB pixel
uint32_t channel = 0; // TODO: allow specifying the start channel
// the current position in the buffer
size_t bufferOffset = 0;

for (size_t currentPacket = 0; currentPacket < packetCount; currentPacket++) {
if (sequenceNumber > 15) sequenceNumber = 0;

if (!ddpUdp.beginPacket(client, DDP_DEFAULT_PORT)) { // port defined in ESPAsyncE131.h
//DEBUG_PRINTLN(F("WiFiUDP.beginPacket returned an error"));
return 1; // problem
}

// the amount of data is AFTER the header in the current packet
size_t packetSize = DDP_CHANNELS_PER_PACKET;

uint8_t flags = DDP_FLAGS_VER1;
if (currentPacket == (packetCount - 1U)) {
// last packet, set the push flag
// TODO: determine if we want to send an empty push packet to each destination after sending the pixel data
flags = DDP_FLAGS_VER1 | DDP_FLAGS_PUSH;
if (channelCount % DDP_CHANNELS_PER_PACKET) {
packetSize = channelCount % DDP_CHANNELS_PER_PACKET;
}
}

// write the header
/*0*/ddpUdp.write(flags);
// TODO: sequence number should be 1-15 as 0 means "unused", it has no bad consequences other than out of sequence packet may be accepted
/*1*/ddpUdp.write(sequenceNumber++ & 0x0F); // sequence may be unnecessary unless we are sending twice (as requested in Sync settings)
/*2*/ddpUdp.write(isRGBW ? DDP_TYPE_RGBW32 : DDP_TYPE_RGB24);
/*3*/ddpUdp.write(DDP_ID_DISPLAY);
// data offset in bytes, 32-bit number, MSB first
/*4*/ddpUdp.write(0xFF & (channel >> 24));
/*5*/ddpUdp.write(0xFF & (channel >> 16));
/*6*/ddpUdp.write(0xFF & (channel >> 8));
/*7*/ddpUdp.write(0xFF & (channel ));
// data length in bytes, 16-bit number, MSB first
/*8*/ddpUdp.write(0xFF & (packetSize >> 8));
/*9*/ddpUdp.write(0xFF & (packetSize ));

// write the colors, the write write(const uint8_t *buffer, size_t size)
// function is just a loop internally too
for (size_t i = 0; i < packetSize; i += (isRGBW?4:3)) {
ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // R
ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // G
ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // B
if (isRGBW) ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // W
}

if (!ddpUdp.endPacket()) {
//DEBUG_PRINTLN(F("WiFiUDP.endPacket returned an error"));
return 1; // problem
}

channel += packetSize;
}
} break;

case 1: //E1.31
{
} break;

case 2: //ArtNet
{
// calculate the number of UDP packets we need to send
const size_t channelCount = length * (isRGBW?4:3); // 1 channel for every R,G,B,(W?) value
const size_t ARTNET_CHANNELS_PER_PACKET = isRGBW?512:510; // 512/4=128 RGBW LEDs, 510/3=170 RGB LEDs
const size_t packetCount = ((channelCount-1)/ARTNET_CHANNELS_PER_PACKET)+1;

uint32_t channel = 0;
size_t bufferOffset = 0;

sequenceNumber++;

for (size_t currentPacket = 0; currentPacket < packetCount; currentPacket++) {

if (sequenceNumber > 255) sequenceNumber = 0;

if (!ddpUdp.beginPacket(client, ARTNET_DEFAULT_PORT)) {
DEBUG_PRINTLN(F("Art-Net WiFiUDP.beginPacket returned an error"));
return 1; // borked
}

size_t packetSize = ARTNET_CHANNELS_PER_PACKET;

if (currentPacket == (packetCount - 1U)) {
// last packet
if (channelCount % ARTNET_CHANNELS_PER_PACKET) {
packetSize = channelCount % ARTNET_CHANNELS_PER_PACKET;
}
}

byte header_buffer[ART_NET_HEADER_SIZE];
memcpy_P(header_buffer, ART_NET_HEADER, ART_NET_HEADER_SIZE);
ddpUdp.write(header_buffer, ART_NET_HEADER_SIZE); // This doesn't change. Hard coded ID, OpCode, and protocol version.
ddpUdp.write(sequenceNumber & 0xFF); // sequence number. 1..255
ddpUdp.write(0x00); // physical - more an FYI, not really used for anything. 0..3
ddpUdp.write((currentPacket) & 0xFF); // Universe LSB. 1 full packet == 1 full universe, so just use current packet number.
ddpUdp.write(0x00); // Universe MSB, unused.
ddpUdp.write(0xFF & (packetSize >> 8)); // 16-bit length of channel data, MSB
ddpUdp.write(0xFF & (packetSize )); // 16-bit length of channel data, LSB

for (size_t i = 0; i < packetSize; i += (isRGBW?4:3)) {
ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // R
ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // G
ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // B
if (isRGBW) ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // W
}

if (!ddpUdp.endPacket()) {
DEBUG_PRINTLN(F("Art-Net WiFiUDP.endPacket returned an error"));
return 1; // borked
}
channel += packetSize;
}
} break;
}
return 0;
}
Loading
Loading