Fix emNET support and add tests

The emNET `wolfSSL_LastError` branches were incorrect. The second one
was never hit and would never compile. The first one inverts error codes
that should not be inverted.

This fixes that code and adds a test with a shim layer to test emNET
calls without using emNET.
This commit is contained in:
Andrew Hutchings
2026-04-23 17:50:04 +01:00
parent d00a137de0
commit bf19d548bb
7 changed files with 488 additions and 6 deletions
+56
View File
@@ -0,0 +1,56 @@
name: emNET non-blocking handshake test
# START OF COMMON SECTION
on:
push:
branches: [ 'master', 'main', 'release/**' ]
pull_request:
branches: [ '*' ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# END OF COMMON SECTION
# Build wolfSSL with -DWOLFSSL_EMNET using the clean-room shim in
# tests/emnet/ (IP/IP.h + emnet_shim.c), link a non-blocking TLS 1.3
# handshake test with -Wl,--wrap=recv,--wrap=send, and run it. The
# test exercises the WOLFSSL_EMNET path in wolfSSL_LastError() and
# asserts that would-block events surface as WANT_READ/WANT_WRITE and
# the handshake completes, guarding against regressions in the emNET
# error-translation logic.
jobs:
emnet_nonblock:
name: wolfSSL emNET non-blocking handshake
if: github.repository_owner == 'wolfssl'
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Checkout wolfSSL
uses: actions/checkout@v4
- name: Install build deps
uses: ./.github/actions/install-apt-deps
with:
packages: autoconf automake libtool build-essential
- name: Bootstrap
run: ./autogen.sh
- name: Configure wolfSSL (WOLFSSL_EMNET + emNET shim headers)
run: |
./configure \
--enable-static --disable-shared \
--enable-tls13 --disable-oldtls \
--enable-ecc --disable-examples \
CFLAGS="-DWOLFSSL_EMNET -I$(pwd)/tests/emnet"
- name: Build wolfSSL
run: make -j$(nproc)
- name: Build emNET non-blocking test
run: make -C tests/emnet
- name: Run emNET non-blocking test
run: make -C tests/emnet run
+5 -5
View File
@@ -151,8 +151,12 @@ static WC_INLINE int wolfSSL_LastError(int err, SOCKET_T sd)
return WSAGetLastError();
#elif defined(EBSNET)
return xn_getlasterror();
#elif defined(WOLFSSL_LINUXKM) || defined(WOLFSSL_EMNET)
#elif defined(WOLFSSL_LINUXKM)
return -err; /* Return provided error value with corrected sign. */
#elif defined(WOLFSSL_EMNET)
/* emNET BSD sockets return the IP_ERR_* value (negative) directly
* from send/recv on failure; no translation needed. */
return err;
#elif defined(FUSION_RTOS)
#include <fclerrno.h>
return FCL_GET_ERRNO;
@@ -170,10 +174,6 @@ static WC_INLINE int wolfSSL_LastError(int err, SOCKET_T sd)
}
return err;
}
#elif defined(WOLFSSL_EMNET)
/* Get the real socket error */
IP_SOCK_getsockopt(sd, SOL_SOCKET, SO_ERROR, &err, (int)sizeof(old));
return err;
#else
return errno;
#endif
+44
View File
@@ -0,0 +1,44 @@
/* IP.h -- clean-room shim for the subset of SEGGER emNET (embOS/IP) API
* that wolfSSL's WOLFSSL_EMNET port compiles against. Written from the
* public API surface documented in SEGGER UM07001; contains no SEGGER
* source.
*
* Scope: enough to build wolfSSL with -DWOLFSSL_EMNET on a POSIX host
* for CI test purposes. Only error constants and IP_SOCK_getsockopt are
* provided here; the runtime behaviour of send/recv under emNET is
* emulated by emnet_shim.c via linker --wrap.
*/
#ifndef WOLFSSL_EMNET_SHIM_IP_H
#define WOLFSSL_EMNET_SHIM_IP_H
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#ifdef __cplusplus
extern "C" {
#endif
/* emNET error codes (UM07001). Values match the public ABI. */
#define IP_ERR_CONN_ABORTED (-5)
#define IP_ERR_WOULD_BLOCK (-6)
#define IP_ERR_CONN_REFUSED (-7)
#define IP_ERR_CONN_RESET (-8)
#define IP_ERR_PIPE (-13)
#define IP_ERR_FAULT (-25)
/* BSD-style socket option retrieval. Signature matches the SEGGER API:
* length is passed by pointer of type int*, unlike POSIX socklen_t*. */
int IP_SOCK_getsockopt(int sd, int level, int optname,
void *optval, int *optlen);
#ifdef __cplusplus
}
#endif
#endif /* WOLFSSL_EMNET_SHIM_IP_H */
+52
View File
@@ -0,0 +1,52 @@
# Build the emNET non-blocking handshake test.
# Requires wolfSSL already configured + built at the repo root with
# WOLFSSL_EMNET and the emnet IP include path, e.g.:
#
# cd $(repo_root)
# ./autogen.sh
# ./configure --enable-static --disable-shared --enable-tls13 \
# --disable-oldtls CFLAGS="-DWOLFSSL_EMNET \
# -I$$(pwd)/tests/emnet"
# make
# make -C tests/emnet run
CURDIR := $(shell pwd)
WOLFSSL_ROOT := $(abspath $(CURDIR)/../..)
WOLFSSL_LIB := $(WOLFSSL_ROOT)/src/.libs/libwolfssl.a
CC ?= cc
CFLAGS_TEST := -Wall -Wextra -O2 -g \
-DWOLFSSL_EMNET \
-I$(CURDIR) \
-I$(WOLFSSL_ROOT)
LDFLAGS_TEST := -Wl,--wrap=recv,--wrap=send
LIBS_TEST := $(WOLFSSL_LIB) -lpthread -lm
TEST_BIN := emnet_nonblock_test
.PHONY: all run clean check-lib
all: $(TEST_BIN)
check-lib:
@if [ ! -f "$(WOLFSSL_LIB)" ]; then \
echo "error: $(WOLFSSL_LIB) not found."; \
echo "Build wolfSSL first (see header of this Makefile)."; \
exit 1; \
fi
$(TEST_BIN): emnet_nonblock_test.o emnet_shim.o check-lib
$(CC) $(LDFLAGS_TEST) -o $@ emnet_nonblock_test.o emnet_shim.o $(LIBS_TEST)
emnet_nonblock_test.o: emnet_nonblock_test.c
$(CC) $(CFLAGS_TEST) -c $< -o $@
emnet_shim.o: emnet_shim.c IP/IP.h
$(CC) $(CFLAGS_TEST) -c $< -o $@
# Run from the repo root so the relative cert paths resolve.
run: $(TEST_BIN)
cd $(WOLFSSL_ROOT) && ./tests/emnet/$(TEST_BIN)
clean:
rm -f *.o $(TEST_BIN)
+223
View File
@@ -0,0 +1,223 @@
/* emnet_nonblock_test.c -- non-blocking TLS 1.3 handshake over a
* socketpair, with wolfSSL built for WOLFSSL_EMNET and the recv/send
* error surface translated by emnet_shim.c.
*
* Asserts the steady-state contract of wolfSSL's WOLFSSL_EMNET path:
* when the underlying socket would block, wolfSSL_get_error returns
* WOLFSSL_ERROR_WANT_READ / WOLFSSL_ERROR_WANT_WRITE and the handshake
* completes without spurious fatal errors. Guards against regressions
* of a prior bug where the WOLFSSL_EMNET branch in wolfSSL_LastError()
* was shadowed by a combined WOLFSSL_LINUXKM||WOLFSSL_EMNET arm that
* inverted the sign of IP_ERR_WOULD_BLOCK, causing TranslateIoReturnCode
* to surface WOLFSSL_CBIO_ERR_GENERAL on would-block.
*/
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <wolfssl/options.h>
#include <wolfssl/ssl.h>
#define MAX_ITERS 200 /* handshake loop safety cap */
#define POLL_MS 500
#define CERT_PATH "certs/server-ecc.pem"
#define KEY_PATH "certs/ecc-key.pem"
#define CA_PATH "certs/ca-ecc-cert.pem"
struct side {
int fd;
const char *name;
WOLFSSL *ssl;
int (*fn)(WOLFSSL *);
int saw_would_block;
int completed;
int failed;
int last_err;
};
static void set_nonblock(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) {
perror("fcntl F_GETFL");
exit(2);
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
perror("fcntl F_SETFL O_NONBLOCK");
exit(2);
}
}
/* Wait for the socket to become readable or writable. Returns 0 on
* success (ready or timeout, caller retries), -1 on hard failure. */
static int wait_io(struct side *s, short events, int iter)
{
struct pollfd pfd = { .fd = s->fd, .events = events };
int r = poll(&pfd, 1, POLL_MS);
if (r >= 0)
return 0;
if (errno == EINTR)
return 0;
fprintf(stderr, "FAIL: %s: poll failed at iter=%d: %s\n",
s->name, iter, strerror(errno));
return -1;
}
static void *run_side(void *arg)
{
struct side *s = (struct side *)arg;
int iter;
for (iter = 0; iter < MAX_ITERS; iter++) {
int ret = s->fn(s->ssl);
if (ret == WOLFSSL_SUCCESS) {
s->completed = 1;
return NULL;
}
int err = wolfSSL_get_error(s->ssl, ret);
s->last_err = err;
if (err == WOLFSSL_ERROR_WANT_READ) {
s->saw_would_block = 1;
if (wait_io(s, POLLIN, iter) < 0) {
s->failed = 1;
return NULL;
}
continue;
}
if (err == WOLFSSL_ERROR_WANT_WRITE) {
s->saw_would_block = 1;
if (wait_io(s, POLLOUT, iter) < 0) {
s->failed = 1;
return NULL;
}
continue;
}
/* Anything else on a non-blocking handshake is a failure. */
fprintf(stderr,
"FAIL: %s: wolfSSL_get_error=%d after iter=%d. "
"Expected WANT_READ/WANT_WRITE on a non-blocking socketpair. "
"Indicates a regression in the WOLFSSL_EMNET error-translation "
"path in src/wolfio.c:wolfSSL_LastError.\n",
s->name, err, iter);
s->failed = 1;
return NULL;
}
fprintf(stderr, "FAIL: %s: handshake did not complete within %d "
"iterations (last err=%d)\n",
s->name, MAX_ITERS, s->last_err);
s->failed = 1;
return NULL;
}
int main(void)
{
int sv[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) {
perror("socketpair");
return 2;
}
set_nonblock(sv[0]);
set_nonblock(sv[1]);
wolfSSL_Init();
WOLFSSL_CTX *sctx = wolfSSL_CTX_new(wolfTLSv1_3_server_method());
WOLFSSL_CTX *cctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method());
if (!sctx || !cctx) {
fprintf(stderr, "wolfSSL_CTX_new failed\n");
return 2;
}
if (wolfSSL_CTX_use_certificate_file(sctx, CERT_PATH,
WOLFSSL_FILETYPE_PEM) != WOLFSSL_SUCCESS) {
fprintf(stderr, "failed to load server cert %s\n", CERT_PATH);
return 2;
}
if (wolfSSL_CTX_use_PrivateKey_file(sctx, KEY_PATH,
WOLFSSL_FILETYPE_PEM) != WOLFSSL_SUCCESS) {
fprintf(stderr, "failed to load server key %s\n", KEY_PATH);
return 2;
}
if (wolfSSL_CTX_load_verify_locations(cctx, CA_PATH, NULL)
!= WOLFSSL_SUCCESS) {
fprintf(stderr, "failed to load CA %s\n", CA_PATH);
return 2;
}
WOLFSSL *server_ssl = wolfSSL_new(sctx);
WOLFSSL *client_ssl = wolfSSL_new(cctx);
if (!server_ssl || !client_ssl) {
fprintf(stderr, "wolfSSL_new failed\n");
return 2;
}
wolfSSL_set_fd(server_ssl, sv[0]);
wolfSSL_set_fd(client_ssl, sv[1]);
struct side server = { .fd = sv[0], .name = "server",
.ssl = server_ssl, .fn = wolfSSL_accept };
struct side client = { .fd = sv[1], .name = "client",
.ssl = client_ssl, .fn = wolfSSL_connect };
pthread_t st, ct;
int prc;
prc = pthread_create(&st, NULL, run_side, &server);
if (prc != 0) {
fprintf(stderr, "FAIL: pthread_create(server): %s\n", strerror(prc));
return 2;
}
prc = pthread_create(&ct, NULL, run_side, &client);
if (prc != 0) {
fprintf(stderr, "FAIL: pthread_create(client): %s\n", strerror(prc));
pthread_join(st, NULL);
return 2;
}
prc = pthread_join(st, NULL);
if (prc != 0) {
fprintf(stderr, "FAIL: pthread_join(server): %s\n", strerror(prc));
return 2;
}
prc = pthread_join(ct, NULL);
if (prc != 0) {
fprintf(stderr, "FAIL: pthread_join(client): %s\n", strerror(prc));
return 2;
}
int rc = 0;
if (server.failed || client.failed) {
rc = 1;
} else if (!server.completed || !client.completed) {
fprintf(stderr, "FAIL: handshake incomplete (server=%d client=%d)\n",
server.completed, client.completed);
rc = 1;
} else if (!server.saw_would_block && !client.saw_would_block) {
fprintf(stderr, "FAIL: handshake completed but never hit a "
"non-blocking path. Test scaffolding not exercising "
"the WOLFSSL_EMNET error-translation code.\n");
rc = 1;
} else {
printf("OK: handshake completed, would-block paths exercised\n");
}
wolfSSL_free(server_ssl);
wolfSSL_free(client_ssl);
wolfSSL_CTX_free(sctx);
wolfSSL_CTX_free(cctx);
wolfSSL_Cleanup();
close(sv[0]);
close(sv[1]);
return rc;
}
+103
View File
@@ -0,0 +1,103 @@
/* emnet_shim.c -- POSIX-backed shim for the emNET (embOS/IP) socket ABI
* used by wolfSSL when WOLFSSL_EMNET is defined.
*
* The goal is to reproduce the error-reporting contract of emNET on top
* of stock Linux BSD sockets: when the underlying socket signals
* would-block, connection reset, etc., the shim surfaces the
* corresponding IP_ERR_* negative constant (emNET convention) instead
* of -1/errno (POSIX convention). This is exactly what wolfSSL's
* WOLFSSL_EMNET branch in wolfio.h/wolfio.c was written to consume, so
* CI can drive the non-blocking handshake paths without the real
* SEGGER stack.
*
* Linker wrapping:
* -Wl,--wrap=recv,--wrap=send
* hooks wolfSSL's RECV_FUNCTION/SEND_FUNCTION (which are the
* unqualified POSIX send/recv on the WOLFSSL_EMNET build) without
* patching any wolfSSL source.
*/
#include "IP/IP.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <stddef.h>
#include <string.h>
/* Forward declarations for the linker's --wrap mechanism. */
ssize_t __real_recv(int sd, void *buf, size_t len, int flags);
ssize_t __real_send(int sd, const void *buf, size_t len, int flags);
/* Translate a POSIX errno value into the emNET IP_ERR_* space. */
static int emnet_errno_to_ip_err(int err)
{
/* Linux, where this shim runs in CI, defines EWOULDBLOCK == EAGAIN,
* so EAGAIN covers both. */
switch (err) {
case EAGAIN:
return IP_ERR_WOULD_BLOCK;
case ECONNRESET:
return IP_ERR_CONN_RESET;
case ECONNREFUSED:
return IP_ERR_CONN_REFUSED;
case ECONNABORTED:
return IP_ERR_CONN_ABORTED;
case EPIPE:
return IP_ERR_PIPE;
default:
return IP_ERR_FAULT;
}
}
/* recv wrapper: preserve success/close semantics; on error return the
* emNET-style negative error code in place of -1/errno. wolfSSL's
* TranslateIoReturnCode uses err < 0 to branch into error handling and
* then compares against SOCKET_EWOULDBLOCK == IP_ERR_WOULD_BLOCK. */
ssize_t __wrap_recv(int sd, void *buf, size_t len, int flags)
{
ssize_t ret = __real_recv(sd, buf, len, flags);
if (ret < 0) {
return (ssize_t)emnet_errno_to_ip_err(errno);
}
return ret;
}
ssize_t __wrap_send(int sd, const void *buf, size_t len, int flags)
{
ssize_t ret = __real_send(sd, buf, len, flags);
if (ret < 0) {
return (ssize_t)emnet_errno_to_ip_err(errno);
}
return ret;
}
/* IP_SOCK_getsockopt: kept to satisfy the emNET ABI surface expected
* by WOLFSSL_EMNET-linked code. Delegates to POSIX getsockopt and, for
* SO_ERROR, maps the returned POSIX errno value into emNET's IP_ERR_*
* space so callers see emNET-style error reporting. */
int IP_SOCK_getsockopt(int sd, int level, int optname,
void *optval, int *optlen)
{
socklen_t posix_len;
int rc;
if (optlen == NULL) {
errno = EINVAL;
return -1;
}
posix_len = (socklen_t)*optlen;
rc = getsockopt(sd, level, optname, optval, &posix_len);
*optlen = (int)posix_len;
if (rc == 0 && level == SOL_SOCKET && optname == SO_ERROR
&& optval != NULL && posix_len >= (socklen_t)sizeof(int)) {
int so_err;
memcpy(&so_err, optval, sizeof(so_err));
if (so_err != 0) {
so_err = emnet_errno_to_ip_err(so_err);
memcpy(optval, &so_err, sizeof(so_err));
}
}
return rc;
}
+5 -1
View File
@@ -81,5 +81,9 @@ EXTRA_DIST += tests/unit.h \
tests/NCONF_test.cnf \
tests/test-tls-downgrade.conf \
tests/TXT_DB.txt \
tests/utils.h
tests/utils.h \
tests/emnet/IP/IP.h \
tests/emnet/emnet_shim.c \
tests/emnet/emnet_nonblock_test.c \
tests/emnet/Makefile
DISTCLEANFILES+= tests/.libs/unit.test