Compare commits

..

2 Commits

Author SHA1 Message Date
Vladimir Dubrovin
48d72538e1 Widen the port window a test binds in where the kernel does not pick it
Some checks failed
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Has been cancelled
bindwithrange() honours a range by setting IP_LOCAL_PORT_RANGE where it
exists, which leaves the kernel to pick a port inside it and skip the ones
still closing. Everywhere else it binds a port out of the range at random,
ten times, and lets the system choose when all ten are taken.

A port which carried a connection cannot be bound again until that close
completes - four minutes of it on Windows - so a window of fifty ports
shared by three services in one configuration can have too few left, and
the connection then binds outside the range and the case fails. The window
is five hundred ports there instead. The Linux one is unchanged: the
kernel picks from it and needs no room to spare.
2026-08-29 22:48:39 +03:00
Vladimir Dubrovin
2b8845f65a Fix: null pointer dereference in ftppr / smtpp 2026-08-29 22:29:56 +03:00
4 changed files with 50 additions and 11 deletions

View File

@ -64,6 +64,13 @@ void * ftpprchild(struct clientparam* param) {
}
else if (!strncasecmp((char *)buf, "PASS ", 5)){
/* The user name carries the server to log in to, and it arrives
with USER. Without it there is nothing to log in to, and what
follows would read the name and the host as if there were. */
if(!param->hostname || !param->extusername){
socksend(param, param->ctrlsock, (unsigned char *)"503 Login with USER first\r\n", 27, conf.timeouts[STRING_S]);
RETURN(805);
}
param->extpassword = (unsigned char *)strdup((char *)buf+5);
inbuf = BUFSIZE;
res = ftplogin(param, (char *)buf, &inbuf);

View File

@ -159,7 +159,10 @@ void * smtppchild(struct clientparam* param) {
i = de64(buf,username,255);
if(i < 1) {RETURN(664);}
username[i] = 0;
parseconnusername((char *)username, param, 0, 587);
/* The name has to carry the host to connect to, and the answer says
whether it did: without one there is nowhere to go, and what follows
reads the name as if there were. */
if(parseconnusername((char *)username, param, 0, 587)) {RETURN(669);}
socksend(param, param->clisock, (unsigned char *)"334 UGFzc3dvcmQ6\r\n", 18,conf.timeouts[STRING_S]);
i = sockgetlinebuf(param, CLIENT, buf, sizeof(buf) - 10, '\n', conf.timeouts[STRING_S]);
if(i < 2) {RETURN(665);}
@ -184,7 +187,7 @@ void * smtppchild(struct clientparam* param) {
}
if(i < 3 || *username) {RETURN(668);}
username[i] = 0;
parseconnusername((char *)username+1, param, 0, 587);
if(parseconnusername((char *)username+1, param, 0, 587)) {RETURN(670);}
res = (int)strlen((char *)username+1) + 2;
if(res < i){
if(param->extpassword) free(param->extpassword);

View File

@ -14,12 +14,19 @@ def _windows():
net.ipv4.ip_local_port_range; a window outside it is ignored and an
ordinary ephemeral port is used, so a fixed low window would be
measuring the kernel's own choice rather than the setting.
Everywhere else the range is honoured by binding a port out of it at
random, ten times before giving up and letting the system choose. A port
which carried a connection a moment ago cannot be bound again while it
waits out its close - four minutes of it on Windows - so the window has
to be wide enough that ten tries do not all land on one. The window the
kernel picks from on Linux needs no such room, since it skips them.
"""
try:
with open("/proc/sys/net/ipv4/ip_local_port_range") as fp:
low, high = (int(part) for part in fp.read().split()[:2])
except (OSError, ValueError):
return (21400, 21449), (21500, 21549)
return (21400, 21899), (22000, 22499)
base = low + 1000 if low + 1150 <= high else low
return (base, base + 49), (base + 100, base + 149)

View File

@ -22,6 +22,7 @@ configurations it needs, starts them, and states what it expects:
import base64
import http.client
import os
import random
import shutil
import socket
import ssl
@ -33,6 +34,11 @@ import threading
import time
# Ports handed out in this run: a service which has finished may still be
# in TIME_WAIT, and another case binding the same port would fail for it.
_PORTS_TAKEN = set()
class Response:
"""A reply, or the reason there wasn't one."""
@ -146,14 +152,30 @@ class Tester:
sock.close()
def free_port(self):
"""A port nothing is listening on. Closed again before it is used,
which is racy in principle and reliable enough in practice."""
s = socket.socket()
"""A port nothing is listening on, and nothing is likely to take.
Asking the system for an ephemeral port hands back one out of the
range it also draws outgoing connections from - 32768 up on Linux,
49152 up on Windows - so between the check here and the bind in the
service, a connection somewhere else in the suite can take it. That
shows up as a service which never listens, or a bind() error deep in
a case which has nothing to do with ports. Ports are taken from below
both ranges instead, and none is handed out twice in a run.
"""
for _ in range(200):
port = random.randint(10000, 19999)
if port in _PORTS_TAKEN:
continue
sock = socket.socket()
try:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
sock.bind(("127.0.0.1", port))
except OSError:
continue
finally:
s.close()
sock.close()
_PORTS_TAKEN.add(port)
return port
raise RuntimeError("no free port in the range the suite uses")
def write_config(self, name, config):
path = os.path.join(self.tmpdir, name + ".cfg")