Compare commits

..
Author SHA1 Message Date
J. Nick Koston e6764f3177 [esp8266] Keep libsodium's SHA-256 round constants in flash 2026-09-05 16:08:38 +02:00
22 changed files with 236 additions and 350 deletions
-1
View File
@@ -553,7 +553,6 @@ file does, and it is the authority when they disagree. The most useful starting
4. **Lint:** Run `prek` to ensure code is compliant.
5. **Commit:** Commit your changes. There is no strict format for commit messages.
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title must start with a `[tag]` prefix. For component work, use the component name (e.g., `[display] Fix bug`, `[abc123] Add new component`); for changes to shared/core code that isn't tied to a single component, use `[core]` (e.g., `[core] Add validator`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template.
7. **Comments:** When commenting on GitHub PRs or issues, don't tag contributors, especially bots. Avoid referring to list items (e.g. from reviews) with the form #nn - this will be interpreted by GitHub as a reference to issue or PR nn. Keep comments short and exclude irrelevant details, backstories, restatement of previous comments and anything that is already obvious to the reader.
* **Documentation Contributions:**
* Documentation is hosted in the separate `esphome/esphome.io` repository.
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.1
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.0
RUN \
platformio settings set enable_telemetry No \
@@ -6,6 +6,7 @@
#include "esphome/components/network/util.h"
#include "esphome/core/log.h"
#include <cerrno>
#include <sys/select.h>
namespace esphome::async_tcp {
@@ -41,15 +42,7 @@ bool AsyncClient::connect(const char *host, uint16_t port) {
return false;
}
if (socket_->setblocking(false) != 0) {
// Capture before the log and close() clobber errno
const int saved_errno = errno;
ESP_LOGE(TAG, "Failed to set nonblocking: errno %d", saved_errno);
close();
if (error_cb_)
error_cb_(error_arg_, this, saved_errno);
return false;
}
socket_->setblocking(false);
int err = socket_->connect((struct sockaddr *) &addr, addrlen);
if (err == 0) {
@@ -104,22 +97,45 @@ void AsyncClient::loop() {
return;
if (connecting_) {
int err = 0;
switch (socket::poll_connect(*socket_, err)) {
case socket::ConnectPollResult::CONNECT_POLL_RESULT_PENDING:
break;
case socket::ConnectPollResult::CONNECT_POLL_RESULT_CONNECTED:
// For connecting, we need to check writability, not readability
// The Application's select() only monitors read FDs, so we do our own check here
// For ESP platforms lwip_select() might be faster, but this code isn't used
// on those platforms anyway. If it was, we'd fix the Application select()
// to report writability instead of doing it this way.
int fd = socket_->get_fd();
if (fd < 0) {
ESP_LOGW(TAG, "Invalid socket fd");
close();
return;
}
fd_set writefds;
FD_ZERO(&writefds);
FD_SET(fd, &writefds);
struct timeval tv = {0, 0};
int ret = select(fd + 1, nullptr, &writefds, nullptr, &tv);
if (ret > 0 && FD_ISSET(fd, &writefds)) {
int error = 0;
socklen_t len = sizeof(error);
if (socket_->getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0) {
connecting_ = false;
connected_ = true;
if (connect_cb_)
connect_cb_(connect_arg_, this);
break;
case socket::ConnectPollResult::CONNECT_POLL_RESULT_ERROR:
ESP_LOGW(TAG, "Connection failed: %d", err);
} else {
ESP_LOGW(TAG, "Connection failed: %d", error);
close();
if (error_cb_)
error_cb_(error_arg_, this, err);
break;
error_cb_(error_arg_, this, error);
}
} else if (ret < 0) {
const int err = errno;
ESP_LOGE(TAG, "Select error: %d", err);
close();
if (error_cb_)
error_cb_(error_arg_, this, err);
}
} else if (connected_) {
// For connected sockets, use the Application's select() results
+2
View File
@@ -321,6 +321,7 @@ async def to_code(config: ConfigType) -> None:
"pre:exclude_updater.py",
"pre:exclude_waveform.py",
"pre:relocate_ratetable.py",
"pre:relocate_sodium_sha256.py",
]
if not enable_scanf_float:
extra_scripts.append("pre:remove_float_scanf.py")
@@ -463,6 +464,7 @@ def copy_files() -> None:
"exclude_waveform",
"remove_float_scanf",
"relocate_ratetable",
"relocate_sodium_sha256",
):
copy_file_if_changed(
dir / f"{script}.py.script",
@@ -24,6 +24,40 @@ _RATETABLE_COMMENT = (
# "_dport0_data_start" line in the earlier .dport0.data section
_RATETABLE_ANCHOR = re.compile(r"^\s*_data_start = ABSOLUTE\(\.\);", re.MULTILINE)
# Move libsodium's SHA-256 round constants from DRAM to flash. The Arduino
# core keeps .rodata in DRAM because flash only allows aligned 32-bit reads,
# but Krnd is a uint32_t[64] that the transform only ever reads word-wise, so
# it is safe in flash and frees 256 bytes of DRAM on every build that links
# libsodium (api or ota encryption). The rule goes inside .irom0.text, which
# the linker script places before the DRAM .rodata rules, so it wins.
SODIUM_SHA256_RULE = "*hash_sha256_cp.c.o(.rodata.Krnd)"
_SODIUM_SHA256_COMMENT = "/* ESPHome: libsodium SHA-256 round constants are read word-wise, keep them in flash */"
_SODIUM_SHA256_ANCHOR = re.compile(
r"^\s*_irom0_text_start = ABSOLUTE\(\.\);", re.MULTILINE
)
def relocate_sodium_sha256(content: str) -> str:
"""Insert the libsodium round-constant flash rule into a generated common
linker script."""
if SODIUM_SHA256_RULE in content:
return content
match = _SODIUM_SHA256_ANCHOR.search(content)
if match is None:
raise RuntimeError(
"'_irom0_text_start' anchor not found in the generated linker script; "
"cannot move the libsodium SHA-256 constants to flash "
"(has the Arduino core linker script changed?)"
)
insert_pos = match.end()
return (
content[:insert_pos]
+ f"\n {_SODIUM_SHA256_COMMENT}"
+ f"\n {SODIUM_SHA256_RULE}"
+ content[insert_pos:]
)
# Memory sizes for testing mode (allow larger builds for CI component grouping)
TESTING_IRAM_SIZE = "0x200000" # 2MB
TESTING_DRAM_SIZE = "0x200000" # 2MB
@@ -0,0 +1,57 @@
# pylint: disable=E0602
Import("env") # noqa
# Move libsodium's SHA-256 round constants from DRAM to flash
#
# The Arduino core linker script keeps every .rodata input section in DRAM,
# because flash-mapped memory only allows aligned 32-bit reads and most
# tables are read byte-wise. libsodium's Krnd (crypto_hash/sha256) is a
# uint32_t[64] that SHA256_Transform only reads word-wise, so it is safe in
# flash; every build that links libsodium (api or ota encryption) gets 256
# bytes of DRAM back. The rule is placed inside the .irom0.text output
# section, which the linker script lists before the DRAM .rodata rules, so it
# claims the section first. Mirrored in build_surgery.py for the native
# toolchain; keep both in sync.
import re
from os.path import join
RULE = "*hash_sha256_cp.c.o(.rodata.Krnd)"
ANCHOR = re.compile(r"^\s*_irom0_text_start = ABSOLUTE\(\.\);", re.MULTILINE)
def relocate_sodium_sha256(source, target, env):
"""Insert the flash rule into the generated linker script.
Runs as a pre-action of the link step; the linker script is a declared
dependency of the elf, so it has already been generated at this point.
"""
ld_path = join(env.subst("$BUILD_DIR"), "ld", "local.eagle.app.v6.common.ld")
with open(ld_path, encoding="utf-8") as f:
contents = f.read()
if RULE in contents:
return # Already patched (incremental build)
match = ANCHOR.search(contents)
if match is None:
raise RuntimeError(
f"ESPHome: '_irom0_text_start' anchor not found in {ld_path}; "
"cannot move the libsodium SHA-256 constants to flash "
"(has the Arduino core linker script changed?)"
)
insert_pos = match.end()
patched = (
contents[:insert_pos]
+ "\n /* ESPHome: libsodium SHA-256 round constants are read word-wise, keep them in flash */"
+ f"\n {RULE}"
+ contents[insert_pos:]
)
with open(ld_path, "w", encoding="utf-8") as f:
f.write(patched)
print("ESPHome: Moved libsodium SHA-256 constants to flash (256 bytes of DRAM)")
# Register the callback to run before the link step
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", relocate_sodium_sha256)
@@ -407,10 +407,7 @@ void ESPHomeOTAComponent::handle_data_() {
tv.tv_usec = 0;
this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
this->client_->setsockopt(SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
if (this->client_->setblocking(true) != 0) {
this->log_socket_error_(LOG_STR("blocking"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
this->client_->setblocking(true);
// Acknowledge auth OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK);
@@ -59,15 +59,13 @@ int BSDSocketImpl::close() {
int BSDSocketImpl::setblocking(bool blocking) {
int fl = ::fcntl(this->fd_, F_GETFL, 0);
if (fl < 0) {
return fl;
}
if (blocking) {
fl &= ~O_NONBLOCK;
} else {
fl |= O_NONBLOCK;
}
return ::fcntl(this->fd_, F_SETFL, fl);
::fcntl(this->fd_, F_SETFL, fl);
return 0;
}
size_t BSDSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
-7
View File
@@ -205,13 +205,6 @@ static constexpr size_t SOCKADDR_STR_LEN = 46; // INET6_ADDRSTRLEN
static constexpr size_t SOCKADDR_STR_LEN = 16; // INET_ADDRSTRLEN
#endif
/// Outcome of polling a non-blocking connect(); see socket::poll_connect().
enum class ConnectPollResult : uint8_t {
CONNECT_POLL_RESULT_PENDING,
CONNECT_POLL_RESULT_CONNECTED,
CONNECT_POLL_RESULT_ERROR,
};
} // namespace esphome::socket
#endif
+65 -143
View File
@@ -48,33 +48,8 @@ static const char *const TAG = "socket";
#ifdef USE_ESP8266
// optimistic_yield() rate limit in microseconds of CONT time; cheap when hot.
static constexpr uint32_t ESP8266_YIELD_INTERVAL_US = 1000;
// Let SYS run so queued WiFi traffic reaches lwip; CONT and SYS are cooperative
static inline void yield_to_sys() { optimistic_yield(ESP8266_YIELD_INTERVAL_US); }
#else
static inline void yield_to_sys() {}
#endif
// errno for a failed tcp_* call
static int lwip_err_to_errno(err_t err) {
switch (err) {
case ERR_MEM:
return ENOMEM;
case ERR_BUF:
return EAGAIN; // transient, e.g. no free local port
case ERR_RTE:
return EHOSTUNREACH; // no route, e.g. no address yet
case ERR_VAL:
case ERR_ARG:
return EINVAL;
case ERR_USE:
return EADDRINUSE;
case ERR_ISCONN:
return EISCONN;
default:
return EIO;
}
}
// set to 1 to enable verbose lwip logging
#if 0 // NOLINT(readability-avoid-unconditional-preprocessor-if)
#define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__)
@@ -87,8 +62,8 @@ static int lwip_err_to_errno(err_t err) {
// Must be called before destroying the object that tcp_arg points to —
// tcp_abort() triggers the err callback synchronously, which would
// otherwise call back into a partially-destroyed object.
// tcp_sent/tcp_poll are never registered and the connect callback cannot
// fire after abort or close, so neither is cleared.
// tcp_sent/tcp_poll are not cleared because this implementation
// never registers them.
static void pcb_detach_abort(struct tcp_pcb *pcb) {
tcp_arg(pcb, nullptr);
tcp_recv(pcb, nullptr);
@@ -101,7 +76,8 @@ static void pcb_detach_abort(struct tcp_pcb *pcb) {
// After tcp_close(), the PCB remains alive during the TCP close handshake
// (FIN_WAIT, TIME_WAIT states). Without clearing callbacks first, LWIP
// would call recv/err on a destroyed socket object, corrupting the heap.
// Callbacks are left as in pcb_detach_abort().
// tcp_sent/tcp_poll are not cleared because this implementation
// never registers them.
// Returns ERR_OK on success; on failure the PCB is aborted instead.
static err_t pcb_detach_close(struct tcp_pcb *pcb) {
tcp_arg(pcb, nullptr);
@@ -125,51 +101,67 @@ LWIPRawCommon::~LWIPRawCommon() {
}
}
bool LWIPRawCommon::sockaddr2ip_(const struct sockaddr *name, socklen_t addrlen, ip_addr_t *ip, uint16_t *port) const {
if (name == nullptr) {
errno = EINVAL;
return false;
}
#if LWIP_IPV6
if (this->family_ == AF_INET6) {
if (addrlen < sizeof(sockaddr_in6)) {
errno = EINVAL;
return false;
}
auto *addr6 = reinterpret_cast<const sockaddr_in6 *>(name);
*port = ntohs(addr6->sin6_port);
inet6_addr_to_ip6addr(ip_2_ip6(ip), &addr6->sin6_addr);
// ANY lets bind() accept both families; connect() picks the concrete type
IP_SET_TYPE_VAL(*ip, IPADDR_TYPE_ANY);
return true;
}
#endif
if (this->family_ != AF_INET || addrlen < sizeof(sockaddr_in)) {
errno = EINVAL;
return false;
}
auto *addr4 = reinterpret_cast<const sockaddr_in *>(name);
*port = ntohs(addr4->sin_port);
ip_addr_set_ip4_u32(ip, addr4->sin_addr.s_addr);
return true;
}
int LWIPRawCommon::bind(const struct sockaddr *name, socklen_t addrlen) {
LWIP_LOCK();
if (this->pcb_ == nullptr) {
errno = EBADF;
return -1;
}
ip_addr_t ip;
uint16_t port;
if (!this->sockaddr2ip_(name, addrlen, &ip, &port)) {
if (name == nullptr) {
errno = EINVAL;
return -1;
}
LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ipaddr_ntoa(&ip), port);
ip_addr_t ip;
in_port_t port;
#if LWIP_IPV6
if (this->family_ == AF_INET) {
if (addrlen < sizeof(sockaddr_in)) {
errno = EINVAL;
return -1;
}
auto *addr4 = reinterpret_cast<const sockaddr_in *>(name);
port = ntohs(addr4->sin_port);
ip.type = IPADDR_TYPE_V4;
ip.u_addr.ip4.addr = addr4->sin_addr.s_addr;
LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ip4addr_ntoa(&ip.u_addr.ip4), port);
} else if (this->family_ == AF_INET6) {
if (addrlen < sizeof(sockaddr_in6)) {
errno = EINVAL;
return -1;
}
auto *addr6 = reinterpret_cast<const sockaddr_in6 *>(name);
port = ntohs(addr6->sin6_port);
ip.type = IPADDR_TYPE_ANY;
memcpy(&ip.u_addr.ip6.addr, &addr6->sin6_addr.un.u8_addr, 16);
LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ip6addr_ntoa(&ip.u_addr.ip6), port);
} else {
errno = EINVAL;
return -1;
}
#else
if (this->family_ != AF_INET) {
errno = EINVAL;
return -1;
}
auto *addr4 = reinterpret_cast<const sockaddr_in *>(name);
port = ntohs(addr4->sin_port);
ip.addr = addr4->sin_addr.s_addr;
LWIP_LOG("tcp_bind(%p ip=%u port=%u)", this->pcb_, ip.addr, port);
#endif
err_t err = tcp_bind(this->pcb_, &ip, port);
if (err == ERR_USE) {
LWIP_LOG(" -> err ERR_USE");
errno = EADDRINUSE;
return -1;
}
if (err == ERR_VAL) {
LWIP_LOG(" -> err ERR_VAL");
errno = EINVAL;
return -1;
}
if (err != ERR_OK) {
LWIP_LOG(" -> err %d", err);
errno = lwip_err_to_errno(err);
errno = EIO;
return -1;
}
return 0;
@@ -186,7 +178,7 @@ int LWIPRawCommon::close() {
this->pcb_ = nullptr;
if (err != ERR_OK) {
LWIP_LOG(" -> err %d", err);
errno = lwip_err_to_errno(err);
errno = err == ERR_MEM ? ENOMEM : EIO;
return -1;
}
return 0;
@@ -213,7 +205,7 @@ int LWIPRawCommon::shutdown(int how) {
err_t err = tcp_shutdown(this->pcb_, shut_rx, shut_tx);
if (err != ERR_OK) {
LWIP_LOG(" -> err %d", err);
errno = lwip_err_to_errno(err);
errno = err == ERR_MEM ? ENOMEM : EIO;
return -1;
}
return 0;
@@ -433,82 +425,7 @@ void LWIPRawImpl::s_err_fn(void *arg, err_t err) {
// ERR_ABRT: aborted through tcp_abort or TCP timer
auto *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err);
if (arg_this->connect_err_ == EINPROGRESS) {
// Refused (RST) or SYN retries exhausted; written before pcb_ so
// poll_connect() never sees a dead pcb without its reason
arg_this->connect_err_ = err == ERR_RST ? ECONNREFUSED : ETIMEDOUT;
}
arg_this->pcb_ = nullptr;
esphome::wake_loop_any_context();
}
err_t LWIPRawImpl::s_connected_fn(void *arg, struct tcp_pcb *pcb, err_t err) {
// LWIP CALLBACK, same constraints as s_err_fn; err is always ERR_OK
auto *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
arg_this->connect_err_ = EISCONN;
esphome::wake_loop_any_context();
return ERR_OK;
}
int LWIPRawImpl::connect(const struct sockaddr *addr, socklen_t addrlen) {
LWIP_LOCK();
if (this->pcb_ == nullptr) {
errno = EBADF;
return -1;
}
if (this->connect_err_ == EINPROGRESS || this->connect_err_ == EISCONN) {
errno = this->connect_err_ == EINPROGRESS ? EALREADY : EISCONN;
return -1;
}
ip_addr_t ip;
uint16_t port;
if (!this->sockaddr2ip_(addr, addrlen, &ip, &port)) {
return -1;
}
#if LWIP_IPV6
// tcp_connect needs a concrete type; a remembered IPv4 peer arrives v4-mapped
if (IP_IS_ANY_TYPE_VAL(ip)) {
if (ip6_addr_isipv4mappedipv6(ip_2_ip6(&ip))) {
unmap_ipv4_mapped_ipv6(ip_2_ip4(&ip), ip_2_ip6(&ip));
IP_SET_TYPE_VAL(ip, IPADDR_TYPE_V4);
} else {
IP_SET_TYPE_VAL(ip, IPADDR_TYPE_V6);
}
}
#endif
LWIP_LOG("tcp_connect(%p ip=%s port=%u)", this->pcb_, ipaddr_ntoa(&ip), port);
err_t err = tcp_connect(this->pcb_, &ip, port, LWIPRawImpl::s_connected_fn);
if (err != ERR_OK) {
LWIP_LOG(" -> err %d", err);
errno = lwip_err_to_errno(err);
return -1;
}
this->connect_err_ = EINPROGRESS;
errno = EINPROGRESS;
return -1;
}
ConnectPollResult LWIPRawImpl::poll_connect(int &err_out) const {
// pcb_ first; see the ordering note on the declaration
if (this->pcb_ == nullptr) {
// Only a recorded connect failure carries its own reason
const bool failed = this->connect_err_ == ECONNREFUSED || this->connect_err_ == ETIMEDOUT;
err_out = failed ? this->connect_err_ : ECONNRESET;
return ConnectPollResult::CONNECT_POLL_RESULT_ERROR;
}
switch (this->connect_err_) {
case EINPROGRESS:
yield_to_sys(); // so the SYN-ACK is processed between polls
return ConnectPollResult::CONNECT_POLL_RESULT_PENDING;
case EISCONN:
return ConnectPollResult::CONNECT_POLL_RESULT_CONNECTED;
case 0:
err_out = EINVAL; // no connect was started
return ConnectPollResult::CONNECT_POLL_RESULT_ERROR;
default:
err_out = this->connect_err_;
return ConnectPollResult::CONNECT_POLL_RESULT_ERROR;
}
}
err_t LWIPRawImpl::s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) {
@@ -623,11 +540,14 @@ ssize_t LWIPRawImpl::read_locked_(void *buf, size_t len) {
}
ssize_t LWIPRawImpl::read(void *buf, size_t len) {
// Let queued WiFi RX reach lwip first; otherwise inbound segments can
// sit unprocessed for seconds while the main loop polls
#ifdef USE_ESP8266
// Would block: yield to SYS so queued WiFi RX reaches lwip and this read
// may succeed. Without this, inbound segments can sit unprocessed for
// seconds while the main loop polls (CONT/SYS are cooperative on ESP8266).
if (this->waiting_for_data_()) {
yield_to_sys();
optimistic_yield(ESP8266_YIELD_INTERVAL_US);
}
#endif
// See waiting_for_data_() for safety of unlocked reads.
if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) {
this->wait_for_data_();
@@ -716,10 +636,12 @@ int LWIPRawImpl::internal_output_() {
return -1;
}
}
#ifdef USE_ESP8266
// Flushed: yield to SYS so the queued segments reach the WiFi driver
// instead of waiting seconds for an unrelated SYS slot. Callers only get
// here after a successful tcp_write, so idle paths never yield.
yield_to_sys();
optimistic_yield(ESP8266_YIELD_INTERVAL_US);
#endif
return 0;
}
@@ -50,8 +50,6 @@ class LWIPRawCommon {
protected:
int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen);
/// sockaddr of this socket's family to lwip address and port; false with errno on mismatch
bool sockaddr2ip_(const struct sockaddr *name, socklen_t addrlen, ip_addr_t *ip, uint16_t *port) const;
// Member ordering optimized to minimize padding on 32-bit systems
struct tcp_pcb *pcb_;
@@ -60,14 +58,7 @@ class LWIPRawCommon {
bool nodelay_ = false;
sa_family_t family_ = 0;
uint8_t recv_timeout_cs_ = 0; // SO_RCVTIMEO in centiseconds (0 = no timeout, max 2.55s)
// 0 before connect(), EINPROGRESS while pending, EISCONN once established,
// else the failure errno the callbacks recorded; fills the padding byte
uint8_t connect_err_ = 0;
static_assert(EINPROGRESS < 256 && EISCONN < 256 && ECONNREFUSED < 256 && ECONNRESET < 256 && ETIMEDOUT < 256,
"connect_err_ stores errno values in a byte");
};
// The connect state must stay in the padding so no socket pays RAM for it
static_assert(sizeof(LWIPRawCommon) == sizeof(struct tcp_pcb *) + 4, "LWIPRawCommon grew past one word of flags");
/// Connected socket implementation for LWIP raw TCP.
/// No virtual methods — callers always use the concrete type.
@@ -92,12 +83,6 @@ class LWIPRawImpl : public LWIPRawCommon {
errno = EOPNOTSUPP;
return -1;
}
/// Non-blocking: returns -1/EINPROGRESS once the SYN is queued, see poll_connect().
/// addr must match the socket family; an IPv4 peer on AF_INET6 arrives v4-mapped.
int connect(const struct sockaddr *addr, socklen_t addrlen);
// Unlocked like ready(): the callbacks write the error byte before pcb_,
// so a torn read only costs one extra poll
ConnectPollResult poll_connect(int &err_out) const;
ssize_t read(void *buf, size_t len);
ssize_t readv(const struct iovec *iov, int iovcnt);
ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
@@ -135,7 +120,6 @@ class LWIPRawImpl : public LWIPRawCommon {
static void s_err_fn(void *arg, err_t err);
static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);
static err_t s_connected_fn(void *arg, struct tcp_pcb *pcb, err_t err);
protected:
// True when the socket could receive data but none has arrived yet.
@@ -153,9 +137,6 @@ class LWIPRawImpl : public LWIPRawCommon {
size_t rx_buf_offset_ = 0;
bool rx_closed_ = false;
};
// rx_buf_, rx_buf_offset_, then rx_closed_ padded to a word
static_assert(sizeof(LWIPRawImpl) == sizeof(LWIPRawCommon) + sizeof(pbuf *) + sizeof(size_t) + 4,
"LWIPRawImpl layout changed");
/// Listening socket implementation for LWIP raw TCP.
/// Separate from LWIPRawImpl — no virtual dispatch needed.
@@ -49,15 +49,13 @@ int LwIPSocketImpl::close() {
int LwIPSocketImpl::setblocking(bool blocking) {
int fl = lwip_fcntl(this->fd_, F_GETFL, 0);
if (fl < 0) {
return fl;
}
if (blocking) {
fl &= ~O_NONBLOCK;
} else {
fl |= O_NONBLOCK;
}
return lwip_fcntl(this->fd_, F_SETFL, fl);
lwip_fcntl(this->fd_, F_SETFL, fl);
return 0;
}
size_t LwIPSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
+2 -54
View File
@@ -2,9 +2,6 @@
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
#include <cerrno>
#include <cstring>
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
#include <sys/select.h>
#endif
#include <string>
#include "esphome/core/log.h"
#include "esphome/core/application.h"
@@ -168,10 +165,7 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
#else
// Use LWIP-specific functions
ip6_addr_t ip6;
if (inet6_aton(ip_address, &ip6) == 0) {
errno = EINVAL;
return 0;
}
inet6_aton(ip_address, &ip6);
memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr));
#endif
return sizeof(sockaddr_in6);
@@ -191,58 +185,12 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
return 0;
}
#else
// Unlike inet_addr(), inet_aton() can signal failure while still
// accepting the broadcast address 255.255.255.255
if (inet_aton(ip_address, &server->sin_addr) == 0) {
errno = EINVAL;
return 0;
}
server->sin_addr.s_addr = inet_addr(ip_address);
#endif
server->sin_port = htons(port);
return sizeof(sockaddr_in);
}
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
ConnectPollResult poll_connect(Socket &sock, int &err_out) {
int fd = sock.get_fd();
if (fd < 0 || fd >= FD_SETSIZE) {
// FD_SET on either is undefined behavior
err_out = EBADF;
return ConnectPollResult::CONNECT_POLL_RESULT_ERROR;
}
// Connect completion is a write event; the main loop only selects on reads
fd_set writefds;
FD_ZERO(&writefds);
FD_SET(fd, &writefds);
struct timeval tv = {0, 0};
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
// LWIP_COMPAT_SOCKETS may be off (LibreTiny), so use the lwip symbol directly
int ret = lwip_select(fd + 1, nullptr, &writefds, nullptr, &tv);
#else
// Global-scope select: the entity namespace esphome::select shadows it here
int ret = ::select(fd + 1, nullptr, &writefds, nullptr, &tv);
#endif
if (ret < 0) {
err_out = errno;
return ConnectPollResult::CONNECT_POLL_RESULT_ERROR;
}
if (ret == 0) {
return ConnectPollResult::CONNECT_POLL_RESULT_PENDING;
}
int error = 0;
socklen_t len = sizeof(error);
if (sock.getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) != 0) {
err_out = errno;
return ConnectPollResult::CONNECT_POLL_RESULT_ERROR;
}
if (error != 0) {
err_out = error;
return ConnectPollResult::CONNECT_POLL_RESULT_ERROR;
}
return ConnectPollResult::CONNECT_POLL_RESULT_CONNECTED;
}
#endif
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) {
#if USE_NETWORK_IPV6
if (addrlen < sizeof(sockaddr_in6)) {
-8
View File
@@ -145,14 +145,6 @@ inline socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const st
/// Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port);
/// Poll a connect() that returned EINPROGRESS. On error, err_out is SO_ERROR (or
/// errno) on fd implementations and the failure the callbacks recorded on raw lwip.
#ifdef USE_SOCKET_IMPL_LWIP_TCP
inline ConnectPollResult poll_connect(Socket &sock, int &err_out) { return sock.poll_connect(err_out); }
#else
ConnectPollResult poll_connect(Socket &sock, int &err_out);
#endif
/// Format sockaddr into caller-provided buffer, returns length written (excluding null)
size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span<char, SOCKADDR_STR_LEN> buf);
+2 -11
View File
@@ -13,12 +13,7 @@ void UDPComponent::setup() {
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
for (const auto &address : this->addresses_) {
struct sockaddr saddr {};
if (socket::set_sockaddr(&saddr, sizeof(saddr), address, this->broadcast_port_) == 0) {
ESP_LOGW(TAG, "Invalid address %s", address);
// A dropped address silently receives nothing; surface the misconfiguration
this->status_set_warning(LOG_STR("invalid address"));
continue;
}
socket::set_sockaddr(&saddr, sizeof(saddr), address, this->broadcast_port_);
this->sockaddrs_.push_back(saddr);
}
// set up broadcast socket
@@ -99,11 +94,7 @@ void UDPComponent::setup() {
// 8266 and RP2040 `Duino
for (const auto &address : this->addresses_) {
auto ipaddr = IPAddress();
if (!ipaddr.fromString(address)) {
ESP_LOGW(TAG, "Invalid address %s", address);
this->status_set_warning(LOG_STR("invalid address"));
continue;
}
ipaddr.fromString(address);
this->ipaddrs_.push_back(ipaddr);
}
if (this->should_listen_)
+5 -7
View File
@@ -434,12 +434,11 @@ void USBUartTypeCdcAcm::on_connected() {
auto err_comm = usb_host_interface_claim(this->handle_, this->device_handle_,
channel->cdc_dev_.interrupt_interface_number, 0);
if (err_comm != ESP_OK) {
// Continue anyway: the interface number stays valid for CDC request addressing
ESP_LOGW(TAG, "Could not claim comm interface %d: %s", channel->cdc_dev_.interrupt_interface_number,
esp_err_to_name(err_comm));
channel->cdc_dev_.interrupt_interface_number = 0xFF; // Mark as unavailable, but continue anyway
} else {
ESP_LOGD(TAG, "Claimed comm interface %d", channel->cdc_dev_.interrupt_interface_number);
channel->cdc_dev_.interrupt_interface_claimed = true;
}
}
auto err =
@@ -466,15 +465,14 @@ void USBUartTypeCdcAcm::on_disconnected() {
usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress);
usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress);
}
// Only tear down the notify pipe when we claimed its interface ourselves;
// no transfer is ever submitted on it, so there is nothing else to cancel.
if (channel->cdc_dev_.notify_ep != nullptr && channel->cdc_dev_.interrupt_interface_claimed) {
if (channel->cdc_dev_.notify_ep != nullptr) {
usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
}
if (channel->cdc_dev_.interrupt_interface_claimed) {
if (channel->cdc_dev_.interrupt_interface_number != 0xFF &&
channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) {
usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.interrupt_interface_number);
channel->cdc_dev_.interrupt_interface_claimed = false;
channel->cdc_dev_.interrupt_interface_number = 0xFF;
}
usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number);
// Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts
-3
View File
@@ -34,10 +34,7 @@ struct CdcEps {
const usb_ep_desc_t *in_ep;
const usb_ep_desc_t *out_ep;
uint8_t bulk_interface_number;
// Also the wIndex target for CDC class requests (SET_LINE_CODING etc.), so it
// must remain valid even when the interface itself is not claimed.
uint8_t interrupt_interface_number;
bool interrupt_interface_claimed{false};
};
enum CH34xChipType : uint8_t {
@@ -34,10 +34,6 @@ void WakeOnLanButton::press_action() {
struct sockaddr_storage saddr {};
auto addr_len =
socket::set_sockaddr(reinterpret_cast<sockaddr *>(&saddr), sizeof(saddr), "255.255.255.255", this->port_);
if (addr_len == 0) {
ESP_LOGW(TAG, "Invalid broadcast address");
return;
}
uint8_t buffer[6 + sizeof this->macaddr_ * 16];
memcpy(buffer, PREFIX, sizeof(PREFIX));
for (size_t i = 0; i != 16; i++) {
@@ -1,4 +0,0 @@
substitutions:
network_enable_ipv6: "true"
<<: !include common.yaml
@@ -1,17 +0,0 @@
esphome:
name: socket-set-sockaddr
on_boot:
then:
- lambda: |-
// 0 for text that is not an address, the length otherwise, broadcast included
struct sockaddr_storage addr;
auto *sa = reinterpret_cast<struct sockaddr *>(&addr);
ESP_LOGI("test", "SET_SOCKADDR invalid=%u valid=%u broadcast=%u",
(unsigned) socket::set_sockaddr(sa, sizeof(addr), "not an address", 1234),
(unsigned) socket::set_sockaddr(sa, sizeof(addr), "192.0.2.1", 1234),
(unsigned) socket::set_sockaddr(sa, sizeof(addr), "255.255.255.255", 1234));
host:
api:
logger:
level: INFO
@@ -1,40 +0,0 @@
"""Integration test for the socket::set_sockaddr failure contract."""
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_socket_set_sockaddr(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""set_sockaddr reports an invalid address with 0 and accepts broadcast."""
loop = asyncio.get_running_loop()
result: asyncio.Future[tuple[int, int, int]] = loop.create_future()
def on_log_line(line: str) -> None:
match = re.search(
r"SET_SOCKADDR invalid=(\d+) valid=(\d+) broadcast=(\d+)", line
)
if match and not result.done():
result.set_result(tuple(int(g) for g in match.groups()))
async with (
run_compiled(yaml_config, line_callback=on_log_line),
api_client_connected() as client,
):
assert (await client.device_info()).name == "socket-set-sockaddr"
try:
invalid, valid, broadcast = await asyncio.wait_for(result, timeout=10.0)
except TimeoutError:
pytest.fail("SET_SOCKADDR marker never appeared")
assert invalid == 0
assert valid > 0
assert broadcast == valid
@@ -12,8 +12,10 @@ from esphome.components.esp8266 import build_surgery
from esphome.components.esp8266.boards import BOARDS, ESP8266_BOARD_BUILD
from esphome.components.esp8266.build_surgery import (
RATETABLE_RULE,
SODIUM_SHA256_RULE,
apply_testing_memory_patches,
relocate_ratetable,
relocate_sodium_sha256,
segment_length,
)
@@ -27,6 +29,17 @@ _COMMON_LD_SNIPPET = """\
_data_start = ABSOLUTE(.);
*(.data)
} >dram0_0_seg :dram0_0_phdr
.irom0.text : ALIGN(4)
{
_irom0_text_start = ABSOLUTE(.);
*(.rodata._ZTV*) /* C++ vtables */
} >irom0_0_seg :irom0_0_phdr
.rodata : ALIGN(4)
{
_rodata_start = ABSOLUTE(.);
*(.rodata)
*(.rodata.*)
} >dram0_0_seg :dram0_0_phdr
"""
# Shaped like the real SDK flash ld scripts: no iram1_0_seg (that lives in
@@ -61,6 +74,21 @@ def test_relocate_ratetable_inserts_after_data_start() -> None:
assert relocate_ratetable(patched) == patched
def test_relocate_sodium_sha256_inserts_in_irom0_text() -> None:
patched = relocate_sodium_sha256(_COMMON_LD_SNIPPET)
assert SODIUM_SHA256_RULE in patched
# Inside .irom0.text, ahead of the DRAM .rodata rules that would win otherwise
assert patched.index("_irom0_text_start") < patched.index(SODIUM_SHA256_RULE)
assert patched.index(SODIUM_SHA256_RULE) < patched.index("*(.rodata)")
# Idempotent on an already-patched script
assert relocate_sodium_sha256(patched) == patched
def test_relocate_sodium_sha256_requires_anchor() -> None:
with pytest.raises(RuntimeError, match="_irom0_text_start"):
relocate_sodium_sha256("SECTIONS { }")
def test_relocate_ratetable_requires_anchor() -> None:
with pytest.raises(RuntimeError, match="_data_start"):
relocate_ratetable("SECTIONS { }")