Netdev List
 help / color / mirror / Atom feed
* [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