[PATCH net 3/4] netlabel: validate CALIPSO option against skb tail in netlbl_skbuff_getattr
Paul Moore
paul at paul-moore.com
Fri May 15 02:18:23 UTC 2026
On Thu, May 14, 2026 at 12:52 PM Qi Tang <tpluszz77 at gmail.com> wrote:
>
> netlbl_skbuff_getattr() locates the CALIPSO option in the IPv6 HBH
> header via calipso_optptr() and hands the bare pointer to
> calipso_getattr() -> calipso_opt_getattr(). The consumer re-reads
> calipso[1] (option data length) and calipso[6] (cat_len/4) and walks
> calipso + 10 for cat_len bytes via netlbl_bitmap_walk().
>
> ipv6_hop_calipso() validates these bytes only at parse time inside
> ipv6_parse_hopopts(). An nftables PRE_ROUTING payload write
> reachable from an unprivileged user namespace can rewrite both bytes
> between parse and the SELinux/Smack peer-label consume path
> (selinux_sock_rcv_skb_compat -> selinux_netlbl_sock_rcv_skb ->
> netlbl_skbuff_getattr). The self-consistency check
> (cat_len + 8 > len) inside calipso_opt_getattr() is defeated by
> mutating both bytes consistently, allowing a ~232-byte
> slab-out-of-bounds read from calipso + 10 whose set bits become MLS
> categories driving the access decision.
>
> netlbl_skbuff_getattr() has the skb; gate the consume on the option
> fitting within skb_tail_pointer(). The IPv6 option layout is
> type(1) + length(1) + length bytes of data, so requiring
> ptr + 2 + ptr[1] <= skb_tail covers the option and its embedded
> bitmap.
>
> Runtime confirmation (Smack peer-label policy + nft HBH mutation):
> Udp6InDatagrams increments to 1 with the mutated cat_len, showing
> selinux/smack_socket_sock_rcv_skb -> netlbl_skbuff_getattr ->
> calipso_opt_getattr -> netlbl_bitmap_walk runs end-to-end past the
> option's true bound; with this patch the consume path short-circuits
> at the bounds check and the counter stays 0.
>
> Reported-by: Qi Tang <tpluszz77 at gmail.com>
> Reported-by: Tong Liu <lyutoon at gmail.com>
> Fixes: 2917f57b6bc1 ("calipso: Allow the lsm to label the skbuff directly.")
> Signed-off-by: Qi Tang <tpluszz77 at gmail.com>
> ---
> net/netlabel/netlabel_kapi.c | 13 +++++++++++--
> 1 file changed, 11 insertions(+), 2 deletions(-)
>
> diff --git a/net/netlabel/netlabel_kapi.c b/net/netlabel/netlabel_kapi.c
> index 3583fa63dd01f..4af8ab76964e0 100644
> --- a/net/netlabel/netlabel_kapi.c
> +++ b/net/netlabel/netlabel_kapi.c
> @@ -1399,11 +1399,20 @@ int netlbl_skbuff_getattr(const struct sk_buff *skb,
> return 0;
> break;
> #if IS_ENABLED(CONFIG_IPV6)
> - case AF_INET6:
> + case AF_INET6: {
> + const unsigned char *tail = skb_tail_pointer(skb);
> + u8 opt_data_len;
> +
> ptr = calipso_optptr(skb);
> - if (ptr && calipso_getattr(ptr, secattr) == 0)
> + if (!ptr || ptr + 2 > tail)
> + break;
Is there a reason why you simply break here and drop down into the
unlabeled code? I would think we would want to return an error here
since we had packet that was munged.
> + opt_data_len = ptr[1]; /* IPv6 option data length */
> + if (ptr + 2 + opt_data_len > tail)
> + break;
Same thing.
> + if (calipso_getattr(ptr, secattr) == 0)
> return 0;
> break;
> + }
> #endif /* IPv6 */
> }
>
> --
> 2.47.3
--
paul-moore.com
More information about the Linux-security-module-archive
mailing list