A FIN consumes a sequence number. If another FIN segment is coalesced into an skb that already carries FIN, end_seq advances to account for the additional FIN, but tcp_flags still contains only one FIN bit. When the preceding gap is filled and tcp_ofo_queue() processes the coalesced skb, rcv_nxt advances to the skb's end_seq. When the receive path consumes all data in the skb, copied_seq advances by the data length and one sequence number for FIN. If three or more FIN segments have been coalesced into one skb, tcp_inq() can still return a positive value after the receive queue becomes empty and the SOCK_DONE adjustment is applied. Software kTLS receive is one consumer affected by this state. When strp->copy_mode is false, tls_strp_msg_consume() calls the TCP helper tcp_read_done() to consume the current TLS record. In the reproducer, tcp_read_done() consumed the record data and one FIN before freeing the receive skb. When kTLS looked for the next record, tcp_inq() reported at least TLS_HEADER_SIZE bytes even though the receive queue was empty. tcp_recv_skb() returned NULL, so tls_strp_load_anchor_with_queue() hit WARN_ON_ONCE() and returned without updating the anchor. Since tcp_inq() had reported at least TLS_HEADER_SIZE, tls_strp_read_sock() proceeded to tls_strp_check_queue_ok(). skb_shinfo(strp->anchor)->frag_list still pointed to the freed receive skb. tls_strp_check_queue_ok() then read TCP_SKB_CB(first)->seq through the stale pointer, causing a use-after-free. The reproducer followed this call path: tls_rx_rec_done() tls_strp_msg_consume() tcp_read_done() // frees the receive skb tls_strp_check_rcv() tls_strp_read_sock() tls_strp_load_anchor_with_queue() // returns after WARN_ON_ONCE() tcp_recv_skb() == NULL tls_strp_check_queue_ok() TCP_SKB_CB(first)->seq // reads the freed receive skb KASAN reported: BUG: KASAN: slab-use-after-free in tls_strp_check_queue_ok.isra.0+0x5f/0x100 Read of size 4 at addr ffff888103169ca8 by task ktls-server/71 ... Call Trace: ... tls_strp_check_queue_ok.isra.0+0x5f/0x100 tls_strp_check_rcv+0x2a9/0x400 ... tls_sw_recvmsg+0x3ce/0xa10 ... sock_recvmsg+0xea/0x100 __sys_recvfrom+0x142/0x1f0 ... __x64_sys_recvfrom+0x76/0x90 do_syscall_64+0xdd/0x4a0 entry_SYSCALL_64_after_hwframe+0x77/0x7f ... ... Freed by task 71: ... napi_skb_cache_put+0x24/0xf0 skb_attempt_defer_free+0x183/0x3b0 tcp_read_done+0x2d4/0x340 tls_strp_msg_consume+0x45/0x80 tls_sw_recvmsg+0x3c4/0xa10 ... In tcp_ooo_try_coalesce() and tcp_add_backlog(), skip coalescing when the destination skb already carries FIN. This still allows a FIN segment to be coalesced into an skb without FIN when the other coalescing checks pass. Fixes: e93a0435f809 ("tcp: allow segment with FIN in tcp_try_coalesce()") Fixes: 4f693b55c3d2 ("tcp: implement coalescing on backlog queue") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Hyunwoo Kim --- net/ipv4/tcp_input.c | 3 ++- net/ipv4/tcp_ipv4.c | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c index e1f2d9765f810..1f9f86028da73 100644 --- a/net/ipv4/tcp_input.c +++ b/net/ipv4/tcp_input.c @@ -5314,7 +5314,8 @@ static bool tcp_ooo_try_coalesce(struct sock *sk, struct sk_buff *from, bool *fragstolen) { - bool res = tcp_try_coalesce(sk, to, from, fragstolen); + bool res = !(TCP_SKB_CB(to)->tcp_flags & TCPHDR_FIN) && + tcp_try_coalesce(sk, to, from, fragstolen); /* In case tcp_drop_reason() is called later, update to->gso_segs */ if (res) { diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c index a7f9242f1e429..ef01768292fbc 100644 --- a/net/ipv4/tcp_ipv4.c +++ b/net/ipv4/tcp_ipv4.c @@ -1949,6 +1949,7 @@ enum skb_drop_reason tcp_add_backlog(struct sock *sk, struct sk_buff *skb) thtail = (struct tcphdr *)tail->data; if (TCP_SKB_CB(tail)->end_seq != TCP_SKB_CB(skb)->seq || + (TCP_SKB_CB(tail)->tcp_flags & TCPHDR_FIN) || TCP_SKB_CB(tail)->ip_dsfield != TCP_SKB_CB(skb)->ip_dsfield || ((TCP_SKB_CB(tail)->tcp_flags | TCP_SKB_CB(skb)->tcp_flags) & (TCPHDR_SYN | TCPHDR_RST | TCPHDR_URG)) || -- 2.43.0