From ad1320865b1826ffa2e81e37c49535751e81335c Mon Sep 17 00:00:00 2001 From: David Lee Date: Fri, 9 Oct 2026 15:42:31 +0800 Subject: [PATCH 1/2] sctp: hold shkey across socket migration sctp_sock_migrate() transfers queued DATA skbs from the old socket to the new one. skb_orphan() invokes sctp_wfree() during that transfer and drops the skb-owned shared-key reference. If userspace has removed that key from the association, this can be the final reference. The following sctp_set_owner_w() then dereferences the freed chunk->shkey while trying to take the new owner reference. Take a temporary shared-key reference before orphaning the skb and release it after the new owner has taken its reference. This preserves the selected authentication key throughout the ownership transfer. Bug found and triaged by OpenAI Security Research and validated by Trail of Bits. Fixes: 1b1e0bc99474 ("sctp: add refcnt support for sh_key") Signed-off-by: Kyle Zeng Signed-off-by: David Lee Acked-by: Xin Long Link: https://patch.msgid.link/20261009074244.3718-2-juny24602@gmail.com Signed-off-by: Jakub Kicinski --- net/sctp/socket.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/net/sctp/socket.c b/net/sctp/socket.c index 321898f22d80..696e67936251 100644 --- a/net/sctp/socket.c +++ b/net/sctp/socket.c @@ -147,9 +147,19 @@ static inline void sctp_set_owner_w(struct sctp_chunk *chunk) static void sctp_clear_owner_w(struct sctp_chunk *chunk) { + /* Keep the shkey alive until the new owner takes its reference. */ + if (chunk->shkey) + sctp_auth_shkey_hold(chunk->shkey); skb_orphan(chunk->skb); } +static void sctp_set_owner_w_migrate(struct sctp_chunk *chunk) +{ + sctp_set_owner_w(chunk); + if (chunk->shkey) + sctp_auth_shkey_release(chunk->shkey); +} + #define traverse_and_process() \ do { \ msg = chunk->msg; \ @@ -9637,7 +9647,7 @@ static int sctp_sock_migrate(struct sock *oldsk, struct sock *newsk, lock_sock_nested(newsk, SINGLE_DEPTH_NESTING); sctp_for_each_tx_datachunk(assoc, true, sctp_clear_owner_w); sctp_assoc_migrate(assoc, newsk); - sctp_for_each_tx_datachunk(assoc, false, sctp_set_owner_w); + sctp_for_each_tx_datachunk(assoc, false, sctp_set_owner_w_migrate); /* If the association on the newsk is already closed before accept() * is called, set RCV_SHUTDOWN flag. From 52ffeaa358d1f3d91aa87a26ad90d77769cff74b Mon Sep 17 00:00:00 2001 From: Jun Yang Date: Fri, 9 Oct 2026 15:42:32 +0800 Subject: [PATCH 2/2] sctp: re-point retained control chunks on association migration sctp_sock_migrate() transfers DATA chunk ownership but leaves retained control chunks pointing at the old socket. A later retransmission can therefore access the socket after it has been freed. Extend the migration walk to cover retained control chunks and use sctp_control_set_owner_w() to assign their new owner. Reuse the DATA ownership check so each chunk is processed once per pass, even when it is reachable through multiple lists or retained pointers. Preserve a control chunk's existing authentication key across migration so its final release can still generate SCTP_AUTH_FREE_KEY when needed. Fixes: d04adf1b3551 ("sctp: reset owner sk for data chunks on out queues when migrating a sock") Cc: stable@kernel.org Reported-by: TencentOS Corvus AI Signed-off-by: Jun Yang Acked-by: Xin Long Link: https://patch.msgid.link/20261009074244.3718-3-juny24602@gmail.com Signed-off-by: Jakub Kicinski --- include/net/sctp/sm.h | 1 + net/sctp/sm_make_chunk.c | 5 ++-- net/sctp/socket.c | 53 ++++++++++++++++++++++++++++++---------- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/include/net/sctp/sm.h b/include/net/sctp/sm.h index 3bfd261a53cc..76605d1ee839 100644 --- a/include/net/sctp/sm.h +++ b/include/net/sctp/sm.h @@ -252,6 +252,7 @@ struct sctp_chunk *sctp_make_fwdtsn(const struct sctp_association *asoc, struct sctp_fwdtsn_skip *skiplist); struct sctp_chunk *sctp_make_auth(const struct sctp_association *asoc, __u16 key_id); +void sctp_control_set_owner_w(struct sctp_chunk *chunk); struct sctp_chunk *sctp_make_strreset_req(const struct sctp_association *asoc, __u16 stream_num, __be16 *stream_list, bool out, bool in); diff --git a/net/sctp/sm_make_chunk.c b/net/sctp/sm_make_chunk.c index 84a4c97d0f75..ddc31a5dbad3 100644 --- a/net/sctp/sm_make_chunk.c +++ b/net/sctp/sm_make_chunk.c @@ -94,7 +94,7 @@ static void sctp_control_release_owner(struct sk_buff *skb) } } -static void sctp_control_set_owner_w(struct sctp_chunk *chunk) +void sctp_control_set_owner_w(struct sctp_chunk *chunk) { struct sctp_association *asoc = chunk->asoc; struct sk_buff *skb = chunk->skb; @@ -107,7 +107,8 @@ static void sctp_control_set_owner_w(struct sctp_chunk *chunk) * For now don't do anything for now. */ if (chunk->auth) { - chunk->shkey = asoc->shkey; + if (!chunk->shkey) + chunk->shkey = asoc->shkey; sctp_auth_shkey_hold(chunk->shkey); } skb->sk = asoc ? asoc->base.sk : NULL; diff --git a/net/sctp/socket.c b/net/sctp/socket.c index 696e67936251..8cf3e955ae18 100644 --- a/net/sctp/socket.c +++ b/net/sctp/socket.c @@ -155,9 +155,24 @@ static void sctp_clear_owner_w(struct sctp_chunk *chunk) static void sctp_set_owner_w_migrate(struct sctp_chunk *chunk) { - sctp_set_owner_w(chunk); - if (chunk->shkey) - sctp_auth_shkey_release(chunk->shkey); + struct sctp_shared_key *shkey = chunk->shkey; + + if (chunk->msg) + sctp_set_owner_w(chunk); + else + sctp_control_set_owner_w(chunk); + + if (shkey) + sctp_auth_shkey_release(shkey); +} + +static void sctp_process_tx_chunk(struct sctp_association *asoc, + struct sctp_chunk *chunk, bool clear, + void (*cb)(struct sctp_chunk *)) +{ + if ((clear && asoc->base.sk == chunk->skb->sk) || + (!clear && asoc->base.sk != chunk->skb->sk)) + cb(chunk); } #define traverse_and_process() \ @@ -165,17 +180,14 @@ do { \ msg = chunk->msg; \ if (msg == prev_msg) \ continue; \ - list_for_each_entry(c, &msg->chunks, frag_list) { \ - if ((clear && asoc->base.sk == c->skb->sk) || \ - (!clear && asoc->base.sk != c->skb->sk)) \ - cb(c); \ - } \ + list_for_each_entry(c, &msg->chunks, frag_list) \ + sctp_process_tx_chunk(asoc, c, clear, cb); \ prev_msg = msg; \ } while (0) -static void sctp_for_each_tx_datachunk(struct sctp_association *asoc, - bool clear, - void (*cb)(struct sctp_chunk *)) +static void sctp_for_each_tx_chunk(struct sctp_association *asoc, + bool clear, + void (*cb)(struct sctp_chunk *)) { struct sctp_datamsg *msg, *prev_msg = NULL; @@ -198,6 +210,21 @@ static void sctp_for_each_tx_datachunk(struct sctp_association *asoc, list_for_each_entry(chunk, &q->out_chunk_list, list) traverse_and_process(); + + list_for_each_entry(chunk, &q->control_chunk_list, list) + sctp_process_tx_chunk(asoc, chunk, clear, cb); + + list_for_each_entry(chunk, &asoc->asconf_ack_list, transmitted_list) + sctp_process_tx_chunk(asoc, chunk, clear, cb); + + list_for_each_entry(chunk, &asoc->addip_chunk_list, list) + sctp_process_tx_chunk(asoc, chunk, clear, cb); + + if (asoc->strreset_chunk) + sctp_process_tx_chunk(asoc, asoc->strreset_chunk, clear, cb); + + if (asoc->addip_last_asconf) + sctp_process_tx_chunk(asoc, asoc->addip_last_asconf, clear, cb); } static void sctp_for_each_rx_skb(struct sctp_association *asoc, struct sock *sk, @@ -9645,9 +9672,9 @@ static int sctp_sock_migrate(struct sock *oldsk, struct sock *newsk, * paths won't try to lock it and then oldsk. */ lock_sock_nested(newsk, SINGLE_DEPTH_NESTING); - sctp_for_each_tx_datachunk(assoc, true, sctp_clear_owner_w); + sctp_for_each_tx_chunk(assoc, true, sctp_clear_owner_w); sctp_assoc_migrate(assoc, newsk); - sctp_for_each_tx_datachunk(assoc, false, sctp_set_owner_w_migrate); + sctp_for_each_tx_chunk(assoc, false, sctp_set_owner_w_migrate); /* If the association on the newsk is already closed before accept() * is called, set RCV_SHUTDOWN flag.