Re: [PATCH net-next v11 06/15] quic: add stream management
Xin Long <[email protected]> Thu, 26 Mar 2026 11:06:26 -0400
| Newsgroups | dev.linux.lists.quic,dev.linux.lists.kernel-tls-handshake,org.kernel.vger.linux-cifs,org.kernel.vger.netdev |
|---|---|
| Message-ID | <CADvbK_eXUfnGGQgGc_k-ecCW3-Co1MpGb7UvMgLonPy3HRhyFA@mail.gmail.com> |
On Tue, Mar 24, 2026 at 11:49=E2=80=AFPM Xin Long <[email protected]> wr= ote: > > This patch introduces 'struct quic_stream_table' for managing QUIC stream= s, > each represented by 'struct quic_stream'. > > It implements mechanisms for acquiring and releasing streams on both the > send and receive paths, ensuring efficient lifecycle management during > transmission and reception. > > - quic_stream_get(): Acquire a send-side stream by ID and flags during > TX path, or a receive-side stream by ID during RX path. > > - quic_stream_put(): Release a send-side stream when sending is done, > or a receive-side stream when receiving is done. > > It includes logic to detect when stream ID limits are reached and when > control frames should be sent to update or request limits from the peer. > > - quic_stream_id_exceeds(): Check a stream ID would exceed local (recv) > or peer (send) limits. > > - quic_stream_max_streams_update(): Determines whether a > MAX_STREAMS_UNI/BIDI frame should be sent to the peer. > > Note stream hash table is per socket, the operations on it are always > protected by the sock lock. > > Signed-off-by: Xin Long <[email protected]> > Acked-by: Paolo Abeni <[email protected]> > --- > v3: > - Merge send/recv stream helpers into unified functions to reduce code: > * quic_stream_id_send/recv() =E2=86=92 quic_stream_id_valid() > * quic_stream_id_send/recv_closed() =E2=86=92 quic_stream_id_closed() > * quic_stream_id_send/recv_exceeds() =E2=86=92 quic_stream_id_exceeds= () > (pointed out by Paolo). > - Clarify in changelog that stream hash table is always protected by so= ck > lock (suggested by Paolo). > - quic_stream_init/free(): adjust for new hashtable type; call > quic_stream_delete() in quic_stream_free() to avoid open-coded logic. > - Receiving streams: delete stream only when fully read or reset, inste= ad > of when no data was received. Prevents freeing a stream while a FIN > with no data is still queued. > v4: > - Replace struct quic_shash_table with struct hlist_head for the > stream hashtable. Since they are protected by the socket lock, > no per-chain lock is needed. > - Initialize stream to NULL in stream creation functions to avoid > warnings from Smatch (reported by Simon). > - Allocate send streams with GFP_KERNEL_ACCOUNT and receive streams > with GFP_ATOMIC | __GFP_ACCOUNT for memory accounting (suggested > by Paolo). > v5: > - Introduce struct quic_stream_limits to merge quic_stream_send_create(= ) > and quic_stream_recv_create(), and to simplify quic_stream_get_param(= ) > (suggested by Paolo). > - Annotate the sock-lock requirement for quic_stream_send/recv_get() > and quic_stream_send/recv_put() (notied by Paolo). > - Add quic_stream_bidi_put() to deduplicate the common logic between > quic_stream_send_put() and quic_stream_recv_put(). > - Remove the unnecessary check when incrementing > streams->send.next_bidi/uni_stream_id in quic_stream_create(). > - Remove the unused 'is_serv' parameter from quic_stream_get_param(). > v7: > - Free the allocated streams on error path in quic_stream_create() (not= ed > by Paolo). > - Merge quic_stream_send_get/put() and quic_stream_recv_get/put() helpe= rs > to quic_stream_get/put() (suggested by Paolo). > - Add more comments in quic_stream_id_exceeds() and quic_stream_create(= ). > v8: > - Replace bitfields with plain u8 in struct quic_stream_limits and stru= ct > quic_stream (suggested by Paolo). > v9: > - Fix grammar in the comment for quic_stream::send.window. > v10: > - Move quic_stream_init() to after sock_prot_inuse_add() ensure counter= s > are incremented before any early return paths in quic_init_sock(), > preventing underflow in quic_destroy_sock() (noted by AI review). > - Initialize the output parameters '*max_uni' and '*max_bidi' to 0 at t= he > start of quic_stream_max_streams_update() > - Use 'stream->recv.state > QUIC_STREAM_RECV_STATE_RECVD' instead of '!= =3D' > for clearer intent. > - Simplify some state checks in quic_stream_put() by using range > comparisons (> or <) instead of multiple !=3D conditions. > - streams_uni/bidi are u16 type, and their overflow is already prevente= d > by QUIC_MAX_STREAMS indirectly. Update comment in quic_stream_create(= ). > - Replace open-coded kzalloc(sizeof(*stream)) with kzalloc_obj(*stream) > in quic_stream_create(). > v11: > - Set maximum line length to 80 characters. > - Change is_serv parameter type to bool in quic_stream_id_local(). > --- > net/quic/Makefile | 2 +- > net/quic/socket.c | 5 + > net/quic/socket.h | 8 + > net/quic/stream.c | 444 ++++++++++++++++++++++++++++++++++++++++++++++ > net/quic/stream.h | 133 ++++++++++++++ > 5 files changed, 591 insertions(+), 1 deletion(-) > create mode 100644 net/quic/stream.c > create mode 100644 net/quic/stream.h > > diff --git a/net/quic/Makefile b/net/quic/Makefile > index 13bf4a4e5442..094e9da5d739 100644 > --- a/net/quic/Makefile > +++ b/net/quic/Makefile > @@ -5,4 +5,4 @@ > > obj-$(CONFIG_IP_QUIC) +=3D quic.o > > -quic-y :=3D common.o family.o protocol.o socket.o > +quic-y :=3D common.o family.o protocol.o socket.o stream.o > diff --git a/net/quic/socket.c b/net/quic/socket.c > index 8dc2cb7628db..0006668551f4 100644 > --- a/net/quic/socket.c > +++ b/net/quic/socket.c > @@ -45,11 +45,16 @@ static int quic_init_sock(struct sock *sk) > sk_sockets_allocated_inc(sk); > sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1); > > + if (quic_stream_init(quic_streams(sk))) > + return -ENOMEM; > + > return 0; > } > > static void quic_destroy_sock(struct sock *sk) > { > + quic_stream_free(quic_streams(sk)); > + > quic_data_free(quic_ticket(sk)); > quic_data_free(quic_token(sk)); > quic_data_free(quic_alpn(sk)); > diff --git a/net/quic/socket.h b/net/quic/socket.h > index 61df0c5867be..e76737b9b74b 100644 > --- a/net/quic/socket.h > +++ b/net/quic/socket.h > @@ -13,6 +13,7 @@ > > #include "common.h" > #include "family.h" > +#include "stream.h" > > #include "protocol.h" > > @@ -33,6 +34,8 @@ struct quic_sock { > struct quic_data ticket; > struct quic_data token; > struct quic_data alpn; > + > + struct quic_stream_table streams; > }; > > struct quic6_sock { > @@ -65,6 +68,11 @@ static inline struct quic_data *quic_alpn(const struct= sock *sk) > return &quic_sk(sk)->alpn; > } > > +static inline struct quic_stream_table *quic_streams(const struct sock *= sk) > +{ > + return &quic_sk(sk)->streams; > +} > + > static inline bool quic_is_serv(const struct sock *sk) > { > return !!sk->sk_max_ack_backlog; > diff --git a/net/quic/stream.c b/net/quic/stream.c > new file mode 100644 > index 000000000000..4d980f9b03ce > --- /dev/null > +++ b/net/quic/stream.c > @@ -0,0 +1,444 @@ > +// SPDX-License-Identifier: GPL-2.0-or-later > +/* QUIC kernel implementation > + * (C) Copyright Red Hat Corp. 2023 > + * > + * This file is part of the QUIC kernel implementation > + * > + * Initialization/cleanup for QUIC protocol support. > + * > + * Written or modified by: > + * Xin Long <[email protected]> > + */ > + > +#include <linux/quic.h> > + > +#include "common.h" > +#include "stream.h" > + > +/* Check if a stream ID is valid for sending or receiving. */ > +static bool quic_stream_id_valid(s64 stream_id, bool is_serv, bool send) > +{ > + u8 type =3D (stream_id & QUIC_STREAM_TYPE_MASK); > + > + if (send) { > + if (is_serv) > + return type !=3D QUIC_STREAM_TYPE_CLIENT_UNI; > + return type !=3D QUIC_STREAM_TYPE_SERVER_UNI; > + } > + if (is_serv) > + return type !=3D QUIC_STREAM_TYPE_SERVER_UNI; > + return type !=3D QUIC_STREAM_TYPE_CLIENT_UNI; > +} > + > +/* Check if a stream ID was initiated locally. */ > +static bool quic_stream_id_local(s64 stream_id, bool is_serv) > +{ > + return is_serv ^ !(stream_id & QUIC_STREAM_TYPE_SERVER_MASK); > +} > + > +/* Check if a stream ID represents a unidirectional stream. */ > +static bool quic_stream_id_uni(s64 stream_id) > +{ > + return stream_id & QUIC_STREAM_TYPE_UNI_MASK; > +} > + > +#define QUIC_STREAM_HT_SIZE 64 > + > +static struct hlist_head *quic_stream_head(struct quic_stream_table *str= eams, > + s64 stream_id) > +{ > + return &streams->head[stream_id & (QUIC_STREAM_HT_SIZE - 1)]; > +} > + > +struct quic_stream *quic_stream_find(struct quic_stream_table *streams, > + s64 stream_id) > +{ > + struct hlist_head *head =3D quic_stream_head(streams, stream_id); > + struct quic_stream *stream; > + > + hlist_for_each_entry(stream, head, node) { > + if (stream->id =3D=3D stream_id) > + break; > + } > + return stream; > +} > + > +static void quic_stream_add(struct quic_stream_table *streams, > + struct quic_stream *stream) > +{ > + struct hlist_head *head; > + > + head =3D quic_stream_head(streams, stream->id); > + hlist_add_head(&stream->node, head); > +} > + > +static void quic_stream_delete(struct quic_stream *stream) > +{ > + hlist_del_init(&stream->node); > + kfree(stream); > +} > + > +/* Create and register new streams for sending or receiving. */ > +static struct quic_stream *quic_stream_create(struct quic_stream_table *= streams, > + s64 max_stream_id, bool sen= d, > + bool is_serv) > +{ > + struct quic_stream_limits *limits =3D &streams->send; > + struct quic_stream *pos, *stream =3D NULL; > + gfp_t gfp =3D GFP_KERNEL_ACCOUNT; > + struct hlist_node *tmp; > + HLIST_HEAD(head); > + s64 stream_id; > + u32 count =3D 0; > + > + if (!send) { > + limits =3D &streams->recv; > + gfp =3D GFP_ATOMIC | __GFP_ACCOUNT; > + } > + stream_id =3D limits->next_bidi_stream_id; > + if (quic_stream_id_uni(max_stream_id)) > + stream_id =3D limits->next_uni_stream_id; > + > + /* rfc9000#section-2.1: A stream ID that is used out of order res= ults in > + * all streams of that type with lower-numbered stream IDs also b= eing > + * opened. > + */ > + while (stream_id <=3D max_stream_id) { > + stream =3D kzalloc_obj(*stream, gfp); > + if (!stream) > + goto free; > + > + stream->id =3D stream_id; > + if (quic_stream_id_uni(stream_id)) { > + if (send) { > + stream->send.max_bytes =3D > + limits->max_stream_data_uni; > + } else { > + stream->recv.max_bytes =3D > + limits->max_stream_data_uni; > + stream->recv.window =3D stream->recv.max_= bytes; > + } > + hlist_add_head(&stream->node, &head); > + stream_id +=3D QUIC_STREAM_ID_STEP; > + continue; > + } > + > + if (quic_stream_id_local(stream_id, is_serv)) { > + stream->send.max_bytes =3D > + streams->send.max_stream_data_bidi_remote= ; > + stream->recv.max_bytes =3D > + streams->recv.max_stream_data_bidi_local; > + } else { > + stream->send.max_bytes =3D > + streams->send.max_stream_data_bidi_local; > + stream->recv.max_bytes =3D > + streams->recv.max_stream_data_bidi_remote= ; > + } > + stream->recv.window =3D stream->recv.max_bytes; > + hlist_add_head(&stream->node, &head); > + stream_id +=3D QUIC_STREAM_ID_STEP; > + } > + > + hlist_for_each_entry_safe(pos, tmp, &head, node) { > + hlist_del_init(&pos->node); > + quic_stream_add(streams, pos); > + count++; > + } > + > + /* Streams must be opened sequentially. Update the next stream ID= so the > + * correct starting point is known if an out-of-order open is req= uested. > + * Note overflow of next_uni/bidi_stream_id is impossible with s6= 4. > + */ > + if (quic_stream_id_uni(stream_id)) { > + limits->next_uni_stream_id =3D stream_id; > + limits->streams_uni +=3D count; > + return stream; > + } > + > + limits->next_bidi_stream_id =3D stream_id; > + limits->streams_bidi +=3D count; > + return stream; > + > +free: > + hlist_for_each_entry_safe(pos, tmp, &head, node) { > + hlist_del_init(&pos->node); > + kfree(pos); > + } > + return NULL; > +} > + > +/* Check if a send or receive stream ID is already closed. */ > +static bool quic_stream_id_closed(struct quic_stream_table *streams, > + s64 stream_id, bool send) > +{ > + struct quic_stream_limits *limits =3D send ? &streams->send : > + &streams->recv; > + > + if (quic_stream_id_uni(stream_id)) > + return stream_id < limits->next_uni_stream_id; > + return stream_id < limits->next_bidi_stream_id; > +} > + > +/* Check if a stream ID would exceed local (recv) or peer (send) limits.= */ > +bool quic_stream_id_exceeds(struct quic_stream_table *streams, s64 strea= m_id, > + bool send) > +{ > + u64 nstreams; > + > + if (!send) { > + /* recv.max_uni_stream_id is updated in > + * quic_stream_max_streams_update() already based on > + * next_uni/bidi_stream_id, max_streams_uni/bidi, and > + * streams_uni/bidi, so only recv.max_uni_stream_id needs= to be > + * checked. > + */ > + if (quic_stream_id_uni(stream_id)) > + return stream_id > streams->recv.max_uni_stream_i= d; > + > + return stream_id > streams->recv.max_bidi_stream_id; > + } > + > + if (quic_stream_id_uni(stream_id)) { > + if (stream_id > streams->send.max_uni_stream_id) > + return true; > + stream_id -=3D streams->send.next_uni_stream_id; > + nstreams =3D quic_stream_id_to_streams(stream_id); > + > + return nstreams + streams->send.streams_uni > > + streams->send.max_streams_uni; > + } > + > + if (stream_id > streams->send.max_bidi_stream_id) > + return true; > + stream_id -=3D streams->send.next_bidi_stream_id; > + nstreams =3D quic_stream_id_to_streams(stream_id); > + > + return nstreams + streams->send.streams_bidi > > + streams->send.max_streams_bidi; > +} > + > +/* Get or create a send or recv stream by ID. Requires sock lock held. *= / > +struct quic_stream *quic_stream_get(struct quic_stream_table *streams, > + s64 stream_id, u32 flags, bool is_ser= v, > + bool send) > +{ > + struct quic_stream *stream; > + > + if (!quic_stream_id_valid(stream_id, is_serv, send)) > + return ERR_PTR(-EINVAL); > + > + stream =3D quic_stream_find(streams, stream_id); > + if (stream) { > + if (send && (flags & MSG_QUIC_STREAM_NEW) && > + stream->send.state !=3D QUIC_STREAM_SEND_STATE_READY) > + return ERR_PTR(-EINVAL); > + return stream; > + } > + > + if (!send && quic_stream_id_local(stream_id, is_serv)) { > + if (quic_stream_id_closed(streams, stream_id, !send)) > + return ERR_PTR(-ENOSTR); > + return ERR_PTR(-EINVAL); > + } > + > + if (quic_stream_id_closed(streams, stream_id, send)) > + return ERR_PTR(-ENOSTR); > + > + if (send && !(flags & MSG_QUIC_STREAM_NEW)) > + return ERR_PTR(-EINVAL); > + > + if (quic_stream_id_exceeds(streams, stream_id, send)) > + return ERR_PTR(-EAGAIN); > + > + stream =3D quic_stream_create(streams, stream_id, send, is_serv); > + if (!stream) > + return ERR_PTR(-ENOSTR); > + > + if (send || quic_stream_id_valid(stream_id, is_serv, !send)) > + streams->send.active_stream_id =3D stream_id; > + > + return stream; > +} > + > +/* Release or clean up a send or recv stream. This function updates stre= am > + * counters and state when a send stream has either successfully sent al= l data > + * or has been reset, or when a recv stream has either consumed all data= or has > + * been reset. Requires sock lock held. > + */ > +void quic_stream_put(struct quic_stream_table *streams, > + struct quic_stream *stream, bool is_serv, bool send) > +{ > + if (quic_stream_id_uni(stream->id)) { > + if (send) { > + /* For uni streams, decrement uni count and delet= e > + * immediately. > + */ > + streams->send.streams_uni--; > + quic_stream_delete(stream); > + return; > + } > + /* For uni streams, decrement uni count and mark done. */ > + if (!stream->recv.done) { > + stream->recv.done =3D 1; > + streams->recv.streams_uni--; > + streams->recv.uni_pending =3D 1; > + } > + /* Delete stream if fully read or reset. */ > + if (stream->recv.state > QUIC_STREAM_RECV_STATE_RECVD) > + quic_stream_delete(stream); > + return; > + } > + > + if (send) { > + /* For bidi streams, only proceed if receive side is in a= final > + * state. > + */ > + if (stream->recv.state < QUIC_STREAM_RECV_STATE_RECVD) > + return; > + } else { > + /* For bidi streams, only proceed if send side is in a fi= nal > + * state. > + */ > + if (stream->send.state !=3D QUIC_STREAM_SEND_STATE_RECVD = && > + stream->send.state !=3D QUIC_STREAM_SEND_STATE_RESET_= RECVD) > + return; > + } > + > + if (quic_stream_id_local(stream->id, is_serv)) { > + /* Local-initiated stream: mark send done and decrement > + * send.bidi count. > + */ > + if (!stream->send.done) { > + stream->send.done =3D 1; > + streams->send.streams_bidi--; > + } > + } else { > + /* Remote-initiated stream: mark recv done and decrement = recv > + * bidi count. > + */ > + if (!stream->recv.done) { > + stream->recv.done =3D 1; > + streams->recv.streams_bidi--; > + streams->recv.bidi_pending =3D 1; > + } > + } > + > + /* Delete stream if fully read or reset. */ > + if (stream->recv.state > QUIC_STREAM_RECV_STATE_RECVD) > + quic_stream_delete(stream); > +} > + > +/* Updates the maximum allowed incoming stream IDs if any streams were r= ecently > + * closed. Recalculates the max_uni and max_bidi stream ID limits based= on the > + * number of open streams and whether any were marked for deletion. > + * > + * Returns true if either max_uni or max_bidi was updated, indicating th= at a > + * MAX_STREAMS_UNI or MAX_STREAMS_BIDI frame should be sent to the peer. > + */ > +bool quic_stream_max_streams_update(struct quic_stream_table *streams, > + s64 *max_uni, s64 *max_bidi) > +{ > + s64 max, rem; > + > + *max_uni =3D 0; > + *max_bidi =3D 0; > + if (streams->recv.uni_pending) { > + rem =3D streams->recv.max_streams_uni - streams->recv.str= eams_uni; > + max =3D streams->recv.next_uni_stream_id - QUIC_STREAM_ID= _STEP + > + (rem << QUIC_STREAM_TYPE_BITS); > + > + streams->recv.max_uni_stream_id =3D max; > + *max_uni =3D quic_stream_id_to_streams(max); > + streams->recv.uni_pending =3D 0; > + } > + if (streams->recv.bidi_pending) { > + rem =3D streams->recv.max_streams_bidi - > + streams->recv.streams_bidi; > + max =3D streams->recv.next_bidi_stream_id - QUIC_STREAM_I= D_STEP + > + (rem << QUIC_STREAM_TYPE_BITS); > + > + streams->recv.max_bidi_stream_id =3D max; > + *max_bidi =3D quic_stream_id_to_streams(max); > + streams->recv.bidi_pending =3D 0; > + } > + > + return *max_uni || *max_bidi; > +} > + > +int quic_stream_init(struct quic_stream_table *streams) > +{ > + struct hlist_head *head; > + int i; > + > + head =3D kmalloc_array(QUIC_STREAM_HT_SIZE, sizeof(*head), GFP_KE= RNEL); > + if (!head) > + return -ENOMEM; > + for (i =3D 0; i < QUIC_STREAM_HT_SIZE; i++) > + INIT_HLIST_HEAD(&head[i]); > + streams->head =3D head; > + return 0; > +} > + > +void quic_stream_free(struct quic_stream_table *streams) > +{ > + struct quic_stream *stream; > + struct hlist_head *head; > + struct hlist_node *tmp; > + int i; > + > + if (!streams->head) > + return; > + > + for (i =3D 0; i < QUIC_STREAM_HT_SIZE; i++) { > + head =3D &streams->head[i]; > + hlist_for_each_entry_safe(stream, tmp, head, node) > + quic_stream_delete(stream); > + } > + kfree(streams->head); The AI report on https://netdev-ai.bots.linux.dev/ai-review.html?id=3D1624d906-c0b6-4e12-a= 63f-5cbfc51b660e#patch-5 is false positive. As the sk_alloc() calls sk_prot_alloc() with __GFP_ZERO, and the streams->h= ead is always initialized to NULL.