|
| StreamSec Tools 4.1.1.354 - 4.1.3.357 - manual fix P02 On Linux, macOS, iOS and Android a socket wait writes past the end of its descriptor set, for a descriptor of 1024 or more. | |
| Henrick Wibell Hellström 2026-09-25 23:23:03 Registered user |
WHAT GOES WRONG
tPosixSocket.WaitForData, and from 4.1.2.355 also WaitForWrite - the waits behind every stSocket connection on Linux, macOS, iOS and Android - wait with select(). A select() descriptor set has room for descriptors 0 to 1023 only, and adding a higher one writes past the end of the set, a variable on the stack; nothing reports it. Each socket holds three descriptors (the socket itself and the pipe that Send uses to end a wait), so a process whose open-file limit has been raised above 1024 reaches that range after a few hundred sockets. From there a wait returns at once instead of waiting, or the process fails with an access violation, in the wait or later, wherever the overwritten memory is next used. How many sockets a server has open is up to its clients: anyone who can connect to it can push its new sockets past descriptor 1024. A process left at the usual limit of 1024 open files never reaches that range. Windows is not affected: its socket layer is separate, and its descriptor sets are bounded. WHAT THE FIX DOES The waits use poll(), which has no such limit, instead of select(). Below descriptor 1024 they behave exactly as before: the same timeouts, the same readiness for data, for a peer that has closed and for a writable socket, and a Send (and from 4.1.2.355 a Wake) still ends a wait in the same way. WHO NEEDS IT Applications built with these releases that run on Linux, macOS, iOS or Android, connect through stSocket - which the TLS, HTTP, snip, SSH, mail and OCSP components do on those platforms - and run with an open-file limit above 1024. Servers matter most. APPLIES TO StreamSec Tools 4.1.1.354, 4.1.2.355, 4.1.3.356 and 4.1.3.357, in stSocket.Posix.pas, which first shipped in 4.1.1.354. The functions the fix replaces are the same in 4.1.2.355, 4.1.3.356 and 4.1.3.357. 4.1.1.354 has only WaitForData, in a simpler form, and has its own replacement (PART 2B). 4.1.3.358 and later carry the fix. HOW TO APPLY - in stSocket.Posix.pas 1. In the implementation section, directly before the line procedure SetNonBlocking(aFd: Integer); insert the declarations of PART 1. 2. Replace the function tPosixSocket.WaitForData, from "function" to its final "end;", with PART 2A - or, in 4.1.1.354, with PART 2B. 3. In 4.1.2.355, 4.1.3.356 and 4.1.3.357 only: replace the function tPosixSocket.WaitForWrite in the same way with PART 3. Leave the uses clause as it is. Then rebuild the applications for Linux, macOS, iOS and Android. The fix is pure Pascal; no assembler is involved. -- PART 1 ---------------------------------------------------------------------- { The waits use poll(), not select(): a select() descriptor set has room for descriptors 0 to 1023 only, and setting a higher one writes past its end. The RTL declares no poll, so it is declared here. } const POLLIN = $0001; POLLOUT = $0004; POLLERR = $0008; POLLHUP = $0010; POLLNVAL = $0020; { Readiness as select() counts it: a peer that has hung up, or a pending socket error, still reads as ready, and the next Receive or Send reports it. } cReadReady = POLLIN or POLLHUP or POLLERR; cWriteReady = POLLOUT or POLLERR; type pollfd = record fd: Integer; events: Int16; revents: Int16; end; Ppollfd = ^pollfd; function poll(fds: Ppollfd; nfds: NativeUInt; timeout: Integer): Integer; cdecl; external libc name _PU + 'poll'; { poll() reads a negative timeout as "wait for ever"; a Cardinal above MaxInt would wrap into one. } function PollTimeout(aMSec: Cardinal): Integer; begin if aMSec > Cardinal(MaxInt) then Result := MaxInt else Result := Integer(aMSec); end; -- PART 2A: WaitForData, 4.1.2.355 - 4.1.3.357 ------------------------------- function tPosixSocket.WaitForData(aMSec: Cardinal): Boolean; var lFds: array[0..1] of pollfd; lRes: Integer; lJunk: array[0..63] of Byte; begin Result := False; if fSocket = cInvalidSocket then Exit; // A wake byte left by a Send while nobody waited must not end this wait. while __read(fWakeRd, @lJunk, SizeOf(lJunk)) > 0 do ; // An explicit Wake must: it is sticky. if TakePendingWake then Exit; lFds[0].fd := fSocket; lFds[0].events := POLLIN; lFds[0].revents := 0; lFds[1].fd := fWakeRd; lFds[1].events := POLLIN; lFds[1].revents := 0; lRes := poll(@lFds[0], 2, PollTimeout(aMSec)); if lRes <= 0 then Exit; // 0 = timeout, <0 = error/EINTR // A descriptor that is not open is not readiness. if ((lFds[0].revents or lFds[1].revents) and POLLNVAL) <> 0 then Exit; if (lFds[1].revents and cReadReady) <> 0 then begin // a Send or a Wake while __read(fWakeRd, @lJunk, SizeOf(lJunk)) > 0 do ; TakePendingWake; // delivered: do not keep it Lock; Unlock; // synchronise with the writer Exit; // report "no read data" end; Result := (lFds[0].revents and cReadReady) <> 0; end; -- PART 2B: WaitForData, 4.1.1.354 -------------------------------------------- function tPosixSocket.WaitForData(aMSec: Cardinal): Boolean; var lFds: array[0..1] of pollfd; lRes: Integer; lJunk: array[0..63] of Byte; begin Result := False; if fSocket = cInvalidSocket then Exit; // A wake byte left by a Send while nobody waited must not end this wait. while __read(fWakeRd, @lJunk, SizeOf(lJunk)) > 0 do ; lFds[0].fd := fSocket; lFds[0].events := POLLIN; lFds[0].revents := 0; lFds[1].fd := fWakeRd; lFds[1].events := POLLIN; lFds[1].revents := 0; lRes := poll(@lFds[0], 2, PollTimeout(aMSec)); if lRes <= 0 then Exit; // 0 = timeout, <0 = error/EINTR // A descriptor that is not open is not readiness. if ((lFds[0].revents or lFds[1].revents) and POLLNVAL) <> 0 then Exit; if (lFds[1].revents and cReadReady) <> 0 then begin // a Send while __read(fWakeRd, @lJunk, SizeOf(lJunk)) > 0 do ; Lock; Unlock; // synchronise with the writer Exit; // report "no read data" end; Result := (lFds[0].revents and cReadReady) <> 0; end; -- PART 3: WaitForWrite, 4.1.2.355 - 4.1.3.357 ------------------------------- function tPosixSocket.WaitForWrite(aMSec: Cardinal): Boolean; var lFds: array[0..1] of pollfd; lRes: Integer; lJunk: array[0..63] of Byte; begin Result := False; if fSocket = cInvalidSocket then Exit; while __read(fWakeRd, @lJunk, SizeOf(lJunk)) > 0 do ; if TakePendingWake then Exit; lFds[0].fd := fSocket; lFds[0].events := POLLOUT; lFds[0].revents := 0; lFds[1].fd := fWakeRd; lFds[1].events := POLLIN; lFds[1].revents := 0; lRes := poll(@lFds[0], 2, PollTimeout(aMSec)); if lRes <= 0 then Exit; // 0 = timeout, <0 = error/EINTR if ((lFds[0].revents or lFds[1].revents) and POLLNVAL) <> 0 then Exit; if (lFds[1].revents and cReadReady) <> 0 then begin // a Wake while __read(fWakeRd, @lJunk, SizeOf(lJunk)) > 0 do ; TakePendingWake; Exit; // woken, not writable end; Result := (lFds[0].revents and cWriteReady) <> 0; end; |