Thread (21 messages) 21 messages, 2 authors, 2026-03-27

Re: [PATCH net-next v11 06/15] quic: add stream management

From: Xin Long <lucien.xin@gmail.com>
Date: 2026-03-26 15:06:40
Also in: linux-cifs

On Tue, Mar 24, 2026 at 11:49 PM Xin Long [off-list ref] wrote:
quoted hunk ↗ jump to hunk
This patch introduces 'struct quic_stream_table' for managing QUIC streams,
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 <lucien.xin@gmail.com>
Acked-by: Paolo Abeni <pabeni@redhat.com>
---
v3:
  - Merge send/recv stream helpers into unified functions to reduce code:
    * quic_stream_id_send/recv() → quic_stream_id_valid()
    * quic_stream_id_send/recv_closed() → quic_stream_id_closed()
    * quic_stream_id_send/recv_exceeds() → quic_stream_id_exceeds()
    (pointed out by Paolo).
  - Clarify in changelog that stream hash table is always protected by sock
    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, instead
    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() (noted
    by Paolo).
  - Merge quic_stream_send_get/put() and quic_stream_recv_get/put() helpers
    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 struct
    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 counters
    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 the
    start of quic_stream_max_streams_update()
  - Use 'stream->recv.state > QUIC_STREAM_RECV_STATE_RECVD' instead of '!='
    for clearer intent.
  - Simplify some state checks in quic_stream_put() by using range
    comparisons (> or <) instead of multiple != conditions.
  - streams_uni/bidi are u16 type, and their overflow is already prevented
    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) += quic.o

