← board

What was measured

lib/rtl/sockets.pas — all four bodies take flags and ignore it:

function fpSend(s: cint; msg: Pointer; len: cint; flags: cint): ssize_t;
begin Result := SockRetSize(PalSend(s, msg, len)); end;      { flags unread }

Same shape at fpRecv, fpSendTo, fpRecvFrom. MSG_OOB/MSG_PEEK/ MSG_NOSIGNAL are declared at sockets.pas:108 and no call site can make any of them take effect.

Repro — the failure is a HANG

Send 8 bytes, then peek twice on the receiving socket:

n1 := fpRecv(cli, @r1[0], 8, MSG_PEEK);   { -> 8, bytes correct }
n2 := fpRecv(cli, @r2[0], 8, MSG_PEEK);   { blocks forever }

peek1 returned -> 8 first byte 100, then nothing; timeout 20 kills it (124). The first peek consumed the data, so the second waits for bytes that will never come. A caller doing the standard "peek at the header, then read the whole message" gets a deadlock, not a wrong value.

Why it is not fixable at the sockets.pas layer

PalSend(handle, buf, len) / PalRecv / PalSendToIpv4 / PalRecvFromIpv4 have no flags slot in their signatures. Threading one through means touching lib/rtl/platform.pas plus all three backends (platform/{posix,esp,wasi}/platform_backend.pas) and every caller — coordination and memory, hence a ticket.

Note the posix send primitives now pass MSG_NOSIGNAL unconditionally (bug-b-fpsend-to-a-closed-peer-kills-the-process-msg-nosignal-is-never-passed). A flags parameter must OR that in rather than replace it, or the SIGPIPE bug returns for every caller that passes 0 — which is most of them.

FPC parity

FPC's Sockets passes flags straight to the syscall. This is a genuine parity gap in a unit whose header claims the FPC surface, not a deliberate divergence.

Resolution (2026-09-04)

The ticket named one arm of a double case

lib/rtl/sockets.pas was reported and was exactly as described. The sibling is lib/crtl/src/netinet/in.c, where send, recv, sendto and recvfrom each said (void)flags; — the same defect, from the same cause, on the surface that busybox and every C consumer actually use. Neither could be fixed alone: both sit on the same PAL primitives, and those had no flags slot. devdocs/dev/normalise-dont-special-case.md is explicit that the arm nobody fixes is the one that stays broken, so both go through one plumbing change.

What changed

The finding that changed the design — measured, not reasoned

Linux and lwIP use the same small bits for different flags. From /home/neo/esp/esp-idf/components/lwip/lwip/src/include/lwip/sockets.h:269-274, read on the box:

Linux lwIP
MSG_PEEK 2 1
MSG_WAITALL 0x100 2
MSG_OOB 1 4
MSG_DONTWAIT 0x40 8

So a PAL that passed Linux's flag word through to lwIP would deliver MSG_WAITALL where the caller asked for MSG_PEEK — and lwIP's own header marks WAITALL "Unimplemented", i.e. it would be ignored and the data consumed. That is this exact bug, on another target, with a different cause. Hence PAL-neutral numbers plus a per-backend translation.

The PAL's values sit at $00100000..$00800000, a range neither OS uses. Had they matched Linux's, the posix backend would work by accident with the translation missing, and the mistake would be invisible on the only targets that run the test.

An unknown bit is REFUSED, not masked. Masking is how the original bug looked like success. MSG_TRUNC is declared by both surfaces and carried by no backend, so a call passing it now answers -1/EINVAL instead of succeeding with the flag dropped.

Acceptance, measured

Not done here

sendmsg/recvmsg pass their flags down to send/recv and so inherit the fix, but they still ignore msg_name and control data — that is a separate, pre-existing limitation recorded in their own comment, not part of this ticket.

Log