From 1509ca289545afa6779772d551c0594d80c6aa9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Rohl=C3=ADnek?= Date: Thu, 8 Jan 2026 15:09:04 +0100 Subject: [PATCH 1/6] fix(storage/esp_libc): Fix incorrect fd checks --- .../esp_libc/src/picolibc/picolibc_init.c | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/components/esp_libc/src/picolibc/picolibc_init.c b/components/esp_libc/src/picolibc/picolibc_init.c index 0756627ef42..2e6731b5bf3 100644 --- a/components/esp_libc/src/picolibc/picolibc_init.c +++ b/components/esp_libc/src/picolibc/picolibc_init.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -138,32 +138,38 @@ void esp_reent_cleanup(void) * Initialize stdin, stdout, and stderr using static memory allocation. * Creating them with fopen() would call malloc() internally. */ -static char write_buf[BUFSIZ]; -static char read_buf[BUFSIZ]; +static char in_buf[BUFSIZ]; +static char out_buf[BUFSIZ]; +static char err_buf[BUFSIZ]; -static struct __file_bufio __stdin = FDEV_SETUP_BUFIO(0, read_buf, BUFSIZ, read, write, lseek, close, __SRD, 0); -static struct __file_bufio __stdout = FDEV_SETUP_BUFIO(1, write_buf, BUFSIZ, read, write, lseek, close, __SWR, __BLBF); +static struct __file_bufio __stdin = FDEV_SETUP_BUFIO(0, in_buf, BUFSIZ, read, write, lseek, close, __SRD, 0); +static struct __file_bufio __stdout = FDEV_SETUP_BUFIO(1, out_buf, BUFSIZ, read, write, lseek, close, __SWR, __BLBF); +static struct __file_bufio __stderr = FDEV_SETUP_BUFIO(2, err_buf, BUFSIZ, read, write, lseek, close, __SWR, __BLBF); FILE *stdin = &__stdin.xfile.cfile.file; FILE *stdout = &__stdout.xfile.cfile.file; -FILE *stderr = &__stdout.xfile.cfile.file; +FILE *stderr = &__stderr.xfile.cfile.file; #if CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY __thread FILE* tls_stdin = &__stdin.xfile.cfile.file; __thread FILE* tls_stdout = &__stdout.xfile.cfile.file; -__thread FILE* tls_stderr = &__stdout.xfile.cfile.file; +__thread FILE* tls_stderr = &__stderr.xfile.cfile.file; #endif #if CONFIG_VFS_SUPPORT_IO void esp_libc_init_global_stdio(const char *stdio_dev) { int stdin_fd = open(stdio_dev, O_RDONLY); - assert(stdin_fd > 0); + assert(stdin_fd >= 0); // Should be 0 __stdin.ptr = (void *)(intptr_t)(stdin_fd); int stdout_fd = open(stdio_dev, O_WRONLY); - assert(stdout_fd > 0); + assert(stdout_fd >= 0); // Should be 1 __stdout.ptr = (void *)(intptr_t)(stdout_fd); + + int stderr_fd = open(stdio_dev, O_WRONLY); + assert(stderr_fd >= 0); // Should be 2 + __stderr.ptr = (void *)(intptr_t)(stderr_fd); } #else /* CONFIG_VFS_SUPPORT_IO */ void esp_libc_init_global_stdio(void) From ba4686ce90850a0bc8f1b36f57338e96e71925de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Rohl=C3=ADnek?= Date: Thu, 18 Dec 2025 00:13:42 +0100 Subject: [PATCH 2/6] feat(storage/stdio): Refactor stdio_vfs multiplexer --- components/esp_stdio/CMakeLists.txt | 6 +- components/esp_stdio/Kconfig | 21 ++ components/esp_stdio/stdio_vfs.c | 421 ++++++++++++++++++++++------ 3 files changed, 366 insertions(+), 82 deletions(-) diff --git a/components/esp_stdio/CMakeLists.txt b/components/esp_stdio/CMakeLists.txt index b929cc4e4a7..c1ec2a0c87b 100644 --- a/components/esp_stdio/CMakeLists.txt +++ b/components/esp_stdio/CMakeLists.txt @@ -7,7 +7,6 @@ if(non_os_build) idf_component_register() return() endif() - set(srcs) set(includes) @@ -18,10 +17,13 @@ if(${target} STREQUAL "linux") "linux/include") else() list(APPEND srcs "stdio_port.c" - "stdio_vfs.c" "stdio_simple.c" "stdio_syscalls_simple.c") list(APPEND includes "include") + + if(CONFIG_VFS_SUPPORT_IO) + list(APPEND srcs "stdio_vfs.c") + endif() endif() idf_component_register(SRCS ${srcs} diff --git a/components/esp_stdio/Kconfig b/components/esp_stdio/Kconfig index a103ba0c526..996ba13ff34 100644 --- a/components/esp_stdio/Kconfig +++ b/components/esp_stdio/Kconfig @@ -185,4 +185,25 @@ menu "ESP-STDIO" If enabled, esp_rom_printf and ESP_EARLY_LOG output will also be sent over USB CDC. Disabling this option saves about 1kB or RAM. + config ESP_STDIO_MAX_FDS + int "Max logical stdio file descriptors" + depends on VFS_SUPPORT_IO + range 0 64 + default 3 + help + Maximum number of logical /dev/console descriptors tracked by esp_stdio VFS. + - 0 enables basic mode with a shared internal descriptor. + - 1..2 is not allowed + - Values >3 enable descriptor-table mode. + + Enabling basic mode decreases RAM usage, but disables checks for per-fd behaviour, such as read-only. + + config ESP_STDIO_MAX_VFS_ENTRIES + int "Maximum number of registerable VFS sinks" + depends on VFS_SUPPORT_IO + range 1 10 + default 2 + help + Maximum number of filesystems that can be registered as sinks for console output. + endmenu diff --git a/components/esp_stdio/stdio_vfs.c b/components/esp_stdio/stdio_vfs.c index 28fd6487582..06d96e1dbee 100644 --- a/components/esp_stdio/stdio_vfs.c +++ b/components/esp_stdio/stdio_vfs.c @@ -5,10 +5,15 @@ */ #include "sdkconfig.h" +#include +#include #include +#include #include "esp_err.h" +#include "esp_log.h" #include "esp_rom_sys.h" #include "esp_stdio.h" +#include "esp_vfs.h" #include #if CONFIG_VFS_SUPPORT_IO @@ -42,112 +47,277 @@ * while the secondary is only used for output. */ +#if CONFIG_VFS_SUPPORT_IO + +_Static_assert(CONFIG_ESP_STDIO_MAX_FDS == 0 || CONFIG_ESP_STDIO_MAX_FDS >= 3, "Invalid value for CONFIG_ESP_STDIO_MAX_FDS"); + +#define ESP_STDIO_IS_BASIC (CONFIG_ESP_STDIO_MAX_FDS <= 0) + typedef struct { - int fd_primary; - int fd_secondary; -} vfs_console_context_t; + const esp_vfs_fs_ops_t *ops; + int vfs_flags; + void *vfs_ctx; + const char *path; + int fd; +} mux_entry_t; -// Secondary register part. -#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG -const static esp_vfs_fs_ops_t *secondary_vfs = NULL; -#endif // Secondary part +typedef struct { + bool in_use; + int flags; +} fd_entry_t; -const static esp_vfs_fs_ops_t *primary_vfs = NULL; +typedef struct { + mux_entry_t entries[CONFIG_ESP_STDIO_MAX_VFS_ENTRIES]; + size_t used; -static vfs_console_context_t vfs_console = {0}; +#if !ESP_STDIO_IS_BASIC + fd_entry_t fds[CONFIG_ESP_STDIO_MAX_FDS]; +#endif -static size_t s_open_count = 0; + size_t fd_count; +} context_t; + +static context_t s_ctx = {0}; + +static const char *TAG = "esp_stdio"; + +static inline bool is_fd_valid(int fd) +{ +#if ESP_STDIO_IS_BASIC + return (fd == 0) && (s_ctx.fd_count > 0); +#else + return (fd >= 0) && (fd < CONFIG_ESP_STDIO_MAX_FDS) && s_ctx.fds[fd].in_use; +#endif +} + +static inline mux_entry_t *get_primary_entry(void) +{ + assert(s_ctx.used > 0); + return &s_ctx.entries[0]; +} + +#ifdef CONFIG_VFS_SUPPORT_TERMIOS +static const mux_entry_t *get_primary_termios_entry(int fd, const esp_vfs_termios_ops_t **out_ops) +{ + if (!is_fd_valid(fd)) { + errno = EBADF; + return NULL; + } + + const mux_entry_t *entry = get_primary_entry(); + assert(entry); + + const esp_vfs_termios_ops_t *termios = entry->ops->termios; + if (!termios) { + errno = ENOSYS; + return NULL; + } + + if (out_ops) { + *out_ops = termios; + } + + return entry; +} +#endif // CONFIG_VFS_SUPPORT_TERMIOS + +static esp_err_t esp_stdio_register_sink(const esp_vfs_fs_ops_t *ops, int vfs_flags, void *vfs_ctx, const char *path, int fd) +{ + if (!ops || !path) { + return ESP_ERR_INVALID_ARG; + } + + if (s_ctx.used >= CONFIG_ESP_STDIO_MAX_VFS_ENTRIES) { + ESP_EARLY_LOGE(TAG, "Too many stdio sinks registered"); + return ESP_ERR_NO_MEM; + } + + int mask = ESP_VFS_FLAG_CONTEXT_PTR | ESP_VFS_FLAG_STATIC; + + if ((vfs_flags & mask) != mask) { + ESP_EARLY_LOGE(TAG, "The sink FS needs to use both context pointer api and be static allocated"); + return ESP_ERR_INVALID_ARG; + } + + if (!ops->write_p) { + ESP_EARLY_LOGE(TAG, "Stdio sink missing mandatory write handler"); + return ESP_ERR_INVALID_ARG; + } + + bool is_primary = (s_ctx.used == 0); + if (is_primary && (!ops->read_p || !ops->fstat_p || !ops->fcntl_p || !ops->fsync_p)) { + ESP_EARLY_LOGE(TAG, "Primary stdio sink missing mandatory read/fstat/fcntl/fsync handlers"); + return ESP_ERR_INVALID_ARG; + } + + size_t idx = s_ctx.used++; + + s_ctx.entries[idx] = (mux_entry_t) { + .ops = ops, + .vfs_flags = vfs_flags, + .vfs_ctx = vfs_ctx, + .path = path, + .fd = fd, + }; + + return ESP_OK; +} int console_open(__attribute__((unused)) void *ctx, const char * path, int flags, int mode) { - if (s_open_count > 0) { - // Underlying fd is already open, so just increment the open count - // and return the same fd - - s_open_count++; - return 0; +#if ESP_STDIO_IS_BASIC + (void) path; + s_ctx.fd_count++; + return 0; +#else + if (!path || strcmp(path, "/") != 0) { + errno = ENOENT; + return -1; } -// Primary port open -#if CONFIG_ESP_CONSOLE_UART - vfs_console.fd_primary = open("/dev/uart/"STRINGIFY(CONFIG_ESP_CONSOLE_UART_NUM), flags, mode); -#elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG - vfs_console.fd_primary = open("/dev/usbserjtag", flags, mode); -#elif CONFIG_ESP_CONSOLE_USB_CDC - vfs_console.fd_primary = open("/dev/cdcacm", flags, mode); + if (s_ctx.fd_count >= CONFIG_ESP_STDIO_MAX_FDS) { + errno = ENFILE; + return -1; + } + + int fd = -1; + for (size_t i = 0; i < CONFIG_ESP_STDIO_MAX_FDS; ++i) { + if (!s_ctx.fds[i].in_use) { + fd = (int)i; + break; + } + } + + if (fd < 0) { + errno = ENFILE; + return -1; + } + + s_ctx.fd_count++; + + s_ctx.fds[fd] = (fd_entry_t) { + .in_use = true, + .flags = flags, + }; + + return fd; +#endif +} + +int console_close(__attribute__((unused)) void *ctx, int fd) +{ + if (!is_fd_valid(fd)) { + errno = EBADF; + return -1; + } + +#if ESP_STDIO_IS_BASIC + s_ctx.fd_count--; #else - vfs_console.fd_primary = open("/dev/null", flags, mode); + s_ctx.fds[fd].in_use = false; + s_ctx.fd_count--; #endif -// Secondary port open -#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG - vfs_console.fd_secondary = open("/dev/secondary", flags, mode); -#endif - - s_open_count++; return 0; } ssize_t console_write(__attribute__((unused)) void *ctx, int fd, const void *data, size_t size) { - write(vfs_console.fd_primary, data, size); -#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG - write(vfs_console.fd_secondary, data, size); -#endif + if (!is_fd_valid(fd)) { + errno = EBADF; + return -1; + } + for (size_t i = 0; i < s_ctx.used; i++) { + const mux_entry_t *entry = s_ctx.entries + i; + if (entry->ops->write_p && entry->fd >= 0) { + entry->ops->write_p(entry->vfs_ctx, entry->fd, data, size); + } + } + return size; } int console_fstat(__attribute__((unused)) void *ctx, int fd, struct stat * st) { - return fstat(vfs_console.fd_primary, st); -} - -int console_close(__attribute__((unused)) void *ctx, int fd) -{ - if (s_open_count == 0) { + if (!is_fd_valid(fd)) { errno = EBADF; return -1; } - s_open_count--; - - // We don't actually close the underlying fd until the open count reaches 0 - if (s_open_count > 0) { - return 0; + const mux_entry_t *entry = get_primary_entry(); + if (!entry->ops->fstat_p) { + errno = ENOSYS; + return -1; } - - // All function calls are to primary, except from write and close, which will be forwarded to both primary and secondary. - close(vfs_console.fd_primary); -#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG - close(vfs_console.fd_secondary); -#endif - return 0; + return entry->ops->fstat_p(entry->vfs_ctx, entry->fd, st); } ssize_t console_read(__attribute__((unused)) void *ctx, int fd, void * dst, size_t size) { - return read(vfs_console.fd_primary, dst, size); + if (!is_fd_valid(fd)) { + errno = EBADF; + return -1; + } + + const mux_entry_t *entry = get_primary_entry(); + if (!entry->ops->read_p) { + errno = ENOSYS; + return -1; + } + return entry->ops->read_p(entry->vfs_ctx, entry->fd, dst, size); } int console_fcntl(__attribute__((unused)) void *ctx, int fd, int cmd, int arg) { - return fcntl(vfs_console.fd_primary, cmd, arg); + if (!is_fd_valid(fd)) { + errno = EBADF; + return -1; + } + + const mux_entry_t *entry = get_primary_entry(); + if (!entry->ops->fcntl_p) { + errno = ENOSYS; + return -1; + } + return entry->ops->fcntl_p(entry->vfs_ctx, entry->fd, cmd, arg); } int console_fsync(__attribute__((unused)) void *ctx, int fd) { - const int ret_val = fsync(vfs_console.fd_primary); -#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG - (void)fsync(vfs_console.fd_secondary); -#endif + if (!is_fd_valid(fd)) { + errno = EBADF; + return -1; + } + + const mux_entry_t *primary = get_primary_entry(); + if (!primary->ops->fsync_p) { + errno = ENOSYS; + return -1; + } + + int ret_val = primary->ops->fsync_p(primary->vfs_ctx, primary->fd); + + for (size_t i = 1; i < s_ctx.used; i++) { + const mux_entry_t *entry = s_ctx.entries + i; + if (entry->ops->fsync_p && entry->fd >= 0) { + entry->ops->fsync_p(entry->vfs_ctx, entry->fd); + } + } + return ret_val; } #ifdef CONFIG_VFS_SUPPORT_DIR int console_access(__attribute__((unused)) void *ctx, const char *path, int amode) { - // currently only UART support DIR. - return access("/dev/uart/"STRINGIFY(CONFIG_ESP_CONSOLE_UART_NUM), amode); + const mux_entry_t *entry = get_primary_entry(); + assert(entry); + if (!entry->ops->dir || !entry->ops->dir->access_p) { + errno = ENOSYS; + return -1; + } + (void) path; + return entry->ops->dir->access_p(entry->vfs_ctx, entry->path, amode); } #endif // CONFIG_VFS_SUPPORT_DIR @@ -156,8 +326,10 @@ static esp_err_t console_start_select(int nfds, fd_set *readfds, fd_set *writefd esp_vfs_select_sem_t select_sem, void **end_select_args) { // start_select is not guaranteed be implemented even though CONFIG_VFS_SUPPORT_SELECT is enabled in sdkconfig - if (primary_vfs->select->start_select) { - return primary_vfs->select->start_select(nfds, readfds, writefds, exceptfds, select_sem, end_select_args); + const mux_entry_t *entry = get_primary_entry(); + assert(entry); + if (entry->fd >= 0 && entry->ops->select && entry->ops->select->start_select) { + return entry->ops->select->start_select(nfds, readfds, writefds, exceptfds, select_sem, end_select_args); } return ESP_ERR_NOT_SUPPORTED; @@ -166,8 +338,10 @@ static esp_err_t console_start_select(int nfds, fd_set *readfds, fd_set *writefd esp_err_t console_end_select(void *end_select_args) { // end_select is not guaranteed be implemented even though CONFIG_VFS_SUPPORT_SELECT is enabled in sdkconfig - if (primary_vfs->select->end_select) { - return primary_vfs->select->end_select(end_select_args); + const mux_entry_t *entry = get_primary_entry(); + assert(entry); + if (entry->fd >= 0 && entry->ops->select && entry->ops->select->end_select) { + return entry->ops->select->end_select(end_select_args); } return ESP_ERR_NOT_SUPPORTED; @@ -179,22 +353,58 @@ esp_err_t console_end_select(void *end_select_args) int console_tcsetattr(__attribute__((unused)) void *ctx, int fd, int optional_actions, const struct termios *p) { - return tcsetattr(vfs_console.fd_primary, optional_actions, p); + const esp_vfs_termios_ops_t *termios = NULL; + const mux_entry_t *entry = get_primary_termios_entry(fd, &termios); + if (!entry) { + return -1; + } + if (!termios->tcsetattr_p) { + errno = ENOSYS; + return -1; + } + return entry->ops->termios->tcsetattr_p(entry->vfs_ctx, entry->fd, optional_actions, p); } int console_tcgetattr(__attribute__((unused)) void *ctx, int fd, struct termios *p) { - return tcgetattr(vfs_console.fd_primary, p); + const esp_vfs_termios_ops_t *termios = NULL; + const mux_entry_t *entry = get_primary_termios_entry(fd, &termios); + if (!entry) { + return -1; + } + if (!termios->tcgetattr_p) { + errno = ENOSYS; + return -1; + } + return entry->ops->termios->tcgetattr_p(entry->vfs_ctx, entry->fd, p); } int console_tcdrain(__attribute__((unused)) void *ctx, int fd) { - return tcdrain(vfs_console.fd_primary); + const esp_vfs_termios_ops_t *termios = NULL; + const mux_entry_t *entry = get_primary_termios_entry(fd, &termios); + if (!entry) { + return -1; + } + if (!termios->tcdrain_p) { + errno = ENOSYS; + return -1; + } + return entry->ops->termios->tcdrain_p(entry->vfs_ctx, entry->fd); } int console_tcflush(__attribute__((unused)) void *ctx, int fd, int select) { - return tcflush(vfs_console.fd_primary, select); + const esp_vfs_termios_ops_t *termios = NULL; + const mux_entry_t *entry = get_primary_termios_entry(fd, &termios); + if (!entry) { + return -1; + } + if (!termios->tcflush_p) { + errno = ENOSYS; + return -1; + } + return entry->ops->termios->tcflush_p(entry->vfs_ctx, entry->fd, select); } #endif // CONFIG_VFS_SUPPORT_TERMIOS @@ -242,31 +452,82 @@ static const esp_vfs_fs_ops_t s_vfs_console = { #endif // CONFIG_VFS_SUPPORT_TERMIOS }; -static esp_err_t esp_vfs_dev_console_register(void) -{ - return esp_vfs_register_fs(ESP_VFS_DEV_CONSOLE, &s_vfs_console, ESP_VFS_FLAG_STATIC | ESP_VFS_FLAG_CONTEXT_PTR, NULL); -} - esp_err_t esp_stdio_register(void) { esp_err_t err = ESP_OK; // Primary vfs part. #if CONFIG_ESP_CONSOLE_UART - primary_vfs = esp_vfs_uart_get_vfs(); + const esp_vfs_fs_ops_t *ops = esp_vfs_uart_get_vfs(); + int fd = ops->open_p(NULL, "/" STRINGIFY(CONFIG_ESP_CONSOLE_UART_NUM), O_RDWR, 0); + if (fd < 0) { + ESP_EARLY_LOGE(TAG, "Failed to open primary UART sink, errno=%d", errno); + return ESP_FAIL; + } + err = esp_stdio_register_sink(ops, + ESP_VFS_FLAG_CONTEXT_PTR | ESP_VFS_FLAG_STATIC, + NULL, + "/" STRINGIFY(CONFIG_ESP_CONSOLE_UART_NUM), + fd); #elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG - primary_vfs = esp_vfs_usb_serial_jtag_get_vfs(); + const esp_vfs_fs_ops_t *ops = esp_vfs_usb_serial_jtag_get_vfs(); + int fd = ops->open_p(NULL, "/", O_RDWR, 0); + if (fd < 0) { + ESP_EARLY_LOGE(TAG, "Failed to open primary USB JTAG sink, errno=%d", errno); + return ESP_FAIL; + } + err = esp_stdio_register_sink(ops, + ESP_VFS_FLAG_CONTEXT_PTR | ESP_VFS_FLAG_STATIC, + NULL, + "/", + fd); #elif CONFIG_ESP_CONSOLE_USB_CDC - primary_vfs = esp_vfs_cdcacm_get_vfs(); + const esp_vfs_fs_ops_t *ops = esp_vfs_cdcacm_get_vfs(); + int fd = ops->open_p(NULL, "/", O_RDWR, 0); + if (fd < 0) { + ESP_EARLY_LOGE(TAG, "Failed to open primary USB CDC sink, errno=%d", errno); + return ESP_FAIL; + } + err = esp_stdio_register_sink(ops, + ESP_VFS_FLAG_CONTEXT_PTR | ESP_VFS_FLAG_STATIC, + NULL, + "/", + fd); #else - primary_vfs = esp_vfs_null_get_vfs(); + const esp_vfs_fs_ops_t *ops = esp_vfs_null_get_vfs(); + int fd = ops->open_p(NULL, "/", O_RDWR, 0); + if (fd < 0) { + ESP_EARLY_LOGE(TAG, "Failed to open primary null sink, errno=%d", errno); + return ESP_FAIL; + } + err = esp_stdio_register_sink(ops, + ESP_VFS_FLAG_CONTEXT_PTR | ESP_VFS_FLAG_STATIC, + NULL, + "/", + fd); #endif + if (err != ESP_OK) { + return err; + } + // Secondary vfs part. #if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG - secondary_vfs = esp_vfs_usb_serial_jtag_get_vfs(); + const esp_vfs_fs_ops_t *ops_secondary = esp_vfs_usb_serial_jtag_get_vfs(); + int fd_secondary = ops_secondary->open_p(NULL, "/", O_RDWR, 0); + if (fd_secondary < 0) { + ESP_EARLY_LOGE(TAG, "Failed to open secondary USB JTAG sink, errno=%d", errno); + return ESP_FAIL; + } + err = esp_stdio_register_sink(ops_secondary, + ESP_VFS_FLAG_CONTEXT_PTR | ESP_VFS_FLAG_STATIC, + NULL, + "/", + fd_secondary); + if (err != ESP_OK) { + return err; + } #endif - err = esp_vfs_dev_console_register(); - return err; + return esp_vfs_register_fs(ESP_VFS_DEV_CONSOLE, &s_vfs_console, ESP_VFS_FLAG_STATIC | ESP_VFS_FLAG_CONTEXT_PTR, NULL); } ESP_SYSTEM_INIT_FN(init_vfs_console, CORE, BIT(0), 119) From 4a7f4bf872fd237db4fa4af86f5df1f68b332532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Rohl=C3=ADnek?= Date: Tue, 17 Mar 2026 19:38:01 +0100 Subject: [PATCH 3/6] feat(system/esp_stdio): Add test coverage for esp_stdio refactor --- components/esp_stdio/Kconfig | 8 +- components/esp_stdio/stdio_vfs.c | 2 +- .../test_apps/stdio/main/test_app_main.c | 115 +++++++++++++++++- .../test_apps/stdio/pytest_esp_stdio_tests.py | 104 +++++++++++++++- .../stdio/sdkconfig.ci.stdio_basic_mode | 1 + .../sdkconfig.ci.stdio_non_basic_small_fd | 1 + 6 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_basic_mode create mode 100644 components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_non_basic_small_fd diff --git a/components/esp_stdio/Kconfig b/components/esp_stdio/Kconfig index 996ba13ff34..a9b1da8c5e6 100644 --- a/components/esp_stdio/Kconfig +++ b/components/esp_stdio/Kconfig @@ -189,12 +189,16 @@ menu "ESP-STDIO" int "Max logical stdio file descriptors" depends on VFS_SUPPORT_IO range 0 64 - default 3 + default 16 help Maximum number of logical /dev/console descriptors tracked by esp_stdio VFS. - 0 enables basic mode with a shared internal descriptor. - 1..2 is not allowed - - Values >3 enable descriptor-table mode. + - Values >= 3 select descriptor-table mode (minimum table size is 3). + + stdin, stdout, and stderr each occupy one slot. The default leaves room for additional + opens of /dev/console (e.g. dup2, reopen). If set to exactly 3, only the three standard + streams fit and further opens fail with ENFILE unless one is closed first. Enabling basic mode decreases RAM usage, but disables checks for per-fd behaviour, such as read-only. diff --git a/components/esp_stdio/stdio_vfs.c b/components/esp_stdio/stdio_vfs.c index 06d96e1dbee..bf0351fde23 100644 --- a/components/esp_stdio/stdio_vfs.c +++ b/components/esp_stdio/stdio_vfs.c @@ -84,7 +84,7 @@ static const char *TAG = "esp_stdio"; static inline bool is_fd_valid(int fd) { #if ESP_STDIO_IS_BASIC - return (fd == 0) && (s_ctx.fd_count > 0); + return (fd >= 0) && (s_ctx.fd_count > 0); #else return (fd >= 0) && (fd < CONFIG_ESP_STDIO_MAX_FDS) && s_ctx.fds[fd].in_use; #endif diff --git a/components/esp_stdio/test_apps/stdio/main/test_app_main.c b/components/esp_stdio/test_apps/stdio/main/test_app_main.c index 8a8663867b6..42548f27566 100644 --- a/components/esp_stdio/test_apps/stdio/main/test_app_main.c +++ b/components/esp_stdio/test_apps/stdio/main/test_app_main.c @@ -1,11 +1,12 @@ /* - * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #include #include #include +#include #include "sdkconfig.h" #include "esp_rom_serial_output.h" @@ -50,6 +51,7 @@ static void console_none_print(void) #endif #if CONFIG_VFS_SUPPORT_IO +#if CONFIG_ESP_STDIO_MAX_FDS <= 0 || CONFIG_ESP_STDIO_MAX_FDS > 3 static void console_open_close_check(void) { printf("Opening /dev/console\n"); @@ -65,6 +67,110 @@ static void console_open_close_check(void) printf("This should be printed to stdout\n"); } +#endif + +static void stdio_fd_mode_behavior_check(void) +{ +#if CONFIG_ESP_STDIO_MAX_FDS <= 0 + printf("STDIO_TEST:MODE=BASIC\n"); + + int fd0 = open("/dev/console", O_RDWR); + int fd1 = open("/dev/console", O_RDWR); + int fd2 = open("/dev/console", O_RDWR); + assert(fd0 >= 0); + assert(fd1 >= 0); + assert(fd2 >= 0); + printf("STDIO_TEST:BASIC:OPEN_OK\n"); + + const char *msg = "STDIO_TEST:BASIC:WRITE_OK\n"; + ssize_t wr = write(fd0, msg, strlen(msg)); + assert(wr == (ssize_t) strlen(msg)); + + assert(close(fd0) == 0); + assert(close(fd1) == 0); + assert(close(fd2) == 0); + printf("STDIO_TEST:BASIC:CLOSE_OK\n"); + + errno = 0; + assert(close(-1) < 0); + assert(errno == EBADF); + + errno = 0; + assert(write(fd0, msg, strlen(msg)) < 0); + assert(errno == EBADF); + printf("STDIO_TEST:BASIC:EBADF_OK\n"); +#else + printf("STDIO_TEST:MODE=NON_BASIC\n"); + + errno = 0; + int fd0 = open("/dev/console", O_RDWR); + if (fd0 < 0) { + /* Small fd limits can be fully consumed by stdin/stdout/stderr. */ + assert(errno == ENFILE); + printf("STDIO_TEST:NON_BASIC:SATURATED_OK\n"); + return; + } + + int fd1 = open("/dev/console", O_RDWR); + int fd2 = open("/dev/console", O_RDWR); + assert(fd0 >= 0); + assert(fd1 >= 0); + assert(fd2 >= 0); + assert(fd0 != fd1 && fd1 != fd2 && fd0 != fd2); + printf("STDIO_TEST:NON_BASIC:UNIQUE_OK\n"); + + assert(close(fd1) == 0); + int fd_reused = open("/dev/console", O_RDWR); + assert(fd_reused == fd1); + printf("STDIO_TEST:NON_BASIC:REUSE_OK\n"); + assert(close(fd0) == 0); + assert(close(fd2) == 0); + assert(close(fd_reused) == 0); + + int fds[CONFIG_ESP_STDIO_MAX_FDS + 2]; + int opened = 0; + while (opened < (int)(sizeof(fds) / sizeof(fds[0]))) { + int fd = open("/dev/console", O_RDWR); + if (fd < 0) { + break; + } + fds[opened++] = fd; + } + assert(opened >= 0); + assert(opened <= CONFIG_ESP_STDIO_MAX_FDS); + errno = 0; + assert(open("/dev/console", O_RDWR) < 0); + assert(errno == ENFILE); + printf("STDIO_TEST:NON_BASIC:LIMIT_OK\n"); + + for (int i = 0; i < opened; ++i) { + assert(close(fds[i]) == 0); + } + + const char *msg = "x"; + errno = 0; + assert(write(-1, msg, 1) < 0); + assert(errno == EBADF); + printf("STDIO_TEST:NON_BASIC:EBADF_OK\n"); +#endif +} + +static void stdio_fd_init_check(void) +{ + int in_fd = fileno(stdin); + int out_fd = fileno(stdout); + int err_fd = fileno(stderr); + + assert(in_fd == 0); + assert(out_fd == 1); + assert(err_fd == 2); + + printf("STDIO_TEST:FD_INIT:IN=%d,OUT=%d,ERR=%d\n", in_fd, out_fd, err_fd); + fprintf(stdout, "STDIO_TEST:STREAM:STDOUT\n"); + fflush(stdout); + fprintf(stderr, "STDIO_TEST:STREAM:STDERR\n"); + fflush(stderr); +} #endif // CONFIG_VFS_SUPPORT_IO void app_main(void) @@ -80,6 +186,8 @@ void app_main(void) assert(fd > 2 && "Incorrect file descriptor returned, stdin, stdout, stderr were not correctly assigned"); close(fd); + + stdio_fd_init_check(); #endif // CONFIG_VFS_SUPPORT_IO #if CONFIG_ESP_CONSOLE_NONE @@ -87,7 +195,12 @@ void app_main(void) #endif // CONFIG_ESP_CONSOLE_NONE #if CONFIG_VFS_SUPPORT_IO +#if CONFIG_ESP_STDIO_MAX_FDS <= 0 || CONFIG_ESP_STDIO_MAX_FDS > 3 console_open_close_check(); +#else + printf("STDIO_TEST:SKIP:OPEN_CLOSE_CHECK\n"); +#endif + stdio_fd_mode_behavior_check(); #endif // CONFIG_VFS_SUPPORT_IO } diff --git a/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py b/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py index 9093e3cb0cf..aad8aa7b080 100644 --- a/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py +++ b/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py @@ -1,5 +1,9 @@ -# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: CC0-1.0 +# +# HW runs: use --embedded-services esp,idf (not idf alone) so serial is attached. +# See README.md in this directory for QEMU vs hardware commands and common failures. +# import pexpect import pytest from pytest_embedded import Dut @@ -7,6 +11,11 @@ from pytest_embedded_idf.utils import idf_parametrize from pytest_embedded_idf.utils import soc_filtered_targets +def _expect_app_main_banner(dut: Dut) -> None: + """Sync to the latest app boot (avoids matching a stale 'Hello World' left in the serial buffer).""" + dut.expect('Calling app_main()') + + def not_expect(dut: Dut, output_regex: str) -> None: try: dut.expect(output_regex, timeout=5) @@ -33,7 +42,11 @@ def test_esp_system_stdio_no_output_uart(dut: Dut) -> None: @pytest.mark.temp_skip_ci(targets=['esp32p4'], reason='p4 rev3 migration') def test_esp_system_stdio_only_serial_jtag(dut: Dut) -> None: dut.expect('2nd stage bootloader') + _expect_app_main_banner(dut) dut.expect('Hello World') + dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:STREAM:STDOUT') + dut.expect('STDIO_TEST:STREAM:STDERR') dut.expect('Opening /dev/console') dut.expect('This should be printed to stdout') dut.expect('Closing /dev/console') @@ -48,6 +61,7 @@ def test_esp_system_stdio_only_serial_jtag(dut: Dut) -> None: @pytest.mark.temp_skip_ci(targets=['esp32p4'], reason='p4 rev3 migration') def test_esp_system_stdio_only_serial_jtag_no_vfs(dut: Dut) -> None: dut.expect('2nd stage bootloader') + _expect_app_main_banner(dut) dut.expect('Hello World') @@ -59,6 +73,7 @@ def test_esp_system_stdio_only_serial_jtag_no_vfs(dut: Dut) -> None: @pytest.mark.temp_skip_ci(targets=['esp32p4'], reason='p4 rev3 migration') def test_esp_system_stdio_secondary_serial_jtag(dut: Dut) -> None: dut.expect('2nd stage bootloader') + _expect_app_main_banner(dut) dut.expect('Hello World') @@ -68,8 +83,95 @@ def test_esp_system_stdio_secondary_serial_jtag(dut: Dut) -> None: @pytest.mark.temp_skip_ci(targets=['esp32p4'], reason='p4 rev3 migration') def test_esp_system_stdio_correct_open_and_close(dut: Dut) -> None: dut.expect('2nd stage bootloader') + _expect_app_main_banner(dut) dut.expect('Hello World') + # Order matches test_app_main.c: stdio_fd_init_check() runs before console_open_close_check() + dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:STREAM:STDOUT') + dut.expect('STDIO_TEST:STREAM:STDERR') dut.expect('Opening /dev/console') dut.expect('This should be printed to stdout') dut.expect('Closing /dev/console') dut.expect('This should be printed to stdout') + dut.expect('STDIO_TEST:MODE=NON_BASIC') + dut.expect('STDIO_TEST:NON_BASIC:UNIQUE_OK') + dut.expect('STDIO_TEST:NON_BASIC:REUSE_OK') + dut.expect('STDIO_TEST:NON_BASIC:LIMIT_OK') + dut.expect('STDIO_TEST:NON_BASIC:EBADF_OK') + + +@pytest.mark.host_test +@pytest.mark.qemu +@pytest.mark.parametrize( + 'config, embedded_services', + [('simple', 'idf,qemu')], +) +@idf_parametrize('target', ['esp32'], indirect=['target']) +def test_esp_stdio_non_basic_default_qemu(dut: Dut) -> None: + dut.expect('Hello World') + dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:MODE=NON_BASIC') + dut.expect('STDIO_TEST:NON_BASIC:UNIQUE_OK') + dut.expect('STDIO_TEST:NON_BASIC:REUSE_OK') + dut.expect('STDIO_TEST:NON_BASIC:LIMIT_OK') + dut.expect('STDIO_TEST:NON_BASIC:EBADF_OK') + + +@pytest.mark.host_test +@pytest.mark.qemu +@pytest.mark.parametrize( + 'config, embedded_services', + [('stdio_non_basic_small_fd', 'idf,qemu')], +) +@idf_parametrize('target', ['esp32'], indirect=['target']) +def test_esp_stdio_non_basic_small_fd_mode_qemu(dut: Dut) -> None: + dut.expect('Hello World') + dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:MODE=NON_BASIC') + dut.expect('STDIO_TEST:NON_BASIC:SATURATED_OK') + + +@pytest.mark.host_test +@pytest.mark.qemu +@pytest.mark.parametrize( + 'config, embedded_services', + [('stdio_basic_mode', 'idf,qemu')], +) +@idf_parametrize('target', ['esp32'], indirect=['target']) +def test_esp_stdio_basic_mode_qemu(dut: Dut) -> None: + dut.expect('Hello World') + dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:MODE=BASIC') + dut.expect('STDIO_TEST:BASIC:OPEN_OK') + dut.expect('STDIO_TEST:BASIC:WRITE_OK') + dut.expect('STDIO_TEST:BASIC:CLOSE_OK') + dut.expect('STDIO_TEST:BASIC:EBADF_OK') + + +@pytest.mark.generic +@idf_parametrize('config', ['stdio_non_basic_small_fd'], indirect=['config']) +@idf_parametrize('target', ['supported_targets'], indirect=['target']) +@pytest.mark.temp_skip_ci(targets=['esp32p4'], reason='p4 rev3 migration') +def test_esp_stdio_non_basic_small_fd_mode(dut: Dut) -> None: + dut.expect('2nd stage bootloader') + _expect_app_main_banner(dut) + dut.expect('Hello World') + dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:MODE=NON_BASIC') + dut.expect('STDIO_TEST:NON_BASIC:SATURATED_OK') + + +@pytest.mark.generic +@idf_parametrize('config', ['stdio_basic_mode'], indirect=['config']) +@idf_parametrize('target', ['supported_targets'], indirect=['target']) +@pytest.mark.temp_skip_ci(targets=['esp32p4'], reason='p4 rev3 migration') +def test_esp_stdio_basic_mode(dut: Dut) -> None: + dut.expect('2nd stage bootloader') + _expect_app_main_banner(dut) + dut.expect('Hello World') + dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:MODE=BASIC') + dut.expect('STDIO_TEST:BASIC:OPEN_OK') + dut.expect('STDIO_TEST:BASIC:WRITE_OK') + dut.expect('STDIO_TEST:BASIC:CLOSE_OK') + dut.expect('STDIO_TEST:BASIC:EBADF_OK') diff --git a/components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_basic_mode b/components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_basic_mode new file mode 100644 index 00000000000..9f404e13d40 --- /dev/null +++ b/components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_basic_mode @@ -0,0 +1 @@ +CONFIG_ESP_STDIO_MAX_FDS=0 diff --git a/components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_non_basic_small_fd b/components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_non_basic_small_fd new file mode 100644 index 00000000000..d077b890e82 --- /dev/null +++ b/components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_non_basic_small_fd @@ -0,0 +1 @@ +CONFIG_ESP_STDIO_MAX_FDS=3 From e041542092b6f9d4896c4a147f667e4dfc56e5a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Rohl=C3=ADnek?= Date: Mon, 23 Mar 2026 19:29:23 +0100 Subject: [PATCH 4/6] test(mspi_test): relax Unity heap leak threshold for lazy stdio --- components/spi_flash/test_apps/mspi_test/main/test_app_main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/spi_flash/test_apps/mspi_test/main/test_app_main.c b/components/spi_flash/test_apps/mspi_test/main/test_app_main.c index abb1c1cd7dc..eaa93bad03f 100644 --- a/components/spi_flash/test_apps/mspi_test/main/test_app_main.c +++ b/components/spi_flash/test_apps/mspi_test/main/test_app_main.c @@ -9,7 +9,7 @@ #include "esp_heap_caps.h" // Some resources are lazy allocated in mspi bus test, the threshold is left for that case -#define TEST_MEMORY_LEAK_THRESHOLD (-600) +#define TEST_MEMORY_LEAK_THRESHOLD (-650) static size_t before_free_8bit; static size_t before_free_32bit; From 59179490a2425ba7a54a4a8be64e3376e5297331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Rohl=C3=ADnek?= Date: Wed, 1 Apr 2026 15:27:00 +0200 Subject: [PATCH 5/6] fix(esp_stdio): remap console select fd bits to sink device Map logical /dev/console fds to the primary sink fd for select. This avoids probing UART1/UART2 for stdout/stderr and keeps UART select state consistent. Add stdio regression checks for logical fd mapping and symmetric uart_end_select teardown. --- components/esp_driver_uart/src/uart_vfs.c | 1 - components/esp_stdio/stdio_vfs.c | 158 +++++++++++++++++- .../test_apps/stdio/main/test_app_main.c | 35 ++++ .../test_apps/stdio/pytest_esp_stdio_tests.py | 6 + 4 files changed, 192 insertions(+), 8 deletions(-) diff --git a/components/esp_driver_uart/src/uart_vfs.c b/components/esp_driver_uart/src/uart_vfs.c index fa9221f2a58..69fef254ed5 100644 --- a/components/esp_driver_uart/src/uart_vfs.c +++ b/components/esp_driver_uart/src/uart_vfs.c @@ -588,7 +588,6 @@ static esp_err_t uart_end_select(void *end_select_args) if (s_uart_select_count[i] == 0) { uart_set_select_notif_callback(i, NULL); } - break; } } portEXIT_CRITICAL(uart_get_selectlock()); diff --git a/components/esp_stdio/stdio_vfs.c b/components/esp_stdio/stdio_vfs.c index bf0351fde23..2b2dab96140 100644 --- a/components/esp_stdio/stdio_vfs.c +++ b/components/esp_stdio/stdio_vfs.c @@ -35,6 +35,8 @@ #include "esp_private/startup_internal.h" #include "esp_private/nullfs.h" +#include "esp_heap_caps.h" +#endif #define STRINGIFY(s) STRINGIFY2(s) #define STRINGIFY2(s) #s @@ -322,29 +324,171 @@ int console_access(__attribute__((unused)) void *ctx, const char *path, int amod #endif // CONFIG_VFS_SUPPORT_DIR #ifdef CONFIG_VFS_SUPPORT_SELECT + +/* + * Logical /dev/console fds (0,1,2,...) are not UART port numbers. uart_vfs select uses fd_set bits as + * SOC UART indices; without remapping, monitoring stdout/stderr selects UART1/UART2 and corrupts + * s_uart_select_count / ISR registration (only UART0 is the console sink). + */ +typedef struct { + void *uart_args; + fd_set *readfds; + fd_set *writefds; + fd_set *exceptfds; + uint64_t interested_read; + uint64_t interested_write; + uint64_t interested_except; +} console_select_ctx_t; + +/* Kconfig allows up to 64 logical fds; uint64_t masks and (1ULL << i) require i < 64. */ +enum { CONSOLE_SELECT_MAX_SCAN = 64 }; + static esp_err_t console_start_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, esp_vfs_select_sem_t select_sem, void **end_select_args) { - // start_select is not guaranteed be implemented even though CONFIG_VFS_SUPPORT_SELECT is enabled in sdkconfig const mux_entry_t *entry = get_primary_entry(); assert(entry); - if (entry->fd >= 0 && entry->ops->select && entry->ops->select->start_select) { - return entry->ops->select->start_select(nfds, readfds, writefds, exceptfds, select_sem, end_select_args); + *end_select_args = NULL; + + if (entry->fd < 0 || entry->ops->select == NULL || entry->ops->select->start_select == NULL) { + return ESP_ERR_NOT_SUPPORTED; } - return ESP_ERR_NOT_SUPPORTED; + uint64_t interested_read = 0; + uint64_t interested_write = 0; + uint64_t interested_except = 0; + + const int scan = nfds < CONSOLE_SELECT_MAX_SCAN ? nfds : CONSOLE_SELECT_MAX_SCAN; + for (int i = 0; i < scan; ++i) { + if (!is_fd_valid(i)) { + continue; + } + if (readfds && FD_ISSET(i, readfds)) { + interested_read |= (1ULL << i); + } + if (writefds && FD_ISSET(i, writefds)) { + interested_write |= (1ULL << i); + } + if (exceptfds && FD_ISSET(i, exceptfds)) { + interested_except |= (1ULL << i); + } + } + + if (interested_read == 0 && interested_write == 0 && interested_except == 0) { + return ESP_ERR_NOT_SUPPORTED; + } + + const int hw = entry->fd; + + if (readfds) { + FD_ZERO(readfds); + if (interested_read) { + FD_SET(hw, readfds); + } + } + if (writefds) { + FD_ZERO(writefds); + if (interested_write) { + FD_SET(hw, writefds); + } + } + if (exceptfds) { + FD_ZERO(exceptfds); + if (interested_except) { + FD_SET(hw, exceptfds); + } + } + + const int forward_nfds = nfds > hw + 1 ? nfds : hw + 1; + + console_select_ctx_t *ctx = heap_caps_malloc(sizeof(console_select_ctx_t), MALLOC_CAP_INTERNAL); + if (ctx == NULL) { + return ESP_ERR_NO_MEM; + } + + ctx->uart_args = NULL; + ctx->readfds = readfds; + ctx->writefds = writefds; + ctx->exceptfds = exceptfds; + ctx->interested_read = interested_read; + ctx->interested_write = interested_write; + ctx->interested_except = interested_except; + + esp_err_t err = entry->ops->select->start_select(forward_nfds, readfds, writefds, exceptfds, select_sem, + &ctx->uart_args); + if (err != ESP_OK) { + if (readfds) { + FD_ZERO(readfds); + for (int i = 0; i < scan; ++i) { + if (interested_read & (1ULL << i)) { + FD_SET(i, readfds); + } + } + } + if (writefds) { + FD_ZERO(writefds); + for (int i = 0; i < scan; ++i) { + if (interested_write & (1ULL << i)) { + FD_SET(i, writefds); + } + } + } + if (exceptfds) { + FD_ZERO(exceptfds); + for (int i = 0; i < scan; ++i) { + if (interested_except & (1ULL << i)) { + FD_SET(i, exceptfds); + } + } + } + heap_caps_free(ctx); + return err; + } + + *end_select_args = ctx; + return ESP_OK; +} + +static void console_expand_fdset(fd_set *fds, uint64_t interested, int hw) +{ + if (fds == NULL) { + return; + } + const bool ready = FD_ISSET(hw, fds); + FD_ZERO(fds); + if (!ready || interested == 0) { + return; + } + for (int i = 0; i < CONSOLE_SELECT_MAX_SCAN; ++i) { + if (interested & (1ULL << i)) { + FD_SET(i, fds); + } + } } esp_err_t console_end_select(void *end_select_args) { - // end_select is not guaranteed be implemented even though CONFIG_VFS_SUPPORT_SELECT is enabled in sdkconfig + if (end_select_args == NULL) { + return ESP_ERR_INVALID_ARG; + } + console_select_ctx_t *ctx = (console_select_ctx_t *)end_select_args; const mux_entry_t *entry = get_primary_entry(); assert(entry); + + esp_err_t ret = ESP_OK; if (entry->fd >= 0 && entry->ops->select && entry->ops->select->end_select) { - return entry->ops->select->end_select(end_select_args); + ret = entry->ops->select->end_select(ctx->uart_args); } - return ESP_ERR_NOT_SUPPORTED; + const int hw = entry->fd; + if (hw >= 0) { + console_expand_fdset(ctx->readfds, ctx->interested_read, hw); + console_expand_fdset(ctx->writefds, ctx->interested_write, hw); + console_expand_fdset(ctx->exceptfds, ctx->interested_except, hw); + } + + heap_caps_free(ctx); + return ret; } #endif // CONFIG_VFS_SUPPORT_SELECT diff --git a/components/esp_stdio/test_apps/stdio/main/test_app_main.c b/components/esp_stdio/test_apps/stdio/main/test_app_main.c index 42548f27566..154b83814b4 100644 --- a/components/esp_stdio/test_apps/stdio/main/test_app_main.c +++ b/components/esp_stdio/test_apps/stdio/main/test_app_main.c @@ -19,6 +19,7 @@ #include #include +#include #if CONFIG_ESP_CONSOLE_NONE /* Set up UART on UART_0 (console) to be able to @@ -171,6 +172,37 @@ static void stdio_fd_init_check(void) fprintf(stderr, "STDIO_TEST:STREAM:STDERR\n"); fflush(stderr); } + +#if CONFIG_VFS_SUPPORT_SELECT && CONFIG_ESP_CONSOLE_UART +static void stdio_select_fd_mapping_check(void) +{ + bool driver_installed_here = false; + esp_err_t err = uart_driver_install(CONFIG_ESP_CONSOLE_UART_NUM, 256, 0, 0, NULL, 0); + if (err == ESP_OK) { + driver_installed_here = true; + } else { + assert(err == ESP_ERR_INVALID_STATE); + } + + fd_set writefds; + FD_ZERO(&writefds); + FD_SET(fileno(stdout), &writefds); + FD_SET(fileno(stderr), &writefds); + + struct timeval tv = { + .tv_sec = 0, + .tv_usec = 0, + }; + + errno = 0; + int ret = select(fileno(stderr) + 1, NULL, &writefds, NULL, &tv); + assert(ret >= 0); + if (driver_installed_here) { + assert(uart_driver_delete(CONFIG_ESP_CONSOLE_UART_NUM) == ESP_OK); + } + printf("STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK\n"); +} +#endif #endif // CONFIG_VFS_SUPPORT_IO void app_main(void) @@ -188,6 +220,9 @@ void app_main(void) close(fd); stdio_fd_init_check(); +#if CONFIG_VFS_SUPPORT_SELECT && CONFIG_ESP_CONSOLE_UART + stdio_select_fd_mapping_check(); +#endif #endif // CONFIG_VFS_SUPPORT_IO #if CONFIG_ESP_CONSOLE_NONE diff --git a/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py b/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py index aad8aa7b080..7386cdd54d9 100644 --- a/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py +++ b/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py @@ -89,6 +89,7 @@ def test_esp_system_stdio_correct_open_and_close(dut: Dut) -> None: dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') dut.expect('STDIO_TEST:STREAM:STDOUT') dut.expect('STDIO_TEST:STREAM:STDERR') + dut.expect('STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK') dut.expect('Opening /dev/console') dut.expect('This should be printed to stdout') dut.expect('Closing /dev/console') @@ -110,6 +111,7 @@ def test_esp_system_stdio_correct_open_and_close(dut: Dut) -> None: def test_esp_stdio_non_basic_default_qemu(dut: Dut) -> None: dut.expect('Hello World') dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK') dut.expect('STDIO_TEST:MODE=NON_BASIC') dut.expect('STDIO_TEST:NON_BASIC:UNIQUE_OK') dut.expect('STDIO_TEST:NON_BASIC:REUSE_OK') @@ -127,6 +129,7 @@ def test_esp_stdio_non_basic_default_qemu(dut: Dut) -> None: def test_esp_stdio_non_basic_small_fd_mode_qemu(dut: Dut) -> None: dut.expect('Hello World') dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK') dut.expect('STDIO_TEST:MODE=NON_BASIC') dut.expect('STDIO_TEST:NON_BASIC:SATURATED_OK') @@ -141,6 +144,7 @@ def test_esp_stdio_non_basic_small_fd_mode_qemu(dut: Dut) -> None: def test_esp_stdio_basic_mode_qemu(dut: Dut) -> None: dut.expect('Hello World') dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK') dut.expect('STDIO_TEST:MODE=BASIC') dut.expect('STDIO_TEST:BASIC:OPEN_OK') dut.expect('STDIO_TEST:BASIC:WRITE_OK') @@ -157,6 +161,7 @@ def test_esp_stdio_non_basic_small_fd_mode(dut: Dut) -> None: _expect_app_main_banner(dut) dut.expect('Hello World') dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK') dut.expect('STDIO_TEST:MODE=NON_BASIC') dut.expect('STDIO_TEST:NON_BASIC:SATURATED_OK') @@ -170,6 +175,7 @@ def test_esp_stdio_basic_mode(dut: Dut) -> None: _expect_app_main_banner(dut) dut.expect('Hello World') dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK') dut.expect('STDIO_TEST:MODE=BASIC') dut.expect('STDIO_TEST:BASIC:OPEN_OK') dut.expect('STDIO_TEST:BASIC:WRITE_OK') From 92725e92d52d17efb8c108530d9beb208b0a39c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Rohl=C3=ADnek?= Date: Tue, 26 May 2026 08:45:50 +0200 Subject: [PATCH 6/6] fix(esp_stdio): add fd-table locking, per-fd flag checks, and Kconfig cleanup --- components/esp_stdio/Kconfig | 19 +-- components/esp_stdio/stdio_vfs.c | 117 +++++++++++++++--- .../test_apps/stdio/main/test_app_main.c | 48 ++++++- .../test_apps/stdio/pytest_esp_stdio_tests.py | 4 + .../stdio/sdkconfig.ci.stdio_basic_mode | 2 +- .../sdkconfig.ci.stdio_non_basic_small_fd | 1 + 6 files changed, 162 insertions(+), 29 deletions(-) diff --git a/components/esp_stdio/Kconfig b/components/esp_stdio/Kconfig index a9b1da8c5e6..b89598510a6 100644 --- a/components/esp_stdio/Kconfig +++ b/components/esp_stdio/Kconfig @@ -185,23 +185,28 @@ menu "ESP-STDIO" If enabled, esp_rom_printf and ESP_EARLY_LOG output will also be sent over USB CDC. Disabling this option saves about 1kB or RAM. + config ESP_STDIO_BASIC_MODE + bool "Use basic /dev/console descriptor handling" + depends on VFS_SUPPORT_IO + default n + help + Enable the low-overhead legacy /dev/console descriptor handling. + + In basic mode, all open() calls share a single internal descriptor. This keeps RAM usage + lower, but disables checks for per-fd behaviour, such as read-only versus write-only access. + config ESP_STDIO_MAX_FDS int "Max logical stdio file descriptors" - depends on VFS_SUPPORT_IO - range 0 64 + depends on VFS_SUPPORT_IO && !ESP_STDIO_BASIC_MODE + range 3 64 default 16 help Maximum number of logical /dev/console descriptors tracked by esp_stdio VFS. - - 0 enables basic mode with a shared internal descriptor. - - 1..2 is not allowed - - Values >= 3 select descriptor-table mode (minimum table size is 3). stdin, stdout, and stderr each occupy one slot. The default leaves room for additional opens of /dev/console (e.g. dup2, reopen). If set to exactly 3, only the three standard streams fit and further opens fail with ENFILE unless one is closed first. - Enabling basic mode decreases RAM usage, but disables checks for per-fd behaviour, such as read-only. - config ESP_STDIO_MAX_VFS_ENTRIES int "Maximum number of registerable VFS sinks" depends on VFS_SUPPORT_IO diff --git a/components/esp_stdio/stdio_vfs.c b/components/esp_stdio/stdio_vfs.c index 2b2dab96140..dc5c6d9aad0 100644 --- a/components/esp_stdio/stdio_vfs.c +++ b/components/esp_stdio/stdio_vfs.c @@ -36,6 +36,7 @@ #include "esp_private/startup_internal.h" #include "esp_private/nullfs.h" #include "esp_heap_caps.h" +#include #endif #define STRINGIFY(s) STRINGIFY2(s) @@ -51,9 +52,11 @@ #if CONFIG_VFS_SUPPORT_IO -_Static_assert(CONFIG_ESP_STDIO_MAX_FDS == 0 || CONFIG_ESP_STDIO_MAX_FDS >= 3, "Invalid value for CONFIG_ESP_STDIO_MAX_FDS"); +#if !CONFIG_ESP_STDIO_BASIC_MODE +_Static_assert(CONFIG_ESP_STDIO_MAX_FDS >= 3, "Invalid value for CONFIG_ESP_STDIO_MAX_FDS"); +#endif -#define ESP_STDIO_IS_BASIC (CONFIG_ESP_STDIO_MAX_FDS <= 0) +#define ESP_STDIO_IS_BASIC (CONFIG_ESP_STDIO_BASIC_MODE) typedef struct { const esp_vfs_fs_ops_t *ops; @@ -80,10 +83,11 @@ typedef struct { } context_t; static context_t s_ctx = {0}; +static _lock_t s_ctx_lock; static const char *TAG = "esp_stdio"; -static inline bool is_fd_valid(int fd) +static inline bool is_fd_valid_nolock(int fd) { #if ESP_STDIO_IS_BASIC return (fd >= 0) && (s_ctx.fd_count > 0); @@ -92,6 +96,37 @@ static inline bool is_fd_valid(int fd) #endif } +static inline bool is_fd_valid(int fd) +{ + bool valid; + + _lock_acquire(&s_ctx_lock); + valid = is_fd_valid_nolock(fd); + _lock_release(&s_ctx_lock); + + return valid; +} + +#if !ESP_STDIO_IS_BASIC +static inline int get_fd_flags_nolock(int fd) +{ + assert((fd >= 0) && (fd < CONFIG_ESP_STDIO_MAX_FDS)); + return s_ctx.fds[fd].flags; +} + +static inline bool fd_allows_read_nolock(int fd) +{ + const int flags = get_fd_flags_nolock(fd); + return (flags & O_ACCMODE) != O_WRONLY; +} + +static inline bool fd_allows_write_nolock(int fd) +{ + const int flags = get_fd_flags_nolock(fd); + return (flags & O_ACCMODE) != O_RDONLY; +} +#endif + static inline mux_entry_t *get_primary_entry(void) { assert(s_ctx.used > 0); @@ -169,15 +204,26 @@ int console_open(__attribute__((unused)) void *ctx, const char * path, int flags { #if ESP_STDIO_IS_BASIC (void) path; + (void) flags; + (void) mode; + + _lock_acquire(&s_ctx_lock); s_ctx.fd_count++; + _lock_release(&s_ctx_lock); + return 0; #else + (void) mode; + if (!path || strcmp(path, "/") != 0) { errno = ENOENT; return -1; } + _lock_acquire(&s_ctx_lock); + if (s_ctx.fd_count >= CONFIG_ESP_STDIO_MAX_FDS) { + _lock_release(&s_ctx_lock); errno = ENFILE; return -1; } @@ -191,24 +237,28 @@ int console_open(__attribute__((unused)) void *ctx, const char * path, int flags } if (fd < 0) { + _lock_release(&s_ctx_lock); errno = ENFILE; return -1; } s_ctx.fd_count++; - s_ctx.fds[fd] = (fd_entry_t) { .in_use = true, .flags = flags, }; + _lock_release(&s_ctx_lock); return fd; #endif } int console_close(__attribute__((unused)) void *ctx, int fd) { - if (!is_fd_valid(fd)) { + _lock_acquire(&s_ctx_lock); + + if (!is_fd_valid_nolock(fd)) { + _lock_release(&s_ctx_lock); errno = EBADF; return -1; } @@ -220,23 +270,43 @@ int console_close(__attribute__((unused)) void *ctx, int fd) s_ctx.fd_count--; #endif + _lock_release(&s_ctx_lock); return 0; } ssize_t console_write(__attribute__((unused)) void *ctx, int fd, const void *data, size_t size) { +#if ESP_STDIO_IS_BASIC if (!is_fd_valid(fd)) { errno = EBADF; return -1; } - for (size_t i = 0; i < s_ctx.used; i++) { +#else + _lock_acquire(&s_ctx_lock); + if (!is_fd_valid_nolock(fd)) { + _lock_release(&s_ctx_lock); + errno = EBADF; + return -1; + } + if (!fd_allows_write_nolock(fd)) { + _lock_release(&s_ctx_lock); + errno = EBADF; + return -1; + } + _lock_release(&s_ctx_lock); +#endif + + const mux_entry_t *primary = get_primary_entry(); + ssize_t ret_val = primary->ops->write_p(primary->vfs_ctx, primary->fd, data, size); + + for (size_t i = 1; i < s_ctx.used; i++) { const mux_entry_t *entry = s_ctx.entries + i; if (entry->ops->write_p && entry->fd >= 0) { - entry->ops->write_p(entry->vfs_ctx, entry->fd, data, size); + (void) entry->ops->write_p(entry->vfs_ctx, entry->fd, data, size); } } - return size; + return ret_val; } int console_fstat(__attribute__((unused)) void *ctx, int fd, struct stat * st) @@ -256,10 +326,25 @@ int console_fstat(__attribute__((unused)) void *ctx, int fd, struct stat * st) ssize_t console_read(__attribute__((unused)) void *ctx, int fd, void * dst, size_t size) { +#if ESP_STDIO_IS_BASIC if (!is_fd_valid(fd)) { errno = EBADF; return -1; } +#else + _lock_acquire(&s_ctx_lock); + if (!is_fd_valid_nolock(fd)) { + _lock_release(&s_ctx_lock); + errno = EBADF; + return -1; + } + if (!fd_allows_read_nolock(fd)) { + _lock_release(&s_ctx_lock); + errno = EBADF; + return -1; + } + _lock_release(&s_ctx_lock); +#endif const mux_entry_t *entry = get_primary_entry(); if (!entry->ops->read_p) { @@ -302,7 +387,7 @@ int console_fsync(__attribute__((unused)) void *ctx, int fd) for (size_t i = 1; i < s_ctx.used; i++) { const mux_entry_t *entry = s_ctx.entries + i; if (entry->ops->fsync_p && entry->fd >= 0) { - entry->ops->fsync_p(entry->vfs_ctx, entry->fd); + (void) entry->ops->fsync_p(entry->vfs_ctx, entry->fd); } } @@ -325,13 +410,8 @@ int console_access(__attribute__((unused)) void *ctx, const char *path, int amod #ifdef CONFIG_VFS_SUPPORT_SELECT -/* - * Logical /dev/console fds (0,1,2,...) are not UART port numbers. uart_vfs select uses fd_set bits as - * SOC UART indices; without remapping, monitoring stdout/stderr selects UART1/UART2 and corrupts - * s_uart_select_count / ISR registration (only UART0 is the console sink). - */ typedef struct { - void *uart_args; + void *sink_select_args; fd_set *readfds; fd_set *writefds; fd_set *exceptfds; @@ -406,7 +486,7 @@ static esp_err_t console_start_select(int nfds, fd_set *readfds, fd_set *writefd return ESP_ERR_NO_MEM; } - ctx->uart_args = NULL; + ctx->sink_select_args = NULL; ctx->readfds = readfds; ctx->writefds = writefds; ctx->exceptfds = exceptfds; @@ -415,7 +495,7 @@ static esp_err_t console_start_select(int nfds, fd_set *readfds, fd_set *writefd ctx->interested_except = interested_except; esp_err_t err = entry->ops->select->start_select(forward_nfds, readfds, writefds, exceptfds, select_sem, - &ctx->uart_args); + &ctx->sink_select_args); if (err != ESP_OK) { if (readfds) { FD_ZERO(readfds); @@ -477,7 +557,7 @@ esp_err_t console_end_select(void *end_select_args) esp_err_t ret = ESP_OK; if (entry->fd >= 0 && entry->ops->select && entry->ops->select->end_select) { - ret = entry->ops->select->end_select(ctx->uart_args); + ret = entry->ops->select->end_select(ctx->sink_select_args); } const int hw = entry->fd; @@ -599,6 +679,7 @@ static const esp_vfs_fs_ops_t s_vfs_console = { esp_err_t esp_stdio_register(void) { esp_err_t err = ESP_OK; + _lock_init(&s_ctx_lock); // Primary vfs part. #if CONFIG_ESP_CONSOLE_UART const esp_vfs_fs_ops_t *ops = esp_vfs_uart_get_vfs(); diff --git a/components/esp_stdio/test_apps/stdio/main/test_app_main.c b/components/esp_stdio/test_apps/stdio/main/test_app_main.c index 154b83814b4..c6f5c8dffd3 100644 --- a/components/esp_stdio/test_apps/stdio/main/test_app_main.c +++ b/components/esp_stdio/test_apps/stdio/main/test_app_main.c @@ -52,7 +52,15 @@ static void console_none_print(void) #endif #if CONFIG_VFS_SUPPORT_IO -#if CONFIG_ESP_STDIO_MAX_FDS <= 0 || CONFIG_ESP_STDIO_MAX_FDS > 3 +#if CONFIG_ESP_STDIO_BASIC_MODE +#define ESP_STDIO_RUN_OPEN_CLOSE_CHECK 1 +#elif CONFIG_ESP_STDIO_MAX_FDS > 3 +#define ESP_STDIO_RUN_OPEN_CLOSE_CHECK 1 +#else +#define ESP_STDIO_RUN_OPEN_CLOSE_CHECK 0 +#endif + +#if ESP_STDIO_RUN_OPEN_CLOSE_CHECK static void console_open_close_check(void) { printf("Opening /dev/console\n"); @@ -72,7 +80,7 @@ static void console_open_close_check(void) static void stdio_fd_mode_behavior_check(void) { -#if CONFIG_ESP_STDIO_MAX_FDS <= 0 +#if CONFIG_ESP_STDIO_BASIC_MODE printf("STDIO_TEST:MODE=BASIC\n"); int fd0 = open("/dev/console", O_RDWR); @@ -87,6 +95,11 @@ static void stdio_fd_mode_behavior_check(void) ssize_t wr = write(fd0, msg, strlen(msg)); assert(wr == (ssize_t) strlen(msg)); + /* Verify write return value matches actual bytes (primary sink propagation) */ + const char *short_msg = "ab"; + wr = write(fd1, short_msg, 2); + assert(wr == 2); + assert(close(fd0) == 0); assert(close(fd1) == 0); assert(close(fd2) == 0); @@ -128,6 +141,35 @@ static void stdio_fd_mode_behavior_check(void) assert(close(fd2) == 0); assert(close(fd_reused) == 0); + int fd_ro = open("/dev/console", O_RDONLY); + assert(fd_ro >= 0); + errno = 0; + assert(write(fd_ro, "x", 1) < 0); + assert(errno == EBADF); + assert(close(fd_ro) == 0); + + int fd_wo = open("/dev/console", O_WRONLY); + assert(fd_wo >= 0); + errno = 0; + char tmp = 0; + assert(read(fd_wo, &tmp, 1) < 0); + assert(errno == EBADF); + assert(close(fd_wo) == 0); + + int fd_rw = open("/dev/console", O_RDWR); + assert(fd_rw >= 0); + const char *wmsg = "z"; + ssize_t wret = write(fd_rw, wmsg, 1); + assert(wret == 1); + assert(close(fd_rw) == 0); + printf("STDIO_TEST:NON_BASIC:FLAGS_OK\n"); + + int fd_fsync = open("/dev/console", O_RDWR); + assert(fd_fsync >= 0); + assert(fsync(fd_fsync) == 0); + assert(close(fd_fsync) == 0); + printf("STDIO_TEST:NON_BASIC:FSYNC_OK\n"); + int fds[CONFIG_ESP_STDIO_MAX_FDS + 2]; int opened = 0; while (opened < (int)(sizeof(fds) / sizeof(fds[0]))) { @@ -230,7 +272,7 @@ void app_main(void) #endif // CONFIG_ESP_CONSOLE_NONE #if CONFIG_VFS_SUPPORT_IO -#if CONFIG_ESP_STDIO_MAX_FDS <= 0 || CONFIG_ESP_STDIO_MAX_FDS > 3 +#if ESP_STDIO_RUN_OPEN_CLOSE_CHECK console_open_close_check(); #else printf("STDIO_TEST:SKIP:OPEN_CLOSE_CHECK\n"); diff --git a/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py b/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py index 7386cdd54d9..082373b8eea 100644 --- a/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py +++ b/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py @@ -97,6 +97,8 @@ def test_esp_system_stdio_correct_open_and_close(dut: Dut) -> None: dut.expect('STDIO_TEST:MODE=NON_BASIC') dut.expect('STDIO_TEST:NON_BASIC:UNIQUE_OK') dut.expect('STDIO_TEST:NON_BASIC:REUSE_OK') + dut.expect('STDIO_TEST:NON_BASIC:FLAGS_OK') + dut.expect('STDIO_TEST:NON_BASIC:FSYNC_OK') dut.expect('STDIO_TEST:NON_BASIC:LIMIT_OK') dut.expect('STDIO_TEST:NON_BASIC:EBADF_OK') @@ -115,6 +117,8 @@ def test_esp_stdio_non_basic_default_qemu(dut: Dut) -> None: dut.expect('STDIO_TEST:MODE=NON_BASIC') dut.expect('STDIO_TEST:NON_BASIC:UNIQUE_OK') dut.expect('STDIO_TEST:NON_BASIC:REUSE_OK') + dut.expect('STDIO_TEST:NON_BASIC:FLAGS_OK') + dut.expect('STDIO_TEST:NON_BASIC:FSYNC_OK') dut.expect('STDIO_TEST:NON_BASIC:LIMIT_OK') dut.expect('STDIO_TEST:NON_BASIC:EBADF_OK') diff --git a/components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_basic_mode b/components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_basic_mode index 9f404e13d40..25d7a763ddc 100644 --- a/components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_basic_mode +++ b/components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_basic_mode @@ -1 +1 @@ -CONFIG_ESP_STDIO_MAX_FDS=0 +CONFIG_ESP_STDIO_BASIC_MODE=y diff --git a/components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_non_basic_small_fd b/components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_non_basic_small_fd index d077b890e82..3703a0c45a6 100644 --- a/components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_non_basic_small_fd +++ b/components/esp_stdio/test_apps/stdio/sdkconfig.ci.stdio_non_basic_small_fd @@ -1 +1,2 @@ +CONFIG_ESP_STDIO_BASIC_MODE=n CONFIG_ESP_STDIO_MAX_FDS=3