← board

ESP PAL: exact POSIX fd semantics over ESP-IDF VFS

Problem

The first ESP-IDF PAL file backend uses newlib stdio (fopen/fread/fwrite/ fseek/fflush/fclose) over ESP-IDF VFS. That gives real file contents on mounted IDF filesystems without touching compiler code, but it is not exact POSIX fd semantics:

Direct IDF/POSIX open/read/write/close would be a better long-term match, but read/write are Pascal keyword tokens today, so a clean direct external binding needs either imported C declarations with safe Pascal names or a compiler-supported external symbol alias that preserves the local Pascal identifier.

Acceptance

Log

Moved to blocked/ (2026-07-20, Track B sweep)

Acceptance is a C3/S3 link-and-run smoke; there is no device and no qemu/IDF runner in this lane. External constraint, so blocked/ rather than backlog.

One thing IS doable without hardware and is worth doing first when this resumes: host-side --platform=esp tests that pin the CURRENT behaviour — PAL_OPEN_EXCL returning PAL_ERR_UNSUPPORTED, and the errno collapse — so the rewrite has a baseline to diff against instead of changing semantics blind.

Moved to blocked/ 2026-07-31 (Track B sweep) — same wall as the rest of the ESP family

This ticket's own acceptance ends with "validated by an ESP-IDF link/run smoke on C3 and S3, not only host --platform=esp unsupported-path tests". That is the part nothing here can do: re-checked rather than assumed, this box has no qemu-system-riscv32, no qemu-system-xtensa, IDF_PATH unset, and no board. An ESP-IDF checkout at ~/esp/esp-idf is enough to LINK and nothing more.

Writing exact POSIX fd semantics that nobody can execute would produce precisely what the honest-refusal discipline in this backend exists to avoid: code that looks right. The current newlib-stdio backend already refuses what it cannot do (PAL_OPEN_EXCL -> PAL_ERR_UNSUPPORTED), which is the correct resting state.

Tagged for later testing with [[feature-esp-peripheral-callback-api]]: when a C3/S3 board or a working qemu-IDF harness appears, both wake up together. Nothing from this ticket enters the regression suite until something can run it.

Inventory 2026-08-02 — what "not a Unix" actually costs, measured

The user's framing: ESP32 is "not a unix at all, just a thin layer of FreeRTOS", so it will carry its own incompatibility set. Counted from lib/rtl/platform/esp/platform_backend.pas, separating the two cases that a naive grep conflates:

Refused even under ESP-IDF — 33 PAL entry points. This is the real gap list:

area refused
process model Vfork VforkAndExec Execve Wait4 Kill Pipe2
filesystem metadata Stat StatAt Fstat Lstat Access GetDents64 Readlink Utimes Fchmod Fchown Ftruncate Fsync Fcntl Dup2
namespace Chdir Getcwd Symlink Link
memory MmapAnon Munmap
IPv6 BindIpv6 ConnectIpv6 AcceptIpv6 SendToIpv6 RecvFromIpv6
time Nanosleep Realtime

Works under IDF, refused only on bare — 25. The IPv4 socket surface (Socket BindIpv4 ConnectIpv4 Listen Accept Send Recv SendToIpv4 SetSockOpt GetSockOpt Ioctl Shutdown SocketClose SetSocketNonBlocking SetSocketReuseAddr) plus basic file I/O over the IDF VFS (Open Read Write Seek Close Flush Delete Rename Mkdir Rmdir).

The shape of it

Sockets work; basic file I/O works; almost everything else Unix-shaped is absent. FreeRTOS has tasks, not processes — so there is no fork/exec/wait/kill and no pipes. There is no working directory, no links, no ownership, no directory enumeration and no stat. There is no virtual memory (the IDF heap replaces mmap). That is not a set of missing features to fill in one by one: it is a different OS model, and code that assumes POSIX will meet it as PAL_ERR_UNSUPPORTED rather than as a wrong answer — which is the right failure mode and worth preserving.

Consequence for lib/crtl

The crtl additions of 2026-08-02 (pipe, kill, dup/dup2, chdir, symlink, link, getuid/getgid/getegid/getppid) are POSIX-shaped and several land on the always-refused list above. They are honest on ESP — the PAL returns unsupported rather than faking — but a C program ported to ESP will hit them. Worth knowing before anyone reads the crtl gap-batch tickets as "crtl is now complete for every target": it is complete for the hosted targets.