Description
Basic Infos
Hardware
Hardware: ESP-8266 in a Sonoff Switch
Core Version: 2.3.0
Description
WiFiClientSecure may read from an invalid buffer, causing any number of undefined behaviours.
To compute available(), the internal SSLContext in WiFiClientSecure calls axTLS's ssl_read(SSL* ssl, uint8_t** in_data), which gives access to a temporary internal buffer through in_data (See notes which indicate to use in_data before performing any other ssl_* operations). SSLContext then proceeds to save a reference to the internal buffer in its _read_ptr member field, as a cache for subsequent calls to SSLContext.read().
However, unless the read() calls are performed immediately after calling available(), the reference saved in _read_ptr may not be valid anymore. In particular, if the context is used for writing in between, the reference will be obsolete and calls to WiFiClientSecure.read() will return garbage (or access invalid memory).
The problem is aggravated by the fact that many functions call available(), including connected().
In particular, I'm using a modified PubSubClient to implement a MQTT client (modified to use an internal WiFiClientSecure as its underlying channel). Using a simple loop as shown below halts the program, since by the time the program actually reads the buffer, the MQTT packet's "message length" is an arbitrary number (typically larger than the real message length, which is very small) and the readPacket() function tries to read beyond what the broker has sent, blocking forever (or until the socket times out).
void loop() {
// ...
// The broker has sent a message to the device (waiting on the ssl socket)
if (mqtt.connected()) {
// connected() -> available() -> ssl_read()
// WiFiClientSecure.read() would return 'device, I love you'
if (!credentials) {
// ask for credentials
mqtt.publish(...) // -> WiFiClientSecure.write() -> ssl_write()
}
// The reference is invalid, WiFiClientSecure.read() may return the complete
// works of Shakespeare (modulo FLASH_MEMORY_SIZE);
mqtt.loop(); // -> readPacket() -> WiFiClientSecure.read()
// this line may never be reached, because loop() halted
}
else {
// connect mqtt
}
}
If the order of the internal operations is reversed, everything works fine.
void loop() {
// ...
// The broker has sent a message to the device (waiting on the ssl socket)
if (mqtt.connected()) {
// connected() -> available() -> ssl_read()
mqtt.loop(); // -> readPacket() -> WiFiClientSecure.read()
// loop() has no problems, since nobody has called
// an ssl_*() function before reading the buffer.
// The device has gotten the message of love from the broker;
// it may now work in peace
if (!credentials) {
// ask for credentials
mqtt.publish(...) // -> WiFiClientSecure.write() -> ssl_write()
}
// The reference is invalid, but nobody cares
}
else {
// connect mqtt
}
}
Some possible solutions:
- Always make a copy of the ssl_read() output. (May be too memory hungry).
- Do not use ssl_read() for anything but actually reading the complete stream. (Would require changes to the axTLS library to expose functions to get available bytes and reading into a fixed-size buffer).
- Forbid write access (i.e. return error code) until all remaining bytes in the ssl_read buffer have been read. (Easiest to implement: if (available > 0) { return READ_BLOCK; }, but it seems too harsh).
- Modify axTLS to lift the requirement, i.e. ensure in_data is valid until the next call to ssl_read. (I don't know how entrenched is this into the library).
Settings in IDE
Module: Generic ESP8266 Module
Flash Size: 1MB
CPU Frequency: 80Mhz
Flash Mode: DIO
Flash Frequency: 40Mhz
Upload Using: SERIAL
Reset Method: ck