-quic-y := common.o family.o protocol.o socket.o
+quic-y := 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 <lucien.xin@gmail.com>
+ */
+
+#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 = (stream_id & QUIC_STREAM_TYPE_MASK);
+
+       if (send) {
+               if (is_serv)
+                       return type != QUIC_STREAM_TYPE_CLIENT_UNI;
+               return type != QUIC_STREAM_TYPE_SERVER_UNI;
+       }
+       if (is_serv)
+               return type != QUIC_STREAM_TYPE_SERVER_UNI;
+       return type != 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 *streams,
+                                          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 = quic_stream_head(streams, stream_id);
+       struct quic_stream *stream;
+
+       hlist_for_each_entry(stream, head, node) {
+               if (stream->id == stream_id)
+                       break;
+       }
+       return stream;
+}
+
+static void quic_stream_add(struct quic_stream_table *streams,
+                           struct quic_stream *stream)
+{
+       struct hlist_head *head;
+
+       head = 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 send,
+                                             bool is_serv)
+{
+       struct quic_stream_limits *limits = &streams->send;
+       struct quic_stream *pos, *stream = NULL;
+       gfp_t gfp = GFP_KERNEL_ACCOUNT;
+       struct hlist_node *tmp;
+       HLIST_HEAD(head);
+       s64 stream_id;
+       u32 count = 0;
+
+       if (!send) {
+               limits = &streams->recv;
+               gfp = GFP_ATOMIC | __GFP_ACCOUNT;
+       }
+       stream_id = limits->next_bidi_stream_id;
+       if (quic_stream_id_uni(max_stream_id))
+               stream_id = limits->next_uni_stream_id;
+
+       /* rfc9000#section-2.1: A stream ID that is used out of order results in
+        * all streams of that type with lower-numbered stream IDs also being
+        * opened.
+        */
+       while (stream_id <= max_stream_id) {
+               stream = kzalloc_obj(*stream, gfp);
+               if (!stream)
+                       goto free;
+
+               stream->id = stream_id;
+               if (quic_stream_id_uni(stream_id)) {
+                       if (send) {
+                               stream->send.max_bytes =
+                                       limits->max_stream_data_uni;
+                       } else {
+                               stream->recv.max_bytes =
+                                       limits->max_stream_data_uni;
+                               stream->recv.window = stream->recv.max_bytes;
+                       }
+                       hlist_add_head(&stream->node, &head);
+                       stream_id += QUIC_STREAM_ID_STEP;
+                       continue;
+               }
+
+               if (quic_stream_id_local(stream_id, is_serv)) {
+                       stream->send.max_bytes =
+                               streams->send.max_stream_data_bidi_remote;
+                       stream->recv.max_bytes =
+                               streams->recv.max_stream_data_bidi_local;
+               } else {
+                       stream->send.max_bytes =
+                               streams->send.max_stream_data_bidi_local;
+                       stream->recv.max_bytes =
+                               streams->recv.max_stream_data_bidi_remote;
+               }
+               stream->recv.window = stream->recv.max_bytes;
+               hlist_add_head(&stream->node, &head);
+               stream_id += 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 requested.
+        * Note overflow of next_uni/bidi_stream_id is impossible with s64.
+        */
+       if (quic_stream_id_uni(stream_id)) {
+               limits->next_uni_stream_id = stream_id;
+               limits->streams_uni += count;
+               return stream;
+       }
+
+       limits->next_bidi_stream_id = stream_id;
+       limits->streams_bidi += 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 = 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 stream_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_id;
+
+               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 -= streams->send.next_uni_stream_id;
+               nstreams = 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 -= streams->send.next_bidi_stream_id;
+       nstreams = 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_serv,
+                                   bool send)
+{
+       struct quic_stream *stream;
+
+       if (!quic_stream_id_valid(stream_id, is_serv, send))
+               return ERR_PTR(-EINVAL);
+
+       stream = quic_stream_find(streams, stream_id);
+       if (stream) {
+               if (send && (flags & MSG_QUIC_STREAM_NEW) &&
+                   stream->send.state != 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 = 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 = stream_id;
+
+       return stream;
+}
+
+/* Release or clean up a send or recv stream. This function updates stream
+ * counters and state when a send stream has either successfully sent all 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 delete
+                        * 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 = 1;
+                       streams->recv.streams_uni--;
+                       streams->recv.uni_pending = 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 final
+                * state.
+                */
+               if (stream->send.state != QUIC_STREAM_SEND_STATE_RECVD &&
+                   stream->send.state != 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 = 1;
+                       streams->send.streams_bidi--;
+               }
+       } else {
+               /* Remote-initiated stream: mark recv done and decrement recv
+                * bidi count.
+                */
+               if (!stream->recv.done) {
+                       stream->recv.done = 1;
+                       streams->recv.streams_bidi--;
+                       streams->recv.bidi_pending = 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 recently
+ * 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 that 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 = 0;
+       *max_bidi = 0;
+       if (streams->recv.uni_pending) {
+               rem = streams->recv.max_streams_uni - streams->recv.streams_uni;
+               max = streams->recv.next_uni_stream_id - QUIC_STREAM_ID_STEP +
+                     (rem << QUIC_STREAM_TYPE_BITS);
+
+               streams->recv.max_uni_stream_id = max;
+               *max_uni = quic_stream_id_to_streams(max);
+               streams->recv.uni_pending = 0;
+       }
+       if (streams->recv.bidi_pending) {
+               rem = streams->recv.max_streams_bidi -
+                     streams->recv.streams_bidi;
+               max = streams->recv.next_bidi_stream_id - QUIC_STREAM_ID_STEP +
+                       (rem << QUIC_STREAM_TYPE_BITS);
+
+               streams->recv.max_bidi_stream_id = max;
+               *max_bidi = quic_stream_id_to_streams(max);
+               streams->recv.bidi_pending = 0;
+       }
+
+       return *max_uni || *max_bidi;
+}
+
+int quic_stream_init(struct quic_stream_table *streams)
+{
+       struct hlist_head *head;
+       int i;
+
+       head = kmalloc_array(QUIC_STREAM_HT_SIZE, sizeof(*head), GFP_KERNEL);
+       if (!head)
+               return -ENOMEM;
+       for (i = 0; i < QUIC_STREAM_HT_SIZE; i++)
+               INIT_HLIST_HEAD(&head[i]);
+       streams->head = 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 = 0; i < QUIC_STREAM_HT_SIZE; i++) {
+               head = &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=1624d906-c0b6-4e12-a63f-5cbfc51b660e#patch-5

is false positive.

As the sk_alloc() calls sk_prot_alloc() with __GFP_ZERO, and the streams->head
is always initialized to NULL.
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help