From 59f2a32b45c904feaa60e079dc79f2a490a429fa Mon Sep 17 00:00:00 2001 From: Greg Haerr Date: Sun, 9 Aug 2026 18:02:40 -0700 Subject: [PATCH] [net] Fix network hang, leak and race issues in kernel and ktcp tcpdev_read()/tcpdev_connect(): Return -EPIPE or -ECONNREFUSED instead of no reply at all, which would hang the network due to the read/connect being blocked on a single reply buffer in the tcpdev ktcp<->kernel transfer driver. Fixes network stack hangs during read or connect when CB is deallocated through a received RST or any other reason that deallocates an active CB. tcp_established(): decrement tcpcb_need_push when a RST frees a CB that still has unread data to stop continual AVAIL_DATA notifications. tcp_synrecv(): free the cloned CB on RST rather than only resetting state, each half-open RST leaked ~4.5K. inet_process_tcpdev() / TDT_ACCEPT: Copy accept() result out immediately and and release bufin_sem, which breaks a bufin_sem/bufout_sem deadlock between ktcp and ftpd under load. --- elks/include/linuxmt/tcpdev.h | 3 +- elks/net/ipv4/af_inet.c | 44 +++++++++++++++++++---------- elkscmd/ktcp/config.h | 2 +- elkscmd/ktcp/tcp.c | 8 ++++-- elkscmd/ktcp/tcpdev.c | 36 ++++++++++++----------- elkscmd/rootfs_template/etc/net.cfg | 2 +- qemu.sh | 11 ++++++-- 7 files changed, 66 insertions(+), 40 deletions(-) diff --git a/elks/include/linuxmt/tcpdev.h b/elks/include/linuxmt/tcpdev.h index 8cf96e3af..03ed1bdc6 100644 --- a/elks/include/linuxmt/tcpdev.h +++ b/elks/include/linuxmt/tcpdev.h @@ -94,7 +94,8 @@ struct tdb_return_data { struct tdb_accept_ret { char type; int ret_value; - struct socket *sock; + struct socket *sock; /* listen socket, for wake_up */ + struct socket *newsock; /* accepted socket, gets addr filled in */ __u32 addr_ip; __u16 addr_port; __u32 locaddr; diff --git a/elks/net/ipv4/af_inet.c b/elks/net/ipv4/af_inet.c index cdf9f0819..792525454 100644 --- a/elks/net/ipv4/af_inet.c +++ b/elks/net/ipv4/af_inet.c @@ -39,7 +39,8 @@ static sem_t rwlock; /* global inet_read/write semaphore*/ int inet_process_tcpdev(register char *buf, int len) { - register struct socket *sock; + struct socket *sock, *newsock; + struct tdb_accept_ret *ar; sock = ((struct tdb_return_data *)buf)->sock; debug_net("INET(%P) process_tcpdev sock %x type %d wait %x\n", @@ -77,8 +78,27 @@ int inet_process_tcpdev(register char *buf, int len) wake_up(sock->wait); break; - case TDT_RETURN: case TDT_ACCEPT: + /* + * Copy accept result into newsock so we can auto-release bufin_sem + * immediately and not depend on the woken process running fast enough + * to consume tdin_buf before ktcp writes its next reply. + */ + ar = (struct tdb_accept_ret *)buf; + newsock = ar->newsock; + if (newsock) { + newsock->remaddr = ar->addr_ip; + newsock->remport = ar->addr_port; + newsock->localaddr = ar->locaddr; + newsock->localport = ar->locport; + newsock->retval = ar->ret_value; + newsock->flags |= SF_CONNECT; + } + tcpdev_clear_data_avail(); + wake_up(sock->wait); + break; + + case TDT_RETURN: case TDT_BIND: debug_net("INET(%P) retval %d bufin %d\n", ((struct tdb_return_data *)buf)->ret_value, bufin_sem); @@ -227,6 +247,7 @@ static int inet_accept(register struct socket *sock, struct socket *newsock, int int ret; debug_tune("INET(%P) accept wait sock %x newsock %x\n", sock, newsock); + newsock->flags &= ~SF_CONNECT; /* cleared until reply arrives */ cmd = (struct tdb_accept *)get_tdout_buf(); cmd->cmd = TDC_ACCEPT; cmd->sock = sock; @@ -235,25 +256,18 @@ static int inet_accept(register struct socket *sock, struct socket *newsock, int tcpdev_inetwrite(cmd, sizeof(struct tdb_accept)); - /* Sleep until tcpdev has news */ - do { /* always sleep once to prevent accept race condition #1082 */ - + /* Sleep until inet_process_tcpdev stores the reply into newsock */ + do { + /* Always sleep once to prevent accept race condition #1082 (unneeded anymore?) */ interruptible_sleep_on(sock->wait); - //interruptible_sleep_on(newsock->wait); - if (current->signal) { - debug_net("INET(%P) accept RESTARTSYS bufin %d\n", bufin_sem); + debug_net("INET(%P) accept RESTARTSYS\n"); return -ERESTARTSYS; } - } while (bufin_sem == 0); + } while (!(newsock->flags & SF_CONNECT)); debug_tune("INET(%P) accepted sock %x newsock %x\n", sock, newsock); - newsock->remaddr = ((struct tdb_accept_ret *)tdin_buf)->addr_ip; - newsock->remport = ((struct tdb_accept_ret *)tdin_buf)->addr_port; - newsock->localaddr = ((struct tdb_accept_ret *)tdin_buf)->locaddr; - newsock->localport = ((struct tdb_accept_ret *)tdin_buf)->locport; - ret = ((struct tdb_accept_ret *)tdin_buf)->ret_value; - tcpdev_clear_data_avail(); + ret = newsock->retval; if (ret >= 0) { newsock->state = SS_CONNECTED; ret = 0; diff --git a/elkscmd/ktcp/config.h b/elkscmd/ktcp/config.h index c8a3bb7f8..05b82121c 100644 --- a/elkscmd/ktcp/config.h +++ b/elkscmd/ktcp/config.h @@ -8,7 +8,7 @@ /* turn these on for ELKS debugging*/ #define USE_DEBUG_EVENT 1 /* use CTRLP to toggle debug output*/ #define DEBUG_STARTDEF 0 /* default startup debug display*/ -#define DEBUG_TCP 0 /* TCP ops*/ +#define DEBUG_TCP 1 /* TCP ops*/ #define DEBUG_TCPPKT 0 /* TCP packets info*/ #define DEBUG_TCPDATA 1 /* TCP packet data display*/ #define DEBUG_CWND 0 /* TCP congestion control*/ diff --git a/elkscmd/ktcp/tcp.c b/elkscmd/ktcp/tcp.c index 938765f93..9364f447e 100644 --- a/elkscmd/ktcp/tcp.c +++ b/elkscmd/ktcp/tcp.c @@ -247,6 +247,8 @@ static void tcp_established(struct iptcp_s *iptcp, struct tcpcb_s *cb) in_ntoa(cb->remaddr), ntohs(h->sport), ntohs(h->dport)); #endif rmv_all_retrans_cb(cb); + if (cb->bytes_to_push > 0) + tcpcb_need_push--; if (cb->state == TS_CLOSE_WAIT) { //cbs_in_user_timeout--; /* CLOSE_WAIT does not timeout */ @@ -354,8 +356,10 @@ static void tcp_synrecv(struct iptcp_s *iptcp, struct tcpcb_s *cb) { struct tcphdr_s *h = iptcp->tcph; - if (h->flags & TF_RST) - cb->state = TS_LISTEN; /* FIXME: not valid, should dealloc extra CB*/ + if (h->flags & TF_RST) { + rmv_all_retrans_cb(cb); + tcpcb_remove_cb(cb); + } else if ((h->flags & TF_ACK) == 0) debug_tcp("tcp: NO ACK IN SYNRECV\n"); else { diff --git a/elkscmd/ktcp/tcpdev.c b/elkscmd/ktcp/tcpdev.c index c1a32c532..c5e8a8d6f 100644 --- a/elkscmd/ktcp/tcpdev.c +++ b/elkscmd/ktcp/tcpdev.c @@ -95,7 +95,8 @@ static void tcpdev_bind(void) struct tcpcb_list_s *n2 = tcpcb_check_port(port); if (n2) { /* port already bound */ if (!db->reuse_addr) { /* no SO_REUSEADDR on socket */ - debug_tune("tcp: port %u already bound, rejecting (use SO_REUSEADDR?)\n", port); + debug_tune("tcp: port %u already bound, rejecting (use SO_REUSEADDR?)\n", + port); reject: tcpcb_remove(n); retval_to_sock(db->sock, -EADDRINUSE); @@ -104,11 +105,11 @@ static void tcpdev_bind(void) /* remove TCB control block on SO_REUSEADDR to save heap space */ if (n2->tcpcb.state == TS_TIME_WAIT) { - LEAVE_TIME_WAIT(&n2->tcpcb); /* entered via FIN_WAIT_2 state on FIN rcvd*/ + LEAVE_TIME_WAIT(&n2->tcpcb); /* entered via FIN_WAIT_2 state on FIN rcvd*/ tcpcb_remove(n2); - debug_tune("tcp: port %u REUSED, freeing previous socket in time_wait\n", port); + debug_tune("tcp: port %u reused, freeing socket in time_wait\n", port); } else { - printf("tcp: port %u NOT reused, previous socket in state %d\n", + debug_tune("tcp: port %u not reused, socket state %d\n", port, n2->tcpcb.state); goto reject; } @@ -168,8 +169,8 @@ static void tcpdev_accept(void) accept_ret.type = TDT_ACCEPT; accept_ret.ret_value = 0; - accept_ret.sock = sock; /* report back listen socket*/ - //accept_ret.sock = db->newsock; /* report back new socket*/ + accept_ret.sock = sock; /* report back listen socket, for wake_up */ + accept_ret.newsock = db->newsock; /* kernel stores addr into this sock */ accept_ret.addr_ip = cb->remaddr; accept_ret.addr_port = htons(cb->remport); accept_ret.locaddr = cb->localaddr; @@ -201,8 +202,8 @@ void tcpdev_notify_accept(struct tcpcb_s *cb) accept_ret.type = TDT_ACCEPT; accept_ret.ret_value = 0; - accept_ret.sock = listencb->sock; /* report back listen socket*/ - //accept_ret.sock = listencb->newsock; /* report back new socket*/ + accept_ret.sock = listencb->sock; /* report back listen socket, for wake_up */ + accept_ret.newsock = listencb->newsock; /* kernel stores addr into this sock */ accept_ret.addr_ip = cb->remaddr; accept_ret.addr_port = htons(cb->remport); accept_ret.locaddr = cb->localaddr; @@ -226,7 +227,8 @@ static void tcpdev_connect(void) n = tcpcb_find_by_sock(db->sock); if (!n || n->tcpcb.state != TS_CLOSED) { - debug_tcp("tcp: panic in connect\n"); + debug_tcp("tcp: connect on socket w/no CB\n"); + retval_to_sock(db->sock, -ECONNREFUSED); return; } @@ -274,13 +276,14 @@ static void tcpdev_read(void) n = tcpcb_find_by_sock(sock); if (!n || n->tcpcb.state == TS_CLOSED) { - printf("ktcp: panic in read\n"); + debug_tcp("tcp: read on socket w/no CB\n"); + retval_to_sock(sock, -EPIPE); return; } cb = &n->tcpcb; if (cb->state == TS_CLOSING || cb->state == TS_LAST_ACK || cb->state == TS_TIME_WAIT) { - printf("tcpdev_read: returning -EPIPE to socket read state %d\n", cb->state); + debug_tcp("tcp: read on socket w/invalid state %d\n", cb->state); retval_to_sock(sock, -EPIPE); return; } @@ -290,7 +293,7 @@ static void tcpdev_read(void) if (data_avail == 0) { if (cb->state == TS_CLOSE_WAIT) { - printf("tcpdev_read: read on CLOSE_WAIT socket, return -EPIPE\n"); + debug_tcp("tcp: read on CLOSE_WAIT socket\n"); retval_to_sock(sock, -EPIPE); } else if (db->nonblock) retval_to_sock(sock, -EAGAIN); @@ -385,17 +388,16 @@ static void tcpdev_write(void) n = tcpcb_find_by_sock(sock); if (!n || n->tcpcb.state == TS_CLOSED) { - printf("tcpdev_write: write to unknown socket\n"); + debug_tcp("tcp: write on socket w/no CB\n"); retval_to_sock(sock, -EPIPE); return; } cb = &n->tcpcb; - if (cb->state != TS_ESTABLISHED - /*&& cb->state != TS_CLOSE_WAIT*/) { // No write data if in CLOSE_WAIT - /* FIXME: May want to delete the printf below, this is not uncommon */ - printf("tcpdev_write: write to socket in improper state %d\n", cb->state); + /* Don't write data if not established (or CLOSE_WAIT) */ + if (cb->state != TS_ESTABLISHED /*&& cb->state != TS_CLOSE_WAIT*/ ) { + debug_tcp("tcp: write on socket w/invalid state %d\n", cb->state); retval_to_sock(sock, -EPIPE); return; } diff --git a/elkscmd/rootfs_template/etc/net.cfg b/elkscmd/rootfs_template/etc/net.cfg index f1a794667..240b57c5f 100644 --- a/elkscmd/rootfs_template/etc/net.cfg +++ b/elkscmd/rootfs_template/etc/net.cfg @@ -25,7 +25,7 @@ netstart="telnetd ftpd" # specific daemon command lines, named in netstart= telnetd="telnetd" -ftpd="ftpd -d -N 10.0.2.2 -P 49152:49153" +ftpd="ftpd -d -P 49152:49159" httpd="httpd" audiorcv="audiorcv -d -r 8000" diff --git a/qemu.sh b/qemu.sh index ff158a00e..04d581f56 100755 --- a/qemu.sh +++ b/qemu.sh @@ -87,13 +87,13 @@ CONSOLE="-serial stdio" # Incoming http forwarding: example: connect to ELKS httpd with 'http://localhost:8080' # HOSTFWD="-net user,hostfwd=tcp:127.0.0.1:8080-10.0.2.15:80" -# Simultaneous telnet, http and ftp forwarding -# Port 4950 is the audiorcv default, so a host audio sender can reach it +# Simultaneous telnet/2323, ftp/2121, http/8080, and audiorcv/4950 forwarding FWD="\ hostfwd=tcp:127.0.0.1:8080-10.0.2.15:80,\ hostfwd=tcp:127.0.0.1:2323-10.0.2.15:23,\ hostfwd=tcp::8020-:20,\ hostfwd=tcp::2121-:21,\ +hostfwd=tcp::4950-:4950,\ hostfwd=tcp::8041-:49821,\ hostfwd=tcp::8042-:49822,\ hostfwd=tcp::8043-:49823,\ @@ -105,7 +105,12 @@ hostfwd=tcp::8048-:49828,\ hostfwd=tcp::8049-:49829,\ hostfwd=tcp::49152-:49152,\ hostfwd=tcp::49153-:49153,\ -hostfwd=tcp::4950-:4950" +hostfwd=tcp::49154-:49154,\ +hostfwd=tcp::49155-:49155,\ +hostfwd=tcp::49156-:49156,\ +hostfwd=tcp::49157-:49157,\ +hostfwd=tcp::49158-:49158,\ +hostfwd=tcp::49159-:49159" # new style #NET="-net nic,model=ne2k_isa -net user,$FWD"