* [PATCH net] sctp: fix transport UAF via the retransmit queue
@ 2026-07-26 6:04 Baul Lee
2026-07-27 17:42 ` Xin Long
2026-07-29 12:49 ` [PATCH net v2] sctp: keep chunk->transport in step with the list it is queued on Baul Lee
0 siblings, 2 replies; 4+ messages in thread
From: Baul Lee @ 2026-07-26 6:04 UTC (permalink / raw)
To: netdev, linux-sctp, linux-kernel
Cc: marcelo.leitner, lucien.xin, davem, edumazet, kuba, pabeni, horms,
federico.kirschbaum, Baul Lee, stable
sctp_assoc_rm_peer() clears the cached chunk->transport back-pointer on
only two of the output queue's chunk lists before freeing the transport:
peer->transmitted and asoc->outqueue.out_chunk_list. A DATA chunk parked
on asoc->outqueue.retransmit keeps pointing at the transport, so once
sctp_transport_free() drops the last reference and the object is
RCU-freed, that chunk is left with a dangling pointer.
While the chunk sits on the retransmit queue the dangling pointer is not
followed: sctp_check_transmitted() skips the flight-size accounting both
for transmitted_queue == &q->retransmit and for gap-acked chunks. Two
steps remove that cover. First, sctp_outq_flush_rtx() moves a gap-acked
chunk onto a live transport's transmitted list without reassigning
chunk->transport, unlike the ordinary resend path, which rebinds it in
__sctp_packet_append_chunk(). Second, a later SACK that reneges on the
TSN clears tsn_gap_acked. The chunk now sits on a transmitted list with
tsn_gap_acked == 0, so the next SACK reaches
tchunk->transport->flight_size -= sctp_data_size(tchunk);
a read-modify-write inside the freed sctp_transport.
The transport is freed by an ASCONF Delete-IP and the faulting accesses
are driven by ordinary SACKs, both coming from the association peer, so
an application that speaks SCTP and accepts multihoming is enough to
reach this. KASAN reports a slab-use-after-free read of 4 bytes at
offset 216 of a freed kmalloc-1k sctp_transport in
sctp_check_transmitted(), the object having been freed from
sctp_assoc_rm_peer() via sctp_process_asconf().
Clear ->transport for chunks on the retransmit queue as well, mirroring
the existing out_chunk_list handling. The sink already guards with
if (tchunk->transport), so a cleared back-pointer is skipped exactly like
an already-acked chunk. The sacked and abandoned queues do not need the
same treatment: chunks there never have ->transport dereferenced and are
never migrated onto the retransmit queue or onto a transport's
transmitted list.
Discovered by XBOW, triaged by Baul Lee <baul.lee@xbow.com>
Reported privately to the maintainers on 2026-07-10 with root-cause
analysis, a PoC, a KASAN log and this fix; posting to the list was
requested as the follow-up.
Fixes: df132eff4638 ("sctp: clear the transport of some out_chunk_list chunks in sctp_assoc_rm_peer")
Reported-by: Federico Kirschbaum <federico.kirschbaum@xbow.com>
Reported-by: Baul Lee <baul.lee@xbow.com>
Cc: stable@vger.kernel.org
Signed-off-by: Baul Lee <baul.lee@xbow.com>
---
net/sctp/associola.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/net/sctp/associola.c b/net/sctp/associola.c
index 62d3cc155809..c95f68d21670 100644
--- a/net/sctp/associola.c
+++ b/net/sctp/associola.c
@@ -569,6 +569,10 @@ void sctp_assoc_rm_peer(struct sctp_association *asoc,
sctp_transport_hold(active);
}
+ list_for_each_entry(ch, &asoc->outqueue.retransmit, transmitted_list)
+ if (ch->transport == peer)
+ ch->transport = NULL;
+
list_for_each_entry(ch, &asoc->outqueue.out_chunk_list, list)
if (ch->transport == peer)
ch->transport = NULL;
--
2.50.1 (Apple Git-155)
^ permalink raw reply related [flat|nested] 4+ messages in thread* Re: [PATCH net] sctp: fix transport UAF via the retransmit queue
2026-07-26 6:04 [PATCH net] sctp: fix transport UAF via the retransmit queue Baul Lee
@ 2026-07-27 17:42 ` Xin Long
2026-07-29 12:49 ` [PATCH net v2] sctp: keep chunk->transport in step with the list it is queued on Baul Lee
1 sibling, 0 replies; 4+ messages in thread
From: Xin Long @ 2026-07-27 17:42 UTC (permalink / raw)
To: Baul Lee
Cc: netdev, linux-sctp, linux-kernel, marcelo.leitner, davem,
edumazet, kuba, pabeni, horms, federico.kirschbaum, stable
On Sun, Jul 26, 2026 at 2:05 AM Baul Lee <baul.lee@xbow.com> wrote:
>
> sctp_assoc_rm_peer() clears the cached chunk->transport back-pointer on
> only two of the output queue's chunk lists before freeing the transport:
> peer->transmitted and asoc->outqueue.out_chunk_list. A DATA chunk parked
> on asoc->outqueue.retransmit keeps pointing at the transport, so once
> sctp_transport_free() drops the last reference and the object is
> RCU-freed, that chunk is left with a dangling pointer.
>
> While the chunk sits on the retransmit queue the dangling pointer is not
> followed: sctp_check_transmitted() skips the flight-size accounting both
> for transmitted_queue == &q->retransmit and for gap-acked chunks. Two
> steps remove that cover. First, sctp_outq_flush_rtx() moves a gap-acked
> chunk onto a live transport's transmitted list without reassigning
> chunk->transport, unlike the ordinary resend path, which rebinds it in
> __sctp_packet_append_chunk(). Second, a later SACK that reneges on the
> TSN clears tsn_gap_acked. The chunk now sits on a transmitted list with
> tsn_gap_acked == 0, so the next SACK reaches
>
> tchunk->transport->flight_size -= sctp_data_size(tchunk);
>
> a read-modify-write inside the freed sctp_transport.
>
> The transport is freed by an ASCONF Delete-IP and the faulting accesses
> are driven by ordinary SACKs, both coming from the association peer, so
> an application that speaks SCTP and accepts multihoming is enough to
> reach this. KASAN reports a slab-use-after-free read of 4 bytes at
> offset 216 of a freed kmalloc-1k sctp_transport in
> sctp_check_transmitted(), the object having been freed from
> sctp_assoc_rm_peer() via sctp_process_asconf().
>
> Clear ->transport for chunks on the retransmit queue as well, mirroring
> the existing out_chunk_list handling. The sink already guards with
> if (tchunk->transport), so a cleared back-pointer is skipped exactly like
> an already-acked chunk. The sacked and abandoned queues do not need the
> same treatment: chunks there never have ->transport dereferenced and are
> never migrated onto the retransmit queue or onto a transport's
> transmitted list.
>
> Discovered by XBOW, triaged by Baul Lee <baul.lee@xbow.com>
> Reported privately to the maintainers on 2026-07-10 with root-cause
> analysis, a PoC, a KASAN log and this fix; posting to the list was
> requested as the follow-up.
>
> Fixes: df132eff4638 ("sctp: clear the transport of some out_chunk_list chunks in sctp_assoc_rm_peer")
I think the issue exists since the very beginning, Fixes tag should be:
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
> Reported-by: Federico Kirschbaum <federico.kirschbaum@xbow.com>
> Reported-by: Baul Lee <baul.lee@xbow.com>
> Cc: stable@vger.kernel.org
> Signed-off-by: Baul Lee <baul.lee@xbow.com>
> ---
> net/sctp/associola.c | 4 ++++
> 1 file changed, 4 insertions(+)
>
> diff --git a/net/sctp/associola.c b/net/sctp/associola.c
> index 62d3cc155809..c95f68d21670 100644
> --- a/net/sctp/associola.c
> +++ b/net/sctp/associola.c
> @@ -569,6 +569,10 @@ void sctp_assoc_rm_peer(struct sctp_association *asoc,
> sctp_transport_hold(active);
> }
>
> + list_for_each_entry(ch, &asoc->outqueue.retransmit, transmitted_list)
> + if (ch->transport == peer)
> + ch->transport = NULL;
> +
```
[sashiko-genimi]
This isn't a problem introduced by this patch, but does the new loop cover
the ordering where the gap-acked migration happens before the ASCONF
Delete-IP?
[sashiko-claude]
This is a pre-existing issue, but can gap-acked data chunks bypass this
cleanup if they were already migrated to another active transport's
transmitted list?
```
Both AI reviews report that the change may not cover the case where the
gap-acked data chunks bypass this cleanup if they were already migrated
to another active transport's transmitted list.
As you described in the changelog, the issue actually was caused by:
> ... sctp_outq_flush_rtx() moves a gap-acked
> chunk onto a live transport's transmitted list without reassigning
> chunk->transport.
Can you try fixing this in sctp_outq_flush_rtx() by updating
chunk->transport after both list_move_tail() calls? This makes sure the
chunk's transport pointer always matches the list it is queued on.
Thanks.
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH net v2] sctp: keep chunk->transport in step with the list it is queued on
2026-07-26 6:04 [PATCH net] sctp: fix transport UAF via the retransmit queue Baul Lee
2026-07-27 17:42 ` Xin Long
@ 2026-07-29 12:49 ` Baul Lee
2026-07-29 15:47 ` Xin Long
1 sibling, 1 reply; 4+ messages in thread
From: Baul Lee @ 2026-07-29 12:49 UTC (permalink / raw)
To: netdev, linux-sctp, linux-kernel
Cc: Marcelo Ricardo Leitner, Xin Long, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, stable, Baul Lee
__sctp_outq_flush_rtx() moves a chunk onto transport->transmitted in two
places but only one of them leaves chunk->transport describing where the
chunk actually is. The ordinary resend path is consistent because
sctp_packet_append_chunk() rebinds the chunk in
__sctp_packet_append_chunk():
list_add_tail(&chunk->list, &packet->chunk_list);
packet->size += chunk_len;
chunk->transport = packet->transport;
The gap-acked path has no such rebind. It parks the chunk on another
transport's transmitted list and leaves the stale back-pointer alone:
if (chunk->tsn_gap_acked) {
list_move_tail(&chunk->transmitted_list,
&transport->transmitted);
continue;
}
So a chunk can sit on a live transport's transmitted list while
chunk->transport still names a different transport. If that transport is
then removed - sctp_assoc_rm_peer() from an ASCONF Delete-IP - the last
reference goes away and sctp_transport_free() RCU-frees it, leaving the
chunk with a dangling pointer. sctp_assoc_rm_peer() scrubs the
back-pointer on peer->transmitted and on asoc->outqueue.out_chunk_list,
but this chunk is on neither.
While tsn_gap_acked stays set the dangling pointer is not followed, since
sctp_check_transmitted() skips the flight accounting for gap-acked chunks.
A later SACK that reneges on the TSN clears the flag, and the next SACK
then reaches
tchunk->transport->flight_size -= sctp_data_size(tchunk);
a read-modify-write inside the freed sctp_transport. KASAN reports a
slab-use-after-free read of 4 bytes at offset 216 of a freed kmalloc-1k
sctp_transport in sctp_check_transmitted(), freed from
sctp_assoc_rm_peer() via sctp_process_asconf(). The removal and the
faulting SACKs all come from the association peer, so an application that
speaks SCTP and accepts multihoming is enough to reach it.
Assign chunk->transport after both list_move_tail() calls, so the
back-pointer always matches the list the chunk is queued on. Doing it on
the ordinary path too keeps the invariant local to the move instead of
resting on a rebind that happens later and only if the chunk is appended to
a packet. Fixing it here rather than by scrubbing more lists in
sctp_assoc_rm_peer() covers the chunks that were already migrated before
the transport went away, which a scrub at removal time cannot see.
Discovered by XBOW, triaged by Baul Lee <baul.lee@xbow.com>
[Reported privately to the maintainers on 2026-07-10 with root-cause
analysis, a PoC, a KASAN log and a fix; posting to the list was requested
as the follow-up.]
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: stable@vger.kernel.org
Signed-off-by: Baul Lee <baul.lee@xbow.com>
---
v2:
- fix the root cause in __sctp_outq_flush_rtx() instead of clearing the
back-pointer for the retransmit queue in sctp_assoc_rm_peer(). The v1
scrub could not see chunks that had already been migrated onto another
transport's transmitted list before the removal, which is the case the
reviews on v1 asked about
- correct the Fixes tag: the inconsistency is as old as the code, not
introduced by df132eff4638
Link to v1: https://lore.kernel.org/netdev/20260726060453.42730-1-baul.lee@xbow.com/
net/sctp/outqueue.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/net/sctp/outqueue.c b/net/sctp/outqueue.c
index f6b8c13da..1b2d08f2e 100644
--- a/net/sctp/outqueue.c
+++ b/net/sctp/outqueue.c
@@ -650,6 +650,7 @@ static int __sctp_outq_flush_rtx(struct sctp_outq *q, struct sctp_packet *pkt,
if (chunk->tsn_gap_acked) {
list_move_tail(&chunk->transmitted_list,
&transport->transmitted);
+ chunk->transport = transport;
continue;
}
@@ -715,6 +716,7 @@ static int __sctp_outq_flush_rtx(struct sctp_outq *q, struct sctp_packet *pkt,
*/
list_move_tail(&chunk->transmitted_list,
&transport->transmitted);
+ chunk->transport = transport;
/* Mark the chunk as ineligible for fast retransmit
* after it is retransmitted.
--
2.53.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* Re: [PATCH net v2] sctp: keep chunk->transport in step with the list it is queued on
2026-07-29 12:49 ` [PATCH net v2] sctp: keep chunk->transport in step with the list it is queued on Baul Lee
@ 2026-07-29 15:47 ` Xin Long
0 siblings, 0 replies; 4+ messages in thread
From: Xin Long @ 2026-07-29 15:47 UTC (permalink / raw)
To: Baul Lee
Cc: netdev, linux-sctp, linux-kernel, Marcelo Ricardo Leitner,
David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, stable
On Wed, Jul 29, 2026 at 8:49 AM Baul Lee <baul.lee@xbow.com> wrote:
>
> __sctp_outq_flush_rtx() moves a chunk onto transport->transmitted in two
> places but only one of them leaves chunk->transport describing where the
> chunk actually is. The ordinary resend path is consistent because
> sctp_packet_append_chunk() rebinds the chunk in
> __sctp_packet_append_chunk():
>
> list_add_tail(&chunk->list, &packet->chunk_list);
> packet->size += chunk_len;
> chunk->transport = packet->transport;
>
> The gap-acked path has no such rebind. It parks the chunk on another
> transport's transmitted list and leaves the stale back-pointer alone:
>
> if (chunk->tsn_gap_acked) {
> list_move_tail(&chunk->transmitted_list,
> &transport->transmitted);
> continue;
> }
>
> So a chunk can sit on a live transport's transmitted list while
> chunk->transport still names a different transport. If that transport is
> then removed - sctp_assoc_rm_peer() from an ASCONF Delete-IP - the last
> reference goes away and sctp_transport_free() RCU-frees it, leaving the
> chunk with a dangling pointer. sctp_assoc_rm_peer() scrubs the
> back-pointer on peer->transmitted and on asoc->outqueue.out_chunk_list,
> but this chunk is on neither.
>
> While tsn_gap_acked stays set the dangling pointer is not followed, since
> sctp_check_transmitted() skips the flight accounting for gap-acked chunks.
> A later SACK that reneges on the TSN clears the flag, and the next SACK
> then reaches
>
> tchunk->transport->flight_size -= sctp_data_size(tchunk);
>
> a read-modify-write inside the freed sctp_transport. KASAN reports a
> slab-use-after-free read of 4 bytes at offset 216 of a freed kmalloc-1k
> sctp_transport in sctp_check_transmitted(), freed from
> sctp_assoc_rm_peer() via sctp_process_asconf(). The removal and the
> faulting SACKs all come from the association peer, so an application that
> speaks SCTP and accepts multihoming is enough to reach it.
>
> Assign chunk->transport after both list_move_tail() calls, so the
> back-pointer always matches the list the chunk is queued on. Doing it on
> the ordinary path too keeps the invariant local to the move instead of
> resting on a rebind that happens later and only if the chunk is appended to
> a packet. Fixing it here rather than by scrubbing more lists in
> sctp_assoc_rm_peer() covers the chunks that were already migrated before
> the transport went away, which a scrub at removal time cannot see.
>
> Discovered by XBOW, triaged by Baul Lee <baul.lee@xbow.com>
> [Reported privately to the maintainers on 2026-07-10 with root-cause
> analysis, a PoC, a KASAN log and a fix; posting to the list was requested
> as the follow-up.]
>
> Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
> Cc: stable@vger.kernel.org
> Signed-off-by: Baul Lee <baul.lee@xbow.com>
> ---
> v2:
> - fix the root cause in __sctp_outq_flush_rtx() instead of clearing the
> back-pointer for the retransmit queue in sctp_assoc_rm_peer(). The v1
> scrub could not see chunks that had already been migrated onto another
> transport's transmitted list before the removal, which is the case the
> reviews on v1 asked about
> - correct the Fixes tag: the inconsistency is as old as the code, not
> introduced by df132eff4638
>
> Link to v1: https://lore.kernel.org/netdev/20260726060453.42730-1-baul.lee@xbow.com/
>
> net/sctp/outqueue.c | 2 ++
> 1 file changed, 2 insertions(+)
>
> diff --git a/net/sctp/outqueue.c b/net/sctp/outqueue.c
> index f6b8c13da..1b2d08f2e 100644
> --- a/net/sctp/outqueue.c
> +++ b/net/sctp/outqueue.c
> @@ -650,6 +650,7 @@ static int __sctp_outq_flush_rtx(struct sctp_outq *q, struct sctp_packet *pkt,
> if (chunk->tsn_gap_acked) {
> list_move_tail(&chunk->transmitted_list,
> &transport->transmitted);
> + chunk->transport = transport;
> continue;
> }
>
> @@ -715,6 +716,7 @@ static int __sctp_outq_flush_rtx(struct sctp_outq *q, struct sctp_packet *pkt,
> */
> list_move_tail(&chunk->transmitted_list,
> &transport->transmitted);
> + chunk->transport = transport;
>
This one seems redundant on the default/SCTP_XMIT_OK path, as it's
already set by:
chunk->transport = packet->transport;
in __sctp_packet_append_chunk().
Could you please remove the second one? (Sorry for not noticing this on v1)
Thanks.
> /* Mark the chunk as ineligible for fast retransmit
> * after it is retransmitted.
> --
> 2.53.0
>
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-07-29 15:47 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-26 6:04 [PATCH net] sctp: fix transport UAF via the retransmit queue Baul Lee
2026-07-27 17:42 ` Xin Long
2026-07-29 12:49 ` [PATCH net v2] sctp: keep chunk->transport in step with the list it is queued on Baul Lee
2026-07-29 15:47 ` Xin Long
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox