* [PATCH v2] sctp: diag: fix uninitialized stack leak via INET_DIAG_LOCALS/PEERS
@ 2026-07-30 2:33 MingXuan
2026-08-03 23:37 ` Jakub Kicinski
0 siblings, 1 reply; 3+ messages in thread
From: MingXuan @ 2026-07-30 2:33 UTC (permalink / raw)
To: marcelo.leitner; +Cc: lucien.xin, linux-sctp, netdev, MingXuan, stable
inet_diag_msg_sctpladdrs_fill() copies sizeof(union sctp_addr) (28 bytes,
the size of sockaddr_in6) from each sctp_sockaddr_entry.a into the netlink
INET_DIAG_LOCALS attribute and then only zeroes the bytes from offset 28 to
sizeof(sockaddr_storage). The same pattern is used by
inet_diag_msg_sctpaddrs_fill() for INET_DIAG_PEERS.
The IPv4 address-filling helpers sctp_v4_from_addr_param() and
sctp_v4_from_skb() only initialize the sockaddr_in portion (16 bytes) of the
union sctp_addr; the trailing 12 bytes (offset 16..27, the sockaddr_in6-only
region) are left uninitialized. Those bytes are propagated verbatim through
sctp_add_bind_addr() (which copies sizeof(union sctp_addr)=28 bytes) and then
copied straight to userspace by the diag fill functions, leaking 12 bytes of
kernel stack residue per local/peer address to any process that can issue a
SOCK_DIAG_BY_FAMILY dump for IPPROTO_SCTP.
Fix it by computing the actually-initialized length of the address from its
sa_family (struct sockaddr_in for AF_INET, the whole union otherwise) and
copying only that many bytes into an already-zeroed sockaddr_storage slot, so
the uninitialized tail is never read and never reaches userspace.
Fixes: 8f840e47f190 ("sctp: add the sctp_diag.c file")
Cc: stable@vger.kernel.org
Signed-off-by: MingXuan <omeux327@gmail.com>
Acked-by: Xin Long <lucien.xin@gmail.com>
---
Changes in v2 (per review feedback on v1):
- trim the Fixes: commit hash to the recommended abbreviated form (12 chars)
- rename the introduced local from addr_len to copy_len so it does not
collide with the existing addrlen (slot stride) in the same scope
No semantic change relative to v1; Xin Long's Acked-by is carried forward.
net/sctp/diag.c | 17 ++++++++++++-----
1 file changed, 12 insertions(+), 5 deletions(-)
diff --git a/net/sctp/diag.c b/net/sctp/diag.c
index d758f5c3e06e..12557e924cc2 100644
--- a/net/sctp/diag.c
+++ b/net/sctp/diag.c
@@ -85,8 +85,12 @@ static int inet_diag_msg_sctpladdrs_fill(struct sk_buff *skb,
info = nla_data(attr);
rcu_read_lock();
list_for_each_entry_rcu(laddr, address_list, list) {
- memcpy(info, &laddr->a, sizeof(laddr->a));
- memset(info + sizeof(laddr->a), 0, addrlen - sizeof(laddr->a));
+ size_t copy_len = laddr->a.sa.sa_family == AF_INET ?
+ sizeof(struct sockaddr_in) :
+ sizeof(struct sockaddr_in6);
+
+ memset(info, 0, addrlen);
+ memcpy(info, &laddr->a, copy_len);
info += addrlen;
if (!--addrcnt)
@@ -113,9 +117,12 @@ static int inet_diag_msg_sctpaddrs_fill(struct sk_buff *skb,
info = nla_data(attr);
list_for_each_entry(from, &asoc->peer.transport_addr_list,
transports) {
- memcpy(info, &from->ipaddr, sizeof(from->ipaddr));
- memset(info + sizeof(from->ipaddr), 0,
- addrlen - sizeof(from->ipaddr));
+ size_t copy_len = from->ipaddr.sa.sa_family == AF_INET ?
+ sizeof(struct sockaddr_in) :
+ sizeof(struct sockaddr_in6);
+
+ memset(info, 0, addrlen);
+ memcpy(info, &from->ipaddr, copy_len);
info += addrlen;
}
--
2.50.1 (Apple Git-155)
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH v2] sctp: diag: fix uninitialized stack leak via INET_DIAG_LOCALS/PEERS
2026-07-30 2:33 [PATCH v2] sctp: diag: fix uninitialized stack leak via INET_DIAG_LOCALS/PEERS MingXuan
@ 2026-08-03 23:37 ` Jakub Kicinski
2026-08-04 21:38 ` Xin Long
0 siblings, 1 reply; 3+ messages in thread
From: Jakub Kicinski @ 2026-08-03 23:37 UTC (permalink / raw)
To: omeux327
Cc: Jakub Kicinski, marcelo.leitner, lucien.xin, linux-sctp, netdev,
stable
This is an AI-generated review of your patch. The human sending this
email says "AI seems to have spotted another related stack leak, and
suggests an alternative fix."
---
sctp: diag: fix uninitialized stack leak via INET_DIAG_LOCALS/PEERS
The patch changes inet_diag_msg_sctpladdrs_fill() and
inet_diag_msg_sctpaddrs_fill() to zero the whole sockaddr_storage slot
first and then copy only the family-dependent prefix of the stored
union sctp_addr, so the sockaddr_in6-only tail of an AF_INET address is
no longer exported through the netlink attributes.
A few questions about the changelog and the placement of the fix.
[Severity: Low]
[Finding: main-1-2]
[Sources: opus-5]
The changelog says:
> inet_diag_msg_sctpladdrs_fill() copies sizeof(union sctp_addr) (28 bytes,
> the size of sockaddr_in6) from each sctp_sockaddr_entry.a into the netlink
> INET_DIAG_LOCALS attribute and then only zeroes the bytes from offset 28 to
> sizeof(sockaddr_storage). The same pattern is used by
> inet_diag_msg_sctpaddrs_fill() for INET_DIAG_PEERS.
Can the INET_DIAG_PEERS path actually leak anything? transport->ipaddr
is allocated zeroed and only ever written with sockaddr_len bytes:
net/sctp/transport.c:sctp_transport_new() {
transport = kzalloc_obj(*transport, gfp);
...
}
net/sctp/transport.c:sctp_transport_init() {
peer->af_specific = sctp_get_af_specific(addr->sa.sa_family);
memcpy(&peer->ipaddr, addr, peer->af_specific->sockaddr_len);
...
}
So bytes 16..27 of from->ipaddr look guaranteed to be zero for AF_INET,
which would make the inet_diag_msg_sctpaddrs_fill() change hardening
rather than a fix. Should the changelog say that instead?
The changelog also says:
> region) are left uninitialized. Those bytes are propagated verbatim through
> sctp_add_bind_addr() (which copies sizeof(union sctp_addr)=28 bytes) and then
sctp_add_bind_addr() copies a caller-supplied length:
net/sctp/bind_addr.c:sctp_add_bind_addr() {
memcpy(&addr->a, new, min_t(size_t, sizeof(*new), new_size));
...
}
Callers such as sctp_do_bind() pass af->sockaddr_len and store nothing
uninitialized; only the callers that pass sizeof(union sctp_addr) do.
Could that be spelled out so the scope of the stable backport is clear?
> diff --git a/net/sctp/diag.c b/net/sctp/diag.c
> index c2a0de2adf6fd..610c937deffd7 100644
> --- a/net/sctp/diag.c
> +++ b/net/sctp/diag.c
> @@ -85,8 +85,12 @@ static int inet_diag_msg_sctpladdrs_fill(struct sk_buff *skb,
> info = nla_data(attr);
> rcu_read_lock();
> list_for_each_entry_rcu(laddr, address_list, list) {
> - memcpy(info, &laddr->a, sizeof(laddr->a));
> - memset(info + sizeof(laddr->a), 0, addrlen - sizeof(laddr->a));
> + size_t copy_len = laddr->a.sa.sa_family == AF_INET ?
> + sizeof(struct sockaddr_in) :
> + sizeof(struct sockaddr_in6);
[Severity: Low]
[Finding: main-1-1]
[Sources: opus-5]
This isn't a bug, but the AF_INET/AF_INET6 to length mapping already
exists as struct sctp_af::sockaddr_len, and the exact "zero a
sockaddr_storage, then copy only the valid part" idiom is already in the
tree:
net/sctp/ulpevent.c:sctp_ulpevent_notify_peer_addr_change() {
memset(&addr, 0, sizeof(struct sockaddr_storage));
memcpy(&addr, &transport->ipaddr,
transport->af_specific->sockaddr_len);
...
}
Would sctp_get_af_specific(laddr->a.sa.sa_family)->sockaddr_len in
inet_diag_msg_sctpladdrs_fill(), and from->af_specific->sockaddr_len in
inet_diag_msg_sctpaddrs_fill(), work here?
Also, the else branch hardcodes sizeof(struct sockaddr_in6) as a stand-in
for "the whole union", so an unexpected sa_family copies the maximum
length rather than the minimum, and the constant silently diverges if
union sctp_addr ever grows a larger member.
> +
> + memset(info, 0, addrlen);
> + memcpy(info, &laddr->a, copy_len);
> info += addrlen;
>
> if (!--addrcnt)
[Severity: Low]
[Finding: main-1-0]
[Sources: opus-5]
Does this leave the uninitialized tail sitting in the bind address list
itself? The residue is stored by the producers, and this hunk only stops
one consumer from exporting it:
net/sctp/socket.c:sctp_send_asconf_add_ip() {
memcpy(&saveaddr, addr, af->sockaddr_len);
retval = sctp_add_bind_addr(bp, &saveaddr,
sizeof(saveaddr),
SCTP_ADDR_NEW, GFP_ATOMIC);
...
}
net/sctp/bind_addr.c:sctp_raw_to_bind_addrs() {
union sctp_addr addr;
...
!af->from_addr_param(&addr, rawaddr, htons(port), 0)
...
retval = sctp_add_bind_addr(bp, &addr, sizeof(addr),
SCTP_ADDR_SRC, gfp);
}
In both cases only 16 bytes of the 28-byte stack union are written for
AF_INET, and sctp_add_bind_addr() then persists all 28 bytes into the
kzalloc'ed entry, so offsets 16..27 of the stored address stay as stack
residue after this patch. The second path is driven by peer-supplied
INIT/INIT-ACK address parameters.
Would zeroing the union in sctp_v4_from_skb() / sctp_v4_from_sk() /
sctp_v4_from_addr_param(), or clamping sctp_add_bind_addr() to
af->sockaddr_len, close this for every present and future consumer? That
seems worth considering given the Cc: stable tag.
> @@ -114,9 +118,12 @@ static int inet_diag_msg_sctpaddrs_fill(struct sk_buff *skb,
> info = nla_data(attr);
> list_for_each_entry(from, &asoc->peer.transport_addr_list,
> transports) {
> - memcpy(info, &from->ipaddr, sizeof(from->ipaddr));
> - memset(info + sizeof(from->ipaddr), 0,
> - addrlen - sizeof(from->ipaddr));
> + size_t copy_len = from->ipaddr.sa.sa_family == AF_INET ?
> + sizeof(struct sockaddr_in) :
> + sizeof(struct sockaddr_in6);
> +
> + memset(info, 0, addrlen);
> + memcpy(info, &from->ipaddr, copy_len);
> info += addrlen;
> }
>
--
pw-bot: cr
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH v2] sctp: diag: fix uninitialized stack leak via INET_DIAG_LOCALS/PEERS
2026-08-03 23:37 ` Jakub Kicinski
@ 2026-08-04 21:38 ` Xin Long
0 siblings, 0 replies; 3+ messages in thread
From: Xin Long @ 2026-08-04 21:38 UTC (permalink / raw)
To: Jakub Kicinski; +Cc: omeux327, marcelo.leitner, linux-sctp, netdev, stable
On Mon, Aug 3, 2026 at 7:37 PM Jakub Kicinski <kuba@kernel.org> wrote:
>
> This is an AI-generated review of your patch. The human sending this
> email says "AI seems to have spotted another related stack leak, and
> suggests an alternative fix."
> ---
> sctp: diag: fix uninitialized stack leak via INET_DIAG_LOCALS/PEERS
>
> The patch changes inet_diag_msg_sctpladdrs_fill() and
> inet_diag_msg_sctpaddrs_fill() to zero the whole sockaddr_storage slot
> first and then copy only the family-dependent prefix of the stored
> union sctp_addr, so the sockaddr_in6-only tail of an AF_INET address is
> no longer exported through the netlink attributes.
>
> A few questions about the changelog and the placement of the fix.
>
> [Severity: Low]
> [Finding: main-1-2]
> [Sources: opus-5]
> The changelog says:
>
> > inet_diag_msg_sctpladdrs_fill() copies sizeof(union sctp_addr) (28 bytes,
> > the size of sockaddr_in6) from each sctp_sockaddr_entry.a into the netlink
> > INET_DIAG_LOCALS attribute and then only zeroes the bytes from offset 28 to
> > sizeof(sockaddr_storage). The same pattern is used by
> > inet_diag_msg_sctpaddrs_fill() for INET_DIAG_PEERS.
>
> Can the INET_DIAG_PEERS path actually leak anything? transport->ipaddr
> is allocated zeroed and only ever written with sockaddr_len bytes:
>
> net/sctp/transport.c:sctp_transport_new() {
> transport = kzalloc_obj(*transport, gfp);
> ...
> }
>
> net/sctp/transport.c:sctp_transport_init() {
> peer->af_specific = sctp_get_af_specific(addr->sa.sa_family);
> memcpy(&peer->ipaddr, addr, peer->af_specific->sockaddr_len);
> ...
> }
>
> So bytes 16..27 of from->ipaddr look guaranteed to be zero for AF_INET,
> which would make the inet_diag_msg_sctpaddrs_fill() change hardening
> rather than a fix. Should the changelog say that instead?
>
> The changelog also says:
>
> > region) are left uninitialized. Those bytes are propagated verbatim through
> > sctp_add_bind_addr() (which copies sizeof(union sctp_addr)=28 bytes) and then
>
> sctp_add_bind_addr() copies a caller-supplied length:
>
> net/sctp/bind_addr.c:sctp_add_bind_addr() {
> memcpy(&addr->a, new, min_t(size_t, sizeof(*new), new_size));
> ...
> }
>
> Callers such as sctp_do_bind() pass af->sockaddr_len and store nothing
> uninitialized; only the callers that pass sizeof(union sctp_addr) do.
> Could that be spelled out so the scope of the stable backport is clear?
>
> > diff --git a/net/sctp/diag.c b/net/sctp/diag.c
> > index c2a0de2adf6fd..610c937deffd7 100644
> > --- a/net/sctp/diag.c
> > +++ b/net/sctp/diag.c
> > @@ -85,8 +85,12 @@ static int inet_diag_msg_sctpladdrs_fill(struct sk_buff *skb,
> > info = nla_data(attr);
> > rcu_read_lock();
> > list_for_each_entry_rcu(laddr, address_list, list) {
> > - memcpy(info, &laddr->a, sizeof(laddr->a));
> > - memset(info + sizeof(laddr->a), 0, addrlen - sizeof(laddr->a));
> > + size_t copy_len = laddr->a.sa.sa_family == AF_INET ?
> > + sizeof(struct sockaddr_in) :
> > + sizeof(struct sockaddr_in6);
>
> [Severity: Low]
> [Finding: main-1-1]
> [Sources: opus-5]
> This isn't a bug, but the AF_INET/AF_INET6 to length mapping already
> exists as struct sctp_af::sockaddr_len, and the exact "zero a
> sockaddr_storage, then copy only the valid part" idiom is already in the
> tree:
>
> net/sctp/ulpevent.c:sctp_ulpevent_notify_peer_addr_change() {
> memset(&addr, 0, sizeof(struct sockaddr_storage));
> memcpy(&addr, &transport->ipaddr,
> transport->af_specific->sockaddr_len);
> ...
> }
>
> Would sctp_get_af_specific(laddr->a.sa.sa_family)->sockaddr_len in
> inet_diag_msg_sctpladdrs_fill(), and from->af_specific->sockaddr_len in
> inet_diag_msg_sctpaddrs_fill(), work here?
>
> Also, the else branch hardcodes sizeof(struct sockaddr_in6) as a stand-in
> for "the whole union", so an unexpected sa_family copies the maximum
> length rather than the minimum, and the constant silently diverges if
> union sctp_addr ever grows a larger member.
>
> > +
> > + memset(info, 0, addrlen);
> > + memcpy(info, &laddr->a, copy_len);
> > info += addrlen;
> >
> > if (!--addrcnt)
>
> [Severity: Low]
> [Finding: main-1-0]
> [Sources: opus-5]
> Does this leave the uninitialized tail sitting in the bind address list
> itself? The residue is stored by the producers, and this hunk only stops
> one consumer from exporting it:
>
> net/sctp/socket.c:sctp_send_asconf_add_ip() {
> memcpy(&saveaddr, addr, af->sockaddr_len);
> retval = sctp_add_bind_addr(bp, &saveaddr,
> sizeof(saveaddr),
> SCTP_ADDR_NEW, GFP_ATOMIC);
> ...
> }
>
> net/sctp/bind_addr.c:sctp_raw_to_bind_addrs() {
> union sctp_addr addr;
> ...
> !af->from_addr_param(&addr, rawaddr, htons(port), 0)
> ...
> retval = sctp_add_bind_addr(bp, &addr, sizeof(addr),
> SCTP_ADDR_SRC, gfp);
> }
>
> In both cases only 16 bytes of the 28-byte stack union are written for
> AF_INET, and sctp_add_bind_addr() then persists all 28 bytes into the
> kzalloc'ed entry, so offsets 16..27 of the stored address stay as stack
> residue after this patch. The second path is driven by peer-supplied
> INIT/INIT-ACK address parameters.
>
> Would zeroing the union in sctp_v4_from_skb() / sctp_v4_from_sk() /
> sctp_v4_from_addr_param(), or clamping q() to
> af->sockaddr_len, close this for every present and future consumer? That
> seems worth considering given the Cc: stable tag.
>
Instead of only copying the valid address length when dumping entries from
address_list, the comment suggests that the unused bytes should be
initialized when the address is added to the list.
For transport_addr_list, the path:
sctp_assoc_add_peer() -> sctp_transport_new() -> sctp_transport_init()
already guarantees this:
transport = kzalloc_obj(*transport, gfp);
...
memcpy(&peer->ipaddr, addr, peer->af_specific->sockaddr_len);
However, for bp->address_list, sctp_add_bind_addr() relies on the new_size
parameter to copy the correct address length. There are currently three
callers passing sizeof(addr) instead of af->sockaddr_len:
- sctp_raw_to_bind_addrs()
- sctp_unpack_cookie()
- sctp_send_asconf_add_ip()
These should be changed to pass af->sockaddr_len to sctp_add_bind_addr(),
as shown below:
diff --git a/net/sctp/bind_addr.c b/net/sctp/bind_addr.c
index 31737f144c7f..1b9bfac17816 100644
--- a/net/sctp/bind_addr.c
+++ b/net/sctp/bind_addr.c
@@ -294,7 +294,7 @@ int sctp_raw_to_bind_addrs(struct sctp_bind_addr
*bp, __u8 *raw_addr_list,
if (sctp_bind_addr_state(bp, &addr) != -1)
goto next;
- retval = sctp_add_bind_addr(bp, &addr, sizeof(addr),
+ retval = sctp_add_bind_addr(bp, &addr, af->sockaddr_len,
SCTP_ADDR_SRC, gfp);
if (retval)
/* Can't finish building the list, clean up. */
diff --git a/net/sctp/sm_make_chunk.c b/net/sctp/sm_make_chunk.c
index b472565fc7f2..8da6d97b6062 100644
--- a/net/sctp/sm_make_chunk.c
+++ b/net/sctp/sm_make_chunk.c
@@ -1861,8 +1861,11 @@ struct sctp_association *sctp_unpack_cookie(
/* Also, add the destination address. */
if (list_empty(&retval->base.bind_addr.address_list)) {
+ struct sctp_af *af;
+
+ af = sctp_get_af_specific(chunk->dest.sa.sa_family);
sctp_add_bind_addr(&retval->base.bind_addr, &chunk->dest,
- sizeof(chunk->dest), SCTP_ADDR_SRC,
+ af->sockaddr_len, SCTP_ADDR_SRC,
GFP_ATOMIC);
}
diff --git a/net/sctp/socket.c b/net/sctp/socket.c
index e4ea57a642c1..1f934a9b2e5a 100644
--- a/net/sctp/socket.c
+++ b/net/sctp/socket.c
@@ -634,7 +634,7 @@ static int sctp_send_asconf_add_ip(struct sock
*sk,
af = sctp_get_af_specific(addr->v4.sin_family);
memcpy(&saveaddr, addr, af->sockaddr_len);
retval = sctp_add_bind_addr(bp, &saveaddr,
- sizeof(saveaddr),
+ af->sockaddr_len,
SCTP_ADDR_NEW, GFP_ATOMIC);
addr_buf += af->sockaddr_len;
}
Note that the same issue does NOT exist in the following functions:
- sctp_copy_local_addr_list()
- sctp_copy_one_addr()
- sctp_bind_addr_dup()
Although they use sizeof(addr), the addresses being copied are already
entries from address_list, where the unused bytes have been properly
initialized.
Hi, MingXuan, can you give it a try to fix it in the proposed way above?
Thanks.
^ permalink raw reply related [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-08-04 21:38 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-30 2:33 [PATCH v2] sctp: diag: fix uninitialized stack leak via INET_DIAG_LOCALS/PEERS MingXuan
2026-08-03 23:37 ` Jakub Kicinski
2026-08-04 21:38 ` Xin Long
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